feat(admin-ui): implementasi tema teal modern dengan UI elements yang lebih baik

This commit is contained in:
ardhikaxx 2026-02-24 15:26:06 +07:00
commit 1138690788
130 changed files with 29650 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

59
README.md Normal file
View File

@ -0,0 +1,59 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

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,78 @@
<?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'];
$totalTicketsSold = TicketOrderItem::whereHas('order', function (Builder $query) use ($activeStatuses) {
$query->whereIn('status', $activeStatuses);
})->count();
$monthlyTicketSales = TicketOrder::whereIn('status', $activeStatuses)
->whereYear('created_at', now()->year)
->whereMonth('created_at', now()->month)
->sum('total_price');
$monthlyBookings = Booking::whereIn('status', $activeStatuses)
->whereYear('created_at', now()->year)
->whereMonth('created_at', now()->month)
->count();
$monthlyRevenue = $monthlyTicketSales + Booking::whereIn('status', $activeStatuses)
->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, $activeStatuses) {
$query->whereYear('visit_date', $date->year)
->whereMonth('visit_date', $date->month)
->whereIn('status', $activeStatuses);
})->count();
$bookingData[] = Booking::whereYear('booking_date', $date->year)
->whereMonth('booking_date', $date->month)
->whereIn('status', $activeStatuses)
->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,75 @@
<?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)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
return back()->withErrors(['email' => 'Email atau password tidak valid.'])->onlyInput('email');
}
$request->session()->regenerate();
$user = $request->user();
if ($user->hasRole('admin')) {
return redirect()->route('admin.dashboard')->with('success', 'Login berhasil sebagai admin.');
}
return redirect()->route('user.tickets.index')->with('success', 'Login berhasil.');
}
public function register(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'],
]);
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'phone' => $data['phone'] ?? null,
'password' => Hash::make($data['password']),
]);
$user->assignRole('user');
Auth::login($user);
$request->session()->regenerate();
return redirect()->route('user.tickets.index')->with('success', 'Registrasi berhasil.');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')->with('success', 'Anda berhasil logout.');
}
}

View File

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

View File

@ -0,0 +1,141 @@
<?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()
{
$place = $this->resolvePendopo();
return view('user.bookings.index', compact('place'));
}
public function store(Request $request)
{
$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 = public_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.',
]);
}
}

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,166 @@
<?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()
{
$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)
{
$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 = public_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(public_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 = public_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');
}
}

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
{
//
}
}

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

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

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),
];

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@banyubiru.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,29 @@
<?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
{
for ($i = 1; $i <= 8; $i++) {
$user = User::updateOrCreate(
['email' => "pengguna{$i}@banyubiru.test"],
[
'name' => "Pengguna Demo {$i}",
'phone' => '081230000'.str_pad((string) $i, 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,
]);
}
}
}
}

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>

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

File diff suppressed because it is too large Load Diff

1945
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());

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320" viewBox="0 0 320 320"><rect x="0" y="0" width="320" height="320" fill="#ffffff"/><g transform="scale(15.238)"><g transform="translate(0,0)"><path fill-rule="evenodd" d="M8 0L8 2L9 2L9 4L8 4L8 9L9 9L9 10L8 10L8 11L7 11L7 10L6 10L6 9L7 9L7 8L5 8L5 9L4 9L4 10L2 10L2 9L3 9L3 8L0 8L0 11L1 11L1 12L0 12L0 13L1 13L1 12L2 12L2 13L7 13L7 12L8 12L8 14L9 14L9 15L8 15L8 16L10 16L10 17L16 17L16 19L15 19L15 18L14 18L14 19L15 19L15 20L14 20L14 21L15 21L15 20L17 20L17 19L18 19L18 17L19 17L19 15L21 15L21 14L20 14L20 13L19 13L19 12L20 12L20 11L18 11L18 10L21 10L21 8L19 8L19 9L17 9L17 8L12 8L12 7L13 7L13 6L12 6L12 7L11 7L11 5L10 5L10 7L9 7L9 4L10 4L10 3L11 3L11 4L12 4L12 5L13 5L13 3L12 3L12 2L13 2L13 0ZM10 1L10 2L11 2L11 1ZM11 9L11 13L9 13L9 14L10 14L10 16L11 16L11 13L12 13L12 14L13 14L13 13L14 13L14 12L15 12L15 14L14 14L14 15L15 15L15 16L17 16L17 17L18 17L18 16L17 16L17 15L15 15L15 14L17 14L17 13L18 13L18 15L19 15L19 13L18 13L18 11L17 11L17 9ZM4 10L4 11L5 11L5 12L7 12L7 11L6 11L6 10ZM14 10L14 11L15 11L15 10ZM9 11L9 12L10 12L10 11ZM12 11L12 12L13 12L13 11ZM16 11L16 12L17 12L17 11ZM12 15L12 16L13 16L13 15ZM20 16L20 17L21 17L21 16ZM8 18L8 21L9 21L9 19L10 19L10 20L11 20L11 19L10 19L10 18ZM19 18L19 19L20 19L20 20L18 20L18 21L21 21L21 18ZM12 19L12 20L13 20L13 19ZM0 0L0 7L7 7L7 0ZM1 1L1 6L6 6L6 1ZM2 2L2 5L5 5L5 2ZM14 0L14 7L21 7L21 0ZM15 1L15 6L20 6L20 1ZM16 2L16 5L19 5L19 2ZM0 14L0 21L7 21L7 14ZM1 15L1 20L6 20L6 15ZM2 16L2 19L5 19L5 16Z" fill="#000000"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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="text" class="form-control" name="phone" value="{{ old('phone') }}" placeholder="08xxxxxxxxxx">
@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="text" class="form-control" name="phone" value="{{ old('phone', $admin->phone) }}" placeholder="08xxxxxxxxxx">
@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,177 @@
@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>
@if($booking->payment_proof)
@php($proofPath = str_starts_with($booking->payment_proof, 'payment-proofs/') ? $booking->payment_proof : 'payment-proofs/'.$booking->payment_proof)
<hr class="my-4" style="border-color: var(--teal-100);">
<a href="{{ asset($proofPath) }}" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-image me-2"></i>Lihat Bukti Pembayaran
</a>
@endif
</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
</div>
</div>
</div>
@endsection

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,408 @@
@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;
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
stopScanner();
// 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 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() {
startScanner();
});
// 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 Aktif</div>
<small class="text-muted mt-2 d-block" style="font-size: 0.75rem;">
<i class="fas fa-arrow-up me-1"></i>Tiket terjual
</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-wallet me-1"></i>Potensi revenue
</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

View File

