Compare commits

..

No commits in common. "461bd99f4f5cde7c1983a51658d1a3d9ddafb577" and "2dbf9f94356e26749153d6b1569506652b35c102" have entirely different histories.

163 changed files with 38232 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4

65
.env.example Normal file
View File

@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=wisata_web
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
Homestead.json
Homestead.yaml
Thumbs.db

593
README.md Normal file
View File

@ -0,0 +1,593 @@
# 🎫 Banyu Biru Ticketing & Booking System
<p align="center">
<a href="#-fitur-utama">
<img src="https://img.shields.io/badge/Features-✨-FF6B6B?style=for-the-badge" alt="Features">
</a>
<a href="#-teknologi-yang-digunakan">
<img src="https://img.shields.io/badge/Tech Stack-🛠️-4ECDC4?style=for-the-badge" alt="Tech Stack">
</a>
<a href="#-instalasi">
<img src="https://img.shields.io/badge/Installation-🚀-45B7D1?style=for-the-badge" alt="Installation">
</a>
<a href="#-license">
<img src="https://img.shields.io/badge/License-MIT-FFEAA7?style=for-the-badge" alt="License">
</a>
</p>
---
## 📋 Table of Contents
- [🎯 Tentang Sistem](#-tentang-sistem)
- [✨ Fitur Utama](#-fitur-utama)
- [👥 Fitur User](#-fitur-user-pengunjung)
- [🔐 Fitur Admin](#-fitur-admin)
- [🛠️ Teknologi yang Digunakan](#-teknologi-yang-digunakan)
- [📋 Persyaratan Sistem](#-persyaratan-sistem)
- [🚀 Instalasi](#-instalasi)
- [👤 Default Login](#-default-login)
- [📁 Struktur Folder](#-struktur-folder)
- [🎨 Tema & Desain](#-tema--desain)
- [📊 Database Schema](#-database-schema)
- [🔐 Keamanan](#-keamanan)
- [📱 Fitur QR Code](#-fitur-qr-code)
- [🔄 Workflow Sistem](#-workflow-sistem)
- [🐛 Troubleshooting](#-troubleshooting)
- [🤝 Kontribusi](#-kontribusi)
- [📝 License](#-license)
- [👨‍💻 Developer](#-developer)
- [📞 Kontak & Support](#-kontak--support)
- [🙏 Acknowledgments](#-acknowledgments)
---
## 🎯 Tentang Sistem
<table>
<tr>
<td>
**Banyu Biru Ticketing & Booking System** adalah sistem manajemen tiket dan booking berbasis web untuk objek wisata Banyu Biru di Nganjuk. Sistem ini memudahkan pengunjung untuk membeli tiket masuk dan melakukan booking pendopo secara online, serta membantu admin dalam mengelola transaksi dan verifikasi pembayaran.
</td>
</tr>
</table>
### 🎯 Tujuan Sistem
| Tujuan | Deskripsi |
|--------|-----------|
| 🚀 | Digitalisasi proses pembelian tiket dan booking pendopo |
| 💳 | Mempermudah pengunjung dalam melakukan transaksi online |
| 📈 | Meningkatkan efisiensi pengelolaan transaksi oleh admin |
| 📱 | Menyediakan sistem verifikasi tiket dengan QR code |
| 📊 | Memberikan laporan statistik penjualan real-time |
---
## ✨ Fitur Utama
### 👥 Fitur User (Pengunjung)
#### 🎟️ Pembelian Tiket
| Fitur | Deskripsi |
|-------|-----------|
| 📋 | Lihat daftar tiket yang tersedia dengan harga |
| 📅 | Pilih tanggal kunjungan dan jumlah tiket |
| 📲 | Generate QR code otomatis untuk setiap tiket |
| 💰 | Upload bukti pembayaran transfer |
| 📄 | Download tiket dalam format PDF setelah dikonfirmasi |
| 🖨️ | Print tiket langsung dari browser |
#### 🏛️ Booking Pendopo
| Fitur | Deskripsi |
|-------|-----------|
| 📆 | Cek ketersediaan tanggal booking secara real-time |
| 📝 | Form booking dengan data pengunjung lengkap |
| 💳 | Upload bukti pembayaran booking |
| 📊 | Lihat status booking (pending/confirmed/rejected) |
| 🔔 | Notifikasi status verifikasi |
#### 📜 Riwayat Transaksi
| Fitur | Deskripsi |
|-------|-----------|
| 📋 | Lihat riwayat pembelian tiket |
| 📑 | Lihat riwayat booking pendopo |
| 🔍 | Filter berdasarkan status transaksi |
| ⬇️ | Download ulang tiket yang sudah dikonfirmasi |
#### 👤 Manajemen Profil
| Fitur | Deskripsi |
|-------|-----------|
| ✏️ | Edit profil (nama, email, telepon, alamat) |
| 🔑 | Ubah password dengan verifikasi password lama |
| 👁️ | Lihat informasi akun |
---
### 🔐 Fitur Admin
#### 📊 Dashboard
| Fitur | Deskripsi |
|-------|-----------|
| 🎫 | Statistik total tiket terjual (confirmed) |
| 💵 | Penjualan bulan ini (tiket + booking) |
| 💰 | Total pendapatan dari transaksi terkonfirmasi |
| 📈 | Chart penjualan 6 bulan terakhir |
| 📋 | Daftar order dan booking terbaru |
#### ✅ Verifikasi Pembayaran
| Fitur | Deskripsi |
|-------|-----------|
| 📋 | Lihat daftar order tiket dengan status |
| 🔎 | Lihat detail order dan bukti pembayaran |
| ✓️ | Konfirmasi pembayaran tiket |
| ❌ | Tolak pembayaran dengan alasan |
| ✔️ | Verifikasi pembayaran booking |
#### 📱 QR Scanner Verifikasi Tiket
| Fitur | Deskripsi |
|-------|-----------|
| 📷 | Scan QR code tiket menggunakan kamera |
| ⌨️ | Input manual kode tiket |
| 🔒 | Verifikasi validitas tiket |
| ✓️ | Tandai tiket sebagai sudah dipakai |
| 📊 | Cek status tiket (valid/sudah dipakai) |
#### 🎫 Manajemen Tiket
| Fitur | Deskripsi |
|-------|-----------|
| | CRUD tiket (Create, Read, Update, Delete) |
| 💵 | Set harga tiket |
| 🔄 | Toggle status aktif/nonaktif tiket |
| 📝 | Deskripsi tiket |
#### 🏦 Manajemen Rekening Bank
| Fitur | Deskripsi |
|-------|-----------|
| | CRUD rekening bank untuk pembayaran |
| 🔄 | Set rekening aktif/nonaktif |
| 👁️ | Tampilkan rekening di halaman pembayaran user |
#### 👨‍💼 Manajemen Admin
| Fitur | Deskripsi |
|-------|-----------|
| | CRUD akun admin |
| 👥 | Assign role admin ke user |
| ✏️ | Edit profil admin |
| 🔑 | Ubah password admin |
---
## 🛠️ Teknologi yang Digunakan
### Backend
<div align="center">
| Technology | Description |
|------------|-------------|
| <img src="https://img.shields.io/badge/Laravel-11.x-FF2D20?style=for-the-badge&logo=laravel&logoColor=white" height="25"> | PHP Framework |
| <img src="https://img.shields.io/badge/PHP-8.2+-777BB4?style=for-the-badge&logo=php&logoColor=white" height="25"> | Programming Language |
| <img src="https://img.shields.io/badge/MySQL-8.0+-4479A1?style=for-the-badge&logo=mysql&logoColor=white" height="25"> | Database |
| <img src="https://img.shields.io/badge/Spatie-0052CC?style=for-the-badge" height="25"> | Role & Permission |
</div>
### Frontend
<div align="center">
| Technology | Description |
|------------|-------------|
| <img src="https://img.shields.io/badge/Bootstrap-5.3-7952B3?style=for-the-badge&logo=bootstrap&logoColor=white" height="25"> | CSS Framework |
| <img src="https://img.shields.io/badge/Chart.js-FF6384?style=for-the-badge&logo=chart.js&logoColor=white" height="25"> | Data Visualization |
| <img src="https://img.shields.io/badge/SweetAlert2-FF6B6B?style=for-the-badge" height="25"> | Beautiful Alerts |
| <img src="https://img.shields.io/badge/Font_Awesome-6-528DD7?style=for-the-badge&logo=font-awesome&logoColor=white" height="25"> | Icons |
| <img src="https://img.shields.io/badge/html5--qrcode-000000?style=for-the-badge" height="25"> | QR Code Scanner |
</div>
### Libraries & Tools
<div align="center">
| Library | Description |
|---------|-------------|
| 🔲 SimpleSoftwareIO/simple-qrcode | QR Code Generator |
| 📄 Barryvdh/laravel-dompdf | PDF Generator |
| 🔐 Laravel Breeze | Authentication Scaffolding |
</div>
---
## 📋 Persyaratan Sistem
| Requirement | Description |
|-------------|-------------|
| 🟢 | PHP >= 8.2 |
| 📦 | Composer |
| 🗄️ | MySQL >= 8.0 |
| 📦 | Node.js & NPM (untuk asset compilation) |
| 🌐 | Web Server (Apache/Nginx) |
### 📦 PHP Extensions Required
```
BCMath, Ctype, Fileinfo, JSON, Mbstring, OpenSSL, PDO, Tokenizer, XML, GD
```
---
## 🚀 Instalasi
### ⚡ Cara Cepat (Setelah Clone)
```bash
# 1. Masuk ke folder project
cd wisata-web
# 2. Install semua dependencies
composer install
npm install
# 3. Buat file .env dan generate application key
cp .env.example .env
php artisan key:generate
# 4. Buat database (via phpMyAdmin atau command)
# Windows (XAMPP):
C:\xampp\mysql\bin\mysql.exe -u root -e "CREATE DATABASE wisata_web CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# 5. Jalankan migrations (tabel sessions sudah termasuk!)
php artisan migrate
# 6. Seed data (opsional - untuk data awal)
php artisan db:seed
# 7. Compile frontend assets
npm run build
# 8. Buat storage link
php artisan storage:link
# 9. Jalankan server
php artisan serve
```
> 🌐 **Akses**: `http://127.0.0.1:8000`
### 📝 Catatan Penting
- ✅ **Tabel `sessions` sudah termasuk** dalam migration Laravel default, tidak perlu command tambahan
- ✅ **Cukup jalankan** `php artisan migrate` untuk membuat semua tabel
- ⚠️ **Pastikan database sudah dibuat** di MySQL/phpMyAdmin sebelum menjalankan migration
---
## 🐛 Troubleshooting
### ❌ Error: Table doesn't exist
Jika muncul error seperti `Table 'wisata_web.sessions' doesn't exist`:
```bash
# Hapus dan buat ulang database
C:\xampp\mysql\bin\mysql.exe -u root -e "DROP DATABASE IF EXISTS wisata_web; CREATE DATABASE wisata_web CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# Jalankan ulang migrations
php artisan migrate:fresh
```
### ❌ Error: Tablespace corrupted
Jika muncul error `Tablespace for table exists. Please DISCARD the tablespace before IMPORT`:
```bash
# Hapus folder database manual (Windows XAMPP)
rmdir /s /q C:\xampp\mysql\data\wisata_web
# Buat database baru
C:\xampp\mysql\bin\mysql.exe -u root -e "CREATE DATABASE wisata_web CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# Jalankan migrations
php artisan migrate:fresh
```
### ❌ Error: .env file not found
```bash
cp .env.example .env
php artisan key:generate
```
---
## 👤 Default Login
| Role | Email | Password |
|------|-------|----------|
| 🔴 Admin | admin@banyubiru.com | password |
| 🔵 User 1 | pengguna1@banyubiru.test | password |
| 🔵 User 2 | pengguna2@banyubiru.test | password |
| ... | ... | ... |
| 🔵 User 8 | pengguna8@banyubiru.test | password |
> ⚠️ **Penting**: Ubah password default setelah login pertama kali!
---
## 📁 Struktur Folder
```
📂 wisata-web/
├── 📂 app/
│ ├── 📂 Http/Controllers/
│ │ ├── 📂 Admin/ # Controller untuk admin
│ │ ├── 📂 User/ # Controller untuk user
│ │ └── AuthController.php
│ ├── 📂 Models/ # Eloquent Models
│ └── 📂 Support/ # Helper functions
├── 📂 database/
│ ├── 📂 migrations/ # Database migrations
│ └── 📂 seeders/ # Database seeders
├── 📂 public/
│ ├── 📂 css/ # Compiled CSS
│ ├── 📂 images/ # Static images
│ ├── 📂 payment-proofs/ # Bukti pembayaran
│ └── 📂 qrcodes/ # Generated QR codes
├── 📂 resources/
│ ├── 📂 css/ # Source CSS
│ ├── 📂 js/ # Source JavaScript
│ └── 📂 views/ # Blade templates
│ ├── 📂 admin/ # Admin views
│ ├── 📂 auth/ # Auth views
│ ├── 📂 user/ # User views
│ ├── 📂 layouts/ # Layout templates
│ └── 📂 partials/ # Reusable components
├── 📂 routes/
│ └── web.php # Web routes
├── 📂 flowcharts/ # Flowchart documentation
├── 📄 database.md # Database documentation
├── 📄 usecase.md # Use case documentation
└── 📄 activity-diagram.md # Activity diagram documentation
```
---
## 🎨 Tema & Desain
### 🎨 Color Palette
| Color | Hex Code | Usage |
|-------|----------|-------|
| 🟦 Primary | `#0f766e` | Main buttons, headers |
| 🟦 Secondary | `#14b8a6` | Accents, highlights |
| 🟩 Accent | `#5eead4` | Hover states |
| ⬜ Background | `#f0fdfa` | Page background |
### ✨ UI/UX Features
- 📱 **Responsive** - Mobile, tablet, dan desktop
- 🌙 **Dark Sidebar** - Dengan gradient teal
- 🃏 **Modern Cards** - Dengan border teal
- 🔘 **Rounded Buttons** - Desain modern
- ✨ **Animations** - Smooth transitions
- 🔔 **SweetAlert** - Konfirmasi yang indah
- 👁️ **Password Toggle** - Show/hide password
- ⏳ **Loading States** - User feedback
---
## 📊 Database Schema
### 📋 Tabel Utama
| No | Tabel | Deskripsi |
|----|-------|-----------|
| 1 | `users` | Data user dan admin |
| 2 | `tickets` | Master data tiket |
| 3 | `ticket_orders` | Order pembelian tiket |
| 4 | `ticket_order_items` | Detail tiket per order |
| 5 | `bookings` | Data booking pendopo |
| 6 | `places` | Master data tempat (pendopo) |
| 7 | `bank_accounts` | Rekening bank untuk pembayaran |
| 8 | `roles & permissions` | Role-based access control |
> 📖 **Lihat dokumentasi lengkap**: [database.md](database.md)
---
## 🔐 Keamanan
| Feature | Description |
|---------|-------------|
| 🔒 | Password hashing menggunakan bcrypt |
| 🛡️ | CSRF protection pada semua form |
| 👥 | Role-based authorization (admin/user) |
| ✅ | Input validation & sanitization |
| 🗄️ | SQL injection protection (Eloquent ORM) |
| 🚫 | XSS protection |
| 📁 | File upload validation (type & size) |
| ⏰ | Session management |
---
## 📱 Fitur QR Code
### 🔲 Generate QR Code
| Feature | Description |
|---------|-------------|
| ⚡ | Otomatis generate saat pembelian tiket |
| 📐 | Format: SVG (scalable) |
| 🔢 | Berisi kode tiket unik (AT-XXXXX) |
| 📁 | Disimpan di `/public/qrcodes/` |
### 📷 Scan QR Code
| Feature | Description |
|---------|-------------|
| 📷 | Menggunakan library html5-qrcode |
| 📸 | Akses kamera untuk scanning |
| 🤖 | Auto-detect dan submit |
| ⌨️ | Fallback: input manual kode tiket |
---
## 🔄 Workflow Sistem
### 📈 Alur Pembelian Tiket
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Login │───▶│ Pilih │───▶│ Jumlah │───▶│ Generate │
│ │ │ Tiket │ │ Tiket │ │ QR │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Download │◀───│ Verified │◀───│ Upload │◀───│ Payment │
│ Tiket │ │ (✓) │ │ Bukti │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
```
### 🏛️ Alur Booking Pendopo
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Login │───▶│ Cek │───▶│ Isi │───▶│ Upload │
│ │ │Ketersediaan│ │ Form │ │ Bukti │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Booking │◀───│ Verified │◀───│ Admin │
│Confirmed │ │ (✓) │ │ Verifikasi│
└──────────┘ └──────────┘ └──────────┘
```
### 📷 Alur Verifikasi Tiket
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Scan │───▶│ Cek │───▶│ Tampilkan│───▶│ Tandai │
│ QR │ │ Validitas│ │ Info │ │ Dipakai │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
```
---
## 🐛 Troubleshooting
### ❌ Error: Class "SimpleSoftwareIO\QrCode\Facades\QrCode" not found
```bash
composer require simplesoftwareio/simple-qrcode
```
### ❌ Error: Storage link not found
```bash
php artisan storage:link
```
### ❌ Error: Permission denied pada folder storage
```bash
chmod -R 775 storage bootstrap/cache
```
### 📷 QR Scanner tidak bisa akses kamera
- ✅ Pastikan browser memiliki izin akses kamera
- ✅ Gunakan HTTPS (atau localhost)
- ✅ Coba browser lain (Chrome/Firefox)
---
## 🤝 Kontribusi
Kontribusi sangat diterima! Silakan:
```
1. 🍴 Fork repository ini
2. 🌿 Buat branch fitur (`git checkout -b feature/AmazingFeature`)
3. 💾 Commit perubahan (`git commit -m 'Add some AmazingFeature'`)
4. 📤 Push ke branch (`git push origin feature/AmazingFeature'`)
5. 🔀 Buat Pull Request
```
---
## 📝 License
<p align="center">
<img src="https://img.shields.io/badge/License-MIT-FF6B6B?style=for-the-badge" alt="License">
</p>
<p align="center">
Sistem ini menggunakan framework Laravel yang berlisensi
<a href="https://opensource.org/licenses/MIT">MIT license</a>
</p>
---
## 👨‍💻 Developer
<div align="center">
| | |
|:---:|:---|
| **Ardhika** | |
| 🔗 | [GitHub](https://github.com/ardhikaxx) |
| 📦 | [Repository](https://github.com/ardhikaxx/banyu-biru-nganjuk) |
</div>
---
## 📞 Kontak & Support
Jika ada pertanyaan atau masalah, silakan:
| Contact | Link |
|---------|------|
| 🐛 | [Buat Issue](https://github.com/ardhikaxx/banyu-biru-nganjuk/issues) |
| 📧 | [Email](mailto:email@example.com) |
---
## 🙏 Acknowledgments
<div align="center">
| Thanks To | Description |
|-----------|-------------|
| 🦁 | Laravel Framework |
| 🎨 | Bootstrap Team |
| 🔰 | Font Awesome |
| 📊 | Chart.js |
| 🔔 | SweetAlert2 |
| 🔲 | SimpleSoftwareIO QR Code |
| 📷 | html5-qrcode Library |
</div>
---
<p align="center">
<img src="https://komarev.com/ghpvc/?username=ardhikaxx&label=Views&color=0f766e&style=flat" alt="Views">
</p>
<p align="center">
<strong>Made with ❤️ for Banyu Biru Nganjuk</strong>
</p>
<p align="center">
<strong>© 2026 Banyu Biru Ticketing & Booking System</strong>
</p>

693
activity-diagram.md Normal file
View File

@ -0,0 +1,693 @@
# Activity Diagram - Sistem Tiket & Booking Banyu Biru
## Daftar Isi
1. [Overview](#overview)
2. [Activity Diagram 1: Proses Pembelian Tiket](#activity-diagram-1-proses-pembelian-tiket)
3. [Activity Diagram 2: Proses Verifikasi Pembayaran Tiket](#activity-diagram-2-proses-verifikasi-pembayaran-tiket)
4. [Activity Diagram 3: Proses Booking Tempat](#activity-diagram-3-proses-booking-tempat)
5. [Activity Diagram 4: Proses Verifikasi Tiket dengan QR Code](#activity-diagram-4-proses-verifikasi-tiket-dengan-qr-code)
6. [Activity Diagram 5: Proses Login](#activity-diagram-5-proses-login)
7. [Panduan Membuat Diagram di Draw.io](#panduan-membuat-diagram-di-drawio)
---
## Overview
Activity diagram menggambarkan alur aktivitas dalam sistem dari awal hingga akhir. Dokumen ini menyediakan 5 activity diagram utama yang mencakup proses bisnis penting dalam sistem.
**Notasi yang Digunakan**:
- **Initial Node** (●): Titik awal aktivitas
- **Activity** (Rounded Rectangle): Aktivitas/aksi yang dilakukan
- **Decision** (◇): Percabangan keputusan
- **Fork/Join** (Bar): Aktivitas paralel
- **Final Node** (◎): Titik akhir aktivitas
- **Swimlane**: Pembagian tanggung jawab (User, System, Admin)
---
## Activity Diagram 1: Proses Pembelian Tiket
### Deskripsi
Menggambarkan alur lengkap pembelian tiket dari user mulai dari memilih tiket hingga upload bukti pembayaran.
### Swimlanes
1. **User** - Pengguna yang membeli tiket
2. **System** - Sistem aplikasi
3. **Payment Gateway** - Sistem pembayaran (eksternal)
### Alur Aktivitas
```
[START]
├─ User: Login ke sistem
├─ System: Validasi kredensial
├─ Decision: Login berhasil?
│ ├─ [NO] → Tampilkan error → [END]
│ └─ [YES] ↓
├─ System: Tampilkan halaman pembelian tiket
├─ User: Pilih tanggal kunjungan
├─ System: Validasi tanggal
├─ Decision: Tanggal valid?
│ ├─ [NO] → Tampilkan error tanggal → Kembali ke pilih tanggal
│ └─ [YES] ↓
├─ User: Pilih jenis tiket
├─ User: Tentukan jumlah tiket
├─ System: Hitung total harga
├─ System: Tampilkan ringkasan pesanan
├─ User: Konfirmasi pesanan
├─ System: Generate kode order unik (AT-XXXXX)
├─ System: Generate QR code untuk setiap tiket
├─ System: Simpan data order dengan status "pending"
├─ System: Tampilkan halaman pembayaran
├─ System: Tampilkan nomor rekening bank
├─ User: Melakukan transfer ke rekening
├─ User: Upload bukti pembayaran
├─ System: Validasi file bukti pembayaran
├─ Decision: File valid?
│ ├─ [NO] → Tampilkan error format → Kembali ke upload
│ └─ [YES] ↓
├─ System: Simpan bukti pembayaran
├─ System: Update status order tetap "pending"
├─ System: Tampilkan halaman download tiket
├─ System: Tampilkan pesan "Menunggu verifikasi admin"
└─ [END]
```
### Decision Points
1. **Login berhasil?** - Validasi email dan password
2. **Tanggal valid?** - Cek apakah tanggal >= hari ini
3. **File valid?** - Cek format dan ukuran file
### Catatan
- QR code di-generate untuk setiap item tiket
- User bisa download tiket tapi tombol disabled sampai dikonfirmasi
- Status order: pending → menunggu verifikasi admin
---
## Activity Diagram 2: Proses Verifikasi Pembayaran Tiket
### Deskripsi
Menggambarkan alur verifikasi pembayaran tiket oleh admin.
### Swimlanes
1. **Admin** - Administrator yang memverifikasi
2. **System** - Sistem aplikasi
3. **User** - Pemilik tiket (menerima notifikasi)
### Alur Aktivitas
```
[START]
├─ Admin: Login ke dashboard admin
├─ System: Tampilkan dashboard
├─ Admin: Akses menu "Pesanan Tiket"
├─ System: Tampilkan daftar pesanan tiket
├─ System: Tampilkan filter status (pending/confirmed/rejected)
├─ Admin: Filter pesanan dengan status "pending"
├─ System: Tampilkan pesanan pending
├─ Admin: Pilih pesanan yang akan diverifikasi
├─ System: Tampilkan detail pesanan
├─ System: Tampilkan bukti pembayaran
├─ System: Tampilkan data user dan tiket
├─ Admin: Periksa bukti pembayaran
├─ Admin: Periksa kesesuaian nominal
├─ Decision: Pembayaran valid?
│ │
│ ├─ [YES] → Admin: Klik tombol "Konfirmasi"
│ │ │
│ │ ├─ System: Update status order menjadi "confirmed"
│ │ │
│ │ ├─ System: Simpan waktu konfirmasi (confirmed_at)
│ │ │
│ │ ├─ System: Simpan ID admin yang konfirmasi (confirmed_by)
│ │ │
│ │ ├─ System: Hitung ke total pendapatan
│ │ │
│ │ ├─ System: Aktifkan tombol download tiket untuk user
│ │ │
│ │ ├─ System: Tampilkan notifikasi sukses
│ │ │
│ │ └─ [END]
│ │
│ └─ [NO] → Admin: Klik tombol "Tolak"
│ │
│ ├─ System: Tampilkan form alasan penolakan
│ │
│ ├─ Admin: Isi alasan penolakan
│ │
│ ├─ Admin: Konfirmasi penolakan
│ │
│ ├─ System: Update status order menjadi "rejected"
│ │
│ ├─ System: Simpan alasan penolakan (rejection_note)
│ │
│ ├─ System: Tampilkan notifikasi penolakan
│ │
│ ├─ System: User bisa upload ulang bukti pembayaran
│ │
│ └─ [END]
```
### Decision Points
1. **Pembayaran valid?** - Admin memutuskan berdasarkan bukti transfer
### Catatan
- Admin bisa konfirmasi atau tolak pembayaran
- Jika ditolak, user bisa upload ulang bukti
- Hanya pesanan "confirmed" yang masuk ke laporan pendapatan
---
## Activity Diagram 3: Proses Booking Tempat
### Deskripsi
Menggambarkan alur booking tempat (pendopo) oleh user.
### Swimlanes
1. **User** - Pengguna yang booking
2. **System** - Sistem aplikasi
### Alur Aktivitas
```
[START]
├─ User: Login ke sistem
├─ System: Validasi kredensial
├─ Decision: Login berhasil?
│ ├─ [NO] → Tampilkan error → [END]
│ └─ [YES] ↓
├─ System: Tampilkan halaman booking
├─ System: Tampilkan informasi tempat (Pendopo)
├─ System: Tampilkan harga per hari
├─ User: Pilih tanggal booking
├─ System: Cek ketersediaan tanggal
├─ System: Query database untuk tanggal tersebut
├─ Decision: Tanggal tersedia?
│ ├─ [NO] → System: Tampilkan pesan "Tanggal sudah dibooking"
│ │ └─ Kembali ke pilih tanggal
│ └─ [YES] ↓
├─ Decision: Tanggal >= hari ini?
│ ├─ [NO] → System: Tampilkan error "Tanggal sudah lewat"
│ │ └─ Kembali ke pilih tanggal
│ └─ [YES] ↓
├─ User: Isi nama pengunjung
├─ User: Isi nomor telepon
├─ User: Isi alamat
├─ User: Isi catatan (opsional)
├─ System: Validasi data input
├─ Decision: Data valid?
│ ├─ [NO] → Tampilkan error validasi → Kembali ke form
│ └─ [YES] ↓
├─ System: Hitung total harga
├─ System: Tampilkan ringkasan booking
├─ User: Konfirmasi booking
├─ System: Generate kode booking unik (AB-XXXXX)
├─ System: Simpan data booking dengan status "pending"
├─ System: Block tanggal untuk booking lain
├─ System: Tampilkan halaman pembayaran
├─ System: Tampilkan nomor rekening bank
├─ User: Melakukan transfer
├─ User: Upload bukti pembayaran
├─ System: Validasi file
├─ Decision: File valid?
│ ├─ [NO] → Tampilkan error → Kembali ke upload
│ └─ [YES] ↓
├─ System: Simpan bukti pembayaran
├─ System: Tampilkan halaman status booking
├─ System: Tampilkan pesan "Menunggu verifikasi admin"
└─ [END]
```
### Decision Points
1. **Login berhasil?** - Validasi kredensial
2. **Tanggal tersedia?** - Cek di database apakah sudah dibooking
3. **Tanggal >= hari ini?** - Validasi tanggal tidak boleh masa lalu
4. **Data valid?** - Validasi form input
5. **File valid?** - Validasi bukti pembayaran
### Catatan
- Satu tempat hanya bisa dibooking 1x per tanggal
- Tanggal otomatis ter-block setelah booking dibuat
- Status awal selalu "pending"
---
## Activity Diagram 4: Proses Verifikasi Tiket dengan QR Code
### Deskripsi
Menggambarkan alur verifikasi tiket menggunakan QR code scanner di pintu masuk.
### Swimlanes
1. **Admin/Petugas** - Petugas di pintu masuk
2. **System** - Sistem aplikasi
3. **User** - Pengunjung dengan tiket
### Alur Aktivitas
```
[START]
├─ User: Datang ke pintu masuk dengan tiket
├─ Admin: Login ke sistem verifikasi
├─ System: Tampilkan halaman verifikasi tiket
├─ System: Tampilkan pilihan metode verifikasi
├─ Decision: Metode verifikasi?
│ │
│ ├─ [Scan QR] → System: Aktifkan kamera
│ │ │
│ │ ├─ System: Tampilkan scanner QR
│ │ │
│ │ ├─ Admin: Arahkan kamera ke QR code tiket
│ │ │
│ │ ├─ System: Baca QR code
│ │ │
│ │ ├─ System: Ekstrak kode tiket dari QR
│ │ │
│ │ └─ [Lanjut ke Validasi]
│ │
│ └─ [Input Manual] → Admin: Ketik kode tiket
│ │
│ ├─ System: Terima input kode
│ │
│ └─ [Lanjut ke Validasi]
├─ [Validasi]
├─ System: Cari tiket berdasarkan kode
├─ Decision: Tiket ditemukan?
│ ├─ [NO] → System: Tampilkan error "Tiket tidak ditemukan"
│ │ └─ [END]
│ └─ [YES] ↓
├─ System: Ambil data tiket dari database
├─ System: Cek status order tiket
├─ Decision: Order sudah dikonfirmasi?
│ ├─ [NO] → System: Tampilkan pesan "Pembayaran belum dikonfirmasi"
│ │ │
│ │ ├─ System: Tampilkan status "Pending"
│ │ │
│ │ └─ [END]
│ └─ [YES] ↓
├─ System: Cek status penggunaan tiket
├─ Decision: Tiket sudah dipakai?
│ ├─ [YES] → System: Tampilkan peringatan "Tiket sudah digunakan"
│ │ │
│ │ ├─ System: Tampilkan waktu penggunaan
│ │ │
│ │ ├─ System: Tampilkan badge merah "Sudah Dipakai"
│ │ │
│ │ └─ [END]
│ └─ [NO] ↓
├─ System: Tampilkan detail tiket
├─ System: Tampilkan data pengunjung
├─ System: Tampilkan tanggal kunjungan
├─ System: Tampilkan badge hijau "Valid"
├─ Admin: Verifikasi data pengunjung
├─ Decision: Data sesuai?
│ ├─ [NO] → Admin: Tolak masuk
│ │ └─ [END]
│ └─ [YES] ↓
├─ Admin: Klik tombol "Tandai Sebagai Dipakai"
├─ System: Update field is_used = true
├─ System: Simpan waktu penggunaan (used_at)
├─ System: Tampilkan notifikasi sukses
├─ System: Hide kamera scanner
├─ System: Tampilkan tombol "Scan Lagi"
├─ Admin: Izinkan pengunjung masuk
└─ [END]
```
### Decision Points
1. **Metode verifikasi?** - Scan QR atau input manual
2. **Tiket ditemukan?** - Cek keberadaan kode di database
3. **Order sudah dikonfirmasi?** - Cek status pembayaran
4. **Tiket sudah dipakai?** - Cek status penggunaan
5. **Data sesuai?** - Admin verifikasi manual
### Catatan
- Tiket hanya bisa dipakai 1x
- Sistem otomatis hide kamera setelah scan berhasil
- Admin bisa scan lagi dengan klik tombol "Scan Lagi"
---
## Activity Diagram 5: Proses Login
### Deskripsi
Menggambarkan alur login user dan admin ke sistem.
### Swimlanes
1. **User/Admin** - Pengguna yang login
2. **System** - Sistem aplikasi
3. **Database** - Database sistem
### Alur Aktivitas
```
[START]
├─ User: Akses halaman login
├─ System: Tampilkan form login
├─ User: Masukkan email
├─ User: Masukkan password
├─ User: Klik tombol "Login"
├─ System: Validasi format email
├─ Decision: Format email valid?
│ ├─ [NO] → System: Tampilkan error "Format email salah"
│ │ └─ Kembali ke form login
│ └─ [YES] ↓
├─ System: Validasi password tidak kosong
├─ Decision: Password terisi?
│ ├─ [NO] → System: Tampilkan error "Password wajib diisi"
│ │ └─ Kembali ke form login
│ └─ [YES] ↓
├─ System: Query database untuk email
├─ Database: Cari user berdasarkan email
├─ Decision: User ditemukan?
│ ├─ [NO] → System: Tampilkan error "Email tidak terdaftar"
│ │ └─ Kembali ke form login
│ └─ [YES] ↓
├─ System: Ambil data user dari database
├─ System: Verifikasi password (hash comparison)
├─ Decision: Password cocok?
│ ├─ [NO] → System: Tampilkan error "Password salah"
│ │ └─ Kembali ke form login
│ └─ [YES] ↓
├─ System: Buat session untuk user
├─ System: Simpan data user ke session
├─ System: Cek role user
├─ Decision: Role user?
│ │
│ ├─ [Admin] → System: Redirect ke dashboard admin
│ │ │
│ │ ├─ System: Tampilkan statistik
│ │ │
│ │ ├─ System: Tampilkan grafik
│ │ │
│ │ └─ [END]
│ │
│ └─ [User] → System: Redirect ke halaman beranda
│ │
│ ├─ System: Tampilkan menu tiket & booking
│ │
│ ├─ System: Tampilkan dropdown user
│ │
│ └─ [END]
```
### Decision Points
1. **Format email valid?** - Validasi format email
2. **Password terisi?** - Validasi password tidak kosong
3. **User ditemukan?** - Cek keberadaan email di database
4. **Password cocok?** - Verifikasi hash password
5. **Role user?** - Tentukan redirect berdasarkan role
### Catatan
- Password di-hash menggunakan bcrypt
- Session disimpan untuk maintain login state
- Redirect berbeda untuk admin dan user
---
## Panduan Membuat Diagram di Draw.io
### Langkah 1: Setup Canvas
1. Buka draw.io
2. Pilih "Blank Diagram"
3. Aktifkan library "UML" dari menu More Shapes
4. Set ukuran canvas: A4 Portrait atau Landscape
### Langkah 2: Buat Swimlanes
1. Drag "Swimlane" dari library UML
2. Buat 2-3 swimlanes sesuai kebutuhan:
- **Vertical Swimlanes** untuk actor yang berbeda
- Contoh: User | System | Admin
3. Beri label pada setiap swimlane
4. Atur lebar swimlane agar proporsional
### Langkah 3: Tambahkan Initial Node
1. Drag "Initial Node" (lingkaran hitam penuh)
2. Letakkan di bagian atas swimlane pertama
3. Ini adalah titik awal aktivitas
### Langkah 4: Tambahkan Activities
1. Drag "Activity" (rounded rectangle) dari library
2. Buat activity untuk setiap langkah:
- Gunakan verb + object (contoh: "Pilih tanggal", "Validasi data")
- Letakkan di swimlane yang sesuai
3. Hubungkan dengan arrow (Control Flow)
### Langkah 5: Tambahkan Decision Nodes
1. Drag "Decision" (diamond/rhombus)
2. Gunakan untuk percabangan kondisi
3. Beri label kondisi pada setiap arrow keluar:
- [YES] atau [NO]
- [Valid] atau [Invalid]
4. Merge kembali dengan "Merge Node" jika perlu
### Langkah 6: Tambahkan Fork/Join (Opsional)
1. Drag "Fork" (horizontal bar) untuk aktivitas paralel
2. Gunakan "Join" untuk menggabungkan kembali
3. Contoh: Generate QR code untuk multiple tiket secara bersamaan
### Langkah 7: Tambahkan Final Node
1. Drag "Final Node" (lingkaran dengan lingkaran dalam)
2. Letakkan di akhir alur
3. Bisa ada multiple final nodes untuk berbagai ending
### Langkah 8: Hubungkan dengan Control Flow
1. Gunakan arrow untuk menghubungkan nodes
2. Pastikan arah flow jelas (top to bottom)
3. Hindari crossing lines jika memungkinkan
### Tips Styling
#### Warna Swimlanes
- **User/Admin**: Biru muda (#E3F2FD)
- **System**: Hijau muda (#E8F5E9)
- **Database**: Abu-abu muda (#F5F5F5)
- **External**: Kuning muda (#FFF9C4)
#### Warna Activities
- **Normal Activity**: Putih dengan border hitam
- **Important Activity**: Hijau teal (#14B8A6)
- **Error/Reject**: Merah muda (#FFCDD2)
- **Success**: Hijau muda (#C8E6C9)
#### Warna Decision
- **Decision Node**: Kuning (#FFF59D)
- **Merge Node**: Abu-abu (#E0E0E0)
#### Font
- **Activity Label**: Arial, 11pt, Bold
- **Decision Label**: Arial, 10pt, Italic
- **Swimlane Label**: Arial, 12pt, Bold
### Best Practices
1. **Konsistensi**:
- Gunakan ukuran yang sama untuk semua activity nodes
- Gunakan spacing yang konsisten
- Align nodes menggunakan grid
2. **Clarity**:
- Gunakan label yang jelas dan singkat
- Hindari terlalu banyak crossing lines
- Gunakan warna untuk membedakan jenis aktivitas
3. **Flow Direction**:
- Utamakan top-to-bottom flow
- Gunakan left-to-right untuk swimlanes
- Loop back dengan arrow yang jelas
4. **Decision Nodes**:
- Selalu beri label pada setiap branch
- Gunakan [YES]/[NO] atau [Valid]/[Invalid]
- Pastikan semua branch memiliki ending
5. **Swimlanes**:
- Letakkan aktivitas di swimlane yang tepat
- Crossing swimlane menunjukkan interaksi
- Jangan terlalu banyak swimlanes (max 4)
### Contoh Layout
```
┌─────────────┬──────────────┬─────────────┐
│ User │ System │ Admin │
├─────────────┼──────────────┼─────────────┤
│ │ │ │
│ (●) │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌─────┐ │ │ │
│ │Login│────┼──────────────┼────────────▶│
│ └─────┘ │ │ │
│ │ ┌──────┐ │ │
│ │ │Validasi │ │
│ │ └──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ◇Valid? │ │
│ │ / \ │ │
│ │ NO YES │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │Error Success│ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ (◎) (◎) │ │
└─────────────┴──────────────┴─────────────┘
```
---
## Checklist Sebelum Finalisasi
- [ ] Semua aktivitas memiliki label yang jelas
- [ ] Semua decision nodes memiliki label kondisi
- [ ] Flow direction konsisten (top-to-bottom)
- [ ] Tidak ada aktivitas yang "menggantung" (tanpa input/output)
- [ ] Swimlanes sudah sesuai dengan actor
- [ ] Warna sudah konsisten
- [ ] Initial dan Final nodes sudah ada
- [ ] Spacing dan alignment rapi
- [ ] Tidak terlalu banyak crossing lines
- [ ] Diagram mudah dibaca dan dipahami
---
## Summary
**Total Activity Diagrams**: 5
1. **Proses Pembelian Tiket** - 3 swimlanes, ~25 activities
2. **Proses Verifikasi Pembayaran Tiket** - 3 swimlanes, ~15 activities
3. **Proses Booking Tempat** - 2 swimlanes, ~30 activities
4. **Proses Verifikasi Tiket dengan QR Code** - 3 swimlanes, ~25 activities
5. **Proses Login** - 3 swimlanes, ~15 activities
**Total Activities**: ~110 activities
**Total Decision Points**: ~20 decisions
**Total Swimlanes**: 8 (across all diagrams)
---
**Dibuat**: 25 Februari 2026
**Versi**: 1.0
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AdminController extends Controller
{
public function index()
{
$admins = User::role('admin')->latest()->get();
return view('admin.admins.index', compact('admins'));
}
public function create()
{
return view('admin.admins.create');
}
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'max:150', 'unique:users,email'],
'phone' => ['nullable', 'string', 'max:20'],
'password' => ['required', 'string', 'min:6', 'confirmed'],
]);
$admin = User::create([
'name' => $data['name'],
'email' => $data['email'],
'phone' => $data['phone'] ?? null,
'password' => Hash::make($data['password']),
]);
$admin->assignRole('admin');
return redirect()->route('admin.admins.index')->with('success', 'Admin baru berhasil dibuat.');
}
public function edit(User $admin)
{
return view('admin.admins.edit', compact('admin'));
}
public function update(Request $request, User $admin)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'max:150', 'unique:users,email,'.$admin->id],
'phone' => ['nullable', 'string', 'max:20'],
'password' => ['nullable', 'string', 'min:6', 'confirmed'],
]);
$payload = [
'name' => $data['name'],
'email' => $data['email'],
'phone' => $data['phone'] ?? null,
];
if (! empty($data['password'])) {
$payload['password'] = Hash::make($data['password']);
}
$admin->update($payload);
if (! $admin->hasRole('admin')) {
$admin->assignRole('admin');
}
return redirect()->route('admin.admins.index')->with('success', 'Data admin berhasil diperbarui.');
}
public function destroy(User $admin)
{
if ($admin->id === auth()->id()) {
return back()->with('error', 'Anda tidak bisa menghapus akun sendiri.');
}
$admin->delete();
return back()->with('success', 'Admin berhasil dihapus.');
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\BankAccount;
use Illuminate\Http\Request;
class BankAccountController extends Controller
{
public function index()
{
$bankAccounts = BankAccount::latest()->get();
return view('admin.bank-accounts.index', compact('bankAccounts'));
}
public function create()
{
return view('admin.bank-accounts.create');
}
public function store(Request $request)
{
$data = $request->validate([
'bank_name' => ['required', 'string', 'max:100'],
'account_number' => ['required', 'string', 'max:50'],
'account_name' => ['required', 'string', 'max:100'],
'is_active' => ['nullable', 'boolean'],
]);
BankAccount::create([
...$data,
'is_active' => $request->boolean('is_active', true),
]);
return redirect()->route('admin.bank-accounts.index')->with('success', 'Rekening bank berhasil ditambah.');
}
public function edit(BankAccount $bank_account)
{
return view('admin.bank-accounts.edit', ['bankAccount' => $bank_account]);
}
public function update(Request $request, BankAccount $bank_account)
{
$data = $request->validate([
'bank_name' => ['required', 'string', 'max:100'],
'account_number' => ['required', 'string', 'max:50'],
'account_name' => ['required', 'string', 'max:100'],
'is_active' => ['nullable', 'boolean'],
]);
$bank_account->update([
...$data,
'is_active' => $request->boolean('is_active', false),
]);
return redirect()->route('admin.bank-accounts.index')->with('success', 'Rekening bank berhasil diperbarui.');
}
public function destroy(BankAccount $bank_account)
{
$bank_account->delete();
return back()->with('success', 'Rekening bank berhasil dihapus.');
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Booking;
use Illuminate\Http\Request;
class BookingController extends Controller
{
public function index()
{
$bookings = Booking::with(['user', 'place'])->latest()->get();
return view('admin.bookings.index', compact('bookings'));
}
public function show(int $id)
{
$booking = Booking::with(['user', 'place'])->findOrFail($id);
return view('admin.bookings.show', compact('booking'));
}
public function confirm(int $id)
{
$booking = Booking::findOrFail($id);
$booking->update([
'status' => 'confirmed',
'confirmed_at' => now(),
'confirmed_by' => auth()->id(),
'rejection_note' => null,
]);
return back()->with('success', 'Booking berhasil dikonfirmasi.');
}
public function reject(Request $request, int $id)
{
$data = $request->validate([
'rejection_note' => ['required', 'string'],
]);
$booking = Booking::findOrFail($id);
$booking->update([
'status' => 'rejected',
'confirmed_by' => auth()->id(),
'rejection_note' => $data['rejection_note'],
]);
return back()->with('success', 'Booking ditolak.');
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Booking;
use App\Models\TicketOrderItem;
use Illuminate\Http\Request;
class CheckController extends Controller
{
public function ticketIndex()
{
return view('admin.check.ticket');
}
public function checkTicket(Request $request)
{
$data = $request->validate([
'ticket_code' => ['required', 'string', 'max:20'],
'mark_used' => ['nullable', 'boolean'],
]);
$item = TicketOrderItem::with(['ticket', 'order.user'])
->where('ticket_code', strtoupper(trim($data['ticket_code'])))
->first();
if (! $item) {
return back()->with('error', 'Kode tiket tidak ditemukan.');
}
if ($request->boolean('mark_used') && ! $item->is_used) {
$item->update([
'is_used' => true,
'used_at' => now(),
]);
}
return back()->with('ticketItem', $item)->with('success', 'Pengecekan tiket berhasil.');
}
public function bookingIndex()
{
return view('admin.check.booking');
}
public function checkBooking(Request $request)
{
$data = $request->validate([
'booking_code' => ['required', 'string', 'max:20'],
'action' => ['nullable', 'in:confirm,reject'],
'rejection_note' => ['nullable', 'string'],
]);
$booking = Booking::with(['user', 'place'])
->where('booking_code', strtoupper(trim($data['booking_code'])))
->first();
if (! $booking) {
return back()->with('error', 'Kode booking tidak ditemukan.');
}
if (($data['action'] ?? null) === 'confirm') {
$booking->update([
'status' => 'confirmed',
'confirmed_at' => now(),
'confirmed_by' => auth()->id(),
'rejection_note' => null,
]);
}
if (($data['action'] ?? null) === 'reject') {
$booking->update([
'status' => 'rejected',
'confirmed_by' => auth()->id(),
'rejection_note' => $data['rejection_note'] ?? 'Ditolak oleh admin.',
]);
}
return back()->with('booking', $booking->fresh(['user', 'place']))->with('success', 'Pengecekan booking berhasil.');
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Booking;
use App\Models\TicketOrder;
use App\Models\TicketOrderItem;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
class DashboardController extends Controller
{
public function index()
{
$activeStatuses = ['pending', 'confirmed'];
$confirmedStatus = ['confirmed'];
$totalTicketsSold = TicketOrderItem::whereHas('order', function (Builder $query) use ($confirmedStatus) {
$query->whereIn('status', $confirmedStatus);
})->count();
$monthlyTicketSales = TicketOrder::whereIn('status', $confirmedStatus)
->whereYear('created_at', now()->year)
->whereMonth('created_at', now()->month)
->sum('total_price');
$monthlyBookings = Booking::whereIn('status', $confirmedStatus)
->whereYear('created_at', now()->year)
->whereMonth('created_at', now()->month)
->count();
$monthlyRevenue = $monthlyTicketSales + Booking::whereIn('status', $confirmedStatus)
->whereYear('created_at', now()->year)
->whereMonth('created_at', now()->month)
->sum('total_price');
$months = [];
$ticketData = [];
$bookingData = [];
for ($i = 11; $i >= 0; $i--) {
$date = Carbon::now()->subMonths($i);
$months[] = $date->translatedFormat('M Y');
$ticketData[] = TicketOrderItem::whereHas('order', function (Builder $query) use ($date, $confirmedStatus) {
$query->whereYear('visit_date', $date->year)
->whereMonth('visit_date', $date->month)
->whereIn('status', $confirmedStatus);
})->count();
$bookingData[] = Booking::whereYear('booking_date', $date->year)
->whereMonth('booking_date', $date->month)
->whereIn('status', $confirmedStatus)
->count();
}
$statusData = [
TicketOrder::where('status', 'pending')->count() + Booking::where('status', 'pending')->count(),
TicketOrder::where('status', 'confirmed')->count() + Booking::where('status', 'confirmed')->count(),
TicketOrder::where('status', 'rejected')->count() + Booking::where('status', 'rejected')->count(),
];
$hasTransactionData = array_sum($statusData) > 0;
return view('admin.dashboard', compact(
'totalTicketsSold',
'monthlyTicketSales',
'monthlyBookings',
'monthlyRevenue',
'months',
'ticketData',
'bookingData',
'statusData',
'hasTransactionData'
));
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class ProfileController extends Controller
{
public function index()
{
$user = auth()->user();
return view('admin.profile', compact('user'));
}
public function update(Request $request)
{
$user = $request->user();
$data = $request->validate([
'name' => ['required', 'string', 'max:100'],
'phone' => ['nullable', 'string', 'max:20'],
'address' => ['nullable', 'string'],
'password' => ['nullable', 'string', 'min:6', 'confirmed'],
]);
$payload = [
'name' => $data['name'],
'phone' => $data['phone'] ?? null,
'address' => $data['address'] ?? null,
];
if (! empty($data['password'])) {
$payload['password'] = Hash::make($data['password']);
}
$user->update($payload);
return back()->with('success', 'Profil admin berhasil diperbarui.');
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use Illuminate\Http\Request;
class TicketController extends Controller
{
public function index()
{
$tickets = Ticket::latest()->get();
return view('admin.tickets.index', compact('tickets'));
}
public function create()
{
return view('admin.tickets.create');
}
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:100'],
'description' => ['nullable', 'string'],
'price' => ['required', 'numeric', 'min:0'],
'quota_per_day' => ['required', 'integer', 'min:1'],
'is_active' => ['nullable', 'boolean'],
]);
Ticket::create([
...$data,
'is_active' => $request->boolean('is_active', true),
]);
return redirect()->route('admin.tickets.index')->with('success', 'Data tiket berhasil ditambah.');
}
public function edit(Ticket $ticket)
{
return view('admin.tickets.edit', compact('ticket'));
}
public function update(Request $request, Ticket $ticket)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:100'],
'description' => ['nullable', 'string'],
'price' => ['required', 'numeric', 'min:0'],
'quota_per_day' => ['required', 'integer', 'min:1'],
'is_active' => ['nullable', 'boolean'],
]);
$ticket->update([
...$data,
'is_active' => $request->boolean('is_active', false),
]);
return redirect()->route('admin.tickets.index')->with('success', 'Data tiket berhasil diperbarui.');
}
public function destroy(Ticket $ticket)
{
$ticket->delete();
return back()->with('success', 'Data tiket berhasil dihapus.');
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\TicketOrder;
use Illuminate\Http\Request;
class TicketOrderController extends Controller
{
public function index()
{
$orders = TicketOrder::with('user')->latest()->get();
return view('admin.ticket-orders.index', compact('orders'));
}
public function show(int $id)
{
$order = TicketOrder::with(['user', 'items.ticket'])->findOrFail($id);
return view('admin.ticket-orders.show', compact('order'));
}
public function confirm(int $id)
{
$order = TicketOrder::findOrFail($id);
$order->update([
'status' => 'confirmed',
'confirmed_at' => now(),
'confirmed_by' => auth()->id(),
'rejection_note' => null,
]);
return back()->with('success', 'Order tiket berhasil dikonfirmasi.');
}
public function reject(Request $request, int $id)
{
$data = $request->validate([
'rejection_note' => ['required', 'string'],
]);
$order = TicketOrder::findOrFail($id);
$order->update([
'status' => 'rejected',
'confirmed_by' => auth()->id(),
'rejection_note' => $data['rejection_note'],
]);
return back()->with('success', 'Order tiket ditolak.');
}
}

View File

@ -0,0 +1,141 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function showLogin()
{
return view('auth.login');
}
public function showRegister()
{
return view('auth.register');
}
public function login(Request $request)
{
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
if ($validator->fails()) {
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => false,
'message' => $validator->errors()->first()
], 422);
}
return back()->withErrors($validator)->withInput()
->with('error_title', 'Login Gagal')
->with('error', 'Silakan periksa kembali email atau password Anda.');
}
if (! Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => false,
'message' => 'Email atau password tidak valid.'
], 401);
}
return back()->withErrors(['email' => 'Email atau password tidak valid.'])->onlyInput('email')
->with('error_title', 'Login Gagal')
->with('error', 'Kredensial tidak valid.');
}
$request->session()->regenerate();
$user = $request->user();
if ($user->hasRole('admin')) {
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'message' => 'Login berhasil sebagai admin.',
'redirect_url' => route('admin.dashboard')
]);
}
return redirect()->route('admin.dashboard')->with('success', 'Login berhasil sebagai admin.');
}
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'message' => 'Login berhasil.',
'redirect_url' => route('home')
]);
}
return redirect()->route('home')->with('success', 'Login berhasil.');
}
public function register(Request $request)
{
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'max:150', 'unique:users,email'],
'phone' => ['nullable', 'string', 'max:20'],
'password' => ['required', 'string', 'min:6', 'confirmed'],
]);
if ($validator->fails()) {
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => false,
'message' => $validator->errors()->first()
], 422);
}
return back()->withErrors($validator)
->withInput()
->with('error_title', 'Registrasi Gagal')
->with('error', 'Silakan periksa kembali data Anda.');
}
try {
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'phone' => $request->phone ?? null,
'password' => Hash::make($request->password),
]);
$user->assignRole('user');
Auth::login($user);
$request->session()->regenerate();
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'message' => 'Registrasi berhasil. Selamat datang!',
'redirect_url' => route('home')
]);
}
return redirect()->route('home')->with('success', 'Registrasi berhasil. Selamat datang!');
} catch (\Exception $e) {
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan pada sistem.'
], 500);
}
return back()->withInput()
->with('error_title', 'Registrasi Gagal')
->with('error', 'Terjadi kesalahan pada sistem.');
}
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('home')->with('success', 'Anda berhasil logout.');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@ -0,0 +1,163 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\BankAccount;
use App\Models\Booking;
use App\Models\Place;
use Illuminate\Http\Request;
class BookingController extends Controller
{
private function resolvePendopo(): Place
{
$pendopo = Place::whereRaw('LOWER(name) = ?', ['pendopo'])
->where('is_active', true)
->first();
if (! $pendopo) {
$pendopo = Place::where('is_active', true)->orderBy('id')->firstOrFail();
}
return $pendopo;
}
public function index()
{
// Check if user is admin
if (auth()->user()->hasRole('admin')) {
return redirect()->route('admin.dashboard')
->with('swal_admin_restriction', true);
}
$place = $this->resolvePendopo();
return view('user.bookings.index', compact('place'));
}
public function store(Request $request)
{
// Check if user is admin
if (auth()->user()->hasRole('admin')) {
return redirect()->route('admin.dashboard')
->with('swal_admin_restriction', true);
}
$data = $request->validate([
'booking_date' => ['required', 'date', 'after_or_equal:today'],
'visitor_name' => ['required', 'string', 'max:100'],
'visitor_phone' => ['required', 'string', 'max:20'],
'visitor_address' => ['required', 'string'],
'notes' => ['nullable', 'string'],
]);
$place = $this->resolvePendopo();
$exists = Booking::where('place_id', $place->id)
->whereDate('booking_date', $data['booking_date'])
->exists();
if ($exists) {
return back()->withInput()->with('error', 'Tanggal sudah dibooking, silakan pilih tanggal lain.');
}
$booking = Booking::create([
'user_id' => $request->user()->id,
'place_id' => $place->id,
'booking_code' => generateUniqueCode('AB', Booking::class, 'booking_code'),
'booking_date' => $data['booking_date'],
'visitor_name' => $data['visitor_name'],
'visitor_phone' => $data['visitor_phone'],
'visitor_address' => $data['visitor_address'],
'num_persons' => 1,
'total_price' => $place->price_per_day,
'notes' => $data['notes'] ?? null,
'status' => 'pending',
]);
return redirect()->route('user.bookings.payment', $booking->booking_code)
->with('success', 'Booking berhasil dibuat. Silakan upload bukti transfer.');
}
public function payment(string $code)
{
$booking = Booking::with('place')
->where('booking_code', $code)
->where('user_id', auth()->id())
->firstOrFail();
$banks = BankAccount::where('is_active', true)->get();
return view('user.bookings.payment', compact('booking', 'banks'));
}
public function uploadProof(Request $request, string $code)
{
$booking = Booking::where('booking_code', $code)
->where('user_id', $request->user()->id)
->firstOrFail();
$data = $request->validate([
'payment_proof' => ['required', 'file', 'mimes:jpg,jpeg,png,pdf', 'max:5120'],
]);
$directory = storage_path('payment-proofs');
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
}
$file = $data['payment_proof'];
$filename = now()->format('YmdHis').'_'.uniqid('proof_', true).'.'.$file->getClientOriginalExtension();
$file->move($directory, $filename);
$path = 'payment-proofs/'.$filename;
$booking->update([
'payment_proof' => $path,
'status' => 'pending',
]);
return redirect()->route('user.bookings.status', $booking->booking_code)
->with('success', 'Bukti transfer booking berhasil diupload.');
}
public function status(string $code)
{
$booking = Booking::with('place')
->where('booking_code', $code)
->where('user_id', auth()->id())
->firstOrFail();
return view('user.bookings.status', compact('booking'));
}
public function checkDate(Request $request)
{
$data = $request->validate([
'booking_date' => ['required', 'date'],
]);
$place = $this->resolvePendopo();
$isBooked = Booking::where('place_id', $place->id)
->whereDate('booking_date', $data['booking_date'])
->exists();
return response()->json([
'available' => ! $isBooked,
'message' => $isBooked
? 'Tanggal ini sudah dibooking. Silakan pilih tanggal lain.'
: 'Tanggal tersedia.',
]);
}
public function history()
{
$bookings = Booking::with('place')
->where('user_id', auth()->id())
->orderBy('created_at', 'desc')
->paginate(10);
return view('user.bookings.history', compact('bookings'));
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class ProfileController extends Controller
{
public function index()
{
$user = auth()->user();
return view('user.profile', compact('user'));
}
public function update(Request $request)
{
$user = $request->user();
$data = $request->validate([
'name' => ['required', 'string', 'max:100'],
'phone' => ['nullable', 'string', 'max:20'],
'address' => ['nullable', 'string'],
'password' => ['nullable', 'string', 'min:6', 'confirmed'],
]);
$payload = [
'name' => $data['name'],
'phone' => $data['phone'] ?? null,
'address' => $data['address'] ?? null,
];
if (! empty($data['password'])) {
$payload['password'] = Hash::make($data['password']);
}
$user->update($payload);
return back()->with('success', 'Profil berhasil diperbarui.');
}
}

View File

@ -0,0 +1,188 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\BankAccount;
use App\Models\Ticket;
use App\Models\TicketOrder;
use App\Models\TicketOrderItem;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
class TicketController extends Controller
{
public function index()
{
// Check if user is admin
if (auth()->user()->hasRole('admin')) {
return redirect()->route('admin.dashboard')
->with('swal_admin_restriction', true);
}
$tickets = Ticket::query()
->where('is_active', true)
->where('name', 'Tiket Masuk')
->orderBy('id')
->limit(1)
->get();
if ($tickets->isEmpty()) {
$tickets = Ticket::query()->where('is_active', true)->orderBy('id')->limit(1)->get();
}
return view('user.tickets.index', compact('tickets'));
}
public function order(Request $request)
{
// Check if user is admin
if (auth()->user()->hasRole('admin')) {
return redirect()->route('admin.dashboard')
->with('swal_admin_restriction', true);
}
$data = $request->validate([
'visit_date' => ['required', 'date', 'after_or_equal:today'],
'items' => ['required', 'array', 'min:1'],
'items.*.ticket_id' => ['required', 'exists:tickets,id'],
'items.*.qty' => ['required', 'integer', 'min:1', 'max:20'],
]);
$ticketIds = collect($data['items'])->pluck('ticket_id')->all();
$tickets = Ticket::whereIn('id', $ticketIds)->where('is_active', true)->get()->keyBy('id');
if ($tickets->count() !== count($ticketIds)) {
return back()->with('error', 'Tiket tidak valid atau tidak aktif.');
}
$order = DB::transaction(function () use ($data, $tickets, $request) {
$totalQty = 0;
$totalPrice = 0;
foreach ($data['items'] as $item) {
$ticket = $tickets[$item['ticket_id']];
$totalQty += (int) $item['qty'];
$totalPrice += ((int) $item['qty'] * (float) $ticket->price);
}
$order = TicketOrder::create([
'user_id' => $request->user()->id,
'order_code' => generateUniqueCode('AT', TicketOrder::class, 'order_code'),
'visit_date' => $data['visit_date'],
'total_qty' => $totalQty,
'total_price' => $totalPrice,
'status' => 'pending',
]);
$qrDirectory = storage_path('qrcodes');
if (! is_dir($qrDirectory)) {
mkdir($qrDirectory, 0755, true);
}
foreach ($data['items'] as $item) {
$ticket = $tickets[$item['ticket_id']];
for ($i = 0; $i < (int) $item['qty']; $i++) {
$ticketCode = generateUniqueCode('AT', TicketOrderItem::class, 'ticket_code');
$qrPath = 'qrcodes/'.$ticketCode.'.svg';
$qrImage = QrCode::format('svg')->size(320)->generate($ticketCode);
file_put_contents(storage_path($qrPath), $qrImage);
TicketOrderItem::create([
'ticket_order_id' => $order->id,
'ticket_id' => $ticket->id,
'ticket_code' => $ticketCode,
'qr_code_path' => $qrPath,
'qty' => 1,
'price' => $ticket->price,
]);
}
}
return $order;
});
return redirect()->route('user.tickets.payment', $order->order_code)
->with('success', 'Pesanan tiket berhasil dibuat, silakan upload bukti bayar.');
}
public function payment(string $code)
{
$order = TicketOrder::with('items.ticket')
->where('order_code', $code)
->where('user_id', auth()->id())
->firstOrFail();
$banks = BankAccount::where('is_active', true)->get();
return view('user.tickets.payment', compact('order', 'banks'));
}
public function uploadProof(Request $request, string $code)
{
$order = TicketOrder::where('order_code', $code)
->where('user_id', $request->user()->id)
->firstOrFail();
$data = $request->validate([
'payment_proof' => ['required', 'file', 'mimes:jpg,jpeg,png,pdf', 'max:5120'],
]);
$directory = storage_path('payment-proofs');
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
}
$file = $data['payment_proof'];
$filename = now()->format('YmdHis').'_'.uniqid('proof_', true).'.'.$file->getClientOriginalExtension();
$file->move($directory, $filename);
$path = 'payment-proofs/'.$filename;
$order->update([
'payment_proof' => $path,
'status' => 'pending',
]);
return redirect()->route('user.tickets.download', $order->order_code)
->with('success', 'Bukti transfer berhasil diupload.');
}
public function download(string $code)
{
$order = TicketOrder::with('items.ticket')
->where('order_code', $code)
->where('user_id', auth()->id())
->firstOrFail();
return view('user.tickets.download', compact('order'));
}
public function downloadPdf(int $itemId)
{
$item = TicketOrderItem::with(['ticket', 'order.user'])
->where('id', $itemId)
->whereHas('order', fn ($q) => $q->where('user_id', auth()->id()))
->firstOrFail();
$order = $item->order;
$pdf = Pdf::loadView('pdf.ticket', compact('item', 'order'));
return $pdf->download('ticket-'.$item->ticket_code.'.pdf');
}
public function history()
{
$orders = TicketOrder::with(['items.ticket'])
->where('user_id', auth()->id())
->orderBy('created_at', 'desc')
->paginate(10);
return view('user.tickets.history', compact('orders'));
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class BankAccount extends Model
{
use HasFactory;
protected $fillable = [
'bank_name',
'account_number',
'account_name',
'is_active',
];
}

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

@ -0,0 +1,49 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Booking extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'place_id',
'booking_code',
'booking_date',
'visitor_name',
'visitor_phone',
'visitor_address',
'num_persons',
'total_price',
'payment_proof',
'status',
'confirmed_at',
'confirmed_by',
'rejection_note',
'notes',
];
protected $casts = [
'booking_date' => 'date',
'confirmed_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function place()
{
return $this->belongsTo(Place::class);
}
public function confirmer()
{
return $this->belongsTo(User::class, 'confirmed_by');
}
}

25
app/Models/Place.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Place extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
'capacity',
'price_per_day',
'image',
'is_active',
];
public function bookings()
{
return $this->hasMany(Booking::class);
}
}

16
app/Models/Setting.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
use HasFactory;
protected $fillable = [
'key',
'value',
];
}

24
app/Models/Ticket.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Ticket extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
'price',
'quota_per_day',
'is_active',
];
public function items()
{
return $this->hasMany(TicketOrderItem::class);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TicketOrder extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'order_code',
'visit_date',
'total_qty',
'total_price',
'payment_proof',
'status',
'confirmed_at',
'confirmed_by',
'rejection_note',
];
protected $casts = [
'visit_date' => 'date',
'confirmed_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function items()
{
return $this->hasMany(TicketOrderItem::class);
}
public function confirmer()
{
return $this->belongsTo(User::class, 'confirmed_by');
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TicketOrderItem extends Model
{
use HasFactory;
protected $fillable = [
'ticket_order_id',
'ticket_id',
'ticket_code',
'qr_code_path',
'qty',
'price',
'is_used',
'used_at',
];
protected $casts = [
'is_used' => 'boolean',
'used_at' => 'datetime',
];
public function order()
{
return $this->belongsTo(TicketOrder::class, 'ticket_order_id');
}
public function ticket()
{
return $this->belongsTo(Ticket::class);
}
}

47
app/Models/User.php Normal file
View File

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasFactory;
use Notifiable;
use HasRoles;
protected $fillable = [
'name',
'email',
'password',
'phone',
'address',
'avatar',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function ticketOrders()
{
return $this->hasMany(TicketOrder::class);
}
public function bookings()
{
return $this->hasMany(Booking::class);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

26
app/Support/helpers.php Normal file
View File

@ -0,0 +1,26 @@
<?php
if (! function_exists('generateUniqueCode')) {
function generateUniqueCode(string $prefix, string $modelClass, string $column = 'order_code'): string
{
$characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
do {
$random = '';
for ($i = 0; $i < 5; $i++) {
$random .= $characters[random_int(0, strlen($characters) - 1)];
}
$code = strtoupper($prefix.'-'.$random);
} while ($modelClass::where($column, $code)->exists());
return $code;
}
}
if (! function_exists('storage_url')) {
function storage_url(string $path): string
{
return route('storage.show', ['path' => $path]);
}
}

18
artisan Normal file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

21
bootstrap/app.php Normal file
View File

@ -0,0 +1,21 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Spatie\Permission\Middleware\RoleMiddleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'role' => RoleMiddleware::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();

2
bootstrap/cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

5
bootstrap/providers.php Normal file
View File

@ -0,0 +1,5 @@
<?php
return [
App\Providers\AppServiceProvider::class,
];

95
composer.json Normal file
View File

@ -0,0 +1,95 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": [
"laravel",
"framework"
],
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"simplesoftwareio/simple-qrcode": "^4.2",
"spatie/laravel-permission": "^6.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^11.5.3"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
},
"files": [
"app/Support/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"npm run dev\" --names='server,queue,vite'"
],
"test": [
"@php artisan config:clear --ansi",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

9163
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
config/app.php Normal file
View File

@ -0,0 +1,23 @@
<?php
return [
'name' => env('APP_NAME', 'Banyu Biru'),
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false),
'url' => env('APP_URL', 'http://localhost'),
'timezone' => 'Asia/Jakarta',
'locale' => 'id',
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'id'),
'faker_locale' => env('APP_FAKER_LOCALE', 'id_ID'),
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];

115
config/auth.php Normal file
View File

@ -0,0 +1,115 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

117
config/cache.php Normal file
View File

@ -0,0 +1,117 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane",
| "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
];

183
config/database.php Normal file
View File

@ -0,0 +1,183 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];

80
config/filesystems.php Normal file
View File

@ -0,0 +1,80 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

132
config/logging.php Normal file
View File

@ -0,0 +1,132 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

118
config/mail.php Normal file
View File

@ -0,0 +1,118 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
];

129
config/queue.php Normal file
View File

@ -0,0 +1,129 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];

38
config/services.php Normal file
View File

@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];

217
config/session.php Normal file
View File

@ -0,0 +1,217 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain and all subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
];

406
database.md Normal file
View File

@ -0,0 +1,406 @@
# Dokumentasi Database - Sistem Tiket & Booking Banyu Biru
## Daftar Isi
1. [Overview](#overview)
2. [Diagram Relasi](#diagram-relasi)
3. [Tabel Users](#tabel-users)
4. [Tabel Tickets](#tabel-tickets)
5. [Tabel Ticket Orders](#tabel-ticket-orders)
6. [Tabel Ticket Order Items](#tabel-ticket-order-items)
7. [Tabel Places](#tabel-places)
8. [Tabel Bookings](#tabel-bookings)
9. [Tabel Bank Accounts](#tabel-bank-accounts)
10. [Tabel Settings](#tabel-settings)
11. [Tabel Permission (Spatie)](#tabel-permission-spatie)
---
## Overview
Database ini digunakan untuk sistem manajemen tiket masuk dan booking pendopo pada Pemandian Air Panas Banyu Biru Nganjuk. Sistem ini mendukung:
- Manajemen user dengan role (admin/user)
- Penjualan tiket masuk dengan QR code
- Booking tempat (pendopo)
- Verifikasi pembayaran
- Manajemen rekening bank
---
## Diagram Relasi
```
users (1) ----< (N) ticket_orders
users (1) ----< (N) bookings
users (1) ----< (N) ticket_orders (confirmed_by)
users (1) ----< (N) bookings (confirmed_by)
tickets (1) ----< (N) ticket_order_items
ticket_orders (1) ----< (N) ticket_order_items
places (1) ----< (N) bookings
```
---
## Tabel: users
**Deskripsi**: Menyimpan data pengguna sistem (admin dan user biasa)
### Struktur Kolom
| Kolom | Tipe | Nullable | Default | Keterangan |
|-------|------|----------|---------|------------|
| id | BIGINT UNSIGNED | NO | AUTO_INCREMENT | Primary Key |
| name | VARCHAR(100) | NO | - | Nama lengkap user |
| email | VARCHAR(150) | NO | - | Email (unique) |
| phone | VARCHAR(20) | YES | NULL | Nomor telepon |
| address | TEXT | YES | NULL | Alamat lengkap |
| avatar | VARCHAR(255) | YES | NULL | Path foto profil |
| email_verified_at | TIMESTAMP | YES | NULL | Waktu verifikasi email |
| password | VARCHAR(255) | NO | - | Password (hashed) |
| remember_token | VARCHAR(100) | YES | NULL | Token remember me |
| created_at | TIMESTAMP | YES | NULL | Waktu dibuat |
| updated_at | TIMESTAMP | YES | NULL | Waktu diupdate |
### Relasi
- **Has Many**: `ticket_orders` (sebagai pembeli)
- **Has Many**: `bookings` (sebagai pembooking)
- **Has Many**: `ticket_orders` (sebagai confirmer via `confirmed_by`)
- **Has Many**: `bookings` (sebagai confirmer via `confirmed_by`)
- **Has Many Through Spatie**: `roles` dan `permissions`
### Index
- PRIMARY KEY: `id`
- UNIQUE: `email`
---
## Tabel: tickets
**Deskripsi**: Menyimpan jenis-jenis tiket yang tersedia
### Struktur Kolom
| Kolom | Tipe | Nullable | Default | Keterangan |
|-------|------|----------|---------|------------|
| id | BIGINT UNSIGNED | NO | AUTO_INCREMENT | Primary Key |
| name | VARCHAR(100) | NO | - | Nama tiket (contoh: Tiket Masuk) |
| description | TEXT | YES | NULL | Deskripsi tiket |
| price | DECIMAL(10,2) | NO | - | Harga tiket |
| quota_per_day | INT | NO | 100 | Kuota per hari |
| is_active | BOOLEAN | NO | TRUE | Status aktif/nonaktif |
| created_at | TIMESTAMP | YES | NULL | Waktu dibuat |
| updated_at | TIMESTAMP | YES | NULL | Waktu diupdate |
### Relasi
- **Has Many**: `ticket_order_items`
### Index
- PRIMARY KEY: `id`
---
## Tabel: ticket_orders
**Deskripsi**: Menyimpan data pesanan tiket dari user
### Struktur Kolom
| Kolom | Tipe | Nullable | Default | Keterangan |
|-------|------|----------|---------|------------|
| id | BIGINT UNSIGNED | NO | AUTO_INCREMENT | Primary Key |
| user_id | BIGINT UNSIGNED | NO | - | Foreign Key ke users |
| order_code | VARCHAR(20) | NO | - | Kode order unik (AT-XXXXX) |
| visit_date | DATE | NO | - | Tanggal kunjungan |
| total_qty | INT | NO | - | Total jumlah tiket |
| total_price | DECIMAL(10,2) | NO | - | Total harga |
| payment_proof | VARCHAR(255) | YES | NULL | Path bukti pembayaran |
| status | ENUM | NO | 'pending' | Status: pending/confirmed/rejected |
| confirmed_at | TIMESTAMP | YES | NULL | Waktu dikonfirmasi |
| confirmed_by | BIGINT UNSIGNED | YES | NULL | Foreign Key ke users (admin) |
| rejection_note | TEXT | YES | NULL | Catatan penolakan |
| created_at | TIMESTAMP | YES | NULL | Waktu dibuat |
| updated_at | TIMESTAMP | YES | NULL | Waktu diupdate |
### Relasi
- **Belongs To**: `users` (via `user_id`)
- **Belongs To**: `users` (via `confirmed_by`)
- **Has Many**: `ticket_order_items`
### Index
- PRIMARY KEY: `id`
- UNIQUE: `order_code`
- FOREIGN KEY: `user_id` REFERENCES `users(id)` ON DELETE CASCADE
- FOREIGN KEY: `confirmed_by` REFERENCES `users(id)` ON DELETE SET NULL
---
## Tabel: ticket_order_items
**Deskripsi**: Menyimpan detail item tiket dalam setiap pesanan (1 item = 1 tiket fisik)
### Struktur Kolom
| Kolom | Tipe | Nullable | Default | Keterangan |
|-------|------|----------|---------|------------|
| id | BIGINT UNSIGNED | NO | AUTO_INCREMENT | Primary Key |
| ticket_order_id | BIGINT UNSIGNED | NO | - | Foreign Key ke ticket_orders |
| ticket_id | BIGINT UNSIGNED | NO | - | Foreign Key ke tickets |
| ticket_code | VARCHAR(20) | NO | - | Kode tiket unik (AT-XXXXX) |
| qr_code_path | VARCHAR(255) | NO | - | Path file QR code |
| qty | INT | NO | 1 | Quantity (selalu 1) |
| price | DECIMAL(10,2) | NO | - | Harga tiket saat dibeli |
| is_used | BOOLEAN | NO | FALSE | Status sudah dipakai/belum |
| used_at | TIMESTAMP | YES | NULL | Waktu digunakan |
| created_at | TIMESTAMP | YES | NULL | Waktu dibuat |
| updated_at | TIMESTAMP | YES | NULL | Waktu diupdate |
### Relasi
- **Belongs To**: `ticket_orders` (via `ticket_order_id`)
- **Belongs To**: `tickets` (via `ticket_id`)
### Index
- PRIMARY KEY: `id`
- UNIQUE: `ticket_code`
- FOREIGN KEY: `ticket_order_id` REFERENCES `ticket_orders(id)` ON DELETE CASCADE
- FOREIGN KEY: `ticket_id` REFERENCES `tickets(id)` ON DELETE CASCADE
---
## Tabel: places
**Deskripsi**: Menyimpan data tempat yang bisa dibooking (contoh: Pendopo)
### Struktur Kolom
| Kolom | Tipe | Nullable | Default | Keterangan |
|-------|------|----------|---------|------------|
| id | BIGINT UNSIGNED | NO | AUTO_INCREMENT | Primary Key |
| name | VARCHAR(100) | NO | - | Nama tempat |
| description | TEXT | YES | NULL | Deskripsi tempat |
| capacity | INT | NO | - | Kapasitas orang |
| price_per_day | DECIMAL(10,2) | NO | - | Harga per hari |
| image | VARCHAR(255) | YES | NULL | Path gambar tempat |
| is_active | BOOLEAN | NO | TRUE | Status aktif/nonaktif |
| created_at | TIMESTAMP | YES | NULL | Waktu dibuat |
| updated_at | TIMESTAMP | YES | NULL | Waktu diupdate |
### Relasi
- **Has Many**: `bookings`
### Index
- PRIMARY KEY: `id`
---
## Tabel: bookings
**Deskripsi**: Menyimpan data booking tempat dari user
### Struktur Kolom
| Kolom | Tipe | Nullable | Default | Keterangan |
|-------|------|----------|---------|------------|
| id | BIGINT UNSIGNED | NO | AUTO_INCREMENT | Primary Key |
| user_id | BIGINT UNSIGNED | NO | - | Foreign Key ke users |
| place_id | BIGINT UNSIGNED | NO | - | Foreign Key ke places |
| booking_code | VARCHAR(20) | NO | - | Kode booking unik (AB-XXXXX) |
| booking_date | DATE | NO | - | Tanggal booking |
| visitor_name | VARCHAR(100) | NO | - | Nama pengunjung |
| visitor_phone | VARCHAR(20) | NO | - | Nomor telepon pengunjung |
| visitor_address | TEXT | NO | - | Alamat pengunjung |
| num_persons | INT | NO | - | Jumlah orang |
| total_price | DECIMAL(10,2) | NO | - | Total harga |
| payment_proof | VARCHAR(255) | YES | NULL | Path bukti pembayaran |
| status | ENUM | NO | 'pending' | Status: pending/confirmed/rejected |
| confirmed_at | TIMESTAMP | YES | NULL | Waktu dikonfirmasi |
| confirmed_by | BIGINT UNSIGNED | YES | NULL | Foreign Key ke users (admin) |
| rejection_note | TEXT | YES | NULL | Catatan penolakan |
| notes | TEXT | YES | NULL | Catatan tambahan |
| created_at | TIMESTAMP | YES | NULL | Waktu dibuat |
| updated_at | TIMESTAMP | YES | NULL | Waktu diupdate |
### Relasi
- **Belongs To**: `users` (via `user_id`)
- **Belongs To**: `places` (via `place_id`)
- **Belongs To**: `users` (via `confirmed_by`)
### Index
- PRIMARY KEY: `id`
- UNIQUE: `booking_code`
- UNIQUE: `place_id, booking_date` (composite - 1 tempat hanya bisa dibooking 1x per hari)
- FOREIGN KEY: `user_id` REFERENCES `users(id)` ON DELETE CASCADE
- FOREIGN KEY: `place_id` REFERENCES `places(id)` ON DELETE CASCADE
- FOREIGN KEY: `confirmed_by` REFERENCES `users(id)` ON DELETE SET NULL
---
## Tabel: bank_accounts
**Deskripsi**: Menyimpan data rekening bank untuk pembayaran
### Struktur Kolom
| Kolom | Tipe | Nullable | Default | Keterangan |
|-------|------|----------|---------|------------|
| id | BIGINT UNSIGNED | NO | AUTO_INCREMENT | Primary Key |
| bank_name | VARCHAR(100) | NO | - | Nama bank |
| account_number | VARCHAR(50) | NO | - | Nomor rekening |
| account_name | VARCHAR(100) | NO | - | Nama pemilik rekening |
| is_active | BOOLEAN | NO | TRUE | Status aktif/nonaktif |
| created_at | TIMESTAMP | YES | NULL | Waktu dibuat |
| updated_at | TIMESTAMP | YES | NULL | Waktu diupdate |
### Relasi
- Tidak ada relasi langsung (standalone table)
### Index
- PRIMARY KEY: `id`
---
## Tabel: settings
**Deskripsi**: Menyimpan pengaturan sistem (key-value pairs)
### Struktur Kolom
| Kolom | Tipe | Nullable | Default | Keterangan |
|-------|------|----------|---------|------------|
| id | BIGINT UNSIGNED | NO | AUTO_INCREMENT | Primary Key |
| key | VARCHAR(100) | NO | - | Key pengaturan (unique) |
| value | TEXT | YES | NULL | Value pengaturan |
| created_at | TIMESTAMP | YES | NULL | Waktu dibuat |
| updated_at | TIMESTAMP | YES | NULL | Waktu diupdate |
### Relasi
- Tidak ada relasi langsung (standalone table)
### Index
- PRIMARY KEY: `id`
- UNIQUE: `key`
---
## Tabel Permission (Spatie)
**Deskripsi**: Tabel dari package Spatie Laravel Permission untuk manajemen role dan permission
### Tabel yang Digunakan
1. **roles** - Menyimpan role (admin, user)
2. **permissions** - Menyimpan permission
3. **model_has_roles** - Relasi many-to-many antara model (users) dan roles
4. **model_has_permissions** - Relasi many-to-many antara model dan permissions
5. **role_has_permissions** - Relasi many-to-many antara roles dan permissions
### Relasi dengan Users
- User dapat memiliki banyak roles (many-to-many via `model_has_roles`)
- User dapat memiliki banyak permissions langsung (many-to-many via `model_has_permissions`)
- Role dapat memiliki banyak permissions (many-to-many via `role_has_permissions`)
---
## Status Enum Values
### ticket_orders.status & bookings.status
- `pending` - Menunggu verifikasi pembayaran
- `confirmed` - Pembayaran sudah dikonfirmasi admin
- `rejected` - Pembayaran ditolak admin
---
## Kode Unik
### Format Kode
- **Ticket Order**: `AT-XXXXX` (AT = Admission Ticket)
- **Ticket Item**: `AT-XXXXX` (setiap item punya kode unik)
- **Booking**: `AB-XXXXX` (AB = Admission Booking)
Kode dihasilkan menggunakan helper function `generateUniqueCode()` yang memastikan keunikan.
---
## Catatan Penting
1. **Cascade Delete**:
- Jika user dihapus, semua ticket_orders dan bookings miliknya ikut terhapus
- Jika ticket_order dihapus, semua ticket_order_items ikut terhapus
2. **Set Null on Delete**:
- Jika admin yang mengkonfirmasi dihapus, field `confirmed_by` di-set NULL
3. **Unique Constraints**:
- 1 tempat hanya bisa dibooking 1x per tanggal (`place_id, booking_date`)
- Setiap kode order/booking/tiket harus unik
4. **QR Code**:
- Setiap ticket_order_item memiliki QR code unik
- QR code disimpan sebagai file SVG di folder `public/qrcodes/`
- QR code berisi `ticket_code` untuk verifikasi
5. **Payment Proof**:
- Bukti pembayaran disimpan di folder `public/payment-proofs/`
- Format: `YYYYMMDDHHMMSS_proof_uniqid.ext`
---
## Query Contoh
### Mendapatkan semua tiket yang sudah dikonfirmasi bulan ini
```sql
SELECT * FROM ticket_orders
WHERE status = 'confirmed'
AND YEAR(created_at) = YEAR(CURDATE())
AND MONTH(created_at) = MONTH(CURDATE());
```
### Mendapatkan total pendapatan bulan ini
```sql
SELECT
(SELECT COALESCE(SUM(total_price), 0) FROM ticket_orders
WHERE status = 'confirmed'
AND YEAR(created_at) = YEAR(CURDATE())
AND MONTH(created_at) = MONTH(CURDATE()))
+
(SELECT COALESCE(SUM(total_price), 0) FROM bookings
WHERE status = 'confirmed'
AND YEAR(created_at) = YEAR(CURDATE())
AND MONTH(created_at) = MONTH(CURDATE()))
AS total_revenue;
```
### Cek ketersediaan booking untuk tanggal tertentu
```sql
SELECT * FROM bookings
WHERE place_id = ?
AND booking_date = '2026-02-25';
```
---
**Dibuat**: 25 Februari 2026
**Versi**: 1.0
**Sistem**: Banyu Biru Ticketing & Booking System

1
database/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.sqlite*

View File

@ -0,0 +1,44 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

View File

@ -0,0 +1,46 @@
<?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('users', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->string('email', 150)->unique();
$table->string('phone', 20)->nullable();
$table->text('address')->nullable();
$table->string('avatar')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
public function down(): void
{
Schema::dropIfExists('sessions');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('users');
}
};

View File

@ -0,0 +1,35 @@
<?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('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@ -0,0 +1,57 @@
<?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('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@ -0,0 +1,62 @@
<?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('permissions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create('roles', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create('model_has_permissions', function (Blueprint $table) {
$table->unsignedBigInteger('permission_id');
$table->string('model_type');
$table->unsignedBigInteger('model_id');
$table->index(['model_id', 'model_type']);
$table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade');
$table->primary(['permission_id', 'model_id', 'model_type']);
});
Schema::create('model_has_roles', function (Blueprint $table) {
$table->unsignedBigInteger('role_id');
$table->string('model_type');
$table->unsignedBigInteger('model_id');
$table->index(['model_id', 'model_type']);
$table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
$table->primary(['role_id', 'model_id', 'model_type']);
});
Schema::create('role_has_permissions', function (Blueprint $table) {
$table->unsignedBigInteger('permission_id');
$table->unsignedBigInteger('role_id');
$table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade');
$table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
$table->primary(['permission_id', 'role_id']);
});
}
public function down(): void
{
Schema::dropIfExists('role_has_permissions');
Schema::dropIfExists('model_has_roles');
Schema::dropIfExists('model_has_permissions');
Schema::dropIfExists('roles');
Schema::dropIfExists('permissions');
}
};

View File

@ -0,0 +1,109 @@
<?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('tickets', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->text('description')->nullable();
$table->decimal('price', 10, 2);
$table->integer('quota_per_day')->default(100);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('ticket_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('order_code', 20)->unique();
$table->date('visit_date');
$table->integer('total_qty');
$table->decimal('total_price', 10, 2);
$table->string('payment_proof')->nullable();
$table->enum('status', ['pending', 'confirmed', 'rejected'])->default('pending');
$table->timestamp('confirmed_at')->nullable();
$table->foreignId('confirmed_by')->nullable()->constrained('users')->nullOnDelete();
$table->text('rejection_note')->nullable();
$table->timestamps();
});
Schema::create('ticket_order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('ticket_order_id')->constrained('ticket_orders')->cascadeOnDelete();
$table->foreignId('ticket_id')->constrained('tickets')->cascadeOnDelete();
$table->string('ticket_code', 20)->unique();
$table->string('qr_code_path');
$table->integer('qty')->default(1);
$table->decimal('price', 10, 2);
$table->boolean('is_used')->default(false);
$table->timestamp('used_at')->nullable();
$table->timestamps();
});
Schema::create('places', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->text('description')->nullable();
$table->integer('capacity');
$table->decimal('price_per_day', 10, 2);
$table->string('image')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('bookings', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('place_id')->constrained('places')->cascadeOnDelete();
$table->string('booking_code', 20)->unique();
$table->date('booking_date');
$table->string('visitor_name', 100);
$table->string('visitor_phone', 20);
$table->text('visitor_address');
$table->integer('num_persons');
$table->decimal('total_price', 10, 2);
$table->string('payment_proof')->nullable();
$table->enum('status', ['pending', 'confirmed', 'rejected'])->default('pending');
$table->timestamp('confirmed_at')->nullable();
$table->foreignId('confirmed_by')->nullable()->constrained('users')->nullOnDelete();
$table->text('rejection_note')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->unique(['place_id', 'booking_date']);
});
Schema::create('bank_accounts', function (Blueprint $table) {
$table->id();
$table->string('bank_name', 100);
$table->string('account_number', 50);
$table->string('account_name', 100);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->string('key', 100)->unique();
$table->text('value')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('settings');
Schema::dropIfExists('bank_accounts');
Schema::dropIfExists('bookings');
Schema::dropIfExists('places');
Schema::dropIfExists('ticket_order_items');
Schema::dropIfExists('ticket_orders');
Schema::dropIfExists('tickets');
}
};

View File

@ -0,0 +1,42 @@
<?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::table('users', function (Blueprint $table) {
if (! Schema::hasColumn('users', 'phone')) {
$table->string('phone', 20)->nullable()->after('email');
}
if (! Schema::hasColumn('users', 'address')) {
$table->text('address')->nullable()->after('phone');
}
if (! Schema::hasColumn('users', 'avatar')) {
$table->string('avatar')->nullable()->after('address');
}
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
if (Schema::hasColumn('users', 'avatar')) {
$table->dropColumn('avatar');
}
if (Schema::hasColumn('users', 'address')) {
$table->dropColumn('address');
}
if (Schema::hasColumn('users', 'phone')) {
$table->dropColumn('phone');
}
});
}
};

View File

@ -0,0 +1,66 @@
<?php
namespace Database\Seeders;
use App\Models\BankAccount;
use App\Models\Place;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Spatie\Permission\Models\Role;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
Role::firstOrCreate(['name' => 'user', 'guard_name' => 'web']);
$admin = User::firstOrCreate(
['email' => 'admin@gmail.com'],
[
'name' => 'Administrator',
'password' => Hash::make('password'),
]
);
$admin->assignRole('admin');
$mainTicket = Ticket::updateOrCreate(
['name' => 'Tiket Masuk'],
[
'price' => 5000,
'quota_per_day' => 500,
'description' => 'Tiket masuk umum Banyu Biru (Rp 5.000 per orang)',
'is_active' => 1,
]
);
Ticket::where('id', '!=', $mainTicket->id)->update(['is_active' => 0]);
$pendopo = Place::updateOrCreate(
['name' => 'Pendopo'],
[
'price_per_day' => 500000,
'capacity' => 50,
'description' => 'Tempat acara keluarga',
'is_active' => 1,
]
);
Place::where('id', '!=', $pendopo->id)->update(['is_active' => 0]);
BankAccount::firstOrCreate(
['account_number' => '1234567890'],
[
'bank_name' => 'BRI',
'account_name' => 'Banyu Biru Nganjuk',
'is_active' => 1,
]
);
$this->call([
PenggunaSeeder::class,
PenggunaTiketSeeder::class,
PenggunaBookingSeeder::class,
]);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Database\Seeders;
use App\Models\Booking;
use App\Models\Place;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
class PenggunaBookingSeeder extends Seeder
{
public function run(): void
{
$place = Place::where('name', 'Pendopo')->first();
$admin = User::where('email', 'admin@banyubiru.com')->first();
$users = User::role('user')->orderBy('id')->get();
if (! $place || $users->isEmpty()) {
return;
}
$statuses = ['pending', 'confirmed', 'rejected'];
for ($i = 1; $i <= 14; $i++) {
$bookingDate = Carbon::now()->subMonths($i % 10)->addDays(($i * 3) % 22 + 1)->toDateString();
$status = $statuses[$i % 3];
$bookingCode = 'AB-DEMO-'.str_pad((string) $i, 3, '0', STR_PAD_LEFT);
Booking::updateOrCreate(
['booking_code' => $bookingCode],
[
'user_id' => $users[$i % $users->count()]->id,
'place_id' => $place->id,
'booking_date' => $bookingDate,
'visitor_name' => 'Pengunjung Demo '.$i,
'visitor_phone' => '081245000'.str_pad((string) $i, 2, '0', STR_PAD_LEFT),
'visitor_address' => 'Nganjuk, Jawa Timur',
'num_persons' => 10 + ($i % 20),
'total_price' => $place->price_per_day,
'status' => $status,
'confirmed_at' => $status === 'confirmed' ? now()->subDays($i) : null,
'confirmed_by' => $status === 'confirmed' && $admin ? $admin->id : null,
'rejection_note' => $status === 'rejected' ? 'Jadwal bentrok dengan reservasi lain.' : null,
'notes' => 'Booking seeded untuk dashboard demo.',
]
);
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class PenggunaSeeder extends Seeder
{
public function run(): void
{
$names = [
'Ahmad Fauzi',
'Siti Rahayu',
'Budi Santoso',
'Dewi Lestari',
'Rudi Hermawan',
'Fitri Amalia',
'Joko Pramono',
'Nur Hidayah'
];
$emails = [
'ahmad.fauzi@gmail.com',
'siti.rahayu@gmail.com',
'budi.santoso@gmail.com',
'dewi.lestari@gmail.com',
'rudi.hermawan@gmail.com',
'fitri.amalia@gmail.com',
'joko.pramono@gmail.com',
'nur.hidayah@gmail.com'
];
for ($i = 0; $i < 8; $i++) {
$user = User::updateOrCreate(
['email' => $emails[$i]],
[
'name' => $names[$i],
'phone' => '081230000'.str_pad((string) ($i+1), 2, '0', STR_PAD_LEFT),
'address' => 'Nganjuk, Jawa Timur',
'password' => Hash::make('password'),
]
);
if (! $user->hasRole('user')) {
$user->assignRole('user');
}
}
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Database\Seeders;
use App\Models\Ticket;
use App\Models\TicketOrder;
use App\Models\TicketOrderItem;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
class PenggunaTiketSeeder extends Seeder
{
public function run(): void
{
$ticket = Ticket::where('name', 'Tiket Masuk')->first();
$admin = User::where('email', 'admin@banyubiru.com')->first();
$users = User::role('user')->orderBy('id')->get();
if (! $ticket || $users->isEmpty()) {
return;
}
$statuses = ['pending', 'confirmed', 'rejected'];
for ($i = 1; $i <= 18; $i++) {
$visitDate = Carbon::now()->subMonths($i % 12)->addDays(($i * 2) % 20 + 1)->toDateString();
$qty = ($i % 4) + 1;
$status = $statuses[$i % 3];
$orderCode = 'AT-DEMO-'.str_pad((string) $i, 3, '0', STR_PAD_LEFT);
$order = TicketOrder::updateOrCreate(
['order_code' => $orderCode],
[
'user_id' => $users[$i % $users->count()]->id,
'visit_date' => $visitDate,
'total_qty' => $qty,
'total_price' => $qty * (float) $ticket->price,
'status' => $status,
'confirmed_at' => $status === 'confirmed' ? now()->subDays($i) : null,
'confirmed_by' => $status === 'confirmed' && $admin ? $admin->id : null,
'rejection_note' => $status === 'rejected' ? 'Data pembayaran tidak valid.' : null,
]
);
TicketOrderItem::where('ticket_order_id', $order->id)->delete();
for ($j = 1; $j <= $qty; $j++) {
$ticketCode = 'AT-ITM-'.str_pad((string) $i, 3, '0', STR_PAD_LEFT).str_pad((string) $j, 2, '0', STR_PAD_LEFT);
TicketOrderItem::create([
'ticket_order_id' => $order->id,
'ticket_id' => $ticket->id,
'ticket_code' => $ticketCode,
'qr_code_path' => 'qrcodes/'.$ticketCode.'.svg',
'qty' => 1,
'price' => $ticket->price,
'is_used' => false,
]);
}
}
}
}

210
flowcharts/README.md Normal file
View File

@ -0,0 +1,210 @@
# Flowchart Dokumentasi - Banyu Biru Ticketing & Booking System
Dokumentasi flowchart lengkap untuk setiap fitur dalam sistem. Setiap file berisi flowchart detail yang dapat digunakan untuk membuat diagram di Draw.io.
## 📋 Daftar Flowchart
### 1. Autentikasi
**File:** [auth-flowchart.md](auth-flowchart.md)
Fitur yang didokumentasikan:
- Login (User & Admin)
- Register (User Baru)
- Logout (dengan konfirmasi SweetAlert)
---
### 2. Pembelian Tiket
**File:** [ticket-purchase-flowchart.md](ticket-purchase-flowchart.md)
Fitur yang didokumentasikan:
- Lihat Daftar Tiket
- Beli Tiket (dengan generate QR code)
- Upload Bukti Pembayaran
- Download Tiket (berdasarkan status)
---
### 3. Booking Pendopo
**File:** [booking-flowchart.md](booking-flowchart.md)
Fitur yang didokumentasikan:
- Lihat Halaman Booking
- Cek Ketersediaan Tanggal (AJAX)
- Buat Booking
- Upload Bukti Pembayaran Booking
- Lihat Status Booking
---
### 4. Verifikasi Pembayaran Admin
**File:** [payment-verification-flowchart.md](payment-verification-flowchart.md)
Fitur yang didokumentasikan:
- Lihat Daftar Order Tiket
- Lihat Detail Order Tiket
- Konfirmasi Pembayaran Tiket
- Tolak Pembayaran Tiket
- Verifikasi Pembayaran Booking
---
### 5. QR Scanner Verifikasi Tiket
**File:** [qr-scanner-flowchart.md](qr-scanner-flowchart.md)
Fitur yang didokumentasikan:
- Scan QR Code Tiket (menggunakan kamera)
- Input Manual Kode Tiket
- Verifikasi Tiket
- Tandai Tiket Sebagai Dipakai
---
### 6. Manajemen Tiket Admin
**File:** [ticket-management-flowchart.md](ticket-management-flowchart.md)
Fitur yang didokumentasikan:
- Lihat Daftar Tiket
- Tambah Tiket Baru
- Edit Tiket
- Hapus Tiket
- Toggle Status Aktif Tiket
---
### 7. Manajemen Admin
**File:** [admin-management-flowchart.md](admin-management-flowchart.md)
Fitur yang didokumentasikan:
- Lihat Daftar Admin
- Tambah Admin Baru
- Edit Admin
- Hapus Admin
- Edit Profil Admin
---
### 8. Dashboard & Statistik
**File:** [dashboard-flowchart.md](dashboard-flowchart.md)
Fitur yang didokumentasikan:
- Tampilkan Dashboard Admin
- Hitung Total Tiket Terjual
- Hitung Penjualan Bulan Ini
- Hitung Total Pendapatan
- Generate Chart Penjualan
- Tampilkan Order Terbaru
- Tampilkan Booking Terbaru
---
### 9. Manajemen Rekening Bank
**File:** [bank-account-flowchart.md](bank-account-flowchart.md)
Fitur yang didokumentasikan:
- Lihat Daftar Rekening Bank
- Tambah Rekening Bank
- Edit Rekening Bank
- Hapus Rekening Bank
- Toggle Status Aktif Rekening
---
### 10. Riwayat User
**File:** [user-history-flowchart.md](user-history-flowchart.md)
Fitur yang didokumentasikan:
- Lihat Riwayat Tiket
- Lihat Riwayat Booking
- Lihat Detail Tiket dari Riwayat
- Lihat Detail Booking dari Riwayat
---
### 11. Profil User
**File:** [user-profile-flowchart.md](user-profile-flowchart.md)
Fitur yang didokumentasikan:
- Lihat Profil User
- Edit Profil User
- Ubah Password User
---
## 🎨 Panduan Umum Draw.io
### Simbol Flowchart Standar
- **Terminator (Oval)** - START/END
- **Process (Rectangle)** - Proses/aksi
- **Decision (Diamond)** - Kondisi/percabangan
- **Data (Parallelogram)** - Input/Output
- **Database (Cylinder)** - Operasi database
- **Document (Rectangle with wavy bottom)** - Form/dokumen
- **Loop (Hexagon)** - Perulangan
- **Predefined Process (Rectangle with double lines)** - Subprocess
### Palet Warna Konsisten
```
START/END: #14B8A6 (Teal)
Process: #E3F2FD (Biru Muda)
Decision: #FFF59D (Kuning)
Database Operation: #E1BEE7 (Ungu Muda)
Form: #BBDEFB (Biru Tua Muda)
Create: #C8E6C9 (Hijau Muda)
Update: #B3E5FC (Biru Muda)
Delete: #FFCDD2 (Merah Muda)
Success: #C8E6C9 (Hijau Muda)
Error: #FFCDD2 (Merah Muda)
Warning: #FFF9C4 (Kuning Muda)
Loop: #FFE0B2 (Oranye Muda)
Security Check: #FFE0B2 (Oranye Muda)
```
### Tips Membuat Diagram
1. **Gunakan Swimlane** untuk memisahkan actor (User, Admin, System, Database)
2. **Konsisten dengan warna** untuk memudahkan identifikasi jenis proses
3. **Label yang jelas** pada setiap decision branch (YES/NO)
4. **Spacing yang rapi** antar simbol untuk readability
5. **Connector yang jelas** dengan arrow yang tepat
6. **Notes/Comments** untuk penjelasan kompleks
7. **Vertical flow** (top to bottom) untuk kemudahan membaca
### Langkah Membuat di Draw.io
1. Buka [draw.io](https://app.diagrams.net/)
2. Pilih "Create New Diagram"
3. Pilih template "Flowchart" atau "Blank"
4. Drag & drop simbol dari panel kiri
5. Gunakan connector tool untuk menghubungkan simbol
6. Atur warna sesuai panduan di atas
7. Export sebagai PNG/PDF/SVG
---
## 📊 Statistik Dokumentasi
- **Total Flowchart Files**: 11
- **Total Fitur Terdokumentasi**: 50+
- **Format**: Markdown dengan ASCII flowchart
- **Kompatibilitas**: Draw.io, Lucidchart, Visio
---
## 🔄 Update History
- **25 Februari 2026**: Pembuatan dokumentasi lengkap untuk semua fitur sistem
---
## 📝 Catatan
- Setiap flowchart sudah disesuaikan dengan implementasi aktual di kode
- Status order/booking: `pending`, `confirmed`, `rejected`
- Kode unik format: `AT-XXXXX` (tiket), `AB-XXXXX` (booking)
- Semua flowchart sudah termasuk validasi dan error handling
- Security checks sudah didokumentasikan (password hashing, authorization, dll)
---
**Sistem**: Banyu Biru Ticketing & Booking System
**Dibuat**: 25 Februari 2026
**Versi**: 1.0

View File

@ -0,0 +1,376 @@
# Flowchart - Fitur Manajemen Admin
## Daftar Isi
1. [Lihat Daftar Admin](#1-lihat-daftar-admin)
2. [Tambah Admin Baru](#2-tambah-admin-baru)
3. [Edit Admin](#3-edit-admin)
4. [Hapus Admin](#4-hapus-admin)
5. [Edit Profil Admin](#5-edit-profil-admin)
---
## 1. Lihat Daftar Admin
### Deskripsi
Flowchart untuk admin melihat daftar semua admin.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Admin mengakses menu "Admin"]
[Query database: ambil users dengan role 'admin']
[Urutkan berdasarkan nama]
[Tampilkan tabel admin:]
- Nama
- Email
- Tanggal Dibuat
- Aksi (Edit, Hapus)
[Tampilkan tombol "Tambah Admin"]
END
```
---
## 2. Tambah Admin Baru
### Deskripsi
Flowchart untuk menambah admin baru ke sistem.
### Flowchart
```
START
[Admin di halaman daftar admin]
[Admin klik tombol "Tambah Admin"]
[Redirect ke halaman form tambah admin]
[Tampilkan form:]
- Nama
- Email
- Password
- Konfirmasi Password
[Admin mengisi form]
[Admin klik tombol "Simpan"]
<Validasi input>
{Nama terisi?}
├─ NO → [Error: "Nama wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Format email valid?}
├─ NO → [Error: "Format email tidak valid"] → [Kembali ke form] → END
└─ YES ↓
[Query database: cek email sudah terdaftar]
{Email sudah ada?}
├─ YES → [Error: "Email sudah terdaftar"] → [Kembali ke form] → END
└─ NO ↓
{Password minimal 8 karakter?}
├─ NO → [Error: "Password minimal 8 karakter"] → [Kembali ke form] → END
└─ YES ↓
{Password = Konfirmasi Password?}
├─ NO → [Error: "Konfirmasi password tidak cocok"] → [Kembali ke form] → END
└─ YES ↓
[Hash password menggunakan bcrypt]
[Simpan data ke tabel users:]
- name
- email
- password (hashed)
[Assign role 'admin' ke user baru]
[Tampilkan notifikasi sukses:]
"Admin berhasil ditambahkan"
[Redirect ke halaman daftar admin]
END
```
---
## 3. Edit Admin
### Deskripsi
Flowchart untuk mengedit data admin.
### Flowchart
```
START
[Admin di halaman daftar admin]
[Admin klik tombol "Edit" pada admin]
[Query database: ambil user berdasarkan ID]
{User ditemukan?}
├─ NO → [Error 404: Admin tidak ditemukan] → END
└─ YES ↓
{User memiliki role 'admin'?}
├─ NO → [Error: "User bukan admin"] → END
└─ YES ↓
[Redirect ke halaman form edit admin]
[Tampilkan form dengan data admin:]
- Nama (terisi)
- Email (terisi)
- Password (kosong, opsional)
- Konfirmasi Password (kosong, opsional)
[Admin mengubah data]
[Admin klik tombol "Update"]
<Validasi input>
{Nama terisi?}
├─ NO → [Error: "Nama wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Format email valid?}
├─ NO → [Error: "Format email tidak valid"] → [Kembali ke form] → END
└─ YES ↓
[Query: cek email sudah digunakan user lain]
{Email digunakan user lain?}
├─ YES → [Error: "Email sudah terdaftar"] → [Kembali ke form] → END
└─ NO ↓
{Password diisi?}
├─ YES → {Password minimal 8 karakter?}
│ ├─ NO → [Error: "Password minimal 8 karakter"] → [Kembali ke form] → END
│ └─ YES ↓
│ ↓
│ {Password = Konfirmasi Password?}
│ ├─ NO → [Error: "Konfirmasi password tidak cocok"] → [Kembali ke form] → END
│ └─ YES ↓
│ ↓
│ [Hash password baru]
│ ↓
│ [Update dengan password baru]
└─ NO → [Update tanpa mengubah password]
[Update tabel users:]
- name
- email
- password (jika diisi)
[Simpan perubahan ke database]
[Tampilkan notifikasi sukses:]
"Admin berhasil diupdate"
[Redirect ke halaman daftar admin]
END
```
---
## 4. Hapus Admin
### Deskripsi
Flowchart untuk menghapus admin dari sistem.
### Flowchart
```
START
[Admin di halaman daftar admin]
[Admin klik tombol "Hapus" pada admin]
[Tampilkan konfirmasi SweetAlert:]
"Yakin ingin menghapus admin ini?"
{Admin konfirmasi?}
├─ NO (Batal) → [Tutup dialog] → END
└─ YES ↓
[Query database: ambil user berdasarkan ID]
{User ditemukan?}
├─ NO → [Error: "Admin tidak ditemukan"] → END
└─ YES ↓
{User adalah admin yang sedang login?}
├─ YES → [Error: "Tidak dapat menghapus akun sendiri"]
│ ↓
│ END
└─ NO ↓
[Cek apakah admin pernah melakukan verifikasi]
[Query: cek ticket_orders dengan confirmed_by = admin_id]
[Query: cek bookings dengan confirmed_by = admin_id]
{Admin pernah verifikasi transaksi?}
├─ YES → [Error: "Admin tidak dapat dihapus karena ada riwayat verifikasi"]
│ ↓
│ [Sarankan: nonaktifkan akun saja]
│ ↓
│ END
└─ NO ↓
[Hapus role 'admin' dari user]
[Hapus user dari database]
[Tampilkan notifikasi sukses:]
"Admin berhasil dihapus"
[Refresh halaman daftar admin]
END
```
---
## 5. Edit Profil Admin
### Deskripsi
Flowchart untuk admin mengedit profil sendiri.
### Flowchart
```
START
[Admin mengakses menu "Profil"]
[Query database: ambil data admin yang login]
[Tampilkan halaman profil]
[Tampilkan form dengan data admin:]
- Nama (terisi)
- Email (terisi)
- Nomor Telepon (terisi/kosong)
- Alamat (terisi/kosong)
- Password Baru (kosong, opsional)
- Konfirmasi Password (kosong, opsional)
[Admin mengubah data]
[Admin klik tombol "Update Profil"]
<Validasi input>
{Nama terisi?}
├─ NO → [Error: "Nama wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Format email valid?}
├─ NO → [Error: "Format email tidak valid"] → [Kembali ke form] → END
└─ YES ↓
[Query: cek email sudah digunakan user lain]
{Email digunakan user lain?}
├─ YES → [Error: "Email sudah terdaftar"] → [Kembali ke form] → END
└─ NO ↓
{Password baru diisi?}
├─ YES → {Password minimal 8 karakter?}
│ ├─ NO → [Error: "Password minimal 8 karakter"] → [Kembali ke form] → END
│ └─ YES ↓
│ ↓
│ {Password = Konfirmasi Password?}
│ ├─ NO → [Error: "Konfirmasi password tidak cocok"] → [Kembali ke form] → END
│ └─ YES ↓
│ ↓
│ [Hash password baru]
│ ↓
│ [Update dengan password baru]
└─ NO → [Update tanpa mengubah password]
[Update tabel users:]
- name
- email
- phone (jika diisi)
- address (jika diisi)
- password (jika diisi)
[Simpan perubahan ke database]
[Update session dengan data baru]
[Tampilkan notifikasi sukses:]
"Profil berhasil diupdate"
[Refresh halaman profil]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Database** (Cylinder) - Operasi database
6. **Document** (Rectangle with wavy bottom) - Form
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **Form**: Biru tua muda (#BBDEFB)
- **Create**: Hijau muda (#C8E6C9)
- **Update**: Biru muda (#B3E5FC)
- **Delete**: Merah muda (#FFCDD2)
- **Security Check**: Oranye muda (#FFE0B2)
### Tips
- Gunakan swimlane untuk memisahkan Admin dan System
- Tandai operasi CRUD dengan warna berbeda
- Gunakan connector yang jelas untuk decision branch
- Tambahkan notes untuk security checks penting
- Highlight proses hashing password
---
**Dibuat**: 25 Februari 2026
**Fitur**: Manajemen Admin
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,203 @@
# Flowchart - Fitur Autentikasi
## Daftar Isi
1. [Login](#1-login)
2. [Register](#2-register)
3. [Logout](#3-logout)
---
## 1. Login
### Deskripsi
Flowchart untuk proses login user dan admin ke sistem.
### Flowchart
```
START
[User mengakses halaman login]
[Tampilkan form login]
[User input email dan password]
[User klik tombol Login]
<Validasi format email>
{Email valid?}
├─ NO → [Tampilkan error: "Format email salah"] → [Kembali ke form] → END
└─ YES ↓
<Validasi password tidak kosong>
{Password terisi?}
├─ NO → [Tampilkan error: "Password wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
[Query database: cari user berdasarkan email]
{User ditemukan?}
├─ NO → [Tampilkan error: "Email tidak terdaftar"] → [Kembali ke form] → END
└─ YES ↓
[Ambil data user dari database]
[Verifikasi password dengan hash]
{Password cocok?}
├─ NO → [Tampilkan error: "Password salah"] → [Kembali ke form] → END
└─ YES ↓
[Buat session untuk user]
[Simpan data user ke session]
[Cek role user]
{Role = Admin?}
├─ YES → [Redirect ke /admin/dashboard]
│ ↓
│ [Tampilkan dashboard admin]
│ ↓
│ END
└─ NO (User) → [Redirect ke /]
[Tampilkan halaman beranda]
END
```
### Simbol yang Digunakan
- `[ ]` = Proses/Action
- `{ }` = Decision/Kondisi
- `< >` = Subprocess
- `→` = Flow direction
- `↓` = Flow continuation
---
## 2. Register
### Deskripsi
Flowchart untuk proses registrasi user baru.
### Flowchart
```
START
[User mengakses halaman register]
[Tampilkan form registrasi]
[User input: nama, email, password, password confirmation]
[User klik tombol Register]
<Validasi input>
{Nama terisi?}
├─ NO → [Error: "Nama wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Format email valid?}
├─ NO → [Error: "Format email salah"] → [Kembali ke form] → END
└─ YES ↓
[Query database: cek email sudah terdaftar]
{Email sudah ada?}
├─ YES → [Error: "Email sudah terdaftar"] → [Kembali ke form] → END
└─ NO ↓
{Password minimal 8 karakter?}
├─ NO → [Error: "Password minimal 8 karakter"] → [Kembali ke form] → END
└─ YES ↓
{Password = Password Confirmation?}
├─ NO → [Error: "Konfirmasi password tidak cocok"] → [Kembali ke form] → END
└─ YES ↓
[Hash password menggunakan bcrypt]
[Simpan data user ke database]
[Assign role "user" ke user baru]
[Buat session untuk user]
[Tampilkan notifikasi sukses]
[Redirect ke halaman beranda]
END
```
---
## 3. Logout
### Deskripsi
Flowchart untuk proses logout user/admin dari sistem.
### Flowchart
```
START
[User klik tombol Logout]
[Tampilkan konfirmasi SweetAlert]
{User konfirmasi logout?}
├─ NO (Batal) → [Tutup dialog] → END
└─ YES ↓
[Tampilkan loading "Logging out..."]
[Hapus session user]
[Hapus remember token]
[Clear authentication]
[Redirect ke halaman login]
[Tampilkan pesan "Berhasil logout"]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Arrow** - Alur/flow
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Error**: Merah muda (#FFCDD2)
- **Success**: Hijau muda (#C8E6C9)
### Layout
- Gunakan vertical flow (top to bottom)
- Spacing konsisten antar simbol
- Label jelas pada setiap decision branch
- Gunakan connector yang rapi
---
**Dibuat**: 25 Februari 2026
**Fitur**: Autentikasi (Login, Register, Logout)
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,293 @@
# Flowchart - Fitur Manajemen Rekening Bank
## Daftar Isi
1. [Lihat Daftar Rekening Bank](#1-lihat-daftar-rekening-bank)
2. [Tambah Rekening Bank](#2-tambah-rekening-bank)
3. [Edit Rekening Bank](#3-edit-rekening-bank)
4. [Hapus Rekening Bank](#4-hapus-rekening-bank)
5. [Toggle Status Aktif Rekening](#5-toggle-status-aktif-rekening)
---
## 1. Lihat Daftar Rekening Bank
### Deskripsi
Flowchart untuk admin melihat daftar rekening bank.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Admin mengakses menu "Rekening Bank"]
[Query database: ambil semua bank_accounts]
[Urutkan berdasarkan ID]
[Tampilkan tabel rekening:]
- Nama Bank
- Nomor Rekening
- Atas Nama
- Status (Aktif/Nonaktif)
- Aksi (Edit, Hapus)
[Tampilkan tombol "Tambah Rekening"]
END
```
---
## 2. Tambah Rekening Bank
### Deskripsi
Flowchart untuk admin menambah rekening bank baru.
### Flowchart
```
START
[Admin di halaman daftar rekening]
[Admin klik tombol "Tambah Rekening"]
[Redirect ke halaman form tambah rekening]
[Tampilkan form:]
- Nama Bank
- Nomor Rekening
- Atas Nama
- Status Aktif (checkbox)
[Admin mengisi form]
[Admin klik tombol "Simpan"]
<Validasi input>
{Nama bank terisi?}
├─ NO → [Error: "Nama bank wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Nomor rekening terisi?}
├─ NO → [Error: "Nomor rekening wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Nomor rekening hanya angka?}
├─ NO → [Error: "Nomor rekening harus angka"] → [Kembali ke form] → END
└─ YES ↓
{Atas nama terisi?}
├─ NO → [Error: "Atas nama wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
[Simpan data ke tabel bank_accounts:]
- bank_name
- account_number
- account_holder
- is_active (default: true)
[Tampilkan notifikasi sukses:]
"Rekening bank berhasil ditambahkan"
[Redirect ke halaman daftar rekening]
END
```
---
## 3. Edit Rekening Bank
### Deskripsi
Flowchart untuk admin mengedit data rekening bank.
### Flowchart
```
START
[Admin di halaman daftar rekening]
[Admin klik tombol "Edit" pada rekening]
[Query database: ambil bank_account berdasarkan ID]
{Rekening ditemukan?}
├─ NO → [Error 404: Rekening tidak ditemukan] → END
└─ YES ↓
[Redirect ke halaman form edit rekening]
[Tampilkan form dengan data rekening:]
- Nama Bank (terisi)
- Nomor Rekening (terisi)
- Atas Nama (terisi)
- Status Aktif (checked/unchecked)
[Admin mengubah data]
[Admin klik tombol "Update"]
<Validasi input>
{Nama bank terisi?}
├─ NO → [Error: "Nama bank wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Nomor rekening terisi?}
├─ NO → [Error: "Nomor rekening wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Nomor rekening hanya angka?}
├─ NO → [Error: "Nomor rekening harus angka"] → [Kembali ke form] → END
└─ YES ↓
{Atas nama terisi?}
├─ NO → [Error: "Atas nama wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
[Update tabel bank_accounts:]
- bank_name
- account_number
- account_holder
- is_active
[Simpan perubahan ke database]
[Tampilkan notifikasi sukses:]
"Rekening bank berhasil diupdate"
[Redirect ke halaman daftar rekening]
END
```
---
## 4. Hapus Rekening Bank
### Deskripsi
Flowchart untuk admin menghapus rekening bank.
### Flowchart
```
START
[Admin di halaman daftar rekening]
[Admin klik tombol "Hapus" pada rekening]
[Tampilkan konfirmasi SweetAlert:]
"Yakin ingin menghapus rekening ini?"
{Admin konfirmasi?}
├─ NO (Batal) → [Tutup dialog] → END
└─ YES ↓
[Query database: ambil bank_account berdasarkan ID]
{Rekening ditemukan?}
├─ NO → [Error: "Rekening tidak ditemukan"] → END
└─ YES ↓
[Hapus rekening dari database]
[Tampilkan notifikasi sukses:]
"Rekening bank berhasil dihapus"
[Refresh halaman daftar rekening]
END
```
---
## 5. Toggle Status Aktif Rekening
### Deskripsi
Flowchart untuk mengaktifkan/menonaktifkan rekening bank.
### Flowchart
```
START
[Admin di halaman edit rekening]
[Tampilkan checkbox "Status Aktif"]
{Rekening saat ini aktif?}
├─ YES → [Checkbox checked]
└─ NO → [Checkbox unchecked]
[Admin toggle checkbox]
[Admin klik tombol "Update"]
[Update tabel bank_accounts:]
- is_active = nilai checkbox (true/false)
{is_active = true?}
├─ YES → [Rekening muncul di halaman pembayaran user]
│ ↓
│ [User dapat transfer ke rekening ini]
│ ↓
│ [Notifikasi: "Rekening diaktifkan"]
└─ NO → [Rekening tidak muncul di halaman pembayaran]
[User tidak dapat transfer ke rekening ini]
[Notifikasi: "Rekening dinonaktifkan"]
[Redirect ke halaman daftar rekening]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Database** (Cylinder) - Operasi database
6. **Document** (Rectangle with wavy bottom) - Form
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **Form**: Biru tua muda (#BBDEFB)
- **Create**: Hijau muda (#C8E6C9)
- **Update**: Biru muda (#B3E5FC)
- **Delete**: Merah muda (#FFCDD2)
- **Toggle**: Oranye muda (#FFE0B2)
### Tips
- Gunakan swimlane untuk memisahkan Admin dan System
- Tandai operasi CRUD dengan warna berbeda
- Gunakan connector yang jelas untuk decision branch
- Tambahkan notes untuk validasi nomor rekening
---
**Dibuat**: 25 Februari 2026
**Fitur**: Manajemen Rekening Bank
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,344 @@
# Flowchart - Fitur Booking Pendopo
## Daftar Isi
1. [Lihat Halaman Booking](#1-lihat-halaman-booking)
2. [Cek Ketersediaan Tanggal](#2-cek-ketersediaan-tanggal)
3. [Buat Booking](#3-buat-booking)
4. [Upload Bukti Pembayaran Booking](#4-upload-bukti-pembayaran-booking)
5. [Lihat Status Booking](#5-lihat-status-booking)
---
## 1. Lihat Halaman Booking
### Deskripsi
Flowchart untuk mengakses halaman booking pendopo.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ YES → [Tampilkan SweetAlert: "Admin tidak dapat melakukan booking"]
│ ↓
│ [Redirect ke dashboard admin]
│ ↓
│ END
└─ NO ↓
[Query database: cari place dengan name = 'pendopo']
{Place 'pendopo' ditemukan?}
├─ NO → [Query: ambil place aktif pertama]
│ ↓
│ [Gunakan place tersebut]
└─ YES ↓
[Ambil data place pendopo]
[Tampilkan halaman booking]
[Tampilkan informasi pendopo:]
- Nama tempat
- Harga per hari
- Deskripsi
[Tampilkan form booking]
END
```
---
## 2. Cek Ketersediaan Tanggal
### Deskripsi
Flowchart untuk mengecek apakah tanggal sudah dibooking atau masih tersedia.
### Flowchart
```
START
[User memilih tanggal booking]
[Trigger event onchange pada input tanggal]
[Kirim AJAX request ke server]
[Server: validasi format tanggal]
{Format tanggal valid?}
├─ NO → [Return error: "Format tanggal tidak valid"] → END
└─ YES ↓
[Query database: cari booking dengan tanggal tersebut]
[Query: WHERE place_id = pendopo_id]
[Query: AND booking_date = tanggal_dipilih]
{Booking ditemukan?}
├─ YES → [Return JSON: available = false]
│ ↓
│ [Tampilkan pesan: "Tanggal sudah dibooking"]
│ ↓
│ [Disable tombol submit]
│ ↓
│ [Tampilkan alert merah]
│ ↓
│ END
└─ NO → [Return JSON: available = true]
[Tampilkan pesan: "Tanggal tersedia"]
[Enable tombol submit]
[Tampilkan alert hijau]
END
```
---
## 3. Buat Booking
### Deskripsi
Flowchart untuk proses pembuatan booking pendopo.
### Flowchart
```
START
[User di halaman booking]
[User mengisi form:]
- Tanggal booking
- Nama pengunjung
- Nomor telepon
- Alamat
- Catatan (opsional)
[User klik tombol "Booking Sekarang"]
<Validasi input>
{Tanggal >= hari ini?}
├─ NO → [Error: "Tanggal harus hari ini atau setelahnya"] → [Kembali ke form] → END
└─ YES ↓
{Nama pengunjung terisi?}
├─ NO → [Error: "Nama wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Nomor telepon terisi?}
├─ NO → [Error: "Nomor telepon wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Alamat terisi?}
├─ NO → [Error: "Alamat wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
[Query database: cek tanggal sudah dibooking]
{Tanggal sudah dibooking?}
├─ YES → [Error: "Tanggal sudah dibooking, pilih tanggal lain"]
│ ↓
│ [Kembali ke form dengan input tersimpan]
│ ↓
│ END
└─ NO ↓
[Generate kode booking unik (AB-XXXXX)]
[Hitung total harga = price_per_day dari place]
[Simpan data ke tabel bookings:]
- user_id = user yang login
- place_id = pendopo_id
- booking_code = AB-XXXXX
- booking_date = tanggal dipilih
- visitor_name
- visitor_phone
- visitor_address
- num_persons = 1
- total_price
- notes
- status = 'pending'
[Tampilkan notifikasi sukses]
[Redirect ke halaman pembayaran]
END
```
---
## 4. Upload Bukti Pembayaran Booking
### Deskripsi
Flowchart untuk upload bukti transfer pembayaran booking.
### Flowchart
```
START
[User di halaman pembayaran booking]
[Tampilkan informasi booking:]
- Kode booking
- Tanggal booking
- Nama pengunjung
- Total harga
[Query database: ambil daftar rekening bank aktif]
[Tampilkan nomor rekening bank]
[User melakukan transfer ke rekening]
[User klik "Upload Bukti Pembayaran"]
[User pilih file bukti transfer]
<Validasi file>
{File dipilih?}
├─ NO → [Error: "File wajib dipilih"] → [Kembali ke form] → END
└─ YES ↓
{Format file valid (jpg, jpeg, png, pdf)?}
├─ NO → [Error: "Format file tidak valid"] → [Kembali ke form] → END
└─ YES ↓
{Ukuran file <= 5MB?}
├─ NO → [Error: "Ukuran file terlalu besar"] → [Kembali ke form] → END
└─ YES ↓
[Generate nama file unik:]
Format: YYYYMMDDHHMMSS_uniqid.ext
[Simpan file ke /public/payment-proofs/]
[Update tabel bookings:]
- payment_proof = path file
- status tetap 'pending'
[Tampilkan notifikasi sukses]
[Redirect ke halaman status booking]
END
```
---
## 5. Lihat Status Booking
### Deskripsi
Flowchart untuk melihat status booking setelah upload bukti pembayaran.
### Flowchart
```
START
[User mengakses halaman status booking]
[Query database: ambil booking berdasarkan booking_code]
{Booking ditemukan?}
├─ NO → [Error 404: Booking tidak ditemukan] → END
└─ YES ↓
{Booking milik user yang login?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Ambil data booking dengan relasi place]
[Cek status booking]
{Status = 'confirmed'?}
├─ YES → [Tampilkan halaman status]
│ ↓
│ [Tampilkan alert sukses:]
│ "Pembayaran berhasil diverifikasi"
│ ↓
│ [Tampilkan informasi booking:]
│ - Kode booking
│ - Status: Confirmed
│ - Tanggal booking
│ - Nama pengunjung
│ - Total harga
│ ↓
│ [Tampilkan tombol "Kembali ke Beranda"]
│ ↓
│ END
├─ Status = 'pending' → [Tampilkan halaman status]
│ ↓
│ [Tampilkan alert warning:]
│ "Menunggu verifikasi admin"
│ ↓
│ [Tampilkan informasi booking]
│ ↓
│ [Tampilkan pesan:]
│ "Bukti pembayaran sedang diverifikasi"
│ ↓
│ END
└─ Status = 'rejected' → [Tampilkan halaman status]
[Tampilkan alert danger:]
"Pembayaran ditolak"
[Tampilkan alasan penolakan]
[Tampilkan tombol "Upload Ulang Bukti"]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Database** (Cylinder) - Operasi database
6. **Document** (Rectangle with wavy bottom) - File/dokumen
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **File Operation**: Oranye muda (#FFE0B2)
- **Error**: Merah muda (#FFCDD2)
- **Success**: Hijau muda (#C8E6C9)
- **Warning**: Kuning muda (#FFF9C4)
### Tips
- Gunakan swimlane untuk memisahkan User, System, dan Database
- Tandai proses AJAX dengan warna berbeda
- Gunakan connector yang jelas untuk decision branch
- Tambahkan notes untuk validasi kompleks
---
**Dibuat**: 25 Februari 2026
**Fitur**: Booking Pendopo
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,401 @@
# Flowchart - Fitur Dashboard & Statistik
## Daftar Isi
1. [Tampilkan Dashboard Admin](#1-tampilkan-dashboard-admin)
2. [Hitung Total Tiket Terjual](#2-hitung-total-tiket-terjual)
3. [Hitung Penjualan Bulan Ini](#3-hitung-penjualan-bulan-ini)
4. [Hitung Total Pendapatan](#4-hitung-total-pendapatan)
5. [Generate Chart Penjualan](#5-generate-chart-penjualan)
---
## 1. Tampilkan Dashboard Admin
### Deskripsi
Flowchart untuk menampilkan dashboard admin dengan statistik.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Admin mengakses dashboard]
[Inisialisasi variabel:]
- confirmedStatus = ['confirmed']
- currentMonth = bulan ini
- currentYear = tahun ini
[Hitung Total Tiket Terjual]
[Lihat flowchart #2]
[Hitung Penjualan Bulan Ini]
[Lihat flowchart #3]
[Hitung Total Pendapatan]
[Lihat flowchart #4]
[Generate Chart Penjualan]
[Lihat flowchart #5]
[Query: ambil 5 order tiket terbaru]
[Query: ambil 5 booking terbaru]
[Tampilkan dashboard dengan:]
- 4 Card statistik
- Chart penjualan
- Tabel order terbaru
- Tabel booking terbaru
END
```
---
## 2. Hitung Total Tiket Terjual
### Deskripsi
Flowchart untuk menghitung total tiket yang sudah dikonfirmasi.
### Flowchart
```
START
[Query database: ticket_orders]
[Filter: status = 'confirmed']
[Sum: total_qty dari semua order confirmed]
[Simpan hasil ke variabel: totalTicketsSold]
[Tampilkan di card:]
- Judul: "Total Tiket Terjual"
- Nilai: totalTicketsSold
- Icon: fas fa-ticket-alt
- Warna: Teal
- Keterangan: "Sudah dikonfirmasi"
END
```
---
## 3. Hitung Penjualan Bulan Ini
### Deskripsi
Flowchart untuk menghitung penjualan tiket dan booking bulan ini.
### Flowchart
```
START
[Ambil bulan dan tahun saat ini]
[Query database: ticket_orders]
[Filter: status = 'confirmed']
[Filter: MONTH(created_at) = bulan ini]
[Filter: YEAR(created_at) = tahun ini]
[Sum: total_qty dari hasil filter]
[Simpan ke: monthlyTicketSales]
[Query database: bookings]
[Filter: status = 'confirmed']
[Filter: MONTH(created_at) = bulan ini]
[Filter: YEAR(created_at) = tahun ini]
[Count: jumlah booking]
[Simpan ke: monthlyBookings]
[Tampilkan di card:]
- Judul: "Penjualan Bulan Ini"
- Nilai: monthlyTicketSales + " tiket, " + monthlyBookings + " booking"
- Icon: fas fa-chart-line
- Warna: Biru
- Keterangan: "Hanya yang terkonfirmasi"
END
```
---
## 4. Hitung Total Pendapatan
### Deskripsi
Flowchart untuk menghitung total pendapatan dari tiket dan booking yang dikonfirmasi.
### Flowchart
```
START
[Query database: ticket_orders]
[Filter: status = 'confirmed']
[Sum: total_price dari semua order confirmed]
[Simpan ke: ticketRevenue]
[Query database: bookings]
[Filter: status = 'confirmed']
[Sum: total_price dari semua booking confirmed]
[Simpan ke: bookingRevenue]
[Hitung: monthlyRevenue = ticketRevenue + bookingRevenue]
[Format: Rp X.XXX.XXX]
[Tampilkan di card:]
- Judul: "Total Pendapatan"
- Nilai: monthlyRevenue (formatted)
- Icon: fas fa-money-bill-wave
- Warna: Hijau
- Keterangan: "Tiket + Booking terkonfirmasi"
END
```
---
## 5. Generate Chart Penjualan
### Deskripsi
Flowchart untuk membuat chart penjualan 6 bulan terakhir.
### Flowchart
```
START
[Inisialisasi array kosong:]
- labels = []
- ticketData = []
- bookingData = []
[Loop: untuk 6 bulan terakhir]
┌─────────────────────────┐
│ [Hitung bulan dan tahun untuk iterasi ke-i]
│ ↓
│ [Format nama bulan: "Jan 2026"]
│ ↓
│ [Tambahkan ke labels array]
│ ↓
│ [Query: ticket_orders confirmed untuk bulan ini]
│ ↓
│ [Sum: total_qty]
│ ↓
│ [Tambahkan ke ticketData array]
│ ↓
│ [Query: bookings confirmed untuk bulan ini]
│ ↓
│ [Count: jumlah booking]
│ ↓
│ [Tambahkan ke bookingData array]
│ ↓
│ [Ulangi untuk bulan berikutnya]
└─────────────────────────┘
[Kirim data ke view:]
- chartLabels = labels (JSON)
- chartTicketData = ticketData (JSON)
- chartBookingData = bookingData (JSON)
[Render chart menggunakan Chart.js]
[Tampilkan line chart dengan:]
- X-axis: Bulan
- Y-axis: Jumlah
- Line 1: Penjualan Tiket (Teal)
- Line 2: Booking (Biru)
- Legend: Aktif
- Tooltip: Aktif
END
```
---
## 6. Tampilkan Order Terbaru
### Deskripsi
Flowchart untuk menampilkan 5 order tiket terbaru.
### Flowchart
```
START
[Query database: ticket_orders]
[Include relasi: user]
[Urutkan: created_at DESC]
[Limit: 5 data]
[Loop untuk setiap order:]
┌─────────────────────────┐
│ [Ambil data order:]
│ - Kode order
│ - Nama user
│ - Total qty
│ - Total harga
│ - Status
│ - Tanggal order
│ ↓
│ [Format tanggal: d M Y]
│ ↓
│ [Format harga: Rp X.XXX]
│ ↓
│ [Tentukan warna badge status:]
│ - pending: kuning
│ - confirmed: hijau
│ - rejected: merah
│ ↓
│ [Tampilkan row di tabel]
└─────────────────────────┘
[Tampilkan tabel di dashboard]
[Tampilkan link "Lihat Semua"]
END
```
---
## 7. Tampilkan Booking Terbaru
### Deskripsi
Flowchart untuk menampilkan 5 booking terbaru.
### Flowchart
```
START
[Query database: bookings]
[Include relasi: user, place]
[Urutkan: created_at DESC]
[Limit: 5 data]
[Loop untuk setiap booking:]
┌─────────────────────────┐
│ [Ambil data booking:]
│ - Kode booking
│ - Nama user
│ - Nama tempat
│ - Tanggal booking
│ - Total harga
│ - Status
│ ↓
│ [Format tanggal: d M Y]
│ ↓
│ [Format harga: Rp X.XXX]
│ ↓
│ [Tentukan warna badge status:]
│ - pending: kuning
│ - confirmed: hijau
│ - rejected: merah
│ ↓
│ [Tampilkan row di tabel]
└─────────────────────────┘
[Tampilkan tabel di dashboard]
[Tampilkan link "Lihat Semua"]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Database** (Cylinder) - Operasi database
6. **Loop** (Hexagon) - Perulangan
7. **Predefined Process** (Rectangle with double lines) - Subprocess
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **Calculation**: Hijau muda (#C8E6C9)
- **Chart/Display**: Biru tua muda (#BBDEFB)
- **Loop**: Oranye muda (#FFE0B2)
- **Aggregate**: Kuning muda (#FFF9C4)
### Tips
- Gunakan swimlane untuk memisahkan Data Processing dan Display
- Tandai proses agregasi (SUM, COUNT) dengan warna khusus
- Gunakan loop symbol untuk iterasi 6 bulan
- Tambahkan notes untuk formula perhitungan
- Highlight filter status 'confirmed' yang penting
### Layout Suggestion
```
┌─────────────────────────────────────────┐
│ Dashboard Controller │
├─────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ │
│ │ Tiket │ │ Booking │ │
│ │ Terjual │ │ Bulan │ │
│ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Penjualan│ │ Total │ │
│ │ Bulan │ │ Pendapatan│ │
│ └──────────┘ └──────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Chart Penjualan 6 Bulan │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Order │ │ Booking │ │
│ │ Terbaru │ │ Terbaru │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────┘
```
---
**Dibuat**: 25 Februari 2026
**Fitur**: Dashboard & Statistik
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,504 @@
# Flowchart - Full Sistem Banyu Biru (SATU KESELURUHAN)
```
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ BANYU BIRU TICKETING & BOOKING SYSTEM │
│ FULL SYSTEM UNIFIED FLOWCHART │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ START │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────┐ │
│ │ USER ACCESSES WEBSITE │ │
│ │ (Buka browser, ketik URL) │ │
│ └───────────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ {USER LOGGED IN?} │ │
│ │ (Cek Session) │ │
│ └─────────────┬──────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ │ │
│ NO YES │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────┐ │ ┌─────────────────────────────┐ │
│ │ SHOW HOME PAGE │ │ │ {CHECK ROLE} │ │
│ │ (Public Content) │ │ └─────────────┬─────────────┘ │
│ │ - Banner │ │ │ │
│ │ - Info Tempat │ │ ┌────────┴────────┐ │
│ │ - Layanan │ │ │ │ │
│ └────────────┬────────────┘ │ ADMIN USER │
│ │ │ │ │ │
│ ▼ │ ▼ ▼ │
│ ┌─────────────────────────┐ │ ┌─────────┐ ┌─────────────┐ │
│ │ USER CLICKS: │ │ │DASHBOARD│ │HOME PAGE │ │
│ │ - LOGIN │ │ │(Admin) │ │(User) │ │
│ │ - REGISTER │ │ └────┬────┘ └──────┬──────┘ │
│ │ - LIHAT TIKET │ │ │ │ │
│ │ - LIHAT BOOKING │ │ │ │ │
│ └────────────┬────────────┘ │ │ │ │
│ │ │ │ │ │
│ ▼ │ │ │ │
│ ┌─────────────────────────┐ │ ┌────┴────┐ ┌────┴──────┐ │
│ │ {ACTION SELECTED?} │ │ │ │ │ │ │
│ └────────────┬────────────┘ │ │ │ │ │ │
│ │ │ ▼ ▼ ▼ ▼ │
│ ┌────────────────────┼────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ │ │ │ │ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ │ │
│ │ LOGIN │ │ REGISTER │ │ LOGOUT │ │ │ │ │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ │ │ │ │ │
│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │ │ │ │
│ │ INPUT EMAIL & │ │ INPUT: NAME, │ │ DESTROY SESSION │ │ │ │ │ │
│ │ PASSWORD │ │ EMAIL, PASSWORD │ │ CLEAR AUTH │ │ │ │ │ │
│ └────────┬──────────┘ └────────┬──────────┘ └────────┬──────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ │ │ │ │ │
│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │ │ │ │
│ │ VALIDATE & │ │ VALIDATE & │ │ REDIRECT TO │ │ │ │ │ │
│ │ CHECK CREDENTIAL │ │ CHECK EMAIL │ │ HOME PAGE │ │ │ │ │ │
│ └────────┬──────────┘ └────────┬──────────┘ └───────────────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
│ ▼ ▼ │ │ │ │ │
│ ┌───────────────────┐ ┌───────────────────┐ │ │ │ │ │
│ │ {VALID?} │ │ {VALID?} │ │ │ │ │ │
│ └────────┬──────────┘ └────────┬──────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
│ ┌──────┴──────┐ ┌──────┴──────┐ │ │ │ │ │
│ NO YES NO YES │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │ │ │ │ │
│ ┌──────┐ ┌───────────────────┐ ┌──────┐ ┌───────────────────┐ │ │ │ │ │
│ │ERROR │ │ CREATE SESSION │ │ERROR │ │ HASH PASSWORD │ │ │ │ │ │
│ └──┬───┘ └────────┬──────────┘ └──┬───┘ └────────┬──────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ▼ │ ▼ │ │ │ │ │
│ │ ┌───────────────────┐ │ ┌───────────────────┐ │ │ │ │ │
│ │ │ {ROLE = ADMIN?} │ │ │ INSERT TO DB │ │ │ │ │ │
│ │ └────────┬──────────┘ │ │ CREATE SESSION │ │ │ │ │ │
│ │ │ │ └────────┬──────────┘ │ │ │ │ │
│ │ ┌──────┴──────┐ │ │ │ │ │ │ │
│ │ YES NO │ │ │ │ │ │ │
│ │ │ │ │ ▼ │ │ │ │ │
│ │ ▼ ▼ │ ┌───────────────────┐ │ │ │ │ │
│ │ ┌────────┐ ┌────────┐ │ │ REDIRECT TO │ │ │ │ │ │
│ │ │DASHBOARD│ │ HOME │ │ │ HOME │ │ │ │ │ │
│ │ └────────┘ └────────┘ │ └───────────────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────┴───────────────────────────────────┘ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ ADMIN MENU OPTIONS │ │
│ │ ┌──────┬──────┬──────┬──────┬────┐ │ │
│ │ │ Tiket│ Bank │ Order│Book │QR │ │ │
│ │ │ Mgmt │ Akun │ Verif│ing │Scan│ │ │
│ │ └──┬───┴──┬───┴──┬───┴────┴────┘ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ ▼ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐│ │
│ │ │CRUD │ │CRUD │ │Verif│ │Verif│ │Scan││ │
│ │ │Tiket│ │Bank │ │Tiket│ │Book │ │QR ││ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘│ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ └─────┼──────┼──────┼─────┼─────┼─────┘ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ USER MENU OPTIONS │ │
│ │ ┌──────┬──────┬──────┬──────┬────┐ │ │
│ │ │Beli │Upload│Downld│Book │Prof│ │ │
│ │ │Tiket│Bukti │Tiket│ing │ile │ │ │
│ │ └──┬───┴──┬───┴──┬───┴────┴────┘ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ ▼ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐│ │
│ │ │Buy │ │Upload│ │Print│ │Book │ │Edit ││ │
│ │ │Ticket│ │Proof │ │PDF │ │Date │ │Prof ││ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘│ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ └─────┼──────┼──────┼─────┼─────┼─────┘ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ SHARED DATABASE │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ users │ │tickets │ │places │ │ │
│ │ │bank_acc│ │ticket_ │ │bookings │ │ │
│ │ │ │ │orders │ │ │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
```
---
## UNIFIED FLOW - DETAIL
```
START
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ USER OPENS WEBSITE │
│ (URL: domain.com) │
└────────────────────────────────────────────────────────┬────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ {IS USER LOGGED IN?} │
│ (Check session/auth cookie) │
└──────────────────────────┬───────────────────────────────────────────────────────────────────────────────────┘
┌──────────────┴──────────────┐
│ │
NO YES
│ │
▼ ▼
┌─────────────────────┐ ┌───────────────────────────────────────────────────────────────────────────────┐
│ SHOW PUBLIC PAGE │ │ {WHAT IS THE USER ROLE?} │
│ - Home banner │ └────────────────────────────┬────────────────────────────────────────────────────┘
│ - Info tempat │ │
│ - Menu: Login │ ┌────────────────┴────────────────┐
│ Register │ │ │
│ │ ▼ ▼
│ ┌─────────────────┴─────────┐ │
│ │ USER CLICKS: │ │
│ │ - Login │ │
│ │ - Register │ │
│ └────────────┬────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │ ┌──────────────────────┐ ┌──────────────────────┐
│ │ LOGIN PROCESS: │ │ │ ADMIN USER │ │ REGULAR USER │
│ │ 1. Show login form │ │ │ │ │ │
│ │ 2. Input email/pass │ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ 3. Validate │ │ │ │ ADMIN │ │ │ │ USER │ │
│ │ 4. Check DB │ │ │ │ DASHBOARD │ │ │ │ HOME PAGE │ │
│ │ 5. Create session │ │ │ │ │ │ │ │ │ │
│ │ 6. Redirect │ │ │ │ ┌────────┐ │ │ │ │ ┌────────┐ │ │
│ └───────────┬───────────┘ │ │ │ │Statistics│ │ │ │ │ │MyTicket│ │ │
│ │ │ │ │ │ - Total │ │ │ │ │ │MyBookg │ │ │
│ │ │ │ │ │ - Revenue│ │ │ │ │ │Profile │ │ │
│ │ │ │ │ └────────┘ │ │ │ │ │Logout │ │ │
│ │ │ │ │ │ │ │ │ └────────┘ │ │
│ ▼ │ │ │ ┌────────┐ │ │ │ │ │ │
│ ┌─────────────────────────┐ │ │ │ │CRUD │ │ │ │ │ ┌────────┐ │ │
│ │ REGISTER PROCESS: │ │ │ │ │Tikets │ │ │ │ │ │BUY │ │ │
│ │ 1. Show register form │ │ │ │ └────────┘ │ │ │ │ │TICKET │ │ │
│ │ 2. Input data │ │ │ │ │ │ │ │ └────────┘ │ │
│ │ 3. Validate │ │ │ │ ┌────────┐ │ │ │ │ │ │
│ │ 4. Hash password │ │ │ │ │CRUD │ │ │ │ │ ┌────────┐ │ │
│ │ 5. Insert DB │ │ │ │ │Bank │ │ │ │ │ │BOOKING │ │ │
│ │ 6. Create session │ │ │ │ └────────┘ │ │ │ │ │PENDOPO │ │ │
│ │ 7. Redirect │ │ │ │ │ │ │ │ └────────┘ │ │
│ └───────────┬───────────┘ │ │ │ ┌────────┐ │ │ │ │ │ │
│ │ │ │ │ │VERIFY │ │ │ │ │ │ │
│ │ │ │ │ │Orders │ │ │ │ │ │ │
│ │ │ │ │ └────────┘ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ ┌────────┐ │ │ │ │ │ │
│ │ │ │ │ │VERIFY │ │ │ │ │ │ │
│ │ │ │ │ │Bookings│ │ │ │ │ │ │
│ │ │ │ │ └────────┘ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ ┌────────┐ │ │ │ │ │ │
│ │ │ │ │ │QR │ │ │ │ │ │ │
│ │ │ │ │ │Scanner │ │ │ │ │ │ │
│ │ │ │ │ └────────┘ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ ┌────────┐ │ │ │ │ │ │
│ │ │ │ │ │CRUD │ │ │ │ │ │ │
│ │ │ │ │ │Admins │ │ │ │ │ │ │
│ │ │ │ │ └────────┘ │ │ │ │ │ │
│ │ │ │ └──────┬───────┘ │ └──┼──────────────┘ │
│ │ │ │ │ │ │ │
│ └───────────────┼───┴───────────┴───────────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────────────────────────────────────────────────┴────────────────────┘
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ BUY TICKET FLOW │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ 1. USER SELECTS: Visit Date, Ticket Type, Quantity (1-20) │ │
│ │ │ 2. SYSTEM CALCULATES: Total Price = qty × ticket_price │ │
│ │ │ 3. USER CLICKS: "Lanjutkan Pembayaran" │ │
│ │ │ 4. SYSTEM GENERATES: Order Code (AT-XXXXX), Ticket Codes (AT-XXXXX), QR Codes │ │
│ │ │ 5. SYSTEM SAVES: ticket_order + ticket_order_items to DB │ │
│ │ │ 6. USER SEES: Payment page with bank account info │ │
│ │ │ 7. USER TRANSFERS: Money to bank account │ │
│ │ │ 8. USER UPLOADS: Payment proof (jpg/png/pdf, max 5MB) │ │
│ │ │ 9. SYSTEM SAVES: Payment proof to /public/payment-proofs/ │ │
│ │ │ 10. USER WAITS: For admin verification │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ BOOKING FLOW │ │
│ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ 1. USER SELECTS: Booking Date │ │ │
│ │ │ │ 2. SYSTEM CHECKS: Is date available? (AJAX) │ │ │
│ │ │ │ 3. USER FILLS: Visitor Name, Phone, Address, Notes │ │ │
│ │ │ │ 4. USER CLICKS: "Booking Sekarang" │ │ │
│ │ │ │ 5. SYSTEM GENERATES: Booking Code (AB-XXXXX) │ │ │
│ │ │ │ 6. SYSTEM SAVES: booking to DB (status=pending) │ │ │
│ │ │ │ 7. USER SEES: Payment page with bank account info │ │ │
│ │ │ │ 8. USER TRANSFERS: Money to bank account │ │ │
│ │ │ │ 9. USER UPLOADS: Payment proof │ │ │
│ │ │ │ 10. USER WAITS: For admin verification │ │ │
│ │ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ ADMIN VERIFICATION FLOW │ │
│ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ 1. ADMIN OPENS: Order Tiket / Booking menu │ │ │
│ │ │ │ 2. ADMIN SEES: List of pending payments │ │ │
│ │ │ │ 3. ADMIN CLICKS: "Detail" on an order │ │ │
│ │ │ │ 4. ADMIN SEES: Customer info, items, payment proof │ │ │
│ │ │ │ 5. ADMIN DECIDES: │ │ │
│ │ │ │ ┌────────────────┬────────────────┐ │ │ │
│ │ │ │ │ KONFIRMASI │ TOLAK │ │ │ │
│ │ │ │ ├────────────────┼────────────────┤ │ │ │
│ │ │ │ │ - Update status│ - Input reason │ │ │ │
│ │ │ │ │ = 'confirmed'│ - Update status│ │ │ │
│ │ │ │ │ - Set confirm │ = 'rejected' │ │ │ │
│ │ │ │ │ _at = now() │ │ │ │ │
│ │ │ │ └────────────────┴────────────────┘ │ │ │
│ │ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ QR SCANNER FLOW (Admin) │ │
│ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ 1. ADMIN OPENS: Verifikasi Tiket page │ │ │
│ │ │ │ 2. ADMIN CHOOSES: Scan QR / Input Manual │ │ │
│ │ │ │ 3. SYSTEM READS: QR Code / Manual Input │ │ │
│ │ │ │ 4. SYSTEM QUERIES: ticket_order_items by ticket_code │ │ │
│ │ │ │ 5. SYSTEM CHECKS: Is ticket valid? (not used) │ │ │
│ │ │ │ 6. SYSTEM SHOWS: Ticket info + Valid/Used status │ │ │
│ │ │ │ 7. ADMIN CLICKS: "Tandai Sudah Dipakai" (if valid) │ │ │
│ │ │ │ 8. SYSTEM UPDATES: is_used = true, used_at = now() │ │ │
│ │ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ DOWNLOAD TICKET FLOW (User after confirmed) │ │
│ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ 1. USER OPENS: My Tickets / Download page │ │ │
│ │ │ │ 2. USER CLICKS: Download/Print button │ │ │
│ │ │ │ 3. SYSTEM GENERATES: PDF with QR Code │ │ │
│ │ │ │ 4. USER DOWNLOADS/PRINTS: Ticket PDF │ │ │
│ │ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ PROFILE & HISTORY FLOW │ │
│ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ USER CAN: View/Edit Profile, Change Password, View Ticket History, View Booking │ │ │
│ │ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────────────────────────────┐ │
│ │ │ LOGOUT PROCESS │ │
│ │ │ 1. User clicks Logout │ │
│ │ │ 2. System destroys session │ │
│ │ │ 3. System redirects to home page │ │
│ │ └─────────────────────┬───────────────────────┘ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────┐ │
│ │ END │ │
│ └────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
```
---
## SUPER SIMPLE UNIFIED FLOW (One Page)
```
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ SISTEM PARKIRAN & BOOKING WISATA BANYU BIRU │
│ (FULL FLOWCHART) │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────┐ │
│ │ START │ │
│ └───┬───┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ USER BUKA WEBSITE │ │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────┐ │
│ │ SUDAH LOGIN? │ │
│ │ (Cek Session) │ │
│ └─────┬──────────────┬──────┘ │
│ ┌───────┴──────┐ ┌───────┴────────┐ │
│ NO │ NO │YES │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ TAMPILKAN HAL. │ │TAMPILKAN HAL. │ │ CEK ROLE: │ │
│ │ PUBLIK │ │ LOGIN/REGISTER │ │ ADMIN / USER │ │
│ │ (Banner, Info) │ │ │ └─────────┬───────────┘ │
│ └────────┬────────┘ └────────┬────────┘ │ │
│ │ │ ┌───┴───┐ │
│ │ │ │ │ │
│ │ │ ADMIN USER │
│ │ │ │ │ │
│ │ │ ▼ ▼ │
│ │ │ ┌─────────┐ ┌─────────┐ │
│ │ │ │DASHBOARD│ │HOME │ │
│ │ │ │ADMIN │ │USER │ │
│ │ │ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ │ │ ┌───────┴───────┴───────┐ │
│ │ │ │ │ │
│ │ │ ▼ ▼ │
│ │ │ ┌──────────┐ ┌──────────┐ │
│ │ │ │ADMIN MENU│ │USER MENU │ │
│ │ │ │──────────│ │──────────│ │
│ │ │ │•Tiket │ │•Beli Tiket│ │
│ │ │ │•Bank │ │•Booking │ │
│ │ │ │•Order │ │•My Tiket │ │
│ │ │ │•Booking │ │•My Book │ │
│ │ │ │•QR Scan │ │•Profile │ │
│ │ │ │•Admin │ │•Logout │ │
│ │ │ │•Logout │ │ │ │
│ │ │ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ │ │ │ ┌─────────────┘ │
│ │ │ │ │ │
│ │ │ ▼ ▼ │
│ │ │ ┌─────────────────────────┐ │
│ │ │ │ DATABASE (MySQL) │ │
│ │ │ │ ─────────────────────│ │
│ │ │ │ • users │ │
│ │ │ │ • tickets │ │
│ │ │ │ • ticket_orders │ │
│ │ │ │ • ticket_order_items │ │
│ │ │ │ • places │ │
│ │ │ │ • bookings │ │
│ │ │ │ • bank_accounts │ │
│ │ │ └─────────────────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ └────────────────────┴────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ ALUR LENGKAP (Dari Login sampai Logout): │ │
│ │ │ │
│ │ 1. USER LOGIN → 2. PILIH MENU → 3. LAKUKAN AKSI → 4. ADMIN VERIFIKASI → 5. SELESAI → LOGOUT │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ JIKA USER MEMILIH "BELI TIKET": │ │ │
│ │ │ ───────────────────────────────────────────────────────────────────────────────────────── │ │
│ │ │ User Pilih Tanggal → Pilih Tiket → Pilih Jumlah → Hitung Total → Bayar → Upload Bukti │ │
│ │ │ ↓ │ │
│ │ │ Admin Verifikasi (Terima/Tolak) → User Download Tiket → Scan QR saat datang │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ JIKA USER MEMILIH "BOOKING PENDOPO": │ │ │
│ │ │ ───────────────────────────────────────────────────────────────────────────────────────── │ │
│ │ │ User Pilih Tanggal → Cek Ketersediaan → Isi Data → Booking → Bayar → Upload Bukti │ │
│ │ │ ↓ │ │
│ │ │ Admin Verifikasi (Terima/Tolak) → BookingConfirmed │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ JIKA ADMIN MEMILIH "VERIFIKASI": │ │ │
│ │ │ ───────────────────────────────────────────────────────────────────────────────────────── │ │
│ │ │ Admin Lihat Daftar Pending → Klik Detail → Periksa Bukti → Konfirmasi / Tolak │ │
│ │ │ ↓ │ │
│ │ │ Status diUpdate → User Dapat/notifikasi │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ JIKA ADMIN MEMILIH "QR SCANNER": │ │ │
│ │ │ ───────────────────────────────────────────────────────────────────────────────────────── │ │
│ │ │ Admin Scan QR / Input Manual → Sistem Cek Validitas → Tampilkan Hasil (Valid/Used) │ │
│ │ │ ↓ │ │
│ │ │ Jika Valid → Tandai Sudah Dipakai │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ │ │
│ ▼ │
│ ┌───────┐ │
│ │ END │ │
│ └───────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
```
---
## KETERANGAN:
### Aktor:
- **GUEST** = Pengunjung yang belum login
- **USER** = Pengunjung yang sudah login (membeli tiket/booking)
- **ADMIN** = Petugas yang mengelola sistem
### Menu Admin:
| Menu | Fungsi |
|------|--------|
| Dashboard | Lihat statistik & chart |
| Tiket | CRUD data tiket |
| Rekening Bank | CRUD rekening bank |
| Order Tiket | Verifikasi pembayaran tiket |
| Booking | Verifikasi pembayaran booking |
| QR Scanner | Scan & verifikasi ticket |
| Admin | CRUD admin lain |
### Menu User:
| Menu | Fungsi |
|------|--------|
| Beli Tiket | Pembelian tiket masuk |
| Booking | Pemesanan pendopo |
| Tiket Saya | Lihat riwayat & download |
| Booking Saya | Lihat riwayat booking |
| Profil | Edit profil & password |
### Status:
- **pending** = Menunggu verifikasi
- **confirmed** = Diterima/confirmed
- **rejected** = Ditolak
---
**Dibuat**: 9 Maret 2026
**Sistem**: Banyu Biru Ticketing & Booking System
**Tipe**: Unified Flowchart (Semua jadi satu)

View File

@ -0,0 +1,360 @@
# Flowchart - Fitur Verifikasi Pembayaran Admin
## Daftar Isi
1. [Lihat Daftar Order Tiket](#1-lihat-daftar-order-tiket)
2. [Lihat Detail Order Tiket](#2-lihat-detail-order-tiket)
3. [Konfirmasi Pembayaran Tiket](#3-konfirmasi-pembayaran-tiket)
4. [Tolak Pembayaran Tiket](#4-tolak-pembayaran-tiket)
5. [Verifikasi Pembayaran Booking](#5-verifikasi-pembayaran-booking)
---
## 1. Lihat Daftar Order Tiket
### Deskripsi
Flowchart untuk admin melihat daftar semua order tiket.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Admin mengakses menu "Order Tiket"]
[Query database: ambil semua ticket_orders]
[Include relasi: user]
[Urutkan: terbaru ke terlama (latest)]
[Tampilkan tabel order tiket:]
- Kode Order
- Nama User
- Tanggal Kunjungan
- Total Qty
- Total Harga
- Status (pending/confirmed/rejected)
- Aksi (Detail)
[Tampilkan badge warna sesuai status:]
- Pending: Kuning
- Confirmed: Hijau
- Rejected: Merah
END
```
---
## 2. Lihat Detail Order Tiket
### Deskripsi
Flowchart untuk admin melihat detail order tiket dan bukti pembayaran.
### Flowchart
```
START
[Admin klik tombol "Detail" pada order]
[Query database: ambil ticket_order berdasarkan ID]
[Include relasi:]
- user
- items.ticket
{Order ditemukan?}
├─ NO → [Error 404: Order tidak ditemukan] → END
└─ YES ↓
[Tampilkan halaman detail order]
[Tampilkan informasi order:]
- Kode Order
- Nama User
- Email User
- Tanggal Kunjungan
- Total Qty
- Total Harga
- Status
- Tanggal Order
[Tampilkan daftar tiket yang dibeli:]
- Kode Tiket
- Jenis Tiket
- Harga
- QR Code
- Status Dipakai
{Bukti pembayaran ada?}
├─ NO → [Tampilkan pesan: "Belum upload bukti"]
└─ YES ↓
[Tampilkan bukti pembayaran (gambar/PDF)]
{Status = 'pending'?}
├─ YES → [Tampilkan tombol:]
│ - "Konfirmasi Pembayaran" (hijau)
│ - "Tolak Pembayaran" (merah)
│ ↓
│ END
├─ Status = 'confirmed' → [Tampilkan badge: "Sudah Dikonfirmasi"]
│ ↓
│ [Tampilkan info:]
│ - Dikonfirmasi oleh: nama admin
│ - Tanggal konfirmasi
│ ↓
│ END
└─ Status = 'rejected' → [Tampilkan badge: "Ditolak"]
[Tampilkan alasan penolakan]
END
```
---
## 3. Konfirmasi Pembayaran Tiket
### Deskripsi
Flowchart untuk admin mengkonfirmasi pembayaran tiket.
### Flowchart
```
START
[Admin di halaman detail order]
[Admin memeriksa bukti pembayaran]
{Bukti pembayaran valid?}
├─ NO → [Admin klik "Tolak Pembayaran"] → [Lihat flowchart #4]
└─ YES ↓
[Admin klik tombol "Konfirmasi Pembayaran"]
[Tampilkan konfirmasi SweetAlert:]
"Yakin ingin konfirmasi pembayaran?"
{Admin konfirmasi?}
├─ NO (Batal) → [Tutup dialog] → END
└─ YES ↓
[Query database: ambil ticket_order berdasarkan ID]
{Order ditemukan?}
├─ NO → [Error: Order tidak ditemukan] → END
└─ YES ↓
[Update tabel ticket_orders:]
- status = 'confirmed'
- confirmed_at = waktu sekarang
- confirmed_by = admin_id yang login
- rejection_note = null
[Simpan perubahan ke database]
[Tampilkan notifikasi sukses:]
"Order tiket berhasil dikonfirmasi"
[Refresh halaman detail order]
[Tampilkan badge "Confirmed"]
[Hide tombol konfirmasi/tolak]
END
```
---
## 4. Tolak Pembayaran Tiket
### Deskripsi
Flowchart untuk admin menolak pembayaran tiket dengan alasan.
### Flowchart
```
START
[Admin di halaman detail order]
[Admin memeriksa bukti pembayaran]
{Bukti pembayaran tidak valid?}
├─ NO → [Admin klik "Konfirmasi"] → [Lihat flowchart #3]
└─ YES ↓
[Admin klik tombol "Tolak Pembayaran"]
[Tampilkan modal form penolakan]
[Tampilkan textarea untuk alasan penolakan]
[Admin mengetik alasan penolakan]
[Admin klik "Submit"]
<Validasi input>
{Alasan penolakan terisi?}
├─ NO → [Error: "Alasan penolakan wajib diisi"]
│ ↓
│ [Kembali ke modal]
│ ↓
│ END
└─ YES ↓
[Query database: ambil ticket_order berdasarkan ID]
{Order ditemukan?}
├─ NO → [Error: Order tidak ditemukan] → END
└─ YES ↓
[Update tabel ticket_orders:]
- status = 'rejected'
- confirmed_by = admin_id yang login
- rejection_note = alasan dari admin
[Simpan perubahan ke database]
[Tampilkan notifikasi sukses:]
"Order tiket ditolak"
[Refresh halaman detail order]
[Tampilkan badge "Rejected"]
[Tampilkan alasan penolakan]
[Hide tombol konfirmasi/tolak]
END
```
---
## 5. Verifikasi Pembayaran Booking
### Deskripsi
Flowchart untuk admin memverifikasi pembayaran booking pendopo.
### Flowchart
```
START
[Admin mengakses menu "Booking"]
[Query database: ambil semua bookings]
[Include relasi: user, place]
[Urutkan: terbaru ke terlama]
[Tampilkan tabel booking:]
- Kode Booking
- Nama User
- Tempat
- Tanggal Booking
- Total Harga
- Status
- Aksi (Detail)
[Admin klik "Detail" pada booking]
[Query database: ambil booking berdasarkan ID]
[Tampilkan detail booking:]
- Kode Booking
- Nama Pengunjung
- Nomor Telepon
- Alamat
- Tanggal Booking
- Total Harga
- Catatan
- Bukti Pembayaran
{Status = 'pending'?}
├─ YES → [Tampilkan tombol:]
│ - "Konfirmasi" (hijau)
│ - "Tolak" (merah)
│ ↓
│ [Admin klik salah satu tombol]
│ ↓
│ {Klik Konfirmasi?}
│ ├─ YES → [Update status = 'confirmed']
│ │ ↓
│ │ [Set confirmed_at = now()]
│ │ ↓
│ │ [Set confirmed_by = admin_id]
│ │ ↓
│ │ [Notifikasi: "Booking dikonfirmasi"]
│ │ ↓
│ │ END
│ │
│ └─ NO (Tolak) → [Tampilkan form alasan]
│ ↓
│ [Admin input alasan]
│ ↓
│ [Update status = 'rejected']
│ ↓
│ [Set rejection_note]
│ ↓
│ [Notifikasi: "Booking ditolak"]
│ ↓
│ END
└─ NO (confirmed/rejected) → [Tampilkan status saja]
[Hide tombol aksi]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Database** (Cylinder) - Operasi database
6. **Manual Input** (Parallelogram with slanted top) - Input manual admin
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **Admin Action**: Biru tua muda (#BBDEFB)
- **Confirm**: Hijau muda (#C8E6C9)
- **Reject**: Merah muda (#FFCDD2)
- **Pending**: Kuning muda (#FFF9C4)
### Tips
- Gunakan swimlane untuk memisahkan Admin dan System
- Tandai proses verifikasi dengan warna khusus
- Gunakan connector yang jelas untuk decision branch
- Tambahkan notes untuk status order
---
**Dibuat**: 25 Februari 2026
**Fitur**: Verifikasi Pembayaran Admin
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,287 @@
# Flowchart - Fitur QR Scanner Verifikasi Tiket
## Daftar Isi
1. [Scan QR Code Tiket](#1-scan-qr-code-tiket)
2. [Input Manual Kode Tiket](#2-input-manual-kode-tiket)
3. [Verifikasi Tiket](#3-verifikasi-tiket)
4. [Tandai Tiket Sebagai Dipakai](#4-tandai-tiket-sebagai-dipakai)
---
## 1. Scan QR Code Tiket
### Deskripsi
Flowchart untuk scan QR code tiket menggunakan kamera.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Admin mengakses halaman "Verifikasi Tiket"]
[Tampilkan 2 metode verifikasi:]
- Scan QR Code (aktif)
- Input Manual
[Initialize html5-qrcode library]
[Request akses kamera]
{Izin kamera diberikan?}
├─ NO → [Tampilkan error: "Tidak dapat mengakses kamera"]
│ ↓
│ [Sarankan gunakan Input Manual]
│ ↓
│ END
└─ YES ↓
[Tampilkan preview kamera]
[Tampilkan frame scanner dengan overlay]
[Mulai scanning dengan fps: 10]
[Loop: Deteksi QR code]
┌─────────────────────────┐
│ [Scan frame kamera]
│ ↓
│ {QR code terdeteksi?}
│ ├─ NO → [Lanjut scan frame berikutnya]
│ │ ↓
│ │ [Ulangi loop]
│ │
│ └─ YES ↓
│ ↓
│ [Decode QR code]
│ ↓
│ [Ambil kode tiket dari QR]
│ ↓
│ [Stop scanner]
│ ↓
│ [Hide kamera]
│ ↓
│ [Tampilkan: "Memproses QR Code..."]
│ ↓
│ [Submit form dengan kode tiket]
│ ↓
│ [Lihat flowchart #3: Verifikasi Tiket]
└─────────────────────────┘
END
```
---
## 2. Input Manual Kode Tiket
### Deskripsi
Flowchart untuk input kode tiket secara manual tanpa scan QR.
### Flowchart
```
START
[Admin di halaman "Verifikasi Tiket"]
[Admin klik metode "Input Manual"]
[Stop scanner QR (jika aktif)]
[Hide section QR scanner]
[Tampilkan section input manual]
[Tampilkan form input:]
- Input text untuk kode tiket
- Placeholder: "AT-XXXXX"
- Tombol "Cek Tiket"
[Admin mengetik kode tiket]
[Admin klik tombol "Cek Tiket"]
<Validasi input>
{Kode tiket terisi?}
├─ NO → [Error: "Kode tiket wajib diisi"]
│ ↓
│ [Focus ke input]
│ ↓
│ END
└─ YES ↓
[Submit form dengan kode tiket]
[Lihat flowchart #3: Verifikasi Tiket]
END
```
---
## 3. Verifikasi Tiket
### Deskripsi
Flowchart untuk memverifikasi kode tiket dan menampilkan hasilnya.
### Flowchart
```
START
[Terima kode tiket dari QR scan atau input manual]
[Query database: cari ticket_order_items]
[WHERE ticket_code = kode yang diinput]
[Include relasi:]
- order.user
- ticket
{Tiket ditemukan?}
├─ NO → [Tampilkan error: "Tiket tidak ditemukan"]
│ ↓
│ [Tampilkan tombol "Scan Lagi"]
│ ↓
│ END
└─ YES ↓
[Ambil data tiket lengkap]
[Cek status tiket]
{Tiket sudah dipakai (is_used = true)?}
├─ YES → [Tampilkan card hasil dengan border merah]
│ ↓
│ [Tampilkan informasi:]
│ - Kode Tiket
│ - Nama Pemesan
│ - Jenis Tiket
│ - Tanggal Kunjungan
│ - Status: "Sudah Dipakai" (badge merah)
│ ↓
│ [Hide tombol "Tandai Sebagai Dipakai"]
│ ↓
│ [Tampilkan tombol "Scan Lagi"]
│ ↓
│ END
└─ NO (is_used = false) → [Tampilkan card hasil dengan border hijau]
[Tampilkan informasi:]
- Kode Tiket
- Nama Pemesan
- Jenis Tiket
- Tanggal Kunjungan
- Status: "Valid" (badge hijau)
[Tampilkan tombol "Tandai Sebagai Dipakai"]
[Tampilkan tombol "Scan Lagi"]
END
```
---
## 4. Tandai Tiket Sebagai Dipakai
### Deskripsi
Flowchart untuk menandai tiket yang sudah digunakan.
### Flowchart
```
START
[Admin melihat hasil verifikasi tiket valid]
[Admin klik tombol "Tandai Sebagai Dipakai"]
[Submit form dengan:]
- ticket_code
- mark_used = 1
[Query database: cari ticket_order_items]
[WHERE ticket_code = kode tiket]
{Tiket ditemukan?}
├─ NO → [Error: "Tiket tidak ditemukan"] → END
└─ YES ↓
{Tiket sudah dipakai?}
├─ YES → [Error: "Tiket sudah ditandai dipakai sebelumnya"]
│ ↓
│ [Kembali ke halaman verifikasi]
│ ↓
│ END
└─ NO ↓
[Update tabel ticket_order_items:]
- is_used = true
- used_at = waktu sekarang
[Simpan perubahan ke database]
[Tampilkan notifikasi sukses:]
"Tiket berhasil ditandai sebagai dipakai"
[Refresh halaman verifikasi]
[Tampilkan hasil dengan status "Sudah Dipakai"]
[Hide tombol "Tandai Sebagai Dipakai"]
[Tampilkan tombol "Scan Lagi"]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Database** (Cylinder) - Operasi database
6. **Manual Operation** (Trapezoid) - Operasi manual admin
7. **Loop** (Hexagon) - Perulangan
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **Camera/Scanner**: Biru tua muda (#BBDEFB)
- **Valid**: Hijau muda (#C8E6C9)
- **Used/Invalid**: Merah muda (#FFCDD2)
- **Loop**: Oranye muda (#FFE0B2)
### Tips
- Gunakan swimlane untuk memisahkan Admin, Camera, dan System
- Tandai proses scanning dengan warna khusus
- Gunakan loop symbol untuk proses scanning berulang
- Tambahkan notes untuk library yang digunakan (html5-qrcode)
---
**Dibuat**: 25 Februari 2026
**Fitur**: QR Scanner Verifikasi Tiket
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,298 @@
# Flowchart - Fitur Manajemen Tiket Admin
## Daftar Isi
1. [Lihat Daftar Tiket](#1-lihat-daftar-tiket)
2. [Tambah Tiket Baru](#2-tambah-tiket-baru)
3. [Edit Tiket](#3-edit-tiket)
4. [Hapus Tiket](#4-hapus-tiket)
5. [Toggle Status Aktif Tiket](#5-toggle-status-aktif-tiket)
---
## 1. Lihat Daftar Tiket
### Deskripsi
Flowchart untuk admin melihat daftar semua tiket.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Admin mengakses menu "Tiket"]
[Query database: ambil semua tickets]
[Urutkan berdasarkan ID]
[Tampilkan tabel tiket:]
- Nama Tiket
- Harga
- Deskripsi
- Status (Aktif/Nonaktif)
- Aksi (Edit, Hapus)
[Tampilkan tombol "Tambah Tiket"]
END
```
---
## 2. Tambah Tiket Baru
### Deskripsi
Flowchart untuk admin menambah tiket baru.
### Flowchart
```
START
[Admin di halaman daftar tiket]
[Admin klik tombol "Tambah Tiket"]
[Redirect ke halaman form tambah tiket]
[Tampilkan form:]
- Nama Tiket
- Harga
- Deskripsi
- Status Aktif (checkbox)
[Admin mengisi form]
[Admin klik tombol "Simpan"]
<Validasi input>
{Nama tiket terisi?}
├─ NO → [Error: "Nama tiket wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Harga terisi dan valid?}
├─ NO → [Error: "Harga wajib diisi dan harus angka"] → [Kembali ke form] → END
└─ YES ↓
{Harga >= 0?}
├─ NO → [Error: "Harga tidak boleh negatif"] → [Kembali ke form] → END
└─ YES ↓
[Simpan data ke tabel tickets:]
- name
- price
- description
- is_active (default: true)
[Tampilkan notifikasi sukses:]
"Tiket berhasil ditambahkan"
[Redirect ke halaman daftar tiket]
END
```
---
## 3. Edit Tiket
### Deskripsi
Flowchart untuk admin mengedit data tiket.
### Flowchart
```
START
[Admin di halaman daftar tiket]
[Admin klik tombol "Edit" pada tiket]
[Query database: ambil tiket berdasarkan ID]
{Tiket ditemukan?}
├─ NO → [Error 404: Tiket tidak ditemukan] → END
└─ YES ↓
[Redirect ke halaman form edit tiket]
[Tampilkan form dengan data tiket:]
- Nama Tiket (terisi)
- Harga (terisi)
- Deskripsi (terisi)
- Status Aktif (checked/unchecked)
[Admin mengubah data]
[Admin klik tombol "Update"]
<Validasi input>
{Nama tiket terisi?}
├─ NO → [Error: "Nama tiket wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Harga terisi dan valid?}
├─ NO → [Error: "Harga wajib diisi dan harus angka"] → [Kembali ke form] → END
└─ YES ↓
{Harga >= 0?}
├─ NO → [Error: "Harga tidak boleh negatif"] → [Kembali ke form] → END
└─ YES ↓
[Update tabel tickets:]
- name
- price
- description
- is_active
[Simpan perubahan ke database]
[Tampilkan notifikasi sukses:]
"Tiket berhasil diupdate"
[Redirect ke halaman daftar tiket]
END
```
---
## 4. Hapus Tiket
### Deskripsi
Flowchart untuk admin menghapus tiket.
### Flowchart
```
START
[Admin di halaman daftar tiket]
[Admin klik tombol "Hapus" pada tiket]
[Tampilkan konfirmasi SweetAlert:]
"Yakin ingin menghapus tiket ini?"
{Admin konfirmasi?}
├─ NO (Batal) → [Tutup dialog] → END
└─ YES ↓
[Query database: ambil tiket berdasarkan ID]
{Tiket ditemukan?}
├─ NO → [Error: "Tiket tidak ditemukan"] → END
└─ YES ↓
[Cek apakah tiket pernah digunakan]
[Query: cek ticket_order_items dengan ticket_id ini]
{Tiket pernah digunakan?}
├─ YES → [Error: "Tiket tidak dapat dihapus karena sudah pernah digunakan"]
│ ↓
│ [Sarankan: nonaktifkan tiket saja]
│ ↓
│ END
└─ NO ↓
[Hapus tiket dari database]
[Tampilkan notifikasi sukses:]
"Tiket berhasil dihapus"
[Refresh halaman daftar tiket]
END
```
---
## 5. Toggle Status Aktif Tiket
### Deskripsi
Flowchart untuk mengaktifkan/menonaktifkan tiket.
### Flowchart
```
START
[Admin di halaman edit tiket]
[Tampilkan checkbox "Status Aktif"]
{Tiket saat ini aktif?}
├─ YES → [Checkbox checked]
└─ NO → [Checkbox unchecked]
[Admin toggle checkbox]
[Admin klik tombol "Update"]
[Update tabel tickets:]
- is_active = nilai checkbox (true/false)
{is_active = true?}
├─ YES → [Tiket akan muncul di halaman user]
│ ↓
│ [Tiket dapat dibeli]
│ ↓
│ [Notifikasi: "Tiket diaktifkan"]
└─ NO → [Tiket tidak muncul di halaman user]
[Tiket tidak dapat dibeli]
[Notifikasi: "Tiket dinonaktifkan"]
[Redirect ke halaman daftar tiket]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Database** (Cylinder) - Operasi database
6. **Document** (Rectangle with wavy bottom) - Form
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **Form**: Biru tua muda (#BBDEFB)
- **Create**: Hijau muda (#C8E6C9)
- **Update**: Biru muda (#B3E5FC)
- **Delete**: Merah muda (#FFCDD2)
- **Toggle**: Oranye muda (#FFE0B2)
### Tips
- Gunakan swimlane untuk memisahkan Admin dan System
- Tandai operasi CRUD dengan warna berbeda
- Gunakan connector yang jelas untuk decision branch
- Tambahkan notes untuk validasi penting
---
**Dibuat**: 25 Februari 2026
**Fitur**: Manajemen Tiket Admin
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,300 @@
# Flowchart - Fitur Pembelian Tiket
## Daftar Isi
1. [Lihat Daftar Tiket](#1-lihat-daftar-tiket)
2. [Beli Tiket](#2-beli-tiket)
3. [Upload Bukti Pembayaran](#3-upload-bukti-pembayaran)
4. [Download Tiket](#4-download-tiket)
---
## 1. Lihat Daftar Tiket
### Deskripsi
Flowchart untuk melihat daftar tiket yang tersedia.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ YES → [Tampilkan SweetAlert: "Admin tidak dapat membeli tiket"]
│ ↓
│ [Redirect ke dashboard admin]
│ ↓
│ END
└─ NO ↓
[Query database: ambil tiket dengan is_active = true]
[Filter: name = "Tiket Masuk"]
{Tiket ditemukan?}
├─ NO → [Query: ambil semua tiket aktif]
│ ↓
│ [Tampilkan tiket pertama]
└─ YES ↓
[Tampilkan halaman pembelian tiket]
[Tampilkan informasi tiket:]
- Nama tiket
- Harga
- Deskripsi
[Tampilkan form pembelian]
END
```
---
## 2. Beli Tiket
### Deskripsi
Flowchart untuk proses pembelian tiket lengkap.
### Flowchart
```
START
[User di halaman pembelian tiket]
[User pilih tanggal kunjungan]
<Validasi tanggal>
{Tanggal >= hari ini?}
├─ NO → [Error: "Tanggal tidak valid"] → [Kembali ke form] → END
└─ YES ↓
[User pilih jenis tiket]
[User tentukan jumlah tiket (1-20)]
{Jumlah valid (1-20)?}
├─ NO → [Error: "Jumlah tidak valid"] → [Kembali ke form] → END
└─ YES ↓
[Hitung total harga = jumlah × harga tiket]
[Tampilkan ringkasan pesanan]
[User klik "Lanjutkan Pembayaran"]
[Mulai database transaction]
[Generate kode order unik (AT-XXXXX)]
[Simpan data ke tabel ticket_orders:]
- user_id
- order_code
- visit_date
- total_qty
- total_price
- status = 'pending'
[Loop untuk setiap tiket yang dibeli]
┌─────────────────────────┐
│ [Generate kode tiket unik (AT-XXXXX)]
│ ↓
│ [Generate QR code SVG]
│ ↓
│ [Simpan QR code ke /public/qrcodes/]
│ ↓
│ [Simpan data ke ticket_order_items:]
│ - ticket_order_id
│ - ticket_id
│ - ticket_code
│ - qr_code_path
│ - qty = 1
│ - price
│ - is_used = false
│ ↓
│ [Ulangi untuk tiket berikutnya]
└─────────────────────────┘
[Commit transaction]
[Redirect ke halaman pembayaran]
END
```
---
## 3. Upload Bukti Pembayaran
### Deskripsi
Flowchart untuk upload bukti transfer pembayaran tiket.
### Flowchart
```
START
[User di halaman pembayaran]
[Tampilkan informasi order:]
- Kode order
- Total harga
- Daftar tiket
[Query database: ambil daftar rekening bank aktif]
[Tampilkan nomor rekening bank]
[User melakukan transfer ke rekening]
[User klik "Upload Bukti Pembayaran"]
[User pilih file bukti transfer]
<Validasi file>
{File dipilih?}
├─ NO → [Error: "File wajib dipilih"] → [Kembali ke form] → END
└─ YES ↓
{Format file valid (jpg, jpeg, png, pdf)?}
├─ NO → [Error: "Format file tidak valid"] → [Kembali ke form] → END
└─ YES ↓
{Ukuran file <= 5MB?}
├─ NO → [Error: "Ukuran file terlalu besar"] → [Kembali ke form] → END
└─ YES ↓
[Generate nama file unik:]
Format: YYYYMMDDHHMMSS_proof_uniqid.ext
[Simpan file ke /public/payment-proofs/]
[Update tabel ticket_orders:]
- payment_proof = path file
- status tetap 'pending'
[Tampilkan notifikasi sukses]
[Redirect ke halaman download tiket]
END
```
---
## 4. Download Tiket
### Deskripsi
Flowchart untuk download tiket setelah pembayaran dikonfirmasi.
### Flowchart
```
START
[User mengakses halaman download]
[Query database: ambil order berdasarkan order_code]
{Order ditemukan?}
├─ NO → [Error 404: Order tidak ditemukan] → END
└─ YES ↓
{Order milik user yang login?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Ambil semua ticket_order_items untuk order ini]
[Cek status order]
{Status = 'confirmed'?}
├─ YES → [Tampilkan halaman download]
│ ↓
│ [Tampilkan pesan: "Pembayaran berhasil diverifikasi"]
│ ↓
│ [Tampilkan daftar tiket dengan QR code]
│ ↓
│ [Aktifkan tombol "Download PDF"]
│ ↓
│ [Aktifkan tombol "Print Tiket"]
│ ↓
│ [User klik tombol download]
│ ↓
│ [Generate PDF tiket dengan:]
│ - Kode tiket
│ - QR code
│ - Nama pengunjung
│ - Tanggal kunjungan
│ - Jenis tiket
│ ↓
│ [Download file PDF]
│ ↓
│ END
├─ Status = 'pending' → [Tampilkan halaman download]
│ ↓
│ [Tampilkan pesan: "Menunggu verifikasi admin"]
│ ↓
│ [Tampilkan daftar tiket]
│ ↓
│ [Disabled tombol "Download PDF" (abu-abu)]
│ ↓
│ [Disabled tombol "Print Tiket" (abu-abu)]
│ ↓
│ [Tampilkan alert: "Menunggu verifikasi admin"]
│ ↓
│ END
└─ Status = 'rejected' → [Tampilkan halaman download]
[Tampilkan pesan: "Pembayaran ditolak"]
[Tampilkan alasan penolakan]
[Disabled tombol download]
[Tampilkan tombol "Upload Ulang Bukti"]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Predefined Process** (Rectangle with double lines) - Subprocess
6. **Loop** (Hexagon) - Perulangan
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **Error**: Merah muda (#FFCDD2)
- **Success**: Hijau muda (#C8E6C9)
- **Loop**: Oranye muda (#FFE0B2)
### Tips
- Gunakan swimlane untuk memisahkan User dan System
- Tandai proses database dengan warna berbeda
- Gunakan connector yang jelas untuk decision branch
- Tambahkan notes untuk penjelasan kompleks
---
**Dibuat**: 25 Februari 2026
**Fitur**: Pembelian Tiket
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,345 @@
# Flowchart - Fitur Riwayat User
## Daftar Isi
1. [Lihat Riwayat Tiket](#1-lihat-riwayat-tiket)
2. [Lihat Riwayat Booking](#2-lihat-riwayat-booking)
3. [Lihat Detail Tiket dari Riwayat](#3-lihat-detail-tiket-dari-riwayat)
4. [Lihat Detail Booking dari Riwayat](#4-lihat-detail-booking-dari-riwayat)
---
## 1. Lihat Riwayat Tiket
### Deskripsi
Flowchart untuk user melihat riwayat pembelian tiket.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ YES → [Redirect ke dashboard admin] → END
└─ NO ↓
[User klik menu "Tiket Saya" di dropdown navbar]
[Query database: ticket_orders]
[Filter: user_id = user yang login]
[Include relasi: items.ticket]
[Urutkan: created_at DESC]
[Pagination: 10 data per halaman]
{Ada data tiket?}
├─ NO → [Tampilkan pesan: "Belum ada riwayat pembelian tiket"]
│ ↓
│ [Tampilkan tombol "Beli Tiket Sekarang"]
│ ↓
│ END
└─ YES ↓
[Loop untuk setiap order:]
┌─────────────────────────┐
│ [Tampilkan card order:]
│ - Kode Order
│ - Tanggal Kunjungan
│ - Total Qty
│ - Total Harga
│ - Status (badge warna)
│ - Tanggal Order
│ ↓
│ [Tampilkan daftar tiket dalam order]
│ ↓
│ {Status = 'confirmed'?}
│ ├─ YES → [Tampilkan tombol "Download Tiket"]
│ │ ↓
│ │ [Tampilkan tombol "Lihat Detail"]
│ │
│ ├─ Status = 'pending' → [Tampilkan badge "Menunggu Verifikasi"]
│ │ ↓
│ │ [Tampilkan tombol "Lihat Detail"]
│ │
│ └─ Status = 'rejected' → [Tampilkan badge "Ditolak"]
│ ↓
│ [Tampilkan tombol "Upload Ulang"]
│ ↓
│ [Ulangi untuk order berikutnya]
└─────────────────────────┘
[Tampilkan pagination]
END
```
---
## 2. Lihat Riwayat Booking
### Deskripsi
Flowchart untuk user melihat riwayat booking pendopo.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ YES → [Redirect ke dashboard admin] → END
└─ NO ↓
[User klik menu "Booking Saya" di dropdown navbar]
[Query database: bookings]
[Filter: user_id = user yang login]
[Include relasi: place]
[Urutkan: created_at DESC]
[Pagination: 10 data per halaman]
{Ada data booking?}
├─ NO → [Tampilkan pesan: "Belum ada riwayat booking"]
│ ↓
│ [Tampilkan tombol "Booking Sekarang"]
│ ↓
│ END
└─ YES ↓
[Loop untuk setiap booking:]
┌─────────────────────────┐
│ [Tampilkan card booking:]
│ - Kode Booking
│ - Nama Tempat
│ - Tanggal Booking
│ - Nama Pengunjung
│ - Total Harga
│ - Status (badge warna)
│ - Tanggal Order
│ ↓
│ {Status = 'confirmed'?}
│ ├─ YES → [Tampilkan badge "Dikonfirmasi"]
│ │ ↓
│ │ [Tampilkan tombol "Lihat Detail"]
│ │
│ ├─ Status = 'pending' → [Tampilkan badge "Menunggu Verifikasi"]
│ │ ↓
│ │ [Tampilkan tombol "Lihat Detail"]
│ │ ↓
│ │ [Tampilkan tombol "Upload Bukti"]
│ │
│ └─ Status = 'rejected' → [Tampilkan badge "Ditolak"]
│ ↓
│ [Tampilkan alasan penolakan]
│ ↓
│ [Tampilkan tombol "Upload Ulang"]
│ ↓
│ [Ulangi untuk booking berikutnya]
└─────────────────────────┘
[Tampilkan pagination]
END
```
---
## 3. Lihat Detail Tiket dari Riwayat
### Deskripsi
Flowchart untuk user melihat detail order tiket dari riwayat.
### Flowchart
```
START
[User di halaman riwayat tiket]
[User klik tombol "Lihat Detail" atau "Download Tiket"]
[Redirect ke halaman download tiket]
[Query database: ticket_orders]
[Filter: order_code = kode yang diklik]
[Filter: user_id = user yang login]
[Include relasi: items.ticket]
{Order ditemukan?}
├─ NO → [Error 404: Order tidak ditemukan] → END
└─ YES ↓
{Order milik user yang login?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Tampilkan halaman detail order]
[Cek status order]
{Status = 'confirmed'?}
├─ YES → [Tampilkan pesan sukses]
│ ↓
│ [Tampilkan daftar tiket dengan QR code]
│ ↓
│ [Enable tombol "Download PDF"]
│ ↓
│ [Enable tombol "Print Tiket"]
│ ↓
│ END
├─ Status = 'pending' → [Tampilkan pesan warning]
│ ↓
│ [Tampilkan daftar tiket]
│ ↓
│ [Disable tombol download (abu-abu)]
│ ↓
│ [Tampilkan: "Menunggu verifikasi admin"]
│ ↓
│ END
└─ Status = 'rejected' → [Tampilkan pesan danger]
[Tampilkan alasan penolakan]
[Tampilkan tombol "Upload Ulang Bukti"]
END
```
---
## 4. Lihat Detail Booking dari Riwayat
### Deskripsi
Flowchart untuk user melihat detail booking dari riwayat.
### Flowchart
```
START
[User di halaman riwayat booking]
[User klik tombol "Lihat Detail"]
[Redirect ke halaman status booking]
[Query database: bookings]
[Filter: booking_code = kode yang diklik]
[Filter: user_id = user yang login]
[Include relasi: place]
{Booking ditemukan?}
├─ NO → [Error 404: Booking tidak ditemukan] → END
└─ YES ↓
{Booking milik user yang login?}
├─ NO → [Error 403: Unauthorized] → END
└─ YES ↓
[Tampilkan halaman detail booking]
[Tampilkan informasi booking:]
- Kode Booking
- Nama Tempat
- Tanggal Booking
- Nama Pengunjung
- Nomor Telepon
- Alamat
- Total Harga
- Catatan
- Bukti Pembayaran
[Cek status booking]
{Status = 'confirmed'?}
├─ YES → [Tampilkan alert sukses:]
│ "Pembayaran berhasil diverifikasi"
│ ↓
│ [Tampilkan badge "Confirmed"]
│ ↓
│ [Tampilkan tombol "Kembali"]
│ ↓
│ END
├─ Status = 'pending' → [Tampilkan alert warning:]
│ "Menunggu verifikasi admin"
│ ↓
│ [Tampilkan badge "Pending"]
│ ↓
│ {Bukti pembayaran sudah diupload?}
│ ├─ YES → [Tampilkan pesan: "Bukti sedang diverifikasi"]
│ └─ NO → [Tampilkan tombol "Upload Bukti"]
│ ↓
│ END
└─ Status = 'rejected' → [Tampilkan alert danger:]
"Pembayaran ditolak"
[Tampilkan badge "Rejected"]
[Tampilkan alasan penolakan]
[Tampilkan tombol "Upload Ulang Bukti"]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Database** (Cylinder) - Operasi database
6. **Loop** (Hexagon) - Perulangan
7. **Display** (Parallelogram with curved sides) - Tampilan UI
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **Display**: Biru tua muda (#BBDEFB)
- **Confirmed**: Hijau muda (#C8E6C9)
- **Pending**: Kuning muda (#FFF9C4)
- **Rejected**: Merah muda (#FFCDD2)
- **Loop**: Oranye muda (#FFE0B2)
### Tips
- Gunakan swimlane untuk memisahkan User dan System
- Tandai status order dengan warna berbeda
- Gunakan loop symbol untuk iterasi data
- Tambahkan notes untuk pagination
- Highlight perbedaan tampilan berdasarkan status
---
**Dibuat**: 25 Februari 2026
**Fitur**: Riwayat User (Tiket & Booking)
**Sistem**: Banyu Biru Ticketing & Booking System

View File

@ -0,0 +1,233 @@
# Flowchart - Fitur Profil User
## Daftar Isi
1. [Lihat Profil User](#1-lihat-profil-user)
2. [Edit Profil User](#2-edit-profil-user)
3. [Ubah Password User](#3-ubah-password-user)
---
## 1. Lihat Profil User
### Deskripsi
Flowchart untuk user melihat profil sendiri.
### Flowchart
```
START
{User sudah login?}
├─ NO → [Redirect ke halaman login] → END
└─ YES ↓
{User adalah admin?}
├─ YES → [Redirect ke profil admin] → END
└─ NO ↓
[User klik menu "Profil Saya" di dropdown navbar]
[Query database: users]
[Filter: id = user yang login]
{User ditemukan?}
├─ NO → [Error: User tidak ditemukan] → [Logout] → END
└─ YES ↓
[Tampilkan halaman profil]
[Tampilkan informasi user:]
- Nama
- Email
- Nomor Telepon
- Alamat
- Tanggal Bergabung
[Tampilkan tombol "Edit Profil"]
END
```
---
## 2. Edit Profil User
### Deskripsi
Flowchart untuk user mengedit profil sendiri.
### Flowchart
```
START
[User di halaman profil]
[User klik tombol "Edit Profil"]
[Tampilkan form edit profil dengan data user:]
- Nama (terisi)
- Email (terisi)
- Nomor Telepon (terisi/kosong)
- Alamat (terisi/kosong)
[User mengubah data]
[User klik tombol "Update Profil"]
<Validasi input>
{Nama terisi?}
├─ NO → [Error: "Nama wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Format email valid?}
├─ NO → [Error: "Format email tidak valid"] → [Kembali ke form] → END
└─ YES ↓
[Query database: cek email sudah digunakan user lain]
{Email digunakan user lain?}
├─ YES → [Error: "Email sudah terdaftar"] → [Kembali ke form] → END
└─ NO ↓
{Nomor telepon diisi?}
├─ YES → {Format nomor telepon valid?}
│ ├─ NO → [Error: "Format nomor telepon tidak valid"] → [Kembali ke form] → END
│ └─ YES → [Lanjut]
└─ NO → [Lanjut]
[Update tabel users:]
- name
- email
- phone (jika diisi)
- address (jika diisi)
[Simpan perubahan ke database]
[Update session dengan data baru]
[Tampilkan notifikasi sukses:]
"Profil berhasil diupdate"
[Refresh halaman profil]
END
```
---
## 3. Ubah Password User
### Deskripsi
Flowchart untuk user mengubah password sendiri.
### Flowchart
```
START
[User di halaman profil]
[User klik tombol "Ubah Password"]
[Tampilkan form ubah password:]
- Password Lama
- Password Baru
- Konfirmasi Password Baru
[User mengisi form]
[User klik tombol "Update Password"]
<Validasi input>
{Password lama terisi?}
├─ NO → [Error: "Password lama wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
[Query database: ambil user yang login]
[Verifikasi password lama dengan hash]
{Password lama cocok?}
├─ NO → [Error: "Password lama salah"] → [Kembali ke form] → END
└─ YES ↓
{Password baru terisi?}
├─ NO → [Error: "Password baru wajib diisi"] → [Kembali ke form] → END
└─ YES ↓
{Password baru minimal 8 karakter?}
├─ NO → [Error: "Password minimal 8 karakter"] → [Kembali ke form] → END
└─ YES ↓
{Password baru = Konfirmasi Password?}
├─ NO → [Error: "Konfirmasi password tidak cocok"] → [Kembali ke form] → END
└─ YES ↓
{Password baru berbeda dari password lama?}
├─ NO → [Error: "Password baru harus berbeda dari password lama"]
│ ↓
│ [Kembali ke form]
│ ↓
│ END
└─ YES ↓
[Hash password baru menggunakan bcrypt]
[Update tabel users:]
- password = password baru (hashed)
[Simpan perubahan ke database]
[Tampilkan notifikasi sukses:]
"Password berhasil diubah"
[Redirect ke halaman profil]
END
```
---
## Panduan Membuat di Draw.io
### Simbol Flowchart
1. **Terminator** (Oval) - START/END
2. **Process** (Rectangle) - Proses/aksi
3. **Decision** (Diamond) - Kondisi/percabangan
4. **Data** (Parallelogram) - Input/Output
5. **Database** (Cylinder) - Operasi database
6. **Document** (Rectangle with wavy bottom) - Form
7. **Manual Input** (Parallelogram with slanted top) - Input user
### Warna yang Disarankan
- **START/END**: Hijau teal (#14B8A6)
- **Process**: Biru muda (#E3F2FD)
- **Decision**: Kuning (#FFF59D)
- **Database Operation**: Ungu muda (#E1BEE7)
- **Form**: Biru tua muda (#BBDEFB)
- **Update**: Biru muda (#B3E5FC)
- **Security Check**: Oranye muda (#FFE0B2)
- **Success**: Hijau muda (#C8E6C9)
- **Error**: Merah muda (#FFCDD2)
### Tips
- Gunakan swimlane untuk memisahkan User dan System
- Tandai proses security check (password verification) dengan warna khusus
- Gunakan connector yang jelas untuk decision branch
- Tambahkan notes untuk validasi password
- Highlight proses hashing password
### Security Notes
- Password lama harus diverifikasi sebelum mengubah password
- Password baru harus di-hash menggunakan bcrypt
- Password baru harus berbeda dari password lama
- Session harus diupdate setelah perubahan data
---
**Dibuat**: 25 Februari 2026
**Fitur**: Profil User
**Sistem**: Banyu Biru Ticketing & Booking System

2378
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",
"tailwindcss": "^4.0.0",
"vite": "^7.0.7"
}
}

35
phpunit.xml Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/>
</php>
</phpunit>

25
public/.htaccess Normal file
View File

@ -0,0 +1,25 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

1454
public/css/admin-teal.css Normal file

File diff suppressed because it is too large Load Diff

2245
public/css/app.css Normal file

File diff suppressed because it is too large Load Diff

0
public/favicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

20
public/index.php Normal file
View File

@ -0,0 +1,20 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@ -0,0 +1,793 @@
/* Minimal Modern Admin Dashboard - Clean & Simple */
:root {
--primary: #0f766e;
--primary-dark: #0b5c56;
--primary-light: #14b8a6;
--secondary: #64748b;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #06b6d4;
--bg-main: #f8fafc;
--bg-card: #ffffff;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--border: #e2e8f0;
--border-light: #f1f5f9;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body.admin-body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-main);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Sidebar - Minimal & Clean */
.admin-sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 260px;
background: var(--bg-card);
border-right: 1px solid var(--border);
z-index: 1000;
display: flex;
flex-direction: column;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar-brand {
padding: 1.5rem 1.25rem;
border-bottom: 1px solid var(--border-light);
}
.sidebar-brand-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-brand-link {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
}
.sidebar-brand-mark {
width: 40px;
height: 40px;
background: var(--primary);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: white;
}
.sidebar-brand-copy strong {
display: block;
font-size: 1.125rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
}
.sidebar-brand-copy small {
display: block;
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.125rem;
}
.sidebar-menu {
flex: 1;
overflow-y: auto;
padding: 1rem 0.75rem;
}
.sidebar-menu::-webkit-scrollbar {
width: 4px;
}
.sidebar-menu::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 10px;
}
.menu-section {
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
padding: 1rem 0.75rem 0.5rem;
margin-top: 0.5rem;
}
.menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 0.75rem;
margin-bottom: 0.25rem;
border-radius: 8px;
text-decoration: none;
color: var(--text-secondary);
font-weight: 500;
font-size: 0.875rem;
transition: all 0.15s ease;
position: relative;
}
.menu-item:hover {
background: var(--bg-main);
color: var(--text-primary);
}
.menu-item.active {
background: var(--primary);
color: white;
font-weight: 600;
}
.menu-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
flex-shrink: 0;
}
.menu-label {
flex: 1;
}
.sidebar-footer {
padding: 1rem 1.25rem;
border-top: 1px solid var(--border-light);
}
.sidebar-status {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.75rem;
background: var(--bg-main);
border-radius: 8px;
}
.status-dot {
width: 8px;
height: 8px;
background: var(--success);
border-radius: 50%;
flex-shrink: 0;
}
.status-text small {
display: block;
font-size: 0.6875rem;
color: var(--text-muted);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-text strong {
display: block;
font-size: 0.8125rem;
color: var(--text-primary);
font-weight: 600;
margin-top: 0.125rem;
}
/* Sidebar Overlay */
.admin-sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
transition: opacity 0.3s ease;
}
.admin-sidebar-overlay.show {
display: block;
opacity: 1;
}
/* Main Wrapper */
.admin-wrapper {
margin-left: 260px;
min-height: 100vh;
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Topbar - Clean & Minimal */
.admin-topbar {
position: sticky;
top: 0;
z-index: 900;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
padding: 1rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.topbar-left {
display: flex;
align-items: center;
gap: 1rem;
}
.topbar-icon-btn {
width: 40px;
height: 40px;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-secondary);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
}
.topbar-icon-btn:hover {
background: var(--bg-main);
border-color: var(--primary);
color: var(--primary);
}
.topbar-title-wrap small {
display: block;
font-size: 0.6875rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 600;
}
.topbar-title-wrap h5 {
margin: 0.25rem 0 0;
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
}
.topbar-actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.topbar-link-btn {
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.15s ease;
border: 1px solid var(--border);
}
.topbar-link-btn:hover {
background: var(--bg-main);
}
.topbar-user {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.375rem 0.75rem 0.375rem 0.375rem;
background: var(--bg-main);
border-radius: 999px;
border: 1px solid var(--border);
}
.user-avatar {
width: 32px;
height: 32px;
background: var(--primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.875rem;
}
/* Main Content */
.admin-content {
padding: 1.5rem;
}
/* Cards - Minimal Design */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: all 0.2s ease;
}
.card:hover {
box-shadow: var(--shadow-md);
}
.card-header {
background: var(--bg-card);
border-bottom: 1px solid var(--border-light);
padding: 1rem 1.25rem;
font-weight: 700;
font-size: 0.9375rem;
}
.card-body {
padding: 1.25rem;
}
/* Stat Cards - Simple & Clean */
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.25rem;
transition: all 0.2s ease;
position: relative;
}
.stat-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
margin-bottom: 0.75rem;
color: white;
}
.stat-icon.primary {
background: var(--primary);
}
.stat-icon.secondary {
background: var(--secondary);
}
.stat-icon.warning {
background: var(--warning);
}
.stat-icon.success {
background: var(--success);
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.25rem;
letter-spacing: -0.02em;
}
.stat-label {
font-size: 0.875rem;
color: var(--text-secondary);
font-weight: 500;
}
/* Buttons - Minimal Style */
.btn {
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.15s ease;
border: 1px solid transparent;
cursor: pointer;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
color: white;
}
.btn-success {
background: var(--success);
color: white;
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-warning {
background: var(--warning);
color: white;
}
.btn-outline-primary {
border-color: var(--primary);
color: var(--primary);
background: transparent;
}
.btn-outline-primary:hover {
background: var(--primary);
color: white;
}
.btn-outline-secondary {
border-color: var(--border);
color: var(--text-secondary);
background: transparent;
}
.btn-outline-secondary:hover {
background: var(--bg-main);
}
.btn-outline-danger {
border-color: var(--danger);
color: var(--danger);
background: transparent;
}
.btn-outline-danger:hover {
background: var(--danger);
color: white;
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
}
/* Forms - Clean Design */
.form-control, .form-select {
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.625rem 0.875rem;
font-size: 0.875rem;
transition: all 0.15s ease;
background: var(--bg-card);
}
.form-control:focus, .form-select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.1);
outline: none;
}
.form-label {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
/* Tables - Minimal & Clean */
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.table thead th {
background: var(--bg-main);
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
font-size: 0.6875rem;
letter-spacing: 0.1em;
padding: 0.75rem 1rem;
border: none;
border-bottom: 1px solid var(--border);
}
.table tbody td {
padding: 1rem;
border-bottom: 1px solid var(--border-light);
vertical-align: middle;
font-size: 0.875rem;
}
.table tbody tr {
transition: background 0.15s ease;
}
.table tbody tr:hover {
background: var(--bg-main);
}
/* Badges - Simple Design */
.badge {
padding: 0.375rem 0.75rem;
border-radius: 6px;
font-weight: 600;
font-size: 0.75rem;
display: inline-flex;
align-items: center;
gap: 0.375rem;
}
.bg-primary, .badge-primary {
background: var(--primary);
color: white;
}
.bg-success, .badge-success {
background: var(--success);
color: white;
}
.bg-warning, .badge-warning {
background: var(--warning);
color: white;
}
.bg-danger, .badge-danger {
background: var(--danger);
color: white;
}
.bg-info, .badge-info {
background: var(--info);
color: white;
}
/* Alerts - Minimal */
.alert {
border: 1px solid;
border-radius: 8px;
padding: 1rem;
font-size: 0.875rem;
}
.alert-info {
background: #f0f9ff;
border-color: #bae6fd;
color: #075985;
}
.alert-success {
background: #f0fdf4;
border-color: #bbf7d0;
color: #166534;
}
.alert-warning {
background: #fffbeb;
border-color: #fde68a;
color: #92400e;
}
.alert-danger {
background: #fef2f2;
border-color: #fecaca;
color: #991b1b;
}
/* Collapsed Sidebar */
body.admin-collapsed .admin-sidebar {
width: 70px;
}
body.admin-collapsed .admin-wrapper {
margin-left: 70px;
}
body.admin-collapsed .sidebar-brand-copy,
body.admin-collapsed .menu-label,
body.admin-collapsed .menu-section,
body.admin-collapsed .status-text {
display: none;
}
body.admin-collapsed .menu-item {
justify-content: center;
padding: 0.625rem;
}
body.admin-collapsed .sidebar-status {
justify-content: center;
}
/* Responsive Design */
@media (max-width: 991.98px) {
.admin-sidebar {
transform: translateX(-100%);
}
.admin-sidebar.show {
transform: translateX(0);
}
.admin-wrapper {
margin-left: 0;
}
body.admin-collapsed .admin-wrapper {
margin-left: 0;
}
}
@media (max-width: 767.98px) {
.admin-topbar {
padding: 0.75rem 1rem;
}
.topbar-title-wrap h5 {
font-size: 1.125rem;
}
.admin-content {
padding: 1rem;
}
.stat-card {
padding: 1rem;
}
.stat-value {
font-size: 1.5rem;
}
.topbar-user > div:not(.user-avatar) {
display: none;
}
}
@media (max-width: 575.98px) {
.topbar-link-btn {
display: none !important;
}
.stat-icon {
width: 40px;
height: 40px;
font-size: 1.125rem;
}
}
/* Dropdown Menu */
.dropdown-menu {
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow-lg);
padding: 0.5rem;
background: var(--bg-card);
}
.dropdown-item {
border-radius: 6px;
padding: 0.5rem 0.75rem;
transition: background 0.15s ease;
font-size: 0.875rem;
font-weight: 500;
}
.dropdown-item:hover {
background: var(--bg-main);
}
/* DataTables Styling */
.dataTables_wrapper .dataTables_length select,
.dataTables_wrapper .dataTables_filter input {
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.375rem 0.625rem;
font-size: 0.875rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
border-radius: 6px !important;
margin: 0 2px;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: var(--primary) !important;
color: white !important;
border: none !important;
}
/* Utility Classes */
.text-primary { color: var(--primary) !important; }
.text-success { color: var(--success) !important; }
.text-warning { color: var(--warning) !important; }
.text-danger { color: var(--danger) !important; }
.text-muted { color: var(--text-muted) !important; }
.fw-semibold { font-weight: 600 !important; }
.fw-bold { font-weight: 700 !important; }
.rounded-2 { border-radius: 8px !important; }
.rounded-3 { border-radius: 12px !important; }
.rounded-circle { border-radius: 50% !important; }
/* Fade Animation */
.fade-in {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Page Shell */
.page-shell {
max-width: 1400px;
margin: 0 auto;
}
/* Additional Spacing */
.mb-4 { margin-bottom: 1.5rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 1rem; }
.mt-4 { margin-top: 1.5rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 1rem; }
.gap-4 { gap: 1.5rem; }
/* Grid System */
.g-4 { gap: 1.5rem; }
.g-3 { gap: 1rem; }
/* Display Utilities */
.d-flex { display: flex; }
.d-block { display: block; }
.d-inline-flex { display: inline-flex; }
.align-items-center { align-items: center; }
.justify-content-between { justify-content: space-between; }
.flex-wrap { flex-wrap: wrap; }

View File

@ -0,0 +1,856 @@
/* Modern Teal Admin Dashboard - Full Implementation */
:root {
/* Teal Color Palette */
--teal-50: #f0fdfa;
--teal-100: #ccfbf1;
--teal-200: #99f6e4;
--teal-300: #5eead4;
--teal-400: #2dd4bf;
--teal-500: #14b8a6;
--teal-600: #0d9488;
--teal-700: #0f766e;
--teal-800: #115e59;
--teal-900: #134e4a;
/* Semantic Colors */
--primary: var(--teal-600);
--primary-dark: var(--teal-700);
--primary-light: var(--teal-500);
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #06b6d4;
/* Neutral Colors */
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
/* Background */
--bg-main: var(--teal-50);
--bg-card: #ffffff;
--text-primary: var(--gray-900);
--text-secondary: var(--gray-600);
--text-muted: var(--gray-400);
--border: var(--teal-100);
--border-light: var(--teal-50);
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(13, 148, 136, 0.05);
--shadow: 0 1px 3px 0 rgba(13, 148, 136, 0.1), 0 1px 2px -1px rgba(13, 148, 136, 0.1);
--shadow-md: 0 4px 6px -1px rgba(13, 148, 136, 0.1), 0 2px 4px -2px rgba(13, 148, 136, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(13, 148, 136, 0.1), 0 4px 6px -4px rgba(13, 148, 136, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(13, 148, 136, 0.1), 0 8px 10px -6px rgba(13, 148, 136, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body.admin-body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-main);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Sidebar - Teal Theme */
.admin-sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 280px;
background: linear-gradient(180deg, var(--teal-800) 0%, var(--teal-900) 100%);
z-index: 1000;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: var(--shadow-xl);
}
.sidebar-brand {
padding: 1.5rem 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-brand-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-brand-link {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
}
.sidebar-brand-mark {
width: 44px;
height: 44px;
background: rgba(255, 255, 255, 0.15);
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
}
.sidebar-brand-copy strong {
display: block;
font-size: 1.25rem;
font-weight: 700;
color: white;
letter-spacing: -0.02em;
}
.sidebar-brand-copy small {
display: block;
font-size: 0.75rem;
color: var(--teal-200);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.125rem;
}
.sidebar-menu {
flex: 1;
overflow-y: auto;
padding: 1rem 0.75rem;
}
.sidebar-menu::-webkit-scrollbar {
width: 4px;
}
.sidebar-menu::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
.menu-section {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--teal-300);
padding: 1rem 0.75rem 0.5rem;
margin-top: 0.5rem;
}
.menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
margin-bottom: 0.25rem;
border-radius: 10px;
text-decoration: none;
color: var(--teal-100);
font-weight: 500;
font-size: 0.9375rem;
transition: all 0.2s ease;
position: relative;
}
.menu-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.menu-item.active {
background: var(--teal-600);
color: white;
font-weight: 600;
box-shadow: 0 4px 12px rgba(20, 184, 166, 0.3);
}
.menu-icon {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.125rem;
flex-shrink: 0;
}
.menu-label {
flex: 1;
}
.sidebar-footer {
padding: 1rem 1.25rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-status {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.08);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.status-dot {
width: 8px;
height: 8px;
background: var(--success);
border-radius: 50%;
flex-shrink: 0;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text small {
display: block;
font-size: 0.6875rem;
color: var(--teal-300);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-text strong {
display: block;
font-size: 0.8125rem;
color: white;
font-weight: 600;
margin-top: 0.125rem;
}
/* Sidebar Overlay */
.admin-sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
transition: opacity 0.3s ease;
}
.admin-sidebar-overlay.show {
display: block;
opacity: 1;
}
/* Main Wrapper */
.admin-wrapper {
margin-left: 280px;
min-height: 100vh;
transition: margin-left 0.3s ease;
}
/* Topbar - Teal Accent */
.admin-topbar {
position: sticky;
top: 0;
z-index: 900;
background: white;
border-bottom: 2px solid var(--teal-100);
padding: 1rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--shadow-sm);
}
.topbar-left {
display: flex;
align-items: center;
gap: 1rem;
}
.topbar-icon-btn {
width: 42px;
height: 42px;
border: 2px solid var(--teal-100);
background: white;
color: var(--teal-700);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.topbar-icon-btn:hover {
background: var(--teal-600);
border-color: var(--teal-600);
color: white;
}
.topbar-title-wrap small {
display: block;
font-size: 0.6875rem;
color: var(--teal-600);
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
}
.topbar-title-wrap h5 {
margin: 0.25rem 0 0;
font-size: 1.5rem;
font-weight: 700;
color: var(--teal-900);
letter-spacing: -0.02em;
}
.topbar-actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.topbar-link-btn {
padding: 0.625rem 1.25rem;
border-radius: 10px;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.2s ease;
border: 2px solid var(--teal-100);
color: var(--teal-700);
}
.topbar-link-btn:hover {
background: var(--teal-50);
border-color: var(--teal-200);
}
.topbar-user {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.375rem 0.875rem 0.375rem 0.375rem;
background: var(--teal-50);
border-radius: 999px;
border: 2px solid var(--teal-100);
}
.user-avatar {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--teal-600), var(--teal-500));
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.875rem;
}
/* Main Content */
.admin-content {
padding: 2rem 1.5rem;
}
/* Cards - Teal Accent */
.card {
background: white;
border: 2px solid var(--teal-100);
border-radius: 16px;
box-shadow: var(--shadow);
overflow: hidden;
transition: all 0.2s ease;
}
.card:hover {
box-shadow: var(--shadow-lg);
border-color: var(--teal-200);
}
.card-header {
background: linear-gradient(135deg, var(--teal-50), white);
border-bottom: 2px solid var(--teal-100);
padding: 1.25rem 1.5rem;
font-weight: 700;
font-size: 1rem;
color: var(--teal-900);
}
.card-body {
padding: 1.5rem;
}
/* Stat Cards - Teal Theme */
.stat-card {
background: white;
border: 2px solid var(--teal-100);
border-radius: 16px;
padding: 1.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--teal-600), var(--teal-400));
}
.stat-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-4px);
border-color: var(--teal-200);
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 1rem;
color: white;
}
.stat-icon.primary {
background: linear-gradient(135deg, var(--teal-600), var(--teal-500));
}
.stat-icon.secondary {
background: linear-gradient(135deg, var(--teal-700), var(--teal-600));
}
.stat-icon.warning {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
}
.stat-icon.success {
background: linear-gradient(135deg, #10b981, #34d399);
}
.stat-value {
font-size: 2rem;
font-weight: 800;
color: var(--teal-900);
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.stat-label {
font-size: 0.9375rem;
color: var(--text-secondary);
font-weight: 600;
}
/* Buttons - Teal Theme */
.btn {
padding: 0.625rem 1.25rem;
border-radius: 10px;
font-weight: 600;
font-size: 0.9375rem;
transition: all 0.2s ease;
border: 2px solid transparent;
cursor: pointer;
}
.btn-primary {
background: linear-gradient(135deg, var(--teal-600), var(--teal-500));
color: white;
border-color: var(--teal-600);
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--teal-700), var(--teal-600));
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #10b981, #34d399);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, #ef4444, #f87171);
color: white;
}
.btn-warning {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
color: white;
}
.btn-outline-primary {
border-color: var(--teal-600);
color: var(--teal-700);
background: white;
}
.btn-outline-primary:hover {
background: var(--teal-600);
color: white;
}
.btn-outline-secondary {
border-color: var(--teal-200);
color: var(--teal-700);
background: white;
}
.btn-outline-secondary:hover {
background: var(--teal-50);
border-color: var(--teal-300);
}
.btn-outline-danger {
border-color: #ef4444;
color: #ef4444;
background: white;
}
.btn-outline-danger:hover {
background: #ef4444;
color: white;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.btn-lg {
padding: 0.875rem 1.75rem;
font-size: 1rem;
}
/* Forms - Teal Accent */
.form-control, .form-select {
border: 2px solid var(--teal-100);
border-radius: 10px;
padding: 0.75rem 1rem;
font-size: 0.9375rem;
transition: all 0.2s ease;
background: white;
}
.form-control:focus, .form-select:focus {
border-color: var(--teal-500);
box-shadow: 0 0 0 4px var(--teal-50);
outline: none;
}
.form-label {
font-weight: 700;
color: var(--teal-900);
margin-bottom: 0.5rem;
font-size: 0.9375rem;
}
.input-group-text {
border: 2px solid var(--teal-100);
background: var(--teal-50);
color: var(--teal-700);
font-weight: 600;
}
/* Tables - Teal Theme */
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.table thead th {
background: linear-gradient(135deg, var(--teal-50), white);
color: var(--teal-900);
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.1em;
padding: 1rem;
border: none;
border-bottom: 2px solid var(--teal-100);
}
.table tbody td {
padding: 1rem;
border-bottom: 1px solid var(--teal-50);
vertical-align: middle;
font-size: 0.9375rem;
}
.table tbody tr {
transition: background 0.2s ease;
}
.table tbody tr:hover {
background: var(--teal-50);
}
/* Badges - Teal Theme */
.badge {
padding: 0.5rem 0.875rem;
border-radius: 8px;
font-weight: 700;
font-size: 0.8125rem;
display: inline-flex;
align-items: center;
gap: 0.375rem;
}
.bg-primary, .badge-primary {
background: linear-gradient(135deg, var(--teal-600), var(--teal-500));
color: white;
}
.bg-success, .badge-success {
background: linear-gradient(135deg, #10b981, #34d399);
color: white;
}
.bg-warning, .badge-warning {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
color: white;
}
.bg-danger, .badge-danger {
background: linear-gradient(135deg, #ef4444, #f87171);
color: white;
}
.bg-info, .badge-info {
background: linear-gradient(135deg, #06b6d4, #22d3ee);
color: white;
}
/* Alerts - Teal Theme */
.alert {
border: 2px solid;
border-radius: 12px;
padding: 1rem 1.25rem;
font-size: 0.9375rem;
font-weight: 500;
}
.alert-info {
background: var(--teal-50);
border-color: var(--teal-200);
color: var(--teal-900);
}
.alert-success {
background: #f0fdf4;
border-color: #bbf7d0;
color: #166534;
}
.alert-warning {
background: #fffbeb;
border-color: #fde68a;
color: #92400e;
}
.alert-danger {
background: #fef2f2;
border-color: #fecaca;
color: #991b1b;
}
/* Breadcrumb - Teal */
.breadcrumb {
background: white;
border: 2px solid var(--teal-100);
border-radius: 10px;
padding: 0.75rem 1rem;
}
.breadcrumb-item a {
color: var(--teal-600);
font-weight: 600;
}
.breadcrumb-item.active {
color: var(--teal-900);
font-weight: 700;
}
/* Collapsed Sidebar */
body.admin-collapsed .admin-sidebar {
width: 70px;
}
body.admin-collapsed .admin-wrapper {
margin-left: 70px;
}
body.admin-collapsed .sidebar-brand-copy,
body.admin-collapsed .menu-label,
body.admin-collapsed .menu-section,
body.admin-collapsed .status-text {
display: none;
}
body.admin-collapsed .menu-item {
justify-content: center;
padding: 0.75rem;
}
body.admin-collapsed .sidebar-status {
justify-content: center;
}
/* Responsive */
@media (max-width: 991.98px) {
.admin-sidebar {
transform: translateX(-100%);
}
.admin-sidebar.show {
transform: translateX(0);
}
.admin-wrapper {
margin-left: 0;
}
body.admin-collapsed .admin-wrapper {
margin-left: 0;
}
}
@media (max-width: 767.98px) {
.admin-topbar {
padding: 0.75rem 1rem;
}
.topbar-title-wrap h5 {
font-size: 1.25rem;
}
.admin-content {
padding: 1.5rem 1rem;
}
.stat-card {
padding: 1.25rem;
}
.stat-value {
font-size: 1.5rem;
}
.topbar-user > div:not(.user-avatar) {
display: none;
}
}
@media (max-width: 575.98px) {
.topbar-link-btn {
display: none !important;
}
}
/* Dropdown Menu */
.dropdown-menu {
border: 2px solid var(--teal-100);
border-radius: 12px;
box-shadow: var(--shadow-lg);
padding: 0.5rem;
background: white;
}
.dropdown-item {
border-radius: 8px;
padding: 0.625rem 1rem;
transition: background 0.2s ease;
font-size: 0.9375rem;
font-weight: 500;
color: var(--teal-900);
}
.dropdown-item:hover {
background: var(--teal-50);
color: var(--teal-900);
}
/* DataTables */
.dataTables_wrapper .dataTables_length select,
.dataTables_wrapper .dataTables_filter input {
border: 2px solid var(--teal-100);
border-radius: 8px;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
border-radius: 8px !important;
margin: 0 2px;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: linear-gradient(135deg, var(--teal-600), var(--teal-500)) !important;
color: white !important;
border: none !important;
}
/* Utility Classes */
.text-primary { color: var(--teal-700) !important; }
.text-success { color: #10b981 !important; }
.text-warning { color: #f59e0b !important; }
.text-danger { color: #ef4444 !important; }
.text-muted { color: var(--text-muted) !important; }
.bg-teal-50 { background: var(--teal-50) !important; }
.bg-teal-100 { background: var(--teal-100) !important; }
.border-teal { border-color: var(--teal-200) !important; }
.fw-semibold { font-weight: 600 !important; }
.fw-bold { font-weight: 700 !important; }
/* Animations */
.fade-in {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Page Shell */
.page-shell {
max-width: 1400px;
margin: 0 auto;
}

624
resources/css/admin-v2.css Normal file
View File

@ -0,0 +1,624 @@
:root {
--admin-bg: #f8fafc;
--admin-ink: #0f172a;
--admin-ink-soft: #475569;
--admin-line: #e2e8f0;
--admin-brand-900: #0c4a6e;
--admin-brand-700: #0369a1;
--admin-brand-500: #0ea5e9;
--admin-brand-400: #38bdf8;
--admin-accent: #f59e0b;
--admin-danger: #ef4444;
--admin-success: #10b981;
--admin-purple: #8b5cf6;
--admin-radius-sm: 10px;
--admin-radius: 16px;
--admin-radius-lg: 24px;
--admin-shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1);
--admin-shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--admin-shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--admin-shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
body.admin-v2 {
margin: 0;
min-height: 100vh;
font-family: 'Manrope', sans-serif;
color: var(--admin-ink);
background:
radial-gradient(760px 260px at -8% -12%, rgba(20, 184, 166, 0.18), transparent 68%),
radial-gradient(760px 300px at 108% -2%, rgba(245, 158, 11, 0.14), transparent 65%),
linear-gradient(180deg, #f5f9fc 0%, var(--admin-bg) 100%);
}
.admin-v2-backdrop {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.admin-v2-backdrop span {
position: absolute;
border-radius: 999px;
filter: blur(8px);
}
.admin-v2-backdrop .blob-a {
width: 360px;
height: 360px;
left: -140px;
top: -100px;
background: radial-gradient(circle, rgba(20, 184, 166, 0.26), rgba(20, 184, 166, 0));
}
.admin-v2-backdrop .blob-b {
width: 420px;
height: 420px;
right: -170px;
top: 24%;
background: radial-gradient(circle, rgba(14, 165, 160, 0.2), rgba(14, 165, 160, 0));
}
.admin-v2-backdrop .blob-c {
width: 340px;
height: 340px;
left: 38%;
bottom: -170px;
background: radial-gradient(circle, rgba(245, 158, 11, 0.18), rgba(245, 158, 11, 0));
}
.admin-v2 > *:not(.admin-v2-backdrop) {
position: relative;
z-index: 1;
}
.admin-v2-sidebar {
position: fixed;
inset: 1rem auto 1rem 1rem;
width: 286px;
border-radius: 24px;
background: linear-gradient(180deg, #072a38 0%, #0a3f50 50%, #0f766e 100%);
border: 1px solid rgba(255, 255, 255, 0.16);
box-shadow: var(--admin-shadow-md);
color: #f2feff;
display: flex;
flex-direction: column;
z-index: 1300;
transition: width 0.24s ease, transform 0.24s ease;
}
.admin-v2-sidebar__brand {
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}
.admin-v2-brand-link {
display: flex;
align-items: center;
gap: 0.7rem;
}
.admin-v2-brand-mark {
width: 44px;
height: 44px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.22);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.06));
display: inline-flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1rem;
}
.admin-v2-brand-copy {
display: flex;
flex-direction: column;
line-height: 1.1;
}
.admin-v2-brand-copy strong {
color: #fff;
font-size: 1.02rem;
font-family: 'Space Grotesk', sans-serif;
letter-spacing: -0.02em;
}
.admin-v2-brand-copy small {
margin-top: 0.2rem;
color: rgba(237, 253, 255, 0.72);
font-size: 0.67rem;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 700;
}
.admin-v2-sidebar__close {
width: 34px;
height: 34px;
border: 1px solid rgba(255, 255, 255, 0.24);
border-radius: 9px;
color: #fff;
background: rgba(255, 255, 255, 0.08);
display: inline-flex;
align-items: center;
justify-content: center;
}
.admin-v2-nav {
flex: 1;
overflow-y: auto;
padding: 0.7rem;
}
.admin-v2-nav::-webkit-scrollbar {
width: 8px;
}
.admin-v2-nav::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(255, 255, 255, 0.26);
}
.admin-v2-nav__section {
margin-top: 0.2rem;
margin-bottom: 0.3rem;
padding: 0.64rem 0.78rem 0.24rem;
color: rgba(236, 252, 255, 0.64);
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
}
.admin-v2-nav__item {
position: relative;
display: flex;
align-items: center;
gap: 0.66rem;
border-radius: 13px;
border: 1px solid transparent;
color: rgba(236, 252, 255, 0.92);
font-weight: 700;
padding: 0.66rem 0.74rem;
margin-bottom: 0.24rem;
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.admin-v2-nav__item:hover {
color: #fff;
border-color: rgba(255, 255, 255, 0.22);
background: rgba(255, 255, 255, 0.1);
transform: translateX(1px);
}
.admin-v2-nav__icon {
width: 34px;
height: 34px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.14);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.92rem;
color: #fff;
flex-shrink: 0;
}
.admin-v2-nav__item.active {
color: #fff;
border-color: rgba(125, 227, 213, 0.62);
background: linear-gradient(135deg, rgba(20, 184, 166, 0.35), rgba(20, 184, 166, 0.16));
}
.admin-v2-nav__item.active::after {
content: '';
position: absolute;
right: 10px;
width: 8px;
height: 8px;
border-radius: 999px;
background: #9ef4e7;
box-shadow: 0 0 0 6px rgba(158, 244, 231, 0.2);
}
.admin-v2-sidebar__footer {
border-top: 1px solid rgba(255, 255, 255, 0.14);
padding: 0.88rem 1rem 0.98rem;
background: rgba(0, 0, 0, 0.12);
}
.admin-v2-status {
display: flex;
align-items: center;
gap: 0.72rem;
}
.admin-v2-status__dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: #4ade80;
box-shadow: 0 0 0 7px rgba(74, 222, 128, 0.2);
}
.admin-v2-status__text small {
display: block;
color: rgba(236, 252, 255, 0.68);
font-size: 0.66rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.admin-v2-status__text strong {
color: #f2feff;
font-size: 0.82rem;
}
.admin-v2-main {
margin-left: 312px;
min-height: 100vh;
padding: 1rem 1rem 1rem 0;
transition: margin-left 0.24s ease;
}
.admin-v2-topbar {
position: sticky;
top: 1rem;
z-index: 1200;
min-height: 74px;
border-radius: 18px;
border: 1px solid rgba(16, 38, 50, 0.1);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
box-shadow: var(--admin-shadow-xs);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
}
.admin-v2-topbar__left {
display: flex;
align-items: center;
gap: 0.66rem;
}
.admin-v2-icon-btn {
width: 40px;
height: 40px;
border-radius: 11px;
border: 1px solid rgba(16, 38, 50, 0.14);
background: #fff;
color: var(--admin-ink-soft);
display: inline-flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.admin-v2-icon-btn:hover {
color: var(--admin-brand-700);
border-color: rgba(15, 118, 110, 0.34);
background: rgba(20, 184, 166, 0.08);
}
.admin-v2-topbar__title small {
display: block;
color: #6c8596;
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 800;
}
.admin-v2-topbar__title h1 {
margin: 0.04rem 0 0;
font-family: 'Space Grotesk', sans-serif;
font-size: 1.08rem;
color: var(--admin-ink);
letter-spacing: -0.02em;
}
.admin-v2-topbar__right {
display: flex;
align-items: center;
gap: 0.58rem;
}
.admin-v2-quick-link {
border-radius: 11px;
}
.admin-v2-meta {
display: flex;
align-items: center;
gap: 0.48rem;
border-radius: 999px;
border: 1px solid rgba(16, 38, 50, 0.1);
background: rgba(255, 255, 255, 0.96);
padding: 0.26rem 0.62rem 0.26rem 0.26rem;
}
.admin-v2-meta__avatar {
width: 38px;
height: 38px;
border-radius: 50%;
background: linear-gradient(135deg, var(--admin-brand-900), var(--admin-brand-500));
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.95rem;
font-weight: 800;
}
.admin-v2-meta__copy strong {
display: block;
color: var(--admin-ink);
font-size: 0.84rem;
line-height: 1.1;
}
.admin-v2-meta__copy small {
color: #6f8898;
font-size: 0.72rem;
}
.admin-v2-content {
padding: 1rem 0 0;
}
.admin-v2-content .container-fluid {
max-width: 1400px;
padding: 0;
}
.admin-page-shell {
display: flex;
flex-direction: column;
gap: 1rem;
}
.admin-page-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
border: 1px solid rgba(16, 38, 50, 0.1);
border-radius: 18px;
background: linear-gradient(180deg, #ffffff, #f4f9fd);
padding: 1rem 1.1rem;
box-shadow: var(--admin-shadow-xs);
}
.admin-page-head h4 {
margin-bottom: 0.22rem;
}
.admin-page-head p {
margin-bottom: 0;
color: #5f798b;
}
.admin-v2 .card {
border: 1px solid rgba(16, 38, 50, 0.11);
border-radius: 18px;
box-shadow: var(--admin-shadow-xs);
background: linear-gradient(180deg, #ffffff, #f9fcff);
}
.admin-v2 .card.border-0.bg-gradient-primary {
border-radius: 16px;
border: 0;
box-shadow: 0 14px 26px rgba(8, 29, 41, 0.16);
}
.admin-v2 .card-header {
border-bottom: 1px solid rgba(16, 38, 50, 0.08);
border-radius: 18px 18px 0 0 !important;
background: linear-gradient(180deg, #fcfeff, #eff6fb);
}
.admin-v2 .card-body {
padding: 1.2rem;
}
.admin-v2 .page-hero {
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: linear-gradient(130deg, #083546 0%, #0f766e 60%, #14b8a6 100%);
box-shadow: var(--admin-shadow-sm);
}
.admin-v2 .page-hero h3,
.admin-v2 .page-hero p,
.admin-v2 .page-hero strong,
.admin-v2 .page-hero small {
color: #f3fdff !important;
}
.admin-v2 .form-control,
.admin-v2 .form-select,
.admin-v2 .input-group-text {
border: 1px solid #d5e3eb;
border-radius: 11px;
min-height: 44px;
}
.admin-v2 .form-control:focus,
.admin-v2 .form-select:focus {
border-color: rgba(20, 184, 166, 0.75);
box-shadow: 0 0 0 0.2rem rgba(20, 184, 166, 0.14);
}
.admin-v2 .table thead th {
font-size: 0.71rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #4f6777;
border: 0;
background: linear-gradient(180deg, #eff6fb, #e5f0f7);
}
.admin-v2 .table tbody td {
border-color: rgba(16, 38, 50, 0.08);
}
.admin-v2 .table tbody tr:hover {
background: rgba(14, 165, 160, 0.06);
}
.admin-v2 .btn-group .btn {
border-radius: 10px !important;
}
.admin-v2 .breadcrumb {
border: 1px solid rgba(16, 38, 50, 0.11);
border-radius: 999px;
background: rgba(255, 255, 255, 0.85);
padding: 0.54rem 0.9rem;
box-shadow: var(--admin-shadow-xs);
}
.admin-v2 .breadcrumb-item,
.admin-v2 .breadcrumb-item a {
color: #6f8797;
font-weight: 700;
}
.admin-v2 .breadcrumb-item.active {
color: #0b3d4a;
}
.admin-v2 .stat-card {
height: 100%;
border: 1px solid rgba(15, 118, 110, 0.2);
border-radius: 18px;
background: linear-gradient(180deg, #ffffff, #f3fbfd);
box-shadow: var(--admin-shadow-xs);
}
.admin-v2 .stat-icon {
box-shadow: 0 12px 20px rgba(15, 118, 110, 0.2);
}
.admin-v2 .badge {
border-radius: 999px;
font-weight: 700;
}
.admin-v2 .dataTables_wrapper .dataTables_filter input,
.admin-v2 .dataTables_wrapper .dataTables_length select {
border: 1px solid #d5e3eb;
border-radius: 10px;
min-height: 38px;
}
.admin-v2 .dataTables_wrapper .dataTables_paginate .paginate_button {
border-radius: 999px !important;
border: 1px solid rgba(16, 38, 50, 0.12) !important;
background: #fff !important;
}
.admin-v2 .dataTables_wrapper .dataTables_paginate .paginate_button.current,
.admin-v2 .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
border-color: transparent !important;
color: #fff !important;
background: linear-gradient(135deg, var(--admin-brand-900), var(--admin-brand-500)) !important;
}
.admin-v2-overlay {
position: fixed;
inset: 0;
background: rgba(7, 27, 38, 0.52);
z-index: 1250;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.admin-v2-overlay.show {
opacity: 1;
visibility: visible;
}
body.admin-v2.admin-v2-collapsed .admin-v2-sidebar {
width: 94px;
}
body.admin-v2.admin-v2-collapsed .admin-v2-main {
margin-left: 120px;
}
body.admin-v2.admin-v2-collapsed .admin-v2-brand-copy,
body.admin-v2.admin-v2-collapsed .admin-v2-nav__section,
body.admin-v2.admin-v2-collapsed .admin-v2-nav__label,
body.admin-v2.admin-v2-collapsed .admin-v2-status__text,
body.admin-v2.admin-v2-collapsed .admin-v2-nav__item.active::after {
display: none;
}
body.admin-v2.admin-v2-collapsed .admin-v2-nav__item {
justify-content: center;
padding: 0.66rem;
}
body.admin-v2.admin-v2-collapsed .admin-v2-status {
justify-content: center;
}
@media (max-width: 991.98px) {
.admin-v2-sidebar {
inset: 0 auto 0 0;
width: 286px;
border-radius: 0;
transform: translateX(-100%);
}
.admin-v2-sidebar.show {
transform: translateX(0);
}
.admin-v2-main,
body.admin-v2.admin-v2-collapsed .admin-v2-main {
margin-left: 0;
padding: 0.76rem;
}
.admin-v2-topbar {
top: 0;
border-radius: 14px;
}
.admin-v2-content {
padding-top: 0.8rem;
}
.admin-v2-content .container-fluid {
padding: 0;
}
.admin-page-head {
flex-direction: column;
align-items: flex-start;
}
}
@media (max-width: 767.98px) {
.admin-v2-topbar {
padding: 0.68rem 0.75rem;
}
.admin-v2-topbar__right .admin-v2-quick-link {
display: none !important;
}
.admin-v2-meta__copy {
display: none;
}
}

1945
resources/css/app.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,873 @@
/* Modern Admin Dashboard CSS - Matching Main Theme */
:root {
--primary: #0f766e;
--primary-dark: #0b3d4a;
--primary-light: #14b8a6;
--secondary: #365161;
--success: #16a34a;
--warning: #d97706;
--danger: #dc2626;
--info: #2563eb;
--purple: #7c3aed;
--orange: #ea580c;
--dark: #102632;
--light: #edf4f7;
--gray-50: #f7fafc;
--gray-100: #f4f9fc;
--gray-200: #e5eff4;
--gray-300: #d3e2ea;
--gray-400: #9eb2be;
--gray-500: #6f8796;
--gray-600: #4b5563;
--gray-700: #365161;
--gray-800: #1f2937;
--gray-900: #102632;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body.admin-body {
font-family: 'Manrope', 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
background:
radial-gradient(760px 250px at -4% -14%, rgba(20, 184, 166, 0.2), transparent 64%),
radial-gradient(860px 260px at 104% 8%, rgba(234, 88, 12, 0.12), transparent 64%),
linear-gradient(180deg, #f4f8fb 0%, #edf3f7 100%);
background-attachment: fixed;
min-height: 100vh;
color: var(--dark);
overflow-x: hidden;
}
.admin-backdrop {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.admin-backdrop .orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.5;
animation: float 25s ease-in-out infinite;
}
.admin-backdrop .orb-b {
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(20, 184, 166, 0.3), transparent);
top: -100px;
right: -100px;
animation-delay: -5s;
}
.admin-backdrop .orb-c {
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(234, 88, 12, 0.2), transparent);
bottom: -50px;
left: -50px;
animation-delay: -10s;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -30px) scale(1.1); }
66% { transform: translate(-20px, 20px) scale(0.9); }
}
/* Sidebar Styles */
.admin-sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 294px;
background: linear-gradient(180deg, #062634 0%, #0b3f50 48%, #0f766e 100%);
border-right: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 4px 0 32px rgba(6, 38, 52, 0.2);
z-index: 1000;
display: flex;
flex-direction: column;
transition: transform 0.3s ease, width 0.3s ease;
}
.sidebar-brand {
padding: 1.5rem 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}
.sidebar-brand-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-brand-link {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
color: white;
}
.sidebar-brand-mark {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.24);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
backdrop-filter: blur(10px);
}
.sidebar-brand-copy {
display: flex;
flex-direction: column;
line-height: 1.1;
}
.sidebar-brand-copy strong {
display: block;
font-size: 1.25rem;
font-weight: 800;
color: white;
font-family: 'Outfit', sans-serif;
}
.sidebar-brand-copy small {
display: block;
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.75);
text-transform: uppercase;
letter-spacing: 1.2px;
font-weight: 700;
margin-top: 0.25rem;
}
.sidebar-menu {
flex: 1;
overflow-y: auto;
padding: 0.75rem 0.5rem;
}
.sidebar-menu::-webkit-scrollbar {
width: 6px;
}
.sidebar-menu::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
.menu-section {
font-size: 0.7rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 1.2px;
color: rgba(217, 245, 239, 0.66);
padding: 0.85rem 1rem 0.5rem;
margin-top: 0.5rem;
}
.menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
margin: 0.25rem 0.5rem;
border-radius: 13px;
text-decoration: none;
color: rgba(236, 250, 247, 0.94);
font-weight: 700;
transition: all 0.2s ease;
position: relative;
border-left: 3px solid transparent;
}
.menu-item:hover {
background: rgba(255, 255, 255, 0.11);
color: white;
transform: translateX(2px);
}
.menu-item.active {
background: rgba(20, 184, 166, 0.29);
color: white;
border-left-color: #9bf7dd;
box-shadow: 0 4px 12px rgba(20, 184, 166, 0.2);
}
.menu-item.active::after {
content: '';
position: absolute;
right: 12px;
width: 8px;
height: 8px;
background: #9bf7dd;
border-radius: 50%;
box-shadow: 0 0 0 4px rgba(155, 247, 221, 0.2);
}
.menu-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 11px;
background: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
font-size: 1.1rem;
transition: all 0.2s ease;
flex-shrink: 0;
}
.menu-item:hover .menu-icon {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
}
.menu-item.active .menu-icon {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.35);
}
.menu-label {
font-size: 0.95rem;
flex: 1;
}
.sidebar-footer {
padding: 1rem 1.25rem;
border-top: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(0, 0, 0, 0.12);
}
.sidebar-status {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.08);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.status-dot {
width: 12px;
height: 12px;
background: #22c55e;
border-radius: 50%;
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.2);
animation: pulse 2s ease-in-out infinite;
flex-shrink: 0;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text small {
display: block;
font-size: 0.7rem;
color: rgba(217, 245, 239, 0.68);
text-transform: uppercase;
letter-spacing: 0.8px;
font-weight: 700;
}
.status-text strong {
display: block;
font-size: 0.85rem;
color: white;
margin-top: 2px;
}
/* Sidebar Overlay for Mobile */
.admin-sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(16, 38, 50, 0.6);
backdrop-filter: blur(4px);
z-index: 999;
opacity: 0;
transition: opacity 0.3s ease;
}
.admin-sidebar-overlay.show {
display: block;
opacity: 1;
}
/* Main Wrapper */
.admin-wrapper {
margin-left: 294px;
min-height: 100vh;
transition: margin-left 0.3s ease;
}
/* Topbar Styles */
.admin-topbar {
position: sticky;
top: 0;
z-index: 900;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(16, 38, 50, 0.08);
padding: 1rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 16px rgba(16, 38, 50, 0.06);
}
.topbar-left {
display: flex;
align-items: center;
gap: 1rem;
}
.topbar-icon-btn {
width: 44px;
height: 44px;
border: 1px solid rgba(16, 38, 50, 0.12);
background: white;
color: var(--gray-700);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.topbar-icon-btn:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
transform: scale(1.05);
}
.topbar-title-wrap small {
display: block;
font-size: 0.7rem;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 1.2px;
font-weight: 800;
}
.topbar-title-wrap h5 {
margin: 0.25rem 0 0;
font-size: 1.5rem;
font-weight: 800;
color: var(--dark);
font-family: 'Outfit', sans-serif;
}
.topbar-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.topbar-link-btn {
padding: 0.625rem 1.25rem;
border-radius: 12px;
font-weight: 700;
transition: all 0.2s ease;
border: 1px solid rgba(16, 38, 50, 0.12);
}
.topbar-link-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.topbar-user {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 1rem 0.5rem 0.5rem;
background: var(--gray-50);
border-radius: 999px;
border: 1px solid rgba(16, 38, 50, 0.08);
}
.user-avatar {
width: 44px;
height: 44px;
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-light) 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 1.1rem;
box-shadow: 0 4px 12px rgba(15, 118, 110, 0.3);
}
/* Main Content */
.admin-content {
padding: 2rem 1.5rem;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Cards */
.card {
background: linear-gradient(180deg, #ffffff, #f7fcff);
border: 1px solid rgba(16, 38, 50, 0.12);
border-radius: 20px;
box-shadow: 0 6px 18px rgba(10, 34, 46, 0.08);
overflow: hidden;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 14px 32px rgba(10, 34, 46, 0.13);
transform: translateY(-4px);
}
.card-header {
background: linear-gradient(180deg, #fcfeff, #eef6f9);
border-bottom: 1px solid rgba(16, 38, 50, 0.09);
padding: 1.25rem 1.5rem;
font-weight: 800;
}
.card-body {
padding: 1.5rem;
}
/* Stat Cards */
.stat-card {
background: linear-gradient(180deg, #ffffff, #f5fbfc);
border-radius: 20px;
padding: 1.5rem;
box-shadow: 0 6px 18px rgba(10, 34, 46, 0.08);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border: 1px solid rgba(15, 118, 110, 0.18);
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary-dark) 0%, var(--primary-light) 100%);
}
.stat-card:hover {
transform: translateY(-8px);
box-shadow: 0 16px 40px rgba(10, 34, 46, 0.16);
}
.stat-icon {
width: 64px;
height: 64px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.75rem;
margin-bottom: 1rem;
color: white;
box-shadow: 0 14px 24px rgba(15, 118, 110, 0.2);
}
.stat-icon.primary {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-light) 100%);
}
.stat-icon.secondary {
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
}
.stat-icon.warning {
background: linear-gradient(135deg, #d97706 0%, #f59e0b 100%);
}
.stat-icon.success {
background: linear-gradient(135deg, #15803d 0%, #22c55e 100%);
}
.stat-value {
font-size: 2rem;
font-weight: 800;
color: var(--dark);
margin-bottom: 0.5rem;
font-family: 'Outfit', sans-serif;
}
.stat-label {
font-size: 0.95rem;
color: var(--gray-600);
font-weight: 700;
}
/* Buttons */
.btn {
padding: 0.625rem 1.5rem;
border-radius: 12px;
font-weight: 700;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-light) 100%);
color: white;
box-shadow: 0 12px 24px rgba(15, 118, 110, 0.24);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 16px 30px rgba(15, 118, 110, 0.3);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #15803d 0%, #22c55e 100%);
box-shadow: 0 12px 24px rgba(22, 163, 74, 0.24);
}
.btn-danger {
background: linear-gradient(135deg, #b91c1c 0%, #ef4444 100%);
box-shadow: 0 12px 24px rgba(220, 38, 38, 0.24);
}
.btn-warning {
background: linear-gradient(135deg, #b45309 0%, #f59e0b 100%);
box-shadow: 0 12px 24px rgba(217, 119, 6, 0.24);
}
.btn-outline-primary {
border-color: rgba(15, 118, 110, 0.36);
color: var(--primary);
background: white;
}
.btn-outline-primary:hover {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-light) 100%);
color: white;
border-color: transparent;
}
.btn-outline-secondary {
border-color: rgba(53, 81, 97, 0.3);
color: var(--gray-700);
background: white;
}
.btn-outline-secondary:hover {
background: var(--gray-100);
border-color: rgba(53, 81, 97, 0.45);
}
.btn-outline-danger {
border-color: rgba(220, 38, 38, 0.36);
color: var(--danger);
background: white;
}
.btn-outline-danger:hover {
background: linear-gradient(135deg, #b91c1c 0%, #ef4444 100%);
color: white;
border-color: transparent;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
/* Forms */
.form-control, .form-select {
border: 1px solid #d3e2ea;
border-radius: 11px;
min-height: 46px;
padding: 0.65rem 0.86rem;
transition: all 0.2s ease;
}
.form-control:focus, .form-select:focus {
border-color: rgba(20, 184, 166, 0.8);
box-shadow: 0 0 0 0.22rem rgba(20, 184, 166, 0.16);
}
.form-label {
font-weight: 700;
color: var(--gray-700);
margin-bottom: 0.5rem;
}
/* Tables */
.table {
border-collapse: separate;
border-spacing: 0;
}
.table thead th {
background: linear-gradient(180deg, #eff6fa, #e5f0f6);
color: #496273;
font-weight: 800;
text-transform: uppercase;
font-size: 0.72rem;
letter-spacing: 1.2px;
padding: 1rem;
border: none;
}
.table tbody td {
padding: 1rem;
border-top: 1px solid rgba(16, 38, 50, 0.08);
vertical-align: middle;
}
.table tbody tr {
transition: all 0.2s ease;
}
.table tbody tr:hover {
background: rgba(14, 165, 160, 0.06);
}
/* Badges */
.badge {
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 700;
font-size: 0.8rem;
}
.bg-primary {
background: linear-gradient(135deg, var(--primary-dark), var(--primary-light)) !important;
}
.bg-success {
background: linear-gradient(135deg, #15803d, #22c55e) !important;
}
.bg-warning {
background: linear-gradient(135deg, #b45309, #f59e0b) !important;
}
.bg-danger {
background: linear-gradient(135deg, #b91c1c, #ef4444) !important;
}
.bg-info {
background: linear-gradient(135deg, #1d4ed8, #2563eb) !important;
}
/* Alerts */
.alert {
border: none;
border-radius: 12px;
padding: 1rem 1.25rem;
box-shadow: 0 6px 18px rgba(10, 34, 46, 0.08);
}
/* Collapsed Sidebar */
body.admin-collapsed .admin-sidebar {
width: 80px;
}
body.admin-collapsed .admin-wrapper {
margin-left: 80px;
}
body.admin-collapsed .sidebar-brand-copy,
body.admin-collapsed .menu-label,
body.admin-collapsed .menu-section,
body.admin-collapsed .status-text {
display: none;
}
body.admin-collapsed .menu-item {
justify-content: center;
padding: 0.75rem;
}
body.admin-collapsed .sidebar-status {
justify-content: center;
}
body.admin-collapsed .menu-item.active::after {
display: none;
}
/* Responsive */
@media (max-width: 991.98px) {
.admin-sidebar {
transform: translateX(-100%);
}
.admin-sidebar.show {
transform: translateX(0);
}
.admin-wrapper {
margin-left: 0;
}
body.admin-collapsed .admin-wrapper {
margin-left: 0;
}
.admin-content {
padding: 1.5rem 1rem;
}
}
@media (max-width: 767.98px) {
.admin-topbar {
padding: 0.75rem 1rem;
}
.topbar-title-wrap h5 {
font-size: 1.25rem;
}
.admin-content {
padding: 1rem;
}
.stat-card {
padding: 1.25rem;
}
.stat-value {
font-size: 1.5rem;
}
.topbar-user {
padding: 0.35rem 0.65rem 0.35rem 0.36rem;
}
.topbar-user > div {
display: none;
}
}
@media (max-width: 575.98px) {
.topbar-link-btn {
display: none !important;
}
.stat-icon {
width: 56px;
height: 56px;
font-size: 1.5rem;
}
}
/* Fade In Animation */
.fade-in {
animation: fadeIn 0.5s ease;
}
/* Page Shell */
.page-shell {
max-width: 1400px;
margin: 0 auto;
}
/* Dropdown Menu */
.dropdown-menu {
border: 1px solid rgba(16, 38, 50, 0.12);
border-radius: 12px;
box-shadow: 0 14px 32px rgba(10, 34, 46, 0.13);
padding: 0.5rem;
}
.dropdown-item {
border-radius: 8px;
padding: 0.75rem 1rem;
transition: all 0.2s ease;
font-weight: 700;
}
.dropdown-item:hover {
background: var(--gray-100);
}
/* DataTables Custom Styling */
.dataTables_wrapper .dataTables_length select,
.dataTables_wrapper .dataTables_filter input {
border: 1px solid #d3e2ea;
border-radius: 8px;
padding: 0.5rem 0.75rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
border-radius: 8px !important;
margin: 0 2px;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-light) 100%) !important;
color: white !important;
border: none !important;
}
/* Utility Classes */
.text-primary {
color: var(--primary) !important;
}
.text-success {
color: var(--success) !important;
}
.text-warning {
color: var(--warning) !important;
}
.text-danger {
color: var(--danger) !important;
}
.fw-semibold {
font-weight: 600 !important;
}
.fw-bold {
font-weight: 700 !important;
}
.rounded-2 {
border-radius: 8px !important;
}
.rounded-3 {
border-radius: 12px !important;
}
.rounded-circle {
border-radius: 50% !important;
}

388
resources/css/modern-ui.css Normal file
View File

@ -0,0 +1,388 @@
/* ========================================
BANYU BIRU - Ultra Modern UI
Glassmorphism & Gradient Design
======================================== */
/* Glassmorphism Cards */
.glass-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
border-radius: 24px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 16px 48px 0 rgba(31, 38, 135, 0.25);
border-color: rgba(255, 255, 255, 0.5);
}
/* Gradient Backgrounds */
.gradient-ocean {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.gradient-sunset {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.gradient-forest {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.gradient-fire {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
/* Animated Gradient Background */
.animated-gradient {
background: linear-gradient(-45deg, #667eea, #764ba2, #f093fb, #4facfe);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Floating Animation */
.float-animation {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
/* Pulse Animation */
.pulse-animation {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Glow Effect */
.glow-effect {
box-shadow: 0 0 20px rgba(102, 126, 234, 0.6),
0 0 40px rgba(102, 126, 234, 0.4),
0 0 60px rgba(102, 126, 234, 0.2);
}
/* Neon Border */
.neon-border {
position: relative;
border: 2px solid transparent;
background: linear-gradient(white, white) padding-box,
linear-gradient(135deg, #667eea, #764ba2) border-box;
border-radius: 24px;
}
/* 3D Card Effect */
.card-3d {
transform-style: preserve-3d;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-3d:hover {
transform: rotateY(5deg) rotateX(5deg);
}
/* Morphing Button */
.morph-button {
position: relative;
overflow: hidden;
border-radius: 50px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.morph-button::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.morph-button:hover::before {
width: 300px;
height: 300px;
}
/* Parallax Effect */
.parallax-layer {
transition: transform 0.3s ease-out;
}
/* Neumorphism */
.neuro-card {
background: #e0e5ec;
box-shadow: 9px 9px 16px rgba(163, 177, 198, 0.6),
-9px -9px 16px rgba(255, 255, 255, 0.5);
border-radius: 24px;
}
.neuro-card:hover {
box-shadow: inset 9px 9px 16px rgba(163, 177, 198, 0.6),
inset -9px -9px 16px rgba(255, 255, 255, 0.5);
}
/* Gradient Text */
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
}
/* Shimmer Effect */
.shimmer {
position: relative;
overflow: hidden;
}
.shimmer::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
/* Ripple Effect */
.ripple {
position: relative;
overflow: hidden;
}
.ripple::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
transform: translate(-50%, -50%);
animation: ripple 0.6s ease-out;
}
@keyframes ripple {
to {
width: 300px;
height: 300px;
opacity: 0;
}
}
/* Tilt Effect */
.tilt-card {
transition: transform 0.3s ease;
}
.tilt-card:hover {
transform: perspective(1000px) rotateX(10deg) rotateY(10deg);
}
/* Blob Animation */
.blob {
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
animation: blob 8s ease-in-out infinite;
}
@keyframes blob {
0%, 100% {
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
}
25% {
border-radius: 58% 42% 75% 25% / 76% 46% 54% 24%;
}
50% {
border-radius: 50% 50% 33% 67% / 55% 27% 73% 45%;
}
75% {
border-radius: 33% 67% 58% 42% / 63% 68% 32% 37%;
}
}
/* Particle Background */
.particle-bg {
position: relative;
overflow: hidden;
}
.particle-bg::before {
content: '';
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background-image:
radial-gradient(circle, rgba(102, 126, 234, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
animation: particleMove 20s linear infinite;
}
@keyframes particleMove {
0% { transform: translate(0, 0); }
100% { transform: translate(50px, 50px); }
}
/* Holographic Effect */
.holographic {
position: relative;
background: linear-gradient(135deg,
rgba(102, 126, 234, 0.1) 0%,
rgba(118, 75, 162, 0.1) 25%,
rgba(240, 147, 251, 0.1) 50%,
rgba(79, 172, 254, 0.1) 75%,
rgba(102, 126, 234, 0.1) 100%);
background-size: 200% 200%;
animation: holographic 3s ease infinite;
}
@keyframes holographic {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Magnetic Button */
.magnetic-btn {
transition: transform 0.2s ease;
}
.magnetic-btn:hover {
transform: scale(1.1);
}
/* Liquid Button */
.liquid-btn {
position: relative;
overflow: hidden;
}
.liquid-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s;
}
.liquid-btn:hover::before {
left: 100%;
}
/* Frosted Glass */
.frosted-glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px) saturate(100%);
-webkit-backdrop-filter: blur(10px) saturate(100%);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Gradient Border */
.gradient-border {
position: relative;
background: white;
border-radius: 24px;
}
.gradient-border::before {
content: '';
position: absolute;
inset: 0;
border-radius: 24px;
padding: 2px;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
/* Spotlight Effect */
.spotlight {
position: relative;
overflow: hidden;
}
.spotlight::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
opacity: 0;
transition: opacity 0.3s;
}
.spotlight:hover::after {
opacity: 1;
}
/* Skew Effect */
.skew-card {
transition: transform 0.3s ease;
}
.skew-card:hover {
transform: skewY(-2deg) scale(1.05);
}
/* Rainbow Border */
.rainbow-border {
position: relative;
border-radius: 24px;
background: white;
}
.rainbow-border::before {
content: '';
position: absolute;
inset: -2px;
border-radius: 24px;
background: linear-gradient(45deg,
#ff0000, #ff7f00, #ffff00, #00ff00,
#0000ff, #4b0082, #9400d3);
background-size: 400%;
animation: rainbow 3s linear infinite;
z-index: -1;
}
@keyframes rainbow {
0% { background-position: 0% 50%; }
100% { background-position: 400% 50%; }
}

1
resources/js/app.js Normal file
View File

@ -0,0 +1 @@
import './bootstrap';

8
resources/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,8 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
import * as bootstrap from 'bootstrap';
window.bootstrap = bootstrap;

View File

@ -0,0 +1,115 @@
@extends('layouts.admin')
@section('title', 'Tambah Admin')
@section('content')
<div class="container-fluid page-shell">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.admins.index') }}">Admin</a></li>
<li class="breadcrumb-item active">Tambah Admin</li>
</ol>
</nav>
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h3 class="mb-1" style="font-weight: 700; font-size: 1.75rem; color: var(--teal-900);">
<i class="fas fa-user-plus me-2" style="color: var(--teal-600);"></i>Tambah Admin Baru
</h3>
<p class="text-muted mb-0" style="font-size: 0.9375rem;">Tambahkan akun administrator baru untuk akses panel manajemen</p>
</div>
<a href="{{ route('admin.admins.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-user-plus me-2"></i>Form Tambah Admin
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.admins.store') }}">
@csrf
<div class="row g-4">
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Informasi:</strong> Admin baru akan memiliki akses penuh ke panel manajemen.
</div>
</div>
<div class="col-md-6">
<label class="form-label">
Nama Lengkap <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" name="name" value="{{ old('name') }}" placeholder="Masukkan nama lengkap" required>
@error('name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">No. Handphone</label>
<input type="tel" class="form-control" name="phone" value="{{ old('phone') }}" placeholder="08xxxxxxxxxx" inputmode="numeric" oninput="this.value = this.value.replace(/[^0-9]/g, '')">
@error('phone')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-12">
<label class="form-label">
Email <span class="text-danger">*</span>
</label>
<input type="email" class="form-control" name="email" value="{{ old('email') }}" placeholder="admin@example.com" required>
@error('email')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Password <span class="text-danger">*</span>
</label>
<input type="password" class="form-control" name="password" placeholder="Minimal 8 karakter" required>
<small class="text-muted">Minimal 8 karakter</small>
@error('password')
<small class="text-danger d-block">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Konfirmasi Password <span class="text-danger">*</span>
</label>
<input type="password" class="form-control" name="password_confirmation" placeholder="Ulangi password" required>
</div>
</div>
<hr class="my-4" style="border-color: var(--teal-100);">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Simpan Admin
</button>
<a href="{{ route('admin.admins.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Batal
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,111 @@
@extends('layouts.admin')
@section('title', 'Edit Admin')
@section('content')
<div class="container-fluid page-shell">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.admins.index') }}">Admin</a></li>
<li class="breadcrumb-item active">Edit Admin</li>
</ol>
</nav>
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h3 class="mb-1" style="font-weight: 700; font-size: 1.75rem; color: var(--teal-900);">
<i class="fas fa-user-edit me-2" style="color: var(--teal-600);"></i>Edit Admin
</h3>
<p class="text-muted mb-0" style="font-size: 0.9375rem;">Ubah informasi akun admin dan atur ulang kredensial</p>
</div>
<a href="{{ route('admin.admins.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-user-edit me-2"></i>Form Edit Admin
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.admins.update', $admin) }}">
@csrf @method('PUT')
<div class="row g-4">
<div class="col-md-6">
<label class="form-label">
Nama Lengkap <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" name="name" value="{{ old('name', $admin->name) }}" placeholder="Masukkan nama lengkap" required>
@error('name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">No. Handphone</label>
<input type="tel" class="form-control" name="phone" value="{{ old('phone', $admin->phone) }}" placeholder="08xxxxxxxxxx" inputmode="numeric" oninput="this.value = this.value.replace(/[^0-9]/g, '')">
@error('phone')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-12">
<label class="form-label">
Email <span class="text-danger">*</span>
</label>
<input type="email" class="form-control" name="email" value="{{ old('email', $admin->email) }}" placeholder="admin@example.com" required>
@error('email')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Kosongkan password jika tidak ingin mengubahnya
</div>
</div>
<div class="col-md-6">
<label class="form-label">Password Baru</label>
<input type="password" class="form-control" name="password" placeholder="Minimal 8 karakter">
<small class="text-muted">Kosongkan jika tidak ingin mengubah</small>
@error('password')
<small class="text-danger d-block">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">Konfirmasi Password</label>
<input type="password" class="form-control" name="password_confirmation" placeholder="Ulangi password baru">
</div>
</div>
<hr class="my-4" style="border-color: var(--teal-100);">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Update Admin
</button>
<a href="{{ route('admin.admins.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Batal
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,165 @@
@extends('layouts.admin')
@section('title', 'Manajemen Admin')
@section('content')
<div class="container-fluid page-shell">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h3 class="mb-2"><i class="fas fa-user-shield me-2 text-primary"></i>Manajemen Admin</h3>
<p class="text-muted mb-0">Kelola akun administrator sistem dan hak akses</p>
</div>
<a href="{{ route('admin.admins.create') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Tambah Admin Baru
</a>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="row g-4 mb-4">
<div class="col-md-6 col-lg-4">
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-users"></i>
</div>
<div class="stat-value">{{ $admins->count() }}</div>
<div class="stat-label">Total Administrator</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-user-shield me-1"></i>Semua akun admin
</small>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="stat-card">
<div class="stat-icon success">
<i class="fas fa-shield-halved"></i>
</div>
<div class="stat-value">{{ $admins->count() }}</div>
<div class="stat-label">Admin Aktif</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-check-circle me-1"></i>Dapat mengakses sistem
</small>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="stat-card">
<div class="stat-icon secondary">
<i class="fas fa-key"></i>
</div>
<div class="stat-value">Full</div>
<div class="stat-label">Hak Akses</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-unlock me-1"></i>Akses penuh sistem
</small>
</div>
</div>
</div>
<!-- Table Card -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-list me-2"></i>Daftar Administrator</h5>
<span class="badge bg-primary">{{ $admins->count() }} Admin</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle" id="tableAdmins">
<thead>
<tr>
<th width="80">ID</th>
<th>Nama Admin</th>
<th>Email</th>
<th>No. HP</th>
<th width="200" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@foreach($admins as $admin)
<tr>
<td><span class="badge bg-primary">#{{ $admin->id }}</span></td>
<td>
<div class="d-flex align-items-center gap-3">
<div class="rounded-circle" style="width: 44px; height: 44px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; font-size: 1rem;">
{{ strtoupper(substr($admin->name, 0, 1)) }}
</div>
<div>
<strong class="d-block">{{ $admin->name }}</strong>
<span class="badge bg-primary-subtle text-primary">
<i class="fas fa-shield-halved me-1"></i>Administrator
</span>
</div>
</div>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-envelope text-primary"></i>
<span>{{ $admin->email }}</span>
</div>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-phone text-primary"></i>
<span>{{ $admin->phone ?? '-' }}</span>
</div>
</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.admins.edit', $admin) }}" class="btn btn-sm btn-outline-primary" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form method="POST" action="{{ route('admin.admins.destroy', $admin) }}" class="d-inline delete-form">
@csrf @method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
$(document).ready(function() {
$('#tableAdmins').DataTable({
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/id.json'
},
order: [[0, 'desc']],
pageLength: 10,
responsive: true
});
// Delete confirmation
$('.delete-form').on('submit', function(e) {
e.preventDefault();
const form = this;
Swal.fire({
title: 'Konfirmasi Hapus',
text: 'Apakah Anda yakin ingin menghapus admin ini? Data yang sudah dihapus tidak dapat dikembalikan.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: '<i class="fas fa-trash me-2"></i>Ya, Hapus!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
form.submit();
}
});
});
});
</script>
@endpush

View File

@ -0,0 +1,122 @@
@extends('layouts.admin')
@section('title', 'Tambah Rekening')
@section('content')
<div class="container-fluid page-shell">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.bank-accounts.index') }}">Rekening Bank</a></li>
<li class="breadcrumb-item active">Tambah Rekening</li>
</ol>
</nav>
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h3 class="mb-1" style="font-weight: 700; font-size: 1.75rem; color: var(--teal-900);">
<i class="fas fa-building-columns me-2" style="color: var(--teal-600);"></i>Tambah Rekening Bank
</h3>
<p class="text-muted mb-0" style="font-size: 0.9375rem;">Daftarkan rekening pembayaran baru untuk proses transfer</p>
</div>
<a href="{{ route('admin.bank-accounts.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-building-columns me-2"></i>Form Tambah Rekening
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.bank-accounts.store') }}">
@csrf
<div class="row g-4">
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Informasi:</strong> Rekening aktif akan ditampilkan sebagai opsi pembayaran untuk user.
</div>
</div>
<div class="col-md-6">
<label class="form-label">
Nama Bank <span class="text-danger">*</span>
</label>
<select class="form-select" name="bank_name" required>
<option value="">Pilih Bank</option>
<option value="BCA">BCA</option>
<option value="Mandiri">Mandiri</option>
<option value="BNI">BNI</option>
<option value="BRI">BRI</option>
<option value="BTN">BTN</option>
<option value="CIMB Niaga">CIMB Niaga</option>
<option value="Danamon">Danamon</option>
<option value="Permata">Permata</option>
<option value="BSI">BSI (Bank Syariah Indonesia)</option>
<option value="Lainnya">Lainnya</option>
</select>
@error('bank_name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Nomor Rekening <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" name="account_number" value="{{ old('account_number') }}" placeholder="1234567890" required>
@error('account_number')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Atas Nama <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" name="account_name" value="{{ old('account_name') }}" placeholder="Nama pemilik rekening" required>
@error('account_name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">Status</label>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" name="is_active" value="1" id="isActive" checked style="width: 3rem; height: 1.5rem;">
<label class="form-check-label ms-2" for="isActive" style="font-weight: 600;">
Aktifkan rekening ini
</label>
</div>
<small class="text-muted">Rekening aktif akan ditampilkan sebagai opsi pembayaran</small>
</div>
</div>
<hr class="my-4" style="border-color: var(--teal-100);">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Simpan Rekening
</button>
<a href="{{ route('admin.bank-accounts.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Batal
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,115 @@
@extends('layouts.admin')
@section('title', 'Edit Rekening')
@section('content')
<div class="container-fluid page-shell">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.bank-accounts.index') }}">Rekening Bank</a></li>
<li class="breadcrumb-item active">Edit Rekening</li>
</ol>
</nav>
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h3 class="mb-1" style="font-weight: 700; font-size: 1.75rem; color: var(--teal-900);">
<i class="fas fa-edit me-2" style="color: var(--teal-600);"></i>Edit Rekening Bank
</h3>
<p class="text-muted mb-0" style="font-size: 0.9375rem;">Perbarui detail rekening untuk memastikan informasi pembayaran akurat</p>
</div>
<a href="{{ route('admin.bank-accounts.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-edit me-2"></i>Form Edit Rekening
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.bank-accounts.update', $bankAccount) }}">
@csrf @method('PUT')
<div class="row g-4">
<div class="col-md-6">
<label class="form-label">
Nama Bank <span class="text-danger">*</span>
</label>
<select class="form-select" name="bank_name" required>
<option value="">Pilih Bank</option>
<option value="BCA" {{ old('bank_name', $bankAccount->bank_name) == 'BCA' ? 'selected' : '' }}>BCA</option>
<option value="Mandiri" {{ old('bank_name', $bankAccount->bank_name) == 'Mandiri' ? 'selected' : '' }}>Mandiri</option>
<option value="BNI" {{ old('bank_name', $bankAccount->bank_name) == 'BNI' ? 'selected' : '' }}>BNI</option>
<option value="BRI" {{ old('bank_name', $bankAccount->bank_name) == 'BRI' ? 'selected' : '' }}>BRI</option>
<option value="BTN" {{ old('bank_name', $bankAccount->bank_name) == 'BTN' ? 'selected' : '' }}>BTN</option>
<option value="CIMB Niaga" {{ old('bank_name', $bankAccount->bank_name) == 'CIMB Niaga' ? 'selected' : '' }}>CIMB Niaga</option>
<option value="Danamon" {{ old('bank_name', $bankAccount->bank_name) == 'Danamon' ? 'selected' : '' }}>Danamon</option>
<option value="Permata" {{ old('bank_name', $bankAccount->bank_name) == 'Permata' ? 'selected' : '' }}>Permata</option>
<option value="BSI" {{ old('bank_name', $bankAccount->bank_name) == 'BSI' ? 'selected' : '' }}>BSI (Bank Syariah Indonesia)</option>
<option value="Lainnya" {{ old('bank_name', $bankAccount->bank_name) == 'Lainnya' ? 'selected' : '' }}>Lainnya</option>
</select>
@error('bank_name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Nomor Rekening <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" name="account_number" value="{{ old('account_number', $bankAccount->account_number) }}" placeholder="1234567890" required>
@error('account_number')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Atas Nama <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" name="account_name" value="{{ old('account_name', $bankAccount->account_name) }}" placeholder="Nama pemilik rekening" required>
@error('account_name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">Status</label>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" name="is_active" value="1" id="isActive" {{ $bankAccount->is_active ? 'checked' : '' }} style="width: 3rem; height: 1.5rem;">
<label class="form-check-label ms-2" for="isActive" style="font-weight: 600;">
Aktifkan rekening ini
</label>
</div>
<small class="text-muted">Rekening aktif akan ditampilkan sebagai opsi pembayaran</small>
</div>
</div>
<hr class="my-4" style="border-color: var(--teal-100);">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Update Rekening
</button>
<a href="{{ route('admin.bank-accounts.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Batal
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,172 @@
@extends('layouts.admin')
@section('title', 'Rekening Bank')
@section('content')
<div class="container-fluid page-shell">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h3 class="mb-2"><i class="fas fa-building-columns me-2 text-primary"></i>Rekening Bank</h3>
<p class="text-muted mb-0">Kelola rekening bank untuk menerima pembayaran dari pelanggan</p>
</div>
<a href="{{ route('admin.bank-accounts.create') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Tambah Rekening Baru
</a>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="row g-4 mb-4">
<div class="col-md-6 col-lg-4">
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-building-columns"></i>
</div>
<div class="stat-value">{{ $bankAccounts->count() }}</div>
<div class="stat-label">Total Rekening</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-layer-group me-1"></i>Semua rekening bank
</small>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="stat-card">
<div class="stat-icon success">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-value">{{ $bankAccounts->where('is_active', 1)->count() }}</div>
<div class="stat-label">Rekening Aktif</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-toggle-on me-1"></i>Tersedia untuk pembayaran
</small>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="stat-card">
<div class="stat-icon warning">
<i class="fas fa-times-circle"></i>
</div>
<div class="stat-value">{{ $bankAccounts->where('is_active', 0)->count() }}</div>
<div class="stat-label">Rekening Nonaktif</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-toggle-off me-1"></i>Tidak tersedia
</small>
</div>
</div>
</div>
<!-- Table Card -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-list me-2"></i>Daftar Rekening Bank</h5>
<span class="badge bg-primary">{{ $bankAccounts->count() }} Rekening</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle" id="tableBank">
<thead>
<tr>
<th width="80">ID</th>
<th>Bank</th>
<th>No. Rekening</th>
<th>Atas Nama</th>
<th width="120" class="text-center">Status</th>
<th width="200" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@foreach ($bankAccounts as $item)
<tr>
<td><span class="badge bg-primary">#{{ $item->id }}</span></td>
<td>
<div class="d-flex align-items-center gap-3">
<div class="rounded-3 p-2" style="width: 48px; height: 48px; background: linear-gradient(135deg, #0ea5e9 0%, #0369a1 100%); display: flex; align-items: center; justify-content: center;">
<i class="fas fa-building-columns text-white fs-5"></i>
</div>
<strong>{{ $item->bank_name }}</strong>
</div>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-credit-card text-primary"></i>
<code class="text-dark fs-6">{{ $item->account_number }}</code>
</div>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-user text-primary"></i>
<strong>{{ $item->account_name }}</strong>
</div>
</td>
<td class="text-center">
@if($item->is_active)
<span class="badge bg-success px-3 py-2">
<i class="fas fa-check-circle me-1"></i>Aktif
</span>
@else
<span class="badge bg-danger px-3 py-2">
<i class="fas fa-times-circle me-1"></i>Nonaktif
</span>
@endif
</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.bank-accounts.edit', $item) }}" class="btn btn-sm btn-outline-primary" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form method="POST" action="{{ route('admin.bank-accounts.destroy', $item) }}" class="d-inline delete-form">
@csrf @method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
$(document).ready(function() {
$('#tableBank').DataTable({
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/id.json'
},
order: [[0, 'desc']],
pageLength: 10,
responsive: true
});
// Delete confirmation
$('.delete-form').on('submit', function(e) {
e.preventDefault();
const form = this;
Swal.fire({
title: 'Konfirmasi Hapus',
text: 'Apakah Anda yakin ingin menghapus rekening ini? Data yang sudah dihapus tidak dapat dikembalikan.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: '<i class="fas fa-trash me-2"></i>Ya, Hapus!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
form.submit();
}
});
});
});
</script>
@endpush

View File

@ -0,0 +1,166 @@
@extends('layouts.admin')
@section('title', 'Daftar Booking')
@section('content')
<div class="container-fluid page-shell">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h3 class="mb-2"><i class="fas fa-calendar-check me-2 text-primary"></i>Daftar Booking</h3>
<p class="text-muted mb-0">Monitoring dan kelola seluruh permintaan booking pendopo</p>
</div>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="row g-4 mb-4">
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon warning">
<i class="fas fa-clock"></i>
</div>
<div class="stat-value">{{ $bookings->where('status', 'pending')->count() }}</div>
<div class="stat-label">Pending</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-hourglass-half me-1"></i>Menunggu verifikasi
</small>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon success">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-value">{{ $bookings->where('status', 'confirmed')->count() }}</div>
<div class="stat-label">Confirmed</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-check-double me-1"></i>Sudah diverifikasi
</small>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);">
<i class="fas fa-times-circle"></i>
</div>
<div class="stat-value">{{ $bookings->where('status', 'rejected')->count() }}</div>
<div class="stat-label">Rejected</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-ban me-1"></i>Ditolak
</small>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-calendar-alt"></i>
</div>
<div class="stat-value">{{ $bookings->count() }}</div>
<div class="stat-label">Total Booking</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-list me-1"></i>Semua reservasi
</small>
</div>
</div>
</div>
<!-- Table Card -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-list me-2"></i>Daftar Reservasi</h5>
<span class="badge bg-primary">{{ $bookings->count() }} Booking</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle" id="tableBookings">
<thead>
<tr>
<th width="80">ID</th>
<th>Kode Booking</th>
<th>Pemesan</th>
<th>Tempat</th>
<th>Tanggal Booking</th>
<th width="130" class="text-center">Status</th>
<th width="120" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@foreach($bookings as $booking)
<tr>
<td><span class="badge bg-primary">#{{ $booking->id }}</span></td>
<td>
<div class="d-flex align-items-center gap-2">
<div class="rounded-2 p-2" style="width: 40px; height: 40px; background: rgba(245, 158, 11, 0.1); display: flex; align-items: center; justify-content: center;">
<i class="fas fa-qrcode text-warning"></i>
</div>
<strong>{{ $booking->booking_code }}</strong>
</div>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<div class="rounded-circle" style="width: 36px; height: 36px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; font-size: 0.9rem;">
{{ strtoupper(substr($booking->user->name, 0, 1)) }}
</div>
<div>
<strong class="d-block">{{ $booking->user->name }}</strong>
<small class="text-muted">{{ $booking->user->email }}</small>
</div>
</div>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-map-marker-alt text-danger"></i>
<strong>{{ $booking->place->name }}</strong>
</div>
</td>
<td>
<i class="fas fa-calendar-day text-primary me-2"></i>
<strong>{{ $booking->booking_date->format('d M Y') }}</strong>
</td>
<td class="text-center">
@if($booking->status === 'confirmed')
<span class="badge bg-success px-3 py-2">
<i class="fas fa-check-circle me-1"></i>Confirmed
</span>
@elseif($booking->status === 'rejected')
<span class="badge bg-danger px-3 py-2">
<i class="fas fa-times-circle me-1"></i>Rejected
</span>
@else
<span class="badge bg-warning px-3 py-2">
<i class="fas fa-clock me-1"></i>Pending
</span>
@endif
</td>
<td class="text-center">
<a class="btn btn-sm btn-primary" href="{{ route('admin.bookings.show', $booking->id) }}">
<i class="fas fa-eye me-1"></i>Detail
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
$(document).ready(function() {
$('#tableBookings').DataTable({
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/id.json'
},
order: [[0, 'desc']],
pageLength: 10,
responsive: true
});
});
</script>
@endpush

View File

@ -0,0 +1,231 @@
@extends('layouts.admin')
@section('title', 'Detail Booking')
@section('content')
<div class="container-fluid page-shell">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.bookings.index') }}">Booking</a></li>
<li class="breadcrumb-item active">Detail Booking</li>
</ol>
</nav>
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h3 class="mb-1" style="font-weight: 700; font-size: 1.75rem; color: var(--teal-900);">
<i class="fas fa-calendar-check me-2" style="color: var(--teal-600);"></i>Detail Booking
</h3>
<p class="text-muted mb-0" style="font-size: 0.9375rem;">Verifikasi data pengunjung dan ambil keputusan</p>
</div>
<a href="{{ route('admin.bookings.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
</div>
</div>
</div>
@php
$statusBadge = match($booking->status) {
'confirmed' => 'success',
'rejected' => 'danger',
default => 'warning',
};
@endphp
<div class="row g-4">
<div class="col-lg-8">
<!-- Booking Info Card -->
<div class="card">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-info-circle me-2"></i>Informasi Booking
</h6>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div class="p-3 rounded" style="background: var(--teal-50); border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Kode Booking</small>
<strong style="font-size: 1.125rem; color: var(--teal-900);">{{ $booking->booking_code }}</strong>
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded" style="background: var(--teal-50); border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Status</small>
<span class="badge bg-{{ $statusBadge }}" style="font-size: 0.875rem;">
<i class="fas fa-circle me-1" style="font-size: 0.5rem;"></i>{{ ucfirst($booking->status) }}
</span>
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded" style="background: white; border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Nama Pengunjung</small>
<strong style="font-size: 1rem; color: var(--teal-900);">{{ $booking->visitor_name }}</strong>
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded" style="background: white; border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">No. Telepon</small>
<strong style="font-size: 1rem; color: var(--teal-900);">
<i class="fas fa-phone me-2" style="color: var(--teal-600);"></i>{{ $booking->visitor_phone }}
</strong>
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded" style="background: white; border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Tempat</small>
<strong style="font-size: 1rem; color: var(--teal-900);">
<i class="fas fa-map-marker-alt me-2" style="color: var(--teal-600);"></i>{{ $booking->place->name }}
</strong>
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded" style="background: white; border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Tanggal Booking</small>
<strong style="font-size: 1rem; color: var(--teal-900);">
<i class="fas fa-calendar-day me-2" style="color: var(--teal-600);"></i>{{ $booking->booking_date->format('d F Y') }}
</strong>
</div>
</div>
<div class="col-12">
<div class="p-3 rounded" style="background: white; border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Alamat</small>
<p class="mb-0" style="font-size: 0.9375rem; color: var(--teal-900);">{{ $booking->visitor_address }}</p>
</div>
</div>
@if($booking->notes)
<div class="col-12">
<div class="p-3 rounded" style="background: var(--teal-50); border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Catatan</small>
<p class="mb-0" style="font-size: 0.9375rem; color: var(--teal-900);">{{ $booking->notes }}</p>
</div>
</div>
@endif
</div>
</div>
</div>
</div>
<!-- Action Cards -->
<div class="col-lg-4">
@if($booking->status === 'pending')
<div class="card mb-4" style="border-color: var(--teal-200);">
<div class="card-header" style="background: linear-gradient(135deg, var(--teal-50), white);">
<h6 class="mb-0" style="font-weight: 700; color: var(--teal-900);">
<i class="fas fa-check-circle me-2"></i>Konfirmasi Booking
</h6>
</div>
<div class="card-body">
<p class="text-muted mb-3" style="font-size: 0.875rem;">Setujui booking ini jika semua data sudah sesuai</p>
<form action="{{ route('admin.bookings.confirm',$booking->id) }}" method="POST">
@csrf
<button class="btn btn-success w-100" type="submit">
<i class="fas fa-check-circle me-2"></i>Konfirmasi Booking
</button>
</form>
</div>
</div>
<div class="card" style="border-color: #fecaca;">
<div class="card-header" style="background: linear-gradient(135deg, #fef2f2, white);">
<h6 class="mb-0" style="font-weight: 700; color: #991b1b;">
<i class="fas fa-times-circle me-2"></i>Tolak Booking
</h6>
</div>
<div class="card-body">
<form action="{{ route('admin.bookings.reject',$booking->id) }}" method="POST">
@csrf
<label class="form-label" style="font-weight: 600;">Alasan Penolakan</label>
<textarea class="form-control mb-3" name="rejection_note" rows="3" placeholder="Masukkan alasan penolakan" required></textarea>
<button class="btn btn-danger w-100" type="submit">
<i class="fas fa-times-circle me-2"></i>Tolak Booking
</button>
</form>
</div>
</div>
@else
<div class="card" style="border-color: var(--teal-200);">
<div class="card-body text-center p-4">
<div class="mb-3">
@if($booking->status === 'confirmed')
<i class="fas fa-check-circle" style="font-size: 3rem; color: var(--success);"></i>
@else
<i class="fas fa-times-circle" style="font-size: 3rem; color: var(--danger);"></i>
@endif
</div>
<h5 style="font-weight: 700; color: var(--teal-900);">Booking {{ ucfirst($booking->status) }}</h5>
<p class="text-muted mb-0" style="font-size: 0.875rem;">Status booking sudah diproses</p>
</div>
</div>
@endif
@if($booking->payment_proof)
@php($proofPath = str_starts_with($booking->payment_proof, 'payment-proofs/') ? $booking->payment_proof : 'payment-proofs/'.$booking->payment_proof)
<div class="card mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-image me-2"></i>Bukti Pembayaran
</h6>
</div>
<div class="card-body text-center bg-light rounded-bottom p-4">
<div class="proof-container position-relative d-inline-block">
<img src="{{ storage_url($proofPath) }}"
alt="Bukti Pembayaran"
class="img-fluid rounded shadow-sm cursor-pointer"
style="max-height: 300px; border: 4px solid white; transition: transform 0.2s;"
data-bs-toggle="modal"
data-bs-target="#proofModal"
onmouseover="this.style.transform='scale(1.02)'"
onmouseout="this.style.transform='scale(1)'">
<div class="mt-3">
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#proofModal">
<i class="fas fa-search-plus me-1"></i>Perbesar
</button>
</div>
</div>
</div>
</div>
@endif
</div>
</div>
</div>
@endsection
@section('modals')
@if($booking->payment_proof)
@php($proofPath = str_starts_with($booking->payment_proof, 'payment-proofs/') ? $booking->payment_proof : 'payment-proofs/'.$booking->payment_proof)
<!-- Payment Proof Zoom Modal -->
<div class="modal fade" id="proofModal" tabindex="-1" aria-labelledby="proofModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content" style="border-radius: 1rem; overflow: hidden; border: none; max-height: 90vh;">
<div class="modal-header border-0 bg-light">
<h5 class="modal-title fw-bold" id="proofModalLabel">Detail Bukti Pembayaran</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0 bg-dark text-center d-flex align-items-center justify-content-center" style="overflow: hidden;">
<img src="{{ storage_url($proofPath) }}" alt="Bukti Pembayaran Full" class="img-fluid" style="max-height: 70vh; object-fit: contain;">
</div>
<div class="modal-footer border-0 bg-light">
<a href="{{ storage_url($proofPath) }}" download class="btn btn-primary btn-sm">
<i class="fas fa-download me-2"></i>Download Gambar
</a>
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
@endif
@endsection
@push('styles')
<style>
.cursor-pointer {
cursor: pointer;
}
</style>
@endpush

View File

@ -0,0 +1,61 @@
@extends('layouts.admin')
@section('title', 'Check Booking')
@section('content')
<div class="container-fluid page-shell admin-page-shell">
<div class="admin-page-head">
<div>
<h4 class="mb-1"><i class="fas fa-clipboard-check me-2 text-primary"></i>Verifikasi Booking</h4>
<p class="mb-0 text-muted">Cari kode booking untuk meninjau status dan memutuskan konfirmasi atau penolakan.</p>
</div>
</div>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ route('admin.check.booking.check') }}">
@csrf
<div class="input-group">
<span class="input-group-text"><i class="fas fa-hashtag"></i></span>
<input class="form-control" name="booking_code" placeholder="AB-XXXXX" required>
<button class="btn btn-primary" type="submit"><i class="fas fa-magnifying-glass me-2"></i>Cek Booking</button>
</div>
</form>
</div>
</div>
@if(session('booking'))
@php($booking = session('booking'))
<div class="card">
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-md-4"><small class="text-muted d-block">Kode</small><strong>{{ $booking->booking_code }}</strong></div>
<div class="col-md-4"><small class="text-muted d-block">Pengunjung</small><strong>{{ $booking->visitor_name }}</strong></div>
<div class="col-md-4"><small class="text-muted d-block">Status</small><span class="badge bg-secondary">{{ ucfirst($booking->status) }}</span></div>
</div>
<div class="row g-3">
<div class="col-md-6">
<form method="POST" action="{{ route('admin.check.booking.check') }}">
@csrf
<input type="hidden" name="booking_code" value="{{ $booking->booking_code }}">
<input type="hidden" name="action" value="confirm">
<button class="btn btn-success w-100" type="submit"><i class="fas fa-check-circle me-2"></i>Konfirmasi</button>
</form>
</div>
<div class="col-md-6">
<form method="POST" action="{{ route('admin.check.booking.check') }}">
@csrf
<input type="hidden" name="booking_code" value="{{ $booking->booking_code }}">
<input type="hidden" name="action" value="reject">
<input class="form-control mb-2" name="rejection_note" placeholder="Alasan penolakan" required>
<button class="btn btn-danger w-100" type="submit"><i class="fas fa-times-circle me-2"></i>Tolak</button>
</form>
</div>
</div>
</div>
</div>
@endif
</div>
@endsection

View File

@ -0,0 +1,460 @@
@extends('layouts.admin')
@section('title', 'Verifikasi Tiket')
@push('styles')
<style>
.scan-methods {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.scan-method-btn {
flex: 1;
padding: 1rem;
border: 2px solid var(--teal-200);
border-radius: 12px;
background: white;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.scan-method-btn:hover {
border-color: var(--teal-500);
background: var(--teal-50);
}
.scan-method-btn.active {
border-color: var(--teal-600);
background: linear-gradient(135deg, var(--teal-50), white);
box-shadow: 0 4px 12px rgba(13, 148, 136, 0.15);
}
.scan-method-btn i {
font-size: 2rem;
color: var(--teal-600);
margin-bottom: 0.5rem;
}
.scan-method-btn .method-title {
font-weight: 700;
color: var(--teal-900);
margin-bottom: 0.25rem;
}
.scan-method-btn .method-desc {
font-size: 0.875rem;
color: var(--gray-600);
}
#qr-reader {
border: 2px solid var(--teal-200);
border-radius: 12px;
overflow: hidden;
max-width: 500px;
margin: 0 auto;
}
#qr-reader__dashboard_section_swaplink {
display: none !important;
}
.qr-scanner-container {
position: relative;
}
.scanner-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 250px;
height: 250px;
border: 3px solid var(--teal-500);
border-radius: 12px;
pointer-events: none;
z-index: 10;
}
.scanner-overlay::before,
.scanner-overlay::after {
content: '';
position: absolute;
width: 30px;
height: 30px;
border: 4px solid var(--teal-600);
}
.scanner-overlay::before {
top: -4px;
left: -4px;
border-right: none;
border-bottom: none;
border-top-left-radius: 12px;
}
.scanner-overlay::after {
bottom: -4px;
right: -4px;
border-left: none;
border-top: none;
border-bottom-right-radius: 12px;
}
.scanner-corners {
position: absolute;
top: -4px;
right: -4px;
width: 30px;
height: 30px;
border: 4px solid var(--teal-600);
border-left: none;
border-bottom: none;
border-top-right-radius: 12px;
}
.scanner-corners-bl {
position: absolute;
bottom: -4px;
left: -4px;
width: 30px;
height: 30px;
border: 4px solid var(--teal-600);
border-right: none;
border-top: none;
border-bottom-left-radius: 12px;
}
.scan-instruction {
text-align: center;
padding: 1rem;
background: var(--teal-50);
border-radius: 12px;
margin-top: 1rem;
}
.scan-instruction i {
font-size: 1.5rem;
color: var(--teal-600);
margin-bottom: 0.5rem;
}
</style>
@endpush
@section('content')
<div class="container-fluid page-shell">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<h3 class="mb-1" style="font-weight: 700; font-size: 1.75rem; color: var(--teal-900);">
<i class="fas fa-qrcode me-2" style="color: var(--teal-600);"></i>Verifikasi Tiket
</h3>
<p class="text-muted mb-0" style="font-size: 0.9375rem;">Scan QR code atau masukkan kode tiket untuk verifikasi</p>
</div>
</div>
<!-- Scan Methods -->
<div class="row mb-4">
<div class="col-12">
<div class="scan-methods">
<div class="scan-method-btn active" id="qrScanBtn" onclick="switchMethod('qr')">
<i class="fas fa-qrcode d-block"></i>
<div class="method-title">Scan QR Code</div>
<div class="method-desc">Gunakan kamera untuk scan</div>
</div>
<div class="scan-method-btn" id="manualInputBtn" onclick="switchMethod('manual')">
<i class="fas fa-keyboard d-block"></i>
<div class="method-title">Input Manual</div>
<div class="method-desc">Ketik kode tiket</div>
</div>
</div>
</div>
</div>
<!-- QR Scanner -->
<div class="row" id="qrScannerSection">
<div class="col-lg-8 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-camera me-2"></i>Scanner QR Code
</h6>
</div>
<div class="card-body">
<div class="qr-scanner-container">
<div id="qr-reader"></div>
</div>
<div class="scan-instruction">
<i class="fas fa-info-circle d-block"></i>
<strong>Arahkan kamera ke QR code tiket</strong>
<p class="mb-0 mt-2 text-muted small">Pastikan QR code terlihat jelas dalam frame scanner</p>
</div>
</div>
</div>
</div>
</div>
<!-- Manual Input -->
<div class="row" id="manualInputSection" style="display: none;">
<div class="col-lg-8 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-keyboard me-2"></i>Input Kode Tiket Manual
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.check.ticket.check') }}" id="manualCheckForm">
@csrf
<div class="mb-3">
<label class="form-label">Kode Tiket</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-ticket-alt"></i></span>
<input class="form-control" name="ticket_code" id="ticketCodeInput" placeholder="AT-XXXXX" required>
<button class="btn btn-primary" type="submit">
<i class="fas fa-magnifying-glass me-2"></i>Cek Tiket
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Result -->
@if(session('ticketItem'))
@php($item = session('ticketItem'))
<div class="row mt-4">
<div class="col-lg-8 mx-auto">
<div class="card" style="border-color: {{ $item->is_used ? '#fca5a5' : 'var(--teal-200)' }};">
<div class="card-header" style="background: {{ $item->is_used ? 'linear-gradient(135deg, #fef2f2, white)' : 'linear-gradient(135deg, var(--teal-50), white)' }};">
<h6 class="mb-0" style="font-weight: 700; color: {{ $item->is_used ? '#991b1b' : 'var(--teal-900)' }};">
<i class="fas fa-{{ $item->is_used ? 'times-circle' : 'check-circle' }} me-2"></i>
Hasil Verifikasi
</h6>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-4">
<div class="p-3 rounded" style="background: var(--teal-50); border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Kode Tiket</small>
<strong style="font-size: 1.125rem; color: var(--teal-900);">{{ $item->ticket_code }}</strong>
</div>
</div>
<div class="col-md-4">
<div class="p-3 rounded" style="background: white; border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Pemesan</small>
<strong style="font-size: 1rem; color: var(--teal-900);">{{ $item->order->user->name }}</strong>
</div>
</div>
<div class="col-md-4">
<div class="p-3 rounded" style="background: white; border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Status</small>
@if($item->is_used)
<span class="badge bg-danger" style="font-size: 0.875rem;">
<i class="fas fa-times-circle me-1"></i>Sudah Dipakai
</span>
@else
<span class="badge bg-success" style="font-size: 0.875rem;">
<i class="fas fa-check-circle me-1"></i>Valid
</span>
@endif
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded" style="background: white; border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Jenis Tiket</small>
<strong style="font-size: 1rem; color: var(--teal-900);">{{ $item->ticket->name }}</strong>
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded" style="background: white; border: 2px solid var(--teal-100);">
<small class="text-muted d-block mb-1" style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Tanggal Kunjungan</small>
<strong style="font-size: 1rem; color: var(--teal-900);">
<i class="fas fa-calendar-day me-2" style="color: var(--teal-600);"></i>
{{ $item->order->visit_date->format('d F Y') }}
</strong>
</div>
</div>
</div>
@if(!$item->is_used)
<hr class="my-4" style="border-color: var(--teal-100);">
<form method="POST" action="{{ route('admin.check.ticket.check') }}">
@csrf
<input type="hidden" name="ticket_code" value="{{ $item->ticket_code }}">
<input type="hidden" name="mark_used" value="1">
<button class="btn btn-warning" type="submit">
<i class="fas fa-check-double me-2"></i>Tandai Sebagai Dipakai
</button>
</form>
@endif
</div>
</div>
</div>
</div>
@endif
</div>
@endsection
@push('scripts')
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<script>
let html5QrCode = null;
let isScanning = false;
function switchMethod(method) {
const qrBtn = document.getElementById('qrScanBtn');
const manualBtn = document.getElementById('manualInputBtn');
const qrSection = document.getElementById('qrScannerSection');
const manualSection = document.getElementById('manualInputSection');
if (method === 'qr') {
qrBtn.classList.add('active');
manualBtn.classList.remove('active');
qrSection.style.display = 'block';
manualSection.style.display = 'none';
startScanner();
} else {
qrBtn.classList.remove('active');
manualBtn.classList.add('active');
qrSection.style.display = 'none';
manualSection.style.display = 'block';
stopScanner();
document.getElementById('ticketCodeInput').focus();
}
}
function startScanner() {
if (isScanning) return;
// Show scanner, hide result if exists
document.getElementById('qr-reader').style.display = 'block';
document.querySelector('.scan-instruction').style.display = 'block';
const scanAgainBtn = document.getElementById('scanAgainBtn');
if (scanAgainBtn) {
scanAgainBtn.style.display = 'none';
}
html5QrCode = new Html5Qrcode("qr-reader");
html5QrCode.start(
{ facingMode: "environment" },
{
fps: 10,
qrbox: { width: 250, height: 250 },
aspectRatio: 1.0
},
(decodedText, decodedResult) => {
// QR code berhasil di-scan
console.log(`QR Code detected: ${decodedText}`);
// Stop scanner dan hide kamera
stopScanner();
hideCameraShowResult();
// Submit form dengan kode yang di-scan
submitTicketCode(decodedText);
},
(errorMessage) => {
// Error saat scanning (normal, tidak perlu ditampilkan)
}
).then(() => {
isScanning = true;
}).catch((err) => {
console.error('Error starting scanner:', err);
alert('Tidak dapat mengakses kamera. Pastikan Anda memberikan izin akses kamera.');
});
}
function stopScanner() {
if (html5QrCode && isScanning) {
html5QrCode.stop().then(() => {
isScanning = false;
html5QrCode = null;
}).catch((err) => {
console.error('Error stopping scanner:', err);
});
}
}
function hideCameraShowResult() {
// Hide kamera dan instruksi
document.getElementById('qr-reader').style.display = 'none';
document.querySelector('.scan-instruction').style.display = 'none';
// Show tombol scan lagi jika belum ada
let scanAgainBtn = document.getElementById('scanAgainBtn');
if (!scanAgainBtn) {
scanAgainBtn = document.createElement('div');
scanAgainBtn.id = 'scanAgainBtn';
scanAgainBtn.className = 'text-center mt-3';
scanAgainBtn.innerHTML = `
<div class="alert alert-info mb-3">
<i class="fas fa-spinner fa-spin me-2"></i>Memproses QR Code...
</div>
`;
document.querySelector('.qr-scanner-container').appendChild(scanAgainBtn);
} else {
scanAgainBtn.style.display = 'block';
}
}
function submitTicketCode(ticketCode) {
// Buat form dan submit
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ route('admin.check.ticket.check') }}';
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_token';
csrfInput.value = '{{ csrf_token() }}';
const codeInput = document.createElement('input');
codeInput.type = 'hidden';
codeInput.name = 'ticket_code';
codeInput.value = ticketCode;
form.appendChild(csrfInput);
form.appendChild(codeInput);
document.body.appendChild(form);
form.submit();
}
// Start scanner on page load
document.addEventListener('DOMContentLoaded', function() {
@if(session('ticketItem'))
// Jika ada hasil, tampilkan tombol scan lagi
const qrReader = document.getElementById('qr-reader');
const scanInstruction = document.querySelector('.scan-instruction');
if (qrReader) qrReader.style.display = 'none';
if (scanInstruction) scanInstruction.style.display = 'none';
let scanAgainBtn = document.getElementById('scanAgainBtn');
if (!scanAgainBtn) {
scanAgainBtn = document.createElement('div');
scanAgainBtn.id = 'scanAgainBtn';
scanAgainBtn.className = 'text-center mt-3';
scanAgainBtn.innerHTML = `
<button class="btn btn-primary btn-lg" onclick="startScanner()">
<i class="fas fa-qrcode me-2"></i>Scan Lagi
</button>
`;
document.querySelector('.qr-scanner-container').appendChild(scanAgainBtn);
}
@else
startScanner();
@endif
});
// Stop scanner when leaving page
window.addEventListener('beforeunload', function() {
stopScanner();
});
</script>
@endpush

View File

@ -0,0 +1,273 @@
@extends('layouts.admin')
@section('title', 'Dashboard')
@section('content')
<div class="container-fluid page-shell">
<!-- Welcome Section -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h3 class="mb-1" style="font-weight: 700; font-size: 1.75rem;">Dashboard</h3>
<p class="text-muted mb-0" style="font-size: 0.875rem;">Selamat datang kembali, {{ auth()->user()->name }}</p>
</div>
<div class="text-end">
<small class="text-muted d-block" style="font-size: 0.75rem;">{{ now()->format('l, d F Y') }}</small>
</div>
</div>
</div>
</div>
@unless($hasTransactionData)
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle me-2"></i>
Belum ada transaksi. Data dashboard akan muncul setelah ada pesanan pertama.
</div>
@endunless
<!-- Stats Cards -->
<div class="row g-3 mb-4">
<div class="col-xl-3 col-md-6">
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-ticket-alt"></i>
</div>
<div class="stat-value">{{ $totalTicketsSold }}</div>
<div class="stat-label">Total Tiket Terjual</div>
<small class="text-muted mt-2 d-block" style="font-size: 0.75rem;">
<i class="fas fa-check-circle me-1"></i>Sudah dikonfirmasi
</small>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="stat-card">
<div class="stat-icon secondary">
<i class="fas fa-money-bill-wave"></i>
</div>
<div class="stat-value">Rp {{ number_format($monthlyTicketSales, 0, ',', '.') }}</div>
<div class="stat-label">Penjualan Bulan Ini</div>
<small class="text-muted mt-2 d-block" style="font-size: 0.75rem;">
<i class="fas fa-calendar me-1"></i>{{ now()->format('F Y') }}
</small>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="stat-card">
<div class="stat-icon warning">
<i class="fas fa-calendar-check"></i>
</div>
<div class="stat-value">{{ $monthlyBookings }}</div>
<div class="stat-label">Booking Bulan Ini</div>
<small class="text-muted mt-2 d-block" style="font-size: 0.75rem;">
<i class="fas fa-clock me-1"></i>Reservasi pendopo
</small>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="stat-card">
<div class="stat-icon success">
<i class="fas fa-chart-line"></i>
</div>
<div class="stat-value">Rp {{ number_format($monthlyRevenue, 0, ',', '.') }}</div>
<div class="stat-label">Total Pendapatan</div>
<small class="text-muted mt-2 d-block" style="font-size: 0.75rem;">
<i class="fas fa-coins me-1"></i>Tiket + Booking terkonfirmasi
</small>
</div>
</div>
</div>
<!-- Charts Section -->
<div class="row g-3 mb-4">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-chart-area me-2 text-primary"></i>Grafik Penjualan Tiket
</h6>
<span class="badge bg-primary">12 Bulan</span>
</div>
<div class="card-body">
<canvas id="salesChart" height="80"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-chart-pie me-2 text-primary"></i>Status Transaksi
</h6>
</div>
<div class="card-body">
<canvas id="statusChart" height="200"></canvas>
<div class="mt-4">
<div class="d-flex justify-content-between align-items-center mb-3 p-2 rounded" style="background: #fffbeb;">
<div class="d-flex align-items-center gap-2">
<div style="width: 10px; height: 10px; background: #f59e0b; border-radius: 50%;"></div>
<span style="font-size: 0.875rem; font-weight: 600;">Pending</span>
</div>
<strong style="font-size: 0.875rem;">{{ $statusData[0] ?? 0 }}</strong>
</div>
<div class="d-flex justify-content-between align-items-center mb-3 p-2 rounded" style="background: #f0fdf4;">
<div class="d-flex align-items-center gap-2">
<div style="width: 10px; height: 10px; background: #10b981; border-radius: 50%;"></div>
<span style="font-size: 0.875rem; font-weight: 600;">Confirmed</span>
</div>
<strong style="font-size: 0.875rem;">{{ $statusData[1] ?? 0 }}</strong>
</div>
<div class="d-flex justify-content-between align-items-center p-2 rounded" style="background: #fef2f2;">
<div class="d-flex align-items-center gap-2">
<div style="width: 10px; height: 10px; background: #ef4444; border-radius: 50%;"></div>
<span style="font-size: 0.875rem; font-weight: 600;">Rejected</span>
</div>
<strong style="font-size: 0.875rem;">{{ $statusData[2] ?? 0 }}</strong>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Booking Chart -->
<div class="row g-3">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-chart-column me-2 text-primary"></i>Grafik Booking Pendopo
</h6>
<span class="badge bg-warning">12 Bulan</span>
</div>
<div class="card-body">
<canvas id="bookingChart" height="60"></canvas>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// Sales Chart
new Chart(document.getElementById('salesChart'), {
type: 'line',
data: {
labels: @json($months),
datasets: [{
label: 'Tiket Terjual',
data: @json($ticketData),
borderColor: '#0f766e',
backgroundColor: 'rgba(15, 118, 110, 0.1)',
tension: 0.4,
fill: true,
borderWidth: 2,
pointRadius: 4,
pointBackgroundColor: '#0f766e',
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
titleFont: { size: 13, weight: '600' },
bodyFont: { size: 12 },
cornerRadius: 8
}
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { font: { size: 11 } }
},
x: {
grid: { display: false },
ticks: { font: { size: 11 } }
}
}
}
});
// Booking Chart
new Chart(document.getElementById('bookingChart'), {
type: 'bar',
data: {
labels: @json($months),
datasets: [{
label: 'Booking Aktif',
data: @json($bookingData),
backgroundColor: '#f59e0b',
borderRadius: 6,
borderSkipped: false
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
titleFont: { size: 13, weight: '600' },
bodyFont: { size: 12 },
cornerRadius: 8
}
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { font: { size: 11 } }
},
x: {
grid: { display: false },
ticks: { font: { size: 11 } }
}
}
}
});
// Status Chart
new Chart(document.getElementById('statusChart'), {
type: 'doughnut',
data: {
labels: ['Pending', 'Confirmed', 'Rejected'],
datasets: [{
data: @json($statusData),
backgroundColor: ['#f59e0b', '#10b981', '#ef4444'],
borderWidth: 0,
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
titleFont: { size: 13, weight: '600' },
bodyFont: { size: 12 },
cornerRadius: 8
}
}
}
});
</script>
@endpush

Some files were not shown because too many files have changed in this diff Show More