@ -0,0 +1,120 @@
@extends('layouts.admin')
@section('title', 'Profil Admin')
@section('content')
<div class="container-fluid page-shell">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<h3 class="mb-2"><i class="fas fa-user-circle me-2 text-primary"></i>Profil Admin</h3>
<p class="text-muted mb-0">Kelola informasi akun administrator dan keamanan akses login sistem</p>
</div>
</div>
<div class="row g-4">
<!-- Profile Card -->
<div class="col-lg-4">
<div class="card">
<div class="card-body text-center p-4">
<div class="rounded-circle mx-auto mb-3" style="width: 120px; height: 120px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 800; font-size: 3rem; box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<h4 class="mb-2">{{ $user->name }}</h4>
<p class="text-muted mb-3">{{ $user->email }}</p>
<span class="badge bg-primary px-3 py-2">
<i class="fas fa-shield-halved me-1"></i>Administrator
</span>
<hr class="my-4">
<div class="text-start">
<div class="mb-3 p-3 rounded" style="background: rgba(14, 165, 233, 0.05);">
<small class="text-muted d-block mb-1">
<i class="fas fa-phone text-primary me-2"></i>No. Handphone
</small>
<strong>{{ $user->phone ?? 'Belum diisi' }}</strong>
</div>
<div class="mb-3 p-3 rounded" style="background: rgba(14, 165, 233, 0.05);">
<small class="text-muted d-block mb-1">
<i class="fas fa-map-marker-alt text-primary me-2"></i>Alamat
</small>
<strong>{{ $user->address ?? 'Belum diisi' }}</strong>
</div>
<div class="p-3 rounded" style="background: rgba(14, 165, 233, 0.05);">
<small class="text-muted d-block mb-1">
<i class="fas fa-calendar-check text-primary me-2"></i>Bergabung Sejak
</small>
<strong>{{ $user->created_at->format('d F Y') }}</strong>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Form -->
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-user-gear me-2"></i>Edit Profil</h5>
</div>
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.profile.update') }}">
@csrf @method('PUT')
<div class="mb-4">
<h6 class="mb-3"><i class="fas fa-user me-2 text-primary"></i>Informasi Pribadi</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Nama Lengkap <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" value="{{ old('name', $user->name) }}" required>
@error('name')<small class="text-danger">{{ $message }}</small>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">No. Handphone</label>
<input type="text" class="form-control" name="phone" value="{{ old('phone', $user->phone) }}" placeholder="08xxxxxxxxxx">
@error('phone')<small class="text-danger">{{ $message }}</small>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">Alamat Lengkap</label>
<textarea class="form-control" name="address" rows="3" placeholder="Masukkan alamat lengkap Anda">{{ old('address', $user->address) }}</textarea>
@error('address')<small class="text-danger">{{ $message }}</small>@enderror
</div>
</div>
</div>
<hr class="my-4">
<div class="mb-4">
<h6 class="mb-3"><i class="fas fa-lock me-2 text-primary"></i>Ubah Password</h6>
<div class="alert alert-info d-flex align-items-start">
<i class="fas fa-info-circle me-2 mt-1"></i>
<small>Kosongkan jika tidak ingin mengubah password. Password minimal 8 karakter.</small>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Password Baru</label>
<input type="password" class="form-control" name="password" placeholder="Minimal 8 karakter">
@error('password')<small class="text-danger d-block">{{ $message }}</small>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Konfirmasi Password</label>
<input type="password" class="form-control" name="password_confirmation" placeholder="Ulangi password baru">
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Simpan Perubahan
</button>
<a href="{{ route('admin.dashboard') }}" 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,163 @@
@extends('layouts.admin')
@section('title', 'Penjualan Tiket')
@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-cart-shopping me-2 text-primary"></i>Penjualan Tiket</h3>
<p class="text-muted mb-0">Monitoring dan kelola seluruh transaksi pembelian tiket</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">{{ $orders->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">{{ $orders->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">{{ $orders->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-receipt"></i>
</div>
<div class="stat-value">{{ $orders->count() }}</div>
<div class="stat-label">Total Transaksi</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-list me-1"></i>Semua order
</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 Transaksi</h5>
<span class="badge bg-primary">{{ $orders->count() }} Transaksi</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle" id="tableTicketOrders">
<thead>
<tr>
<th width="80">ID</th>
<th>Kode Order</th>
<th>Pembeli</th>
<th>Tanggal Kunjungan</th>
<th>Total Harga</th>
<th width="130" class="text-center">Status</th>
<th width="120" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@foreach($orders as $order)
<tr>
<td><span class="badge bg-primary">#{{ $order->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(14, 165, 233, 0.1); display: flex; align-items: center; justify-content: center;">
<i class="fas fa-barcode text-primary"></i>
</div>
<strong>{{ $order->order_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($order->user->name, 0, 1)) }}
</div>
<div>
<strong class="d-block">{{ $order->user->name }}</strong>
<small class="text-muted">{{ $order->user->email }}</small>
</div>
</div>
</td>
<td>
<i class="fas fa-calendar-day text-primary me-2"></i>
<strong>{{ $order->visit_date->format('d M Y') }}</strong>
</td>
<td>
<strong class="text-success fs-6">Rp {{ number_format($order->total_price, 0, ',', '.') }}</strong>
</td>
<td class="text-center">
@if($order->status === 'confirmed')
<span class="badge bg-success px-3 py-2">
<i class="fas fa-check-circle me-1"></i>Confirmed
</span>
@elseif($order->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 href="{{ route('admin.ticket-orders.show', $order->id) }}" class="btn btn-sm btn-primary">
<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() {
$('#tableTicketOrders').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,193 @@
@extends('layouts.admin')
@section('title', 'Detail Order Tiket')
@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.ticket-orders.index') }}">Order Tiket</a></li>
<li class="breadcrumb-item active">Detail Order</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-receipt me-2" style="color: var(--teal-600);"></i>Detail Order Tiket
</h3>
<p class="text-muted mb-0" style="font-size: 0.9375rem;">Tinjau detail transaksi dan lakukan konfirmasi</p>
</div>
<a href="{{ route('admin.ticket-orders.index') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
</div>
</div>
</div>
@php
$statusBadge = match($order->status) {
'confirmed' => 'success',
'rejected' => 'danger',
default => 'warning',
};
@endphp
<div class="row g-4">
<div class="col-lg-8">
<!-- Order Info Card -->
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-info-circle me-2"></i>Informasi Order
</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 Order</small>
<strong style="font-size: 1.125rem; color: var(--teal-900);">{{ $order->order_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($order->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;">Pemesan</small>
<strong style="font-size: 1rem; color: var(--teal-900);">{{ $order->user->name }}</strong>
<br><small class="text-muted">{{ $order->user->email }}</small>
</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>{{ $order->visit_date->format('d F Y') }}
</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;">Total Tiket</small>
<strong style="font-size: 1.25rem; color: var(--teal-900);">{{ $order->total_qty }} Tiket</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;">Total Bayar</small>
<strong style="font-size: 1.25rem; color: var(--teal-900);">Rp {{ number_format($order->total_price,0,',','.') }}</strong>
</div>
</div>
</div>
@if($order->payment_proof)
@php($proofPath = str_starts_with($order->payment_proof, 'payment-proofs/') ? $order->payment_proof : 'payment-proofs/'.$order->payment_proof)
<hr class="my-4" style="border-color: var(--teal-100);">
<a href="{{ asset($proofPath) }}" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-image me-2"></i>Lihat Bukti Pembayaran
</a>
@endif
</div>
</div>
<!-- Ticket Items Card -->
<div class="card">
<div class="card-header">
<h6 class="mb-0" style="font-weight: 700;">
<i class="fas fa-ticket-alt me-2"></i>Daftar Tiket
</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th width="60">#</th>
<th>Nama Tiket</th>
<th>Kode Tiket</th>
<th>Harga</th>
</tr>
</thead>
<tbody>
@foreach($order->items as $index => $item)
<tr>
<td><span class="badge bg-primary">{{ $index + 1 }}</span></td>
<td><strong>{{ $item->ticket->name }}</strong></td>
<td><code style="background: var(--teal-50); padding: 0.25rem 0.5rem; border-radius: 6px; color: var(--teal-900); font-weight: 600;">{{ $item->ticket_code }}</code></td>
<td><strong style="color: var(--teal-700);">Rp {{ number_format($item->price, 0, ',', '.') }}</strong></td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Action Cards -->
<div class="col-lg-4">
@if($order->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 Order
</h6>
</div>
<div class="card-body">
<p class="text-muted mb-3" style="font-size: 0.875rem;">Setujui order ini jika pembayaran sudah sesuai</p>
<form action="{{ route('admin.ticket-orders.confirm',$order->id) }}" method="POST">
@csrf
<button class="btn btn-success w-100" type="submit">
<i class="fas fa-check-circle me-2"></i>Konfirmasi Order
</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 Order
</h6>
</div>
<div class="card-body">
<form action="{{ route('admin.ticket-orders.reject',$order->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 Order
</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($order->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);">Order {{ ucfirst($order->status) }}</h5>
<p class="text-muted mb-0" style="font-size: 0.875rem;">Status order sudah diproses</p>
</div>
</div>
@endif
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,115 @@
@extends('layouts.admin')
@section('title', 'Tambah Tiket')
@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.tickets.index') }}">Tiket</a></li>
<li class="breadcrumb-item active">Tambah Tiket</li>
</ol>
</nav>
<!-- 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-plus-circle me-2" style="color: var(--teal-600);"></i>Tambah Tiket Baru
</h3>
<p class="text-muted mb-0" style="font-size: 0.9375rem;">Buat data tiket baru yang akan tersedia untuk pembelian</p>
</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-ticket-alt me-2"></i>Form Tambah Tiket
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.tickets.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> Pastikan semua data diisi dengan benar sebelum menyimpan.
</div>
</div>
<div class="col-md-6">
<label class="form-label">
Nama Tiket <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" name="name" value="{{ old('name') }}" placeholder="Contoh: Tiket Dewasa" required>
@error('name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Harga <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text">Rp</span>
<input type="number" class="form-control" name="price" value="{{ old('price') }}" placeholder="50000" required>
</div>
@error('price')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Kuota Per Hari <span class="text-danger">*</span>
</label>
<input type="number" class="form-control" name="quota_per_day" value="{{ old('quota_per_day') }}" placeholder="100" required>
<small class="text-muted">Jumlah maksimal tiket yang dapat dijual per hari</small>
@error('quota_per_day')
<small class="text-danger d-block">{{ $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 tiket ini
</label>
</div>
<small class="text-muted">Tiket aktif akan ditampilkan untuk pembelian</small>
</div>
<div class="col-12">
<label class="form-label">Deskripsi</label>
<textarea class="form-control" name="description" rows="4" placeholder="Deskripsi tiket (opsional)">{{ old('description') }}</textarea>
@error('description')
<small class="text-danger">{{ $message }}</small>
@enderror
</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 Tiket
</button>
<a href="{{ route('admin.tickets.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 Tiket')
@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.tickets.index') }}">Tiket</a></li>
<li class="breadcrumb-item active">Edit Tiket</li>
</ol>
</nav>
<!-- 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-edit me-2" style="color: var(--teal-600);"></i>Edit Tiket
</h3>
<p class="text-muted mb-0" style="font-size: 0.9375rem;">Perbarui informasi tiket, harga, kuota, dan status</p>
</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 Tiket
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.tickets.update', $ticket) }}">
@csrf @method('PUT')
<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> Perubahan akan langsung mempengaruhi tampilan di halaman user.
</div>
</div>
<div class="col-md-6">
<label class="form-label">
Nama Tiket <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" name="name" value="{{ old('name', $ticket->name) }}" placeholder="Contoh: Tiket Dewasa" required>
@error('name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Harga <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text">Rp</span>
<input type="number" class="form-control" name="price" value="{{ old('price', $ticket->price) }}" placeholder="50000" required>
</div>
@error('price')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
<div class="col-md-6">
<label class="form-label">
Kuota Per Hari <span class="text-danger">*</span>
</label>
<input type="number" class="form-control" name="quota_per_day" value="{{ old('quota_per_day', $ticket->quota_per_day) }}" placeholder="100" required>
<small class="text-muted">Jumlah maksimal tiket yang dapat dijual per hari</small>
@error('quota_per_day')
<small class="text-danger d-block">{{ $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" {{ $ticket->is_active ? 'checked' : '' }} style="width: 3rem; height: 1.5rem;">
<label class="form-check-label ms-2" for="isActive" style="font-weight: 600;">
Aktifkan tiket ini
</label>
</div>
<small class="text-muted">Tiket aktif akan ditampilkan untuk pembelian</small>
</div>
<div class="col-12">
<label class="form-label">Deskripsi</label>
<textarea class="form-control" name="description" rows="4" placeholder="Deskripsi tiket (opsional)">{{ old('description', $ticket->description) }}</textarea>
@error('description')
<small class="text-danger">{{ $message }}</small>
@enderror
</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 Tiket
</button>
<a href="{{ route('admin.tickets.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,169 @@
@extends('layouts.admin')
@section('title', 'Manajemen Tiket')
@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-1" style="font-weight: 700; font-size: 1.75rem;">Manajemen Tiket</h3>
<p class="text-muted mb-0" style="font-size: 0.875rem;">Kelola tiket masuk pemandian</p>
</div>
<a href="{{ route('admin.tickets.create') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Tambah Tiket
</a>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="row g-3 mb-4">
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-ticket-alt"></i>
</div>
<div class="stat-value">{{ $tickets->count() }}</div>
<div class="stat-label">Total Tiket</div>
</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">{{ $tickets->where('is_active', 1)->count() }}</div>
<div class="stat-label">Tiket Aktif</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon warning">
<i class="fas fa-times-circle"></i>
</div>
<div class="stat-value">{{ $tickets->where('is_active', 0)->count() }}</div>
<div class="stat-label">Tiket Nonaktif</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon secondary">
<i class="fas fa-users"></i>
</div>
<div class="stat-value">{{ $tickets->sum('quota_per_day') }}</div>
<div class="stat-label">Total Kuota/Hari</div>
</div>
</div>
</div>
<!-- Table Card -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0" style="font-weight: 700;">Daftar Tiket</h6>
<span class="badge bg-primary">{{ $tickets->count() }} Tiket</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle" id="tableTickets">
<thead>
<tr>
<th width="60">ID</th>
<th>Nama Tiket</th>
<th>Harga</th>
<th>Kuota/Hari</th>
<th width="100" class="text-center">Status</th>
<th width="180" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@foreach($tickets as $ticket)
<tr>
<td><span class="badge bg-primary">#{{ $ticket->id }}</span></td>
<td>
<div class="d-flex align-items-center gap-2">
<div style="width: 36px; height: 36px; background: #0f766e; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white;">
<i class="fas fa-ticket-alt"></i>
</div>
<div>
<strong class="d-block" style="font-size: 0.875rem;">{{ $ticket->name }}</strong>
@if($ticket->description)
<small class="text-muted" style="font-size: 0.75rem;">{{ Str::limit($ticket->description, 40) }}</small>
@endif
</div>
</div>
</td>
<td>
<strong style="font-size: 0.875rem; color: #10b981;">Rp {{ number_format($ticket->price, 0, ',', '.') }}</strong>
</td>
<td>
<span class="badge bg-info">{{ $ticket->quota_per_day }} tiket</span>
</td>
<td class="text-center">
@if($ticket->is_active)
<span class="badge bg-success">
<i class="fas fa-check-circle me-1"></i>Aktif
</span>
@else
<span class="badge bg-danger">
<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.tickets.edit', $ticket) }}" class="btn btn-sm btn-outline-primary" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form method="POST" action="{{ route('admin.tickets.destroy', $ticket) }}" 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() {
$('#tableTickets').DataTable({
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/id.json'
},
order: [[0, 'desc']],
pageLength: 10,
responsive: true
});
$('.delete-form').on('submit', function(e) {
e.preventDefault();
const form = this;
Swal.fire({
title: 'Konfirmasi Hapus',
text: 'Apakah Anda yakin ingin menghapus tiket ini?',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: 'Ya, Hapus!',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
form.submit();
}
});
});
});
</script>
@endpush

View File

@ -0,0 +1,55 @@
@extends('layouts.auth')
@section('title', 'Login')
@section('content')
<div class="container py-4 py-lg-5">
<div class="row min-vh-100 align-items-center justify-content-center">
<div class="col-xl-10">
<div class="row g-0 auth-panel">
<div class="col-lg-6 d-none d-lg-block auth-side p-5 d-flex align-items-center">
<div>
<h2 class="mb-3"><i class="fas fa-water me-2"></i>Selamat Datang</h2>
<p class="mb-4">Masuk untuk membeli tiket dan booking pendopo dengan proses yang cepat.</p>
<ul class="list-unstyled mb-0">
<li class="mb-2"><i class="fas fa-check-circle me-2"></i>Transaksi aman</li>
<li class="mb-2"><i class="fas fa-check-circle me-2"></i>Tiket digital otomatis</li>
<li><i class="fas fa-check-circle me-2"></i>Status pesanan real-time</li>
</ul>
</div>
</div>
<div class="col-lg-6 bg-white p-4 p-lg-5">
<h3 class="mb-2">Login Akun</h3>
<p class="text-muted mb-4">Masukkan email dan password Anda.</p>
<form method="POST" action="{{ url('/login') }}">
@csrf
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" placeholder="nama@email.com" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" placeholder="Masukkan password" required>
</div>
<div class="form-check mb-4">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">Ingat saya</label>
</div>
<button type="submit" class="btn btn-primary w-100 mb-3">
<i class="fas fa-right-to-bracket me-1"></i>Masuk
</button>
<p class="text-center mb-0">Belum punya akun? <a href="{{ route('register') }}" class="fw-semibold">Daftar sekarang</a></p>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,74 @@
@extends('layouts.auth')
@section('title', 'Register')
@section('content')
<div class="container py-4 py-lg-5">
<div class="row min-vh-100 align-items-center justify-content-center">
<div class="col-xl-10">
<div class="row g-0 auth-panel">
<div class="col-lg-7 bg-white p-4 p-lg-5">
<h3 class="mb-2">Buat Akun Baru</h3>
<p class="text-muted mb-4">Lengkapi data untuk mulai pembelian tiket dan booking.</p>
<form method="POST" action="{{ url('/register') }}">
@csrf
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Nama Lengkap</label>
<input type="text" name="name" class="form-control" placeholder="Nama Anda" required autofocus>
</div>
<div class="col-md-6">
<label class="form-label">No. Handphone</label>
<input type="text" name="phone" class="form-control" placeholder="08xxxxxxxxxx">
</div>
<div class="col-12">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" placeholder="nama@email.com" required>
</div>
<div class="col-md-6">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" placeholder="Minimal 8 karakter" required>
</div>
<div class="col-md-6">
<label class="form-label">Konfirmasi Password</label>
<input type="password" name="password_confirmation" class="form-control" placeholder="Ulangi password" required>
</div>
<div class="col-12">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="terms" required>
<label class="form-check-label" for="terms">Saya setuju dengan syarat dan ketentuan</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 mt-4 mb-3">
<i class="fas fa-user-plus me-1"></i>Daftar
</button>
<p class="text-center mb-0">Sudah punya akun? <a href="{{ route('login') }}" class="fw-semibold">Login di sini</a></p>
</form>
</div>
<div class="col-lg-5 d-none d-lg-block auth-side p-4 p-lg-5 d-flex align-items-center">
<div>
<h4 class="mb-3">Kenapa daftar?</h4>
<ul class="list-unstyled mb-0">
<li class="mb-2"><i class="fas fa-check-circle me-2"></i>Akses pembelian tiket online</li>
<li class="mb-2"><i class="fas fa-check-circle me-2"></i>Booking pendopo dari rumah</li>
<li class="mb-2"><i class="fas fa-check-circle me-2"></i>Histori transaksi lebih rapi</li>
<li><i class="fas fa-check-circle me-2"></i>Verifikasi pembayaran lebih cepat</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,793 @@
@extends('layouts.app')
@section('title', 'Banyu Biru - Tiket & Booking')
@push('styles')
<style>
.hero-showcase {
position: relative;
overflow: hidden;
border-radius: 36px;
padding: 7rem 0 5rem;
margin-bottom: 3rem;
background: linear-gradient(130deg, #083446 0%, #0f766e 52%, #14b8a6 100%);
box-shadow: 0 32px 56px rgba(8, 52, 70, 0.24);
}
.hero-showcase::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(120deg, rgba(8, 52, 70, 0.82), rgba(8, 52, 70, 0.42)), url('{{ asset('images/background.jpg') }}') center/cover no-repeat;
opacity: 0.52;
}
.hero-showcase::after {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(380px 240px at 84% 14%, rgba(255, 255, 255, 0.2), transparent 72%),
radial-gradient(330px 220px at 10% 90%, rgba(251, 146, 60, 0.22), transparent 74%);
}
.hero-inner {
position: relative;
z-index: 2;
}
.hero-kicker {
display: inline-flex;
align-items: center;
gap: 0.55rem;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.34);
padding: 0.56rem 1rem;
color: #fff;
font-size: 0.8rem;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 700;
background: rgba(255, 255, 255, 0.14);
backdrop-filter: blur(8px);
}
.hero-title {
color: #fff;
font-size: clamp(2.1rem, 5vw, 4rem);
line-height: 1.08;
margin: 1.2rem 0 1rem;
letter-spacing: -0.03em;
}
.hero-subtitle {
color: rgba(241, 253, 255, 0.95);
font-size: 1.06rem;
max-width: 640px;
margin-bottom: 1.8rem;
line-height: 1.66;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1.75rem;
}
.hero-actions .btn {
border-radius: 12px;
padding-inline: 1.3rem;
}
.hero-stats {
margin-top: 0.45rem;
}
.hero-stat-card {
height: 100%;
border: 1px solid rgba(255, 255, 255, 0.26);
border-radius: 16px;
padding: 1rem 1.05rem;
background: rgba(255, 255, 255, 0.13);
backdrop-filter: blur(8px);
box-shadow: 0 12px 24px rgba(6, 31, 41, 0.18);
}
.hero-stat-value {
color: #fff;
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.16rem;
}
.hero-stat-label {
color: rgba(241, 253, 255, 0.85);
font-size: 0.82rem;
font-weight: 600;
}
.hero-side-card {
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 22px;
padding: 1.35rem;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(12px);
box-shadow: 0 24px 36px rgba(6, 31, 41, 0.22);
}
.hero-side-card h5 {
color: #fff;
font-size: 1.15rem;
}
.hero-side-list {
margin: 0;
padding: 0;
list-style: none;
}
.hero-side-list li {
display: flex;
gap: 0.65rem;
align-items: flex-start;
color: rgba(241, 253, 255, 0.9);
margin-bottom: 0.82rem;
font-size: 0.92rem;
}
.hero-side-list li i {
color: #fde68a;
margin-top: 0.2rem;
}
.hero-side-price {
margin: 1rem 0 1.1rem;
border: 1px solid rgba(255, 255, 255, 0.24);
border-radius: 14px;
padding: 0.82rem 0.95rem;
background: rgba(255, 255, 255, 0.1);
}
.hero-side-price small {
display: block;
color: rgba(241, 253, 255, 0.86);
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
}
.hero-side-price strong {
color: #fff;
font-size: 1.45rem;
line-height: 1.2;
}
.section-block {
padding: 5rem 0;
}
.section-head {
text-align: center;
margin-bottom: 2.6rem;
}
.section-head h2 {
color: #0f172a;
font-size: clamp(1.9rem, 4vw, 2.9rem);
margin-bottom: 0.7rem;
}
.section-head p {
max-width: 640px;
margin: 0 auto;
color: #516b7d;
}
.facility-card-pro {
height: 100%;
border: 1px solid rgba(16, 38, 50, 0.1);
border-radius: 20px;
padding: 1.6rem;
background: linear-gradient(180deg, #ffffff, #f5fbff);
box-shadow: 0 12px 26px rgba(10, 34, 46, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.facility-card-pro:hover {
transform: translateY(-4px);
box-shadow: 0 22px 34px rgba(10, 34, 46, 0.14);
}
.facility-icon-pro {
width: 58px;
height: 58px;
border-radius: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #fff;
background: linear-gradient(130deg, #0f766e, #14b8a6);
box-shadow: 0 14px 24px rgba(15, 118, 110, 0.2);
margin-bottom: 1rem;
}
.facility-card-pro h5 {
margin-bottom: 0.56rem;
}
.facility-card-pro p {
margin-bottom: 0;
color: #597385;
}
.ticket-panel {
border: 1px solid rgba(16, 38, 50, 0.11);
border-radius: 26px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(10, 34, 46, 0.12);
background: #fff;
}
.ticket-head {
padding: 1.9rem 2rem 1.7rem;
background: linear-gradient(132deg, #093443 0%, #0f766e 62%, #14b8a6 100%);
color: #fff;
}
.ticket-head h4,
.ticket-head p,
.ticket-head small {
color: #effdff;
margin-bottom: 0;
}
.ticket-head-price {
font-size: clamp(2rem, 4vw, 2.8rem);
font-weight: 800;
line-height: 1.08;
color: #fff;
}
.ticket-body {
padding: 1.7rem 2rem;
}
.ticket-point-pro {
display: flex;
gap: 0.72rem;
margin-bottom: 0.75rem;
color: #365161;
}
.ticket-point-pro i {
margin-top: 0.2rem;
color: #0f766e;
}
.ticket-cta-box {
border: 1px solid rgba(16, 38, 50, 0.1);
border-radius: 16px;
padding: 0.9rem;
background: #f4fbfb;
}
.ticket-cta-box small {
color: #5f7889;
}
.booking-shell {
border: 1px solid rgba(16, 38, 50, 0.1);
border-radius: 26px;
background: linear-gradient(180deg, #ffffff, #f6fbff);
box-shadow: 0 20px 40px rgba(10, 34, 46, 0.1);
overflow: hidden;
}
.booking-side {
height: 100%;
background: linear-gradient(150deg, #0b3d4a, #0f766e);
color: #fff;
padding: 1.5rem;
}
.booking-side h5,
.booking-side p {
color: #ebfdff;
}
.booking-step {
display: flex;
gap: 0.7rem;
margin-bottom: 1rem;
align-items: flex-start;
}
.booking-step span {
width: 26px;
height: 26px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.28);
display: inline-flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 0.74rem;
font-weight: 700;
flex-shrink: 0;
}
.booking-step p {
margin: 0;
color: rgba(235, 253, 255, 0.9);
font-size: 0.91rem;
}
.booking-form-wrap {
padding: 1.6rem;
}
.booking-note-pro {
border: 1px solid #b7ddf4;
border-radius: 14px;
background: linear-gradient(135deg, #e3f5ff, #eef8ff);
padding: 0.9rem 1rem;
}
.home-modal .modal-header {
background: linear-gradient(130deg, #0b3d4a, #0f766e 62%, #14b8a6);
color: #fff;
}
.home-modal .btn-close {
filter: invert(1);
}
.order-summary-pro {
border: 1px solid #b4d7f8;
border-radius: 18px;
background: linear-gradient(130deg, #e2f3ff, #f0f9ff);
}
@media (max-width: 991.98px) {
.hero-showcase {
padding: 5.8rem 0 3.7rem;
border-radius: 26px;
}
.section-block {
padding: 4rem 0;
}
.ticket-head,
.ticket-body,
.booking-form-wrap {
padding: 1.2rem;
}
}
@media (max-width: 767.98px) {
.hero-actions {
flex-direction: column;
}
.hero-actions .btn {
width: 100%;
}
}
</style>
@endpush
@push('body_classes')
home-shell
@endpush
@section('content')
<section id="home" class="hero-showcase">
<div class="container hero-inner">
<div class="row align-items-center g-4">
<div class="col-lg-7">
<span class="hero-kicker" data-aos="fade-right">
<i class="fas fa-circle-check"></i>Tiket online resmi Banyu Biru
</span>
<h1 class="hero-title" data-aos="fade-up" data-aos-delay="80">
Pusat Tiket dan Booking
<br>
Pemandian Air Panas Banyu Biru
</h1>
<p class="hero-subtitle" data-aos="fade-up" data-aos-delay="140">
Satu halaman untuk pemesanan tiket masuk dan booking pendopo. Proses singkat, verifikasi jelas, dan siap dipakai untuk operasional harian wisata.
</p>
<div class="hero-actions" data-aos="fade-up" data-aos-delay="200">
<a href="#tiket" class="btn btn-light btn-lg">
<i class="fas fa-ticket-alt me-2"></i>Beli Tiket
</a>
<a href="#booking" class="btn btn-outline-light btn-lg">
<i class="fas fa-calendar-check me-2"></i>Booking Pendopo
</a>
</div>
<div class="row g-3 hero-stats" data-aos="fade-up" data-aos-delay="260">
<div class="col-sm-4">
<div class="hero-stat-card">
<div class="hero-stat-value">Rp 5.000</div>
<div class="hero-stat-label">Harga tiket per orang</div>
</div>
</div>
<div class="col-sm-4">
<div class="hero-stat-card">
<div class="hero-stat-value">Pendopo</div>
<div class="hero-stat-label">Tempat booking aktif</div>
</div>
</div>
<div class="col-sm-4">
<div class="hero-stat-card">
<div class="hero-stat-value">Digital</div>
<div class="hero-stat-label">Validasi lebih cepat</div>
</div>
</div>
</div>
</div>
<div class="col-lg-5" data-aos="fade-left" data-aos-delay="180">
<div class="hero-side-card">
<h5 class="mb-3">Kenapa pakai sistem ini?</h5>
<ul class="hero-side-list">
<li><i class="fas fa-shield-alt"></i><span>Pembayaran lebih aman dengan upload bukti transfer.</span></li>
<li><i class="fas fa-bolt"></i><span>Konfirmasi pesanan lebih cepat lewat panel admin.</span></li>
<li><i class="fas fa-qrcode"></i><span>Tiket punya kode unik untuk proses scan di pintu masuk.</span></li>
</ul>
<div class="hero-side-price">
<small>Harga dasar tiket</small>
<strong>Rp 5.000 / orang</strong>
</div>
<a href="#tiket" class="btn btn-warning w-100">
<i class="fas fa-arrow-right me-2"></i>Mulai Pemesanan
</a>
</div>
</div>
</div>
</div>
</section>
<section id="fasilitas" class="section-block">
<div class="container">
<div class="section-head" data-aos="fade-up">
<h2>Fasilitas Utama</h2>
<p>Pengalaman wisata Banyu Biru dirancang nyaman untuk pengunjung keluarga, rombongan, dan acara komunitas.</p>
</div>
<div class="row g-4">
<div class="col-md-6 col-lg-4" data-aos="fade-up" data-aos-delay="80">
<div class="facility-card-pro">
<div class="facility-icon-pro"><i class="fas fa-water-ladder"></i></div>
<h5>Kolam Air Panas</h5>
<p>Kolam utama dengan suhu hangat alami untuk relaksasi dan aktivitas wisata harian.</p>
</div>
</div>
<div class="col-md-6 col-lg-4" data-aos="fade-up" data-aos-delay="160">
<div class="facility-card-pro">
<div class="facility-icon-pro"><i class="fas fa-umbrella-beach"></i></div>
<h5>Area Santai</h5>
<p>Zona duduk dan tunggu yang nyaman sebelum masuk kolam atau saat istirahat.</p>
</div>
</div>
<div class="col-md-6 col-lg-4" data-aos="fade-up" data-aos-delay="240">
<div class="facility-card-pro">
<div class="facility-icon-pro"><i class="fas fa-store"></i></div>
<h5>Tenant Kuliner</h5>
<p>Pilihan makanan dan minuman yang membantu kunjungan lebih praktis untuk rombongan.</p>
</div>
</div>
</div>
</div>
</section>
<section id="tiket" class="section-block">
<div class="container">
<div class="section-head" data-aos="fade-up">
<h2>Tiket Masuk</h2>
<p>Pembelian tiket dilakukan per orang dengan harga tetap dan proses lanjut pembayaran melalui bukti transfer.</p>
</div>
<div class="ticket-panel" data-aos="zoom-in">
@forelse($tickets as $ticket)
<div class="ticket-head d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h4 class="mb-1">{{ $ticket->name }}</h4>
<p>Tiket berlaku satu kali masuk sesuai tanggal kunjungan.</p>
</div>
<div class="text-lg-end">
<div class="ticket-head-price">{{ 'Rp ' . number_format($ticket->price, 0, ',', '.') }}</div>
<small>Harga tetap per pengunjung</small>
</div>
</div>
<div class="ticket-body">
<div class="row align-items-center g-4">
<div class="col-lg-8">
<div class="ticket-point-pro"><i class="fas fa-circle-check"></i><span>Akses area kolam utama Banyu Biru.</span></div>
<div class="ticket-point-pro"><i class="fas fa-circle-check"></i><span>Termasuk fasilitas area bilas dan area umum.</span></div>
<div class="ticket-point-pro mb-0"><i class="fas fa-circle-check"></i><span>Tiket digital dilengkapi kode unik untuk verifikasi.</span></div>
</div>
<div class="col-lg-4">
<div class="ticket-cta-box">
<small class="d-block mb-2">Lanjutkan ke modal ringkasan sebelum checkout.</small>
<button class="btn btn-primary btn-lg w-100 buy-ticket-btn" type="button" data-ticket-id="{{ $ticket->id }}" data-ticket-name="{{ $ticket->name }}" data-price="{{ (int) $ticket->price }}">
Beli Tiket Sekarang
</button>
</div>
</div>
</div>
</div>
@empty
<div class="p-4">
<div class="alert alert-warning mb-0">Tiket belum tersedia saat ini.</div>
</div>
@endforelse
</div>
</div>
</section>
<section id="booking" class="section-block">
<div class="container">
<div class="section-head" data-aos="fade-up">
<h2>Booking Tempat</h2>
<p>Booking online saat ini hanya untuk tempat <strong>Pendopo</strong>. Pilih tanggal, isi data pengunjung, lalu lanjutkan pembayaran.</p>
</div>
<div class="booking-shell" data-aos="fade-up">
<div class="row g-0">
<div class="col-lg-4">
<div class="booking-side h-100">
<h5 class="mb-2">Alur Booking</h5>
<p class="mb-4">Ikuti langkah berikut untuk memastikan booking diproses lebih cepat.</p>
<div class="booking-step">
<span>1</span>
<p>Isi data lengkap pengunjung dan tanggal booking.</p>
</div>
<div class="booking-step">
<span>2</span>
<p>Sistem cek ketersediaan tanggal secara otomatis.</p>
</div>
<div class="booking-step">
<span>3</span>
<p>Lakukan pembayaran dan upload bukti transfer.</p>
</div>
<div class="booking-step mb-0">
<span>4</span>
<p>Admin melakukan konfirmasi status booking Anda.</p>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="booking-form-wrap">
<div class="booking-note-pro d-flex align-items-start gap-2 mb-4">
<i class="fas fa-circle-info mt-1 text-info"></i>
<div>
<strong class="d-block text-info-emphasis">Informasi Penting</strong>
<span class="text-info-emphasis">Tempat booking otomatis adalah <strong>Pendopo</strong> dengan validasi tanggal aktif.</span>
</div>
</div>
<form id="bookingForm" method="POST" action="{{ route('user.bookings.store') }}">
@csrf
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold mb-2">Nama Lengkap *</label>
<input type="text" class="form-control form-control-lg" name="visitor_name" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-2">No. Telepon *</label>
<input type="tel" class="form-control form-control-lg" id="phone" name="visitor_phone" required>
</div>
<div class="col-12">
<label class="form-label fw-semibold mb-2">Alamat *</label>
<textarea class="form-control form-control-lg" name="visitor_address" rows="3" required></textarea>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-2">Tanggal Booking *</label>
<input type="date" class="form-control form-control-lg" id="booking_date" name="booking_date" min="{{ now()->toDateString() }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-2">Catatan (Opsional)</label>
<input type="text" class="form-control form-control-lg" name="notes" placeholder="Contoh: Rombongan sekolah, agenda keluarga, dll.">
</div>
<div class="col-12 d-grid mt-2">
<button type="submit" class="btn btn-primary btn-lg py-3">
Booking Sekarang
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="modal fade home-modal" id="ticketModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content rounded-4 border-0 shadow-lg">
<div class="modal-header text-white p-4">
<h5 class="modal-title fw-bold fs-4">Beli Tiket <span id="modalTicketType"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 p-md-5">
<form id="ticketPurchaseForm">
<input type="hidden" id="selectedTicketId">
<div class="mb-4">
<label class="form-label fw-semibold mb-2">Jumlah Tiket</label>
<input type="number" class="form-control form-control-lg" id="modalQuantity" min="1" max="20" value="1">
</div>
<div class="mb-4">
<label class="form-label fw-semibold mb-2">Tanggal Kunjungan</label>
<input type="date" class="form-control form-control-lg" id="modalDate" min="{{ now()->toDateString() }}">
</div>
<div class="order-summary-pro p-4 mb-4">
<h6 class="fw-bold fs-5 text-info mb-3">Ringkasan Pesanan</h6>
<p class="mb-2">Tiket: <span id="modalTicketName" class="fw-semibold">-</span></p>
<p class="mb-2">Harga Satuan: Rp <span id="modalPrice" class="fw-semibold">0</span></p>
<p class="mb-3">Jumlah: <span id="modalQtyDisplay" class="fw-semibold">1</span> tiket</p>
<hr class="my-3 border-info-subtle">
<h5 class="mb-0 fw-bold text-info">Total: Rp <span id="modalTotal">0</span></h5>
</div>
<div class="d-grid gap-3">
<button type="submit" class="btn btn-primary btn-lg">Lanjutkan Pembayaran</button>
<button type="button" class="btn btn-outline-secondary btn-lg" data-bs-dismiss="modal">Batal</button>
</div>
</form>
</div>
</div>
</div>
</div>
<form id="ticketOrderSubmitForm" method="POST" action="{{ route('user.tickets.order') }}" class="d-none">
@csrf
<input type="hidden" name="visit_date" id="submitVisitDate">
<input type="hidden" name="items[0][ticket_id]" id="submitTicketId">
<input type="hidden" name="items[0][qty]" id="submitTicketQty">
</form>
@endsection
@push('scripts')
<script>
const isGuest = {{ auth()->check() ? 'false' : 'true' }};
let selectedPrice = 0;
function showNeedLogin() {
Swal.fire({
icon: 'warning',
title: 'Login Diperlukan',
text: 'Silakan login melalui menu Login di navbar.',
confirmButtonColor: '#0f766e'
});
}
document.addEventListener('DOMContentLoaded', function () {
const modalElement = document.getElementById('ticketModal');
const bookingForm = document.getElementById('bookingForm');
const bookingDateInput = document.getElementById('booking_date');
const phoneInput = document.getElementById('phone');
const modalDate = document.getElementById('modalDate');
if (!modalElement || !bookingForm || !bookingDateInput || !phoneInput || !modalDate) {
return;
}
const modal = new bootstrap.Modal(modalElement);
modalDate.value = new Date().toISOString().split('T')[0];
document.querySelectorAll('.buy-ticket-btn').forEach((button) => {
button.addEventListener('click', function () {
const ticketId = this.dataset.ticketId;
const ticketName = this.dataset.ticketName;
selectedPrice = Number(this.dataset.price || 0);
document.getElementById('selectedTicketId').value = ticketId;
document.getElementById('modalTicketType').textContent = ticketName;
document.getElementById('modalTicketName').textContent = ticketName;
document.getElementById('modalPrice').textContent = selectedPrice.toLocaleString('id-ID');
document.getElementById('modalQuantity').value = 1;
document.getElementById('modalQtyDisplay').textContent = '1';
document.getElementById('modalTotal').textContent = selectedPrice.toLocaleString('id-ID');
modal.show();
});
});
document.getElementById('modalQuantity').addEventListener('input', function () {
const qty = Math.max(1, Number(this.value || 1));
const total = selectedPrice * qty;
document.getElementById('modalQtyDisplay').textContent = qty;
document.getElementById('modalTotal').textContent = total.toLocaleString('id-ID');
});
document.getElementById('ticketPurchaseForm').addEventListener('submit', function (e) {
e.preventDefault();
if (isGuest) {
modal.hide();
showNeedLogin();
return;
}
const visitDate = modalDate.value;
const ticketId = document.getElementById('selectedTicketId').value;
const qty = document.getElementById('modalQuantity').value;
if (!visitDate || !ticketId || !qty) {
Swal.fire({
icon: 'error',
title: 'Data belum lengkap',
text: 'Tanggal dan jumlah tiket wajib diisi.'
});
return;
}
document.getElementById('submitVisitDate').value = visitDate;
document.getElementById('submitTicketId').value = ticketId;
document.getElementById('submitTicketQty').value = qty;
document.getElementById('ticketOrderSubmitForm').submit();
});
bookingForm.addEventListener('submit', function (e) {
if (isGuest) {
e.preventDefault();
showNeedLogin();
}
});
bookingDateInput.addEventListener('change', async function () {
if (isGuest) {
this.value = '';
showNeedLogin();
return;
}
const bookingDate = this.value;
if (!bookingDate) return;
try {
const url = `{{ route('user.bookings.checkDate') }}?booking_date=${bookingDate}`;
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
if (!response.ok) return;
const data = await response.json();
if (!data.available) {
Swal.fire({
icon: 'warning',
title: 'Tanggal Tidak Tersedia',
text: data.message
});
this.value = '';
}
} catch (error) {
console.error(error);
}
});
phoneInput.addEventListener('input', function () {
this.value = this.value.replace(/[^0-9+]/g, '');
});
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
window.scrollTo({
top: target.offsetTop - 88,
behavior: 'smooth'
});
}
});
});
});
</script>
@endpush

View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Admin Banyu Biru')</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css" rel="stylesheet">
<link href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css" rel="stylesheet">
<link href="{{ asset('css/admin-teal.css') }}" rel="stylesheet">
@stack('styles')
</head>
<body class="admin-body">
<div class="admin-backdrop" aria-hidden="true">
<span class="orb orb-b"></span>
<span class="orb orb-c"></span>
</div>
@include('partials.admin.sidebar')
<div class="admin-sidebar-overlay" id="adminSidebarOverlay" onclick="toggleSidebar()"></div>
<div class="admin-wrapper">
@include('partials.admin.topbar')
<main class="admin-content fade-in">
@yield('content')
</main>
</div>
@include('partials.alerts')
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
$.extend(true, $.fn.dataTable.defaults, {
language: { url: '//cdn.datatables.net/plug-ins/1.13.8/i18n/id.json' },
pageLength: 10,
responsive: true
});
const COLLAPSE_KEY = 'admin_sidebar_collapsed_v1';
function applySidebarPreference() {
const collapsed = localStorage.getItem(COLLAPSE_KEY) === '1';
if (window.innerWidth >= 992 && collapsed) {
document.body.classList.add('admin-collapsed');
} else {
document.body.classList.remove('admin-collapsed');
}
}
function toggleSidebar() {
const sidebar = document.querySelector('.admin-sidebar');
const overlay = document.getElementById('adminSidebarOverlay');
sidebar.classList.toggle('show');
overlay.classList.toggle('show');
}
function toggleSidebarCollapse() {
document.body.classList.toggle('admin-collapsed');
localStorage.setItem(COLLAPSE_KEY, document.body.classList.contains('admin-collapsed') ? '1' : '0');
}
applySidebarPreference();
window.addEventListener('resize', () => {
if (window.innerWidth >= 992) {
document.querySelector('.admin-sidebar')?.classList.remove('show');
document.getElementById('adminSidebarOverlay')?.classList.remove('show');
applySidebarPreference();
} else {
document.body.classList.remove('admin-collapsed');
}
});
document.querySelectorAll('.admin-sidebar .menu-item').forEach((item) => {
item.addEventListener('click', () => {
if (window.innerWidth < 992) {
document.querySelector('.admin-sidebar')?.classList.remove('show');
document.getElementById('adminSidebarOverlay')?.classList.remove('show');
}
});
});
// Smooth dropdown animation
document.addEventListener('DOMContentLoaded', function() {
// Disable Bootstrap's default dropdown animation
const style = document.createElement('style');
style.textContent = `
.dropdown-menu { display: block !important; }
.dropdown-menu:not(.show) { opacity: 0; pointer-events: none; }
`;
document.head.appendChild(style);
});
</script>
@stack('scripts')
</body>
</html>

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Banyu Biru') - Pemandian Air Panas</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Outfit:wght@600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css" rel="stylesheet">
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<link href="{{ asset('css/app.css') }}?v={{ filemtime(public_path('css/app.css')) }}" rel="stylesheet">
@stack('styles')
</head>
<body class="site-body @stack('body_classes')">
<div class="site-backdrop" aria-hidden="true">
<span class="orb orb-a"></span>
<span class="orb orb-b"></span>
<span class="orb orb-c"></span>
</div>
@include('partials.navbar')
<main class="main-content">
<div class="container site-main-container">
@yield('content')
</div>
</main>
@include('partials.footer')
@include('partials.alerts')
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script>
AOS.init({ duration: 700, once: true, offset: 80 });
</script>
@stack('scripts')
</body>
</html>

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Autentikasi') - Banyu Biru</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Outfit:wght@600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css" rel="stylesheet">
<link href="{{ asset('css/app.css') }}?v={{ filemtime(public_path('css/app.css')) }}" rel="stylesheet">
</head>
<body class="auth-body">
<div class="auth-backdrop" aria-hidden="true">
<span class="orb orb-a"></span>
<span class="orb orb-b"></span>
<span class="orb orb-c"></span>
</div>
<div class="auth-wrap">
@yield('content')
</div>
@include('partials.alerts')
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</body>
</html>

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