Update user management, booking model, and mobile screens

This commit is contained in:
micko samawa 2026-03-14 11:11:28 +07:00
parent 1a277067f4
commit b192557bb8
25 changed files with 7639 additions and 131 deletions

View File

@ -0,0 +1,168 @@
# Auto-Verify Bookings Scheduler - Dokumentasi
## Tujuan
Sistem otomatis yang menjalankan verifikasi booking setiap hari tengah malam. Ketika booking sudah mencapai check-in date dan pembayaran sudah lunas, maka status booking otomatis diubah menjadi "checked_in" dan kontrakan menjadi "occupied".
## Cara Kerja
### Flow Sistem
```
Setiap hari jam 00:00 (tengah malam):
1. Scheduler jalan otomatis
2. Query semua booking:
- Payment status = PAID (sudah bayar)
- Status = PENDING atau CONFIRMED (belum checked_in)
- Start date <= hari ini (check-in date sudah tiba)
- Checked_in_at = NULL (belum ada check-in)
3. Untuk setiap booking yang match:
- Update status = "checked_in"
- Update checked_in_at = sekarang
- Update kontrakan status = "occupied"
4. Log hasil ke system logs
5. Notif/email bisa ditambah di masa depan
```
### Contoh
```
Booking:
- Mahasiswa: Andi
- Kontrakan: Rumah Nyaman
- Check-in date: 7 Mar 2026
- Payment status: PAID
- Status: CONFIRMED
- checked_in_at: NULL
Hasil setelah scheduler jalan (7 Mar, 00:00):
- Status: checked_in ✓
- checked_in_at: 7 Mar 2026 00:00:00
- Kontrakan status: occupied ✓
```
## File Yang Ditambah
### 1. AutoVerifyBookings Command
**File:** `app/Console/Commands/AutoVerifyBookings.php`
Command ini yang melakukan pengecekan dan update. Bisa dijalankan manual dengan:
```bash
php artisan bookings:auto-verify
```
### 2. Console Kernel
**File:** `app/Console/Kernel.php`
File ini yang mendefinisikan jadwal scheduler. Scheduler akan otomatis jalan setiap hari jam 00:00.
### 3. Booking Model Update
**File:** `app/Models/Booking.php`
Tambah scope `readyForAutoVerify()` untuk query booking yang siap di-verify.
## Setup untuk Production
### Requirement: Laravel Task Scheduler (Laravel Scheduler)
Scheduler memerlukan cronjob yang berjalan di server. Tambahkan satu line ke crontab:
```bash
* * * * * cd /path/to/project && php artisan schedule:run >> /dev/null 2>&1
```
Ini menjalankan `schedule:run` setiap menit, yang kemudian akan menjalankan command yang dijadwalkan.
### Step by Step Setup:
1. **Pastikan migration sudah berjalan (opsional):**
```bash
php artisan migrate
```
2. **Test command manual (testing dulu):**
```bash
# Test tanpa production
php artisan bookings:auto-verify
```
Output akan seperti:
```
🚀 Memulai auto-verify bookings...
✅ Booking #1 (Andi) auto-verified
✅ Booking #5 (Budi) auto-verified
✨ Total 2 bookings berhasil di-verify
```
3. **Setup cronjob di server:**
Akses server via SSH, edit crontab:
```bash
crontab -e
```
Tambahkan line:
```bash
* * * * * cd /var/www/spk_kontrakan && php artisan schedule:run >> /dev/null 2>&1
```
4. **Verify sudah berjalan:**
```bash
# Di server
tail -f storage/logs/laravel.log
```
Harusnya ada log seperti:
```
[2026-03-07 00:00:15] local.INFO: Auto-verify bookings executed
[2026-03-07 00:00:15] local.INFO: ✅ Booking #1 (Andi) auto-verified
```
## Database Fields yang Digunakan
Dari `bookings` table:
- `payment_status` - 'paid' / 'unpaid'
- `status` - 'pending', 'confirmed', 'checked_in', 'completed', 'cancelled'
- `start_date` - Tanggal check-in
- `checked_in_at` - Timestamp check-in (diupdate oleh scheduler)
- Relasi: `kontrakan_id` untuk update status kontrakan
## Monitoring & Troubleshooting
### Logs
Lihat log di: `storage/logs/laravel.log`
### Manual Test
```bash
# Test tanpa harus tunggu jam 12 malam
php artisan bookings:auto-verify
```
### Debugging
Jika ada issue, tambah debug di command:
```bash
php artisan bookings:auto-verify --verbose
```
### Jika Server Tidak Ada Cron
Jika hosting tidak support cron, alternatif:
1. Pakai external service seperti cron-job.org
2. Buat manual trigger via controller (kurang ideal)
3. Upgrade hosting yang support cron
## Future Improvements
- [ ] Tambah notification email saat booking auto-verified
- [ ] Dashboard admin untuk monitor scheduler runs
- [ ] Grace period (jika booking cancel before check-in date)
- [ ] Webhook notification ke mobile app
- [ ] Activity logging untuk audit trail
## Kesimpulan
Dengan scheduler ini:
✅ No more manual update status
✅ Kontrakan otomatis marked as occupied
✅ No double-booking bisa terjadi
✅ Admin zero effort
✅ Aman & automated

View File

@ -0,0 +1,94 @@
<?php
namespace App\Console\Commands;
use App\Models\Booking;
use App\Models\Kontrakan;
use Carbon\Carbon;
use Illuminate\Console\Command;
class AutoVerifyBookings extends Command
{
/**
* Nama command yang dijalankan
*/
protected $signature = 'bookings:auto-verify';
/**
* Deskripsi command
*/
protected $description = 'Auto-verify bookings yang sudah payment_paid dan check-in date sudah tiba';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle()
{
$this->info('🚀 Memulai auto-verify bookings...');
// Get today's date
$today = Carbon::now()->toDateString();
try {
// Cari semua booking dengan:
// 1. Status = pending atau confirmed (belum checked_in)
// 2. Payment status = paid (sudah bayar)
// 3. Start date = hari ini atau lebih lama
// 4. Belum di-checked_in (checked_in_at masih NULL)
$bookings = Booking::where('payment_status', 'paid')
->whereIn('status', ['pending', 'confirmed'])
->where('start_date', '<=', $today)
->whereNull('checked_in_at')
->get();
$verified_count = 0;
foreach ($bookings as $booking) {
// Update booking status ke checked_in
$booking->update([
'status' => 'checked_in',
'checked_in_at' => now(),
]);
// Update kontrakan status ke occupied
Kontrakan::where('id', $booking->kontrakan_id)
->update(['status' => 'occupied']);
$verified_count++;
$this->line("✅ Booking #{$booking->id} ({$booking->user->name}) auto-verified");
}
if ($verified_count > 0) {
$this->info("✨ Total {$verified_count} bookings berhasil di-verify");
} else {
$this->info(" Tidak ada booking yang perlu di-verify hari ini");
}
// Log untuk audit trail
\Log::info("Auto-verify bookings executed", [
'verified_count' => $verified_count,
'executed_at' => now(),
]);
return self::SUCCESS;
} catch (\Exception $e) {
$this->error("❌ Error saat auto-verify: {$e->getMessage()}");
\Log::error('Auto-verify bookings error', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// Auto-verify bookings setiap hari jam 12 malam (00:00)
// Ini akan check booking dengan payment_paid dan check-in date sudah tiba
// Kemudian auto-update status menjadi "checked_in"
$schedule->command('bookings:auto-verify')
->dailyAt('00:00') // Jam 12 malam setiap hari
->description('Auto-verify bookings yang sudah payment_paid dan check-in date tiba');
// Log setiap kali scheduler jalan
$schedule->command('bookings:auto-verify')
->dailyAt('00:00')
->onSuccess(function () {
\Log::info('✅ Auto-verify bookings scheduler selesai dijalankan');
})
->onFailure(function () {
\Log::error('❌ Auto-verify bookings scheduler gagal');
});
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View File

@ -53,7 +53,7 @@ public function store(Request $request)
'name' => 'required|string|min:3|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:8|confirmed',
'role' => 'required|in:admin,super_admin',
'role' => 'required|in:admin,super_admin,user',
], [
'name.required' => 'Nama harus diisi',
'email.required' => 'Email harus diisi',
@ -85,7 +85,7 @@ public function update(Request $request, User $user)
$validated = $request->validate([
'name' => 'required|string|min:3|max:255',
'email' => 'required|email|unique:users,email,' . $user->id,
'role' => 'required|in:admin,super_admin',
'role' => 'required|in:admin,super_admin,user',
'password' => 'nullable|string|min:8|confirmed',
]);

View File

@ -182,6 +182,19 @@ public function scopeCurrentlyActive(Builder $query): Builder
->where('end_date', '>=', $today);
}
/**
* Scope: Booking yang siap untuk auto-verify
* (pembayaran sudah, belum checked_in, check-in date sudah tiba)
*/
public function scopeReadyForAutoVerify(Builder $query): Builder
{
$today = now()->toDateString();
return $query->where('payment_status', self::PAYMENT_PAID)
->whereIn('status', [self::STATUS_PENDING, self::STATUS_CONFIRMED])
->where('start_date', '<=', $today)
->whereNull('checked_in_at');
}
// ========== HELPERS ==========
/**

View File

@ -50,6 +50,32 @@ protected function casts(): array
];
}
/**
* Append role_label dan user_type ke JSON response
*/
protected $appends = ['role_label', 'user_type'];
/**
* Get role label accessor
*/
public function getRoleLabelAttribute(): string
{
return match($this->role) {
'super_admin' => 'Super Admin',
'admin' => 'Admin',
'user' => 'Mahasiswa',
default => $this->role,
};
}
/**
* Get user type accessor (untuk kompatibilitas API)
*/
public function getUserTypeAttribute(): string
{
return $this->role === 'user' ? 'mahasiswa' : $this->role;
}
/**
* Helper function untuk cek apakah user adalah Super Admin
*
@ -71,7 +97,7 @@ public function isAdmin()
}
/**
* Helper function untuk cek apakah user adalah User biasa
* Helper function untuk cek apakah user adalah Mahasiswa (User biasa)
*
* @return bool
*/
@ -80,6 +106,31 @@ public function isUser()
return $this->role === 'user';
}
/**
* Helper function untuk cek apakah user adalah Mahasiswa
*
* @return bool
*/
public function isMahasiswa()
{
return $this->role === 'user';
}
/**
* Get role label yang user-friendly
*
* @return string
*/
public function getRoleLabel()
{
return match($this->role) {
'super_admin' => 'Super Admin',
'admin' => 'Admin',
'user' => 'Mahasiswa',
default => $this->role,
};
}
/**
* Helper function untuk cek role
*

View File

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('bookings', function (Blueprint $table) {
// Owner verification fields
$table->enum('owner_verification_status', ['pending', 'verified', 'rejected'])->default('pending')->after('status');
$table->timestamp('owner_verified_at')->nullable()->after('owner_verification_status');
$table->text('owner_verification_note')->nullable()->after('owner_verified_at');
// Notify owner field
$table->timestamp('owner_notified_at')->nullable()->after('owner_verification_note');
// Index untuk query
$table->index(['kontrakan_id', 'owner_verification_status']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('bookings', function (Blueprint $table) {
$table->dropIndex(['kontrakan_id', 'owner_verification_status']);
$table->dropColumn([
'owner_verification_status',
'owner_verified_at',
'owner_verification_note',
'owner_notified_at',
]);
});
}
};

View File

@ -67,15 +67,16 @@ class="form-control @error('email') is-invalid @enderror"
</label>
<select class="form-select @error('role') is-invalid @enderror" id="role" name="role">
<option value="">-- Pilih Role --</option>
<option value="admin" {{ old('role') == 'admin' ? 'selected' : '' }}>Admin</option>
<option value="super_admin" {{ old('role') == 'super_admin' ? 'selected' : '' }}>Super Admin</option>
<option value="admin" {{ old('role') == 'admin' ? 'selected' : '' }}>Admin</option>
<option value="user" {{ old('role') == 'user' ? 'selected' : '' }}>Mahasiswa</option>
</select>
@error('role')
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
<small class="text-muted d-block mt-2">
<i class="bi bi-info-circle me-1"></i>
Super Admin memiliki akses penuh ke semua fitur sistem
<strong>Super Admin:</strong> Akses penuh ke semua fitur | <strong>Admin:</strong> Kelola konten | <strong>Mahasiswa:</strong> User umum aplikasi mobile
</small>
</div>

View File

@ -67,12 +67,17 @@ class="form-control @error('email') is-invalid @enderror"
<i class="bi bi-shield me-2"></i>Role
</label>
<select class="form-select @error('role') is-invalid @enderror" id="role" name="role">
<option value="admin" {{ old('role', $user->role) == 'admin' ? 'selected' : '' }}>Admin</option>
<option value="super_admin" {{ old('role', $user->role) == 'super_admin' ? 'selected' : '' }}>Super Admin</option>
<option value="admin" {{ old('role', $user->role) == 'admin' ? 'selected' : '' }}>Admin</option>
<option value="user" {{ old('role', $user->role) == 'user' ? 'selected' : '' }}>Mahasiswa</option>
</select>
@error('role')
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
<small class="text-muted d-block mt-2">
<i class="bi bi-info-circle me-1"></i>
<strong>Super Admin:</strong> Akses penuh | <strong>Admin:</strong> Kelola konten | <strong>Mahasiswa:</strong> User aplikasi mobile
</small>
</div>
<div class="alert alert-info border-0 rounded-3 mb-4">

View File

@ -40,6 +40,11 @@
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
color: white;
}
.badge-user {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
</style>
<!-- Header -->
@ -80,8 +85,9 @@
<label class="form-label fw-semibold mb-2">Role</label>
<select name="role" class="form-select">
<option value="">Semua Role</option>
<option value="admin" {{ request('role') == 'admin' ? 'selected' : '' }}>Admin</option>
<option value="super_admin" {{ request('role') == 'super_admin' ? 'selected' : '' }}>Super Admin</option>
<option value="admin" {{ request('role') == 'admin' ? 'selected' : '' }}>Admin</option>
<option value="user" {{ request('role') == 'user' ? 'selected' : '' }}>Mahasiswa</option>
</select>
</div>
@ -152,8 +158,12 @@
<td>
@if($user->role === 'super_admin')
<span class="badge badge-super-admin">Super Admin</span>
@else
@elseif($user->role === 'admin')
<span class="badge badge-admin">Admin</span>
@elseif($user->role === 'user')
<span class="badge badge-user">Mahasiswa</span>
@else
<span class="badge bg-secondary">{{ $user->role }}</span>
@endif
</td>
<td>

View File

@ -0,0 +1,163 @@
<?php $__env->startSection('title', 'Activity Log'); ?>
<?php $__env->startSection('content'); ?>
<div class="container-fluid px-4">
<style>
.action-badge {
font-size: 0.8rem;
padding: 0.35rem 0.65rem;
border-radius: 6px;
}
.action-create { background: #d4edda; color: #155724; }
.action-update { background: #d1ecf1; color: #0c5460; }
.action-delete { background: #f8d7da; color: #721c24; }
.action-export { background: #fff3cd; color: #856404; }
.action-login { background: #cfe2ff; color: #084298; }
</style>
<!-- Header -->
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 2rem; color: white; margin-bottom: 2rem;">
<h2 class="mb-2">
<i class="bi bi-clock-history me-3"></i>Activity Log
</h2>
<p class="mb-0 fs-6">Riwayat semua aktivitas user dalam sistem</p>
</div>
<!-- Filter Card -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<form action="<?php echo e(route('admin.activity-logs.index')); ?>" method="GET" class="row g-3">
<div class="col-md-3">
<label class="form-label fw-semibold mb-2">User</label>
<select name="user_id" class="form-select">
<option value="">Semua User</option>
<?php $__currentLoopData = \App\Models\User::all(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $u): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<option value="<?php echo e($u->id); ?>" <?php echo e(request('user_id') == $u->id ? 'selected' : ''); ?>><?php echo e($u->name); ?></option>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold mb-2">Action</label>
<select name="action" class="form-select">
<option value="">Semua Action</option>
<option value="create" <?php echo e(request('action') == 'create' ? 'selected' : ''); ?>>Create</option>
<option value="update" <?php echo e(request('action') == 'update' ? 'selected' : ''); ?>>Update</option>
<option value="delete" <?php echo e(request('action') == 'delete' ? 'selected' : ''); ?>>Delete</option>
<option value="export" <?php echo e(request('action') == 'export' ? 'selected' : ''); ?>>Export</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold mb-2">Model Type</label>
<select name="model_type" class="form-select">
<option value="">Semua</option>
<option value="Kontrakan" <?php echo e(request('model_type') == 'Kontrakan' ? 'selected' : ''); ?>>Kontrakan</option>
<option value="Laundry" <?php echo e(request('model_type') == 'Laundry' ? 'selected' : ''); ?>>Laundry</option>
<option value="Kriteria" <?php echo e(request('model_type') == 'Kriteria' ? 'selected' : ''); ?>>Kriteria</option>
<option value="User" <?php echo e(request('model_type') == 'User' ? 'selected' : ''); ?>>User</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end gap-2">
<button type="submit" class="btn w-100" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<i class="bi bi-funnel me-2"></i>Filter
</button>
<a href="<?php echo e(route('admin.activity-logs.index')); ?>" class="btn btn-outline-secondary w-100">
<i class="bi bi-arrow-clockwise me-2"></i>Reset
</a>
</div>
</form>
</div>
</div>
<!-- Activity Table -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 py-3">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-list-ul me-2"></i>Daftar Aktivitas (<?php echo e($logs->total()); ?>)
</h5>
<a href="<?php echo e(route('admin.activity-logs.export', request()->query())); ?>" class="btn btn-sm" style="border: 2px solid #667eea; color: #667eea; background: transparent;">
<i class="bi bi-download me-1"></i>Export CSV
</a>
</div>
</div>
<div class="card-body p-0">
<?php if($logs->isEmpty()): ?>
<div class="text-center py-5">
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
<h5 class="text-muted mt-3">Tidak ada aktivitas ditemukan</h5>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Waktu</th>
<th>User</th>
<th>Action</th>
<th>Deskripsi</th>
<th>Model</th>
<th>IP Address</th>
</tr>
</thead>
<tbody>
<?php $__currentLoopData = $logs; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $log): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<tr class="align-middle">
<td>
<small><?php echo e($log->created_at->format('d M Y H:i')); ?></small>
</td>
<td>
<strong><?php echo e($log->user->name ?? 'Unknown'); ?></strong><br>
<small class="text-muted"><?php echo e($log->user->email ?? ''); ?></small>
</td>
<td>
<span class="action-badge action-<?php echo e(strtolower($log->action)); ?>">
<i class="bi bi-circle-fill" style="font-size: 0.5rem;"></i> <?php echo e(ucfirst($log->action)); ?>
</span>
</td>
<td>
<?php echo e($log->description); ?>
</td>
<td>
<?php if($log->model_type): ?>
<small class="text-muted">
<?php echo e($log->model_type); ?>
<?php if($log->model_id): ?>
#<?php echo e($log->model_id); ?>
<?php endif; ?>
</small>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<small class="text-muted font-monospace"><?php echo e($log->ip_address); ?></small>
</td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Pagination -->
<div class="d-flex justify-content-center mt-4">
<?php echo e($logs->links()); ?>
</div>
</div>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\TA\spk_kontrakan\resources\views/admin/activity-logs/index.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,231 @@
<?php $__env->startSection('title', 'Kelola Booking'); ?>
<?php $__env->startSection('content'); ?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">
<i class="bi bi-calendar-check me-2" style="color: #667eea;"></i>Kelola Booking
</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="<?php echo e(route('dashboard')); ?>" style="color: #667eea;">Dashboard</a></li>
<li class="breadcrumb-item active">Booking</li>
</ol>
</nav>
</div>
<a href="<?php echo e(route('admin.bookings.create')); ?>" class="btn" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<i class="bi bi-plus-lg me-1"></i>Booking Baru
</a>
</div>
<?php if(session('success')): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i><?php echo e(session('success')); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if(session('error')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-circle me-2"></i><?php echo e(session('error')); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<label class="form-label small text-muted">Status</label>
<select name="status" class="form-select">
<option value="">Semua Status</option>
<option value="pending" <?php echo e(request('status') == 'pending' ? 'selected' : ''); ?>>Menunggu Konfirmasi</option>
<option value="confirmed" <?php echo e(request('status') == 'confirmed' ? 'selected' : ''); ?>>Dikonfirmasi</option>
<option value="checked_in" <?php echo e(request('status') == 'checked_in' ? 'selected' : ''); ?>>Sedang Ditempati</option>
<option value="completed" <?php echo e(request('status') == 'completed' ? 'selected' : ''); ?>>Selesai</option>
<option value="cancelled" <?php echo e(request('status') == 'cancelled' ? 'selected' : ''); ?>>Dibatalkan</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label small text-muted">Kontrakan</label>
<select name="kontrakan_id" class="form-select">
<option value="">Semua Kontrakan</option>
<?php $__currentLoopData = $kontrakans; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $k): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<option value="<?php echo e($k->id); ?>" <?php echo e(request('kontrakan_id') == $k->id ? 'selected' : ''); ?>><?php echo e($k->nama); ?></option>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn me-2" style="border: 2px solid #667eea; color: #667eea; background: transparent;">
<i class="bi bi-filter me-1"></i>Filter
</button>
<a href="<?php echo e(route('admin.bookings.index')); ?>" class="btn btn-outline-secondary">
<i class="bi bi-x-lg"></i>
</a>
</div>
</form>
</div>
</div>
<div class="row g-3 mb-4">
<?php
$stats = [
['status' => 'pending', 'label' => 'Menunggu', 'color' => 'warning', 'icon' => 'hourglass-split'],
['status' => 'confirmed', 'label' => 'Dikonfirmasi', 'color' => 'info', 'icon' => 'check-circle'],
['status' => 'checked_in', 'label' => 'Ditempati', 'color' => 'success', 'icon' => 'house-door'],
['status' => 'completed', 'label' => 'Selesai', 'color' => 'secondary', 'icon' => 'check-all'],
];
?>
<?php $__currentLoopData = $stats; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $stat): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center">
<i class="bi bi-<?php echo e($stat['icon']); ?> text-<?php echo e($stat['color']); ?> fs-3"></i>
<h4 class="fw-bold mb-0 mt-2">
<?php echo e(\App\Models\Booking::where('status', $stat['status'])->count()); ?>
</h4>
<small class="text-muted"><?php echo e($stat['label']); ?></small>
</div>
</div>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-3">ID</th>
<th>Kontrakan</th>
<th>Penyewa</th>
<th>Periode</th>
<th>Biaya</th>
<th>Status</th>
<th>Pembayaran</th>
<th class="text-end pe-3">Aksi</th>
</tr>
</thead>
<tbody>
<?php $__empty_1 = true; $__currentLoopData = $bookings; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $booking): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<tr>
<td class="ps-3">
<span class="badge bg-light text-dark">#<?php echo e($booking->id); ?></span>
</td>
<td>
<a href="<?php echo e(route('kontrakan.show', $booking->kontrakan_id)); ?>" class="text-decoration-none fw-semibold">
<?php echo e($booking->kontrakan->nama ?? '-'); ?>
</a>
</td>
<td>
<div class="fw-semibold"><?php echo e($booking->tenant_name); ?></div>
<small class="text-muted"><?php echo e($booking->tenant_phone); ?></small>
</td>
<td>
<div><?php echo e($booking->start_date->format('d M Y')); ?></div>
<small class="text-muted">s/d <?php echo e($booking->end_date->format('d M Y')); ?></small>
<br>
<span class="badge bg-light text-dark"><?php echo e($booking->duration_days); ?> hari</span>
</td>
<td>
<span class="fw-semibold">Rp <?php echo e(number_format($booking->amount, 0, ',', '.')); ?></span>
</td>
<td>
<span class="badge <?php echo e($booking->status_badge_class); ?>">
<?php echo e($booking->status_label); ?>
</span>
</td>
<td>
<span class="badge <?php echo e($booking->payment_status == 'paid' ? 'bg-success' : 'bg-secondary'); ?>">
<?php echo e($booking->payment_status_label); ?>
</span>
<?php if($booking->payment_proof): ?>
<a href="<?php echo e(asset('storage/' . $booking->payment_proof)); ?>" target="_blank" class="btn btn-sm btn-outline-success ms-1" title="Lihat Bukti Transfer">
<i class="bi bi-image"></i>
</a>
<?php endif; ?>
<?php if($booking->booking_source == 'user'): ?>
<span class="badge bg-info ms-1" title="Booking dari User">
<i class="bi bi-person"></i>
</span>
<?php endif; ?>
</td>
<td class="text-end pe-3">
<div class="btn-group">
<a href="<?php echo e(route('admin.bookings.show', $booking->id)); ?>" class="btn btn-sm btn-outline-primary" title="Detail">
<i class="bi bi-eye"></i>
</a>
<?php if($booking->status == 'pending'): ?>
<form action="<?php echo e(route('admin.bookings.confirm', $booking->id)); ?>" method="POST" class="d-inline">
<?php echo csrf_field(); ?>
<button type="submit" class="btn btn-sm btn-success" title="Konfirmasi" onclick="return confirm('Konfirmasi booking ini?')">
<i class="bi bi-check-lg"></i>
</button>
</form>
<?php endif; ?>
<?php if($booking->status == 'confirmed'): ?>
<form action="<?php echo e(route('admin.bookings.check-in', $booking->id)); ?>" method="POST" class="d-inline">
<?php echo csrf_field(); ?>
<button type="submit" class="btn btn-sm btn-info" title="Check-in" onclick="return confirm('Penyewa check-in?')">
<i class="bi bi-box-arrow-in-right"></i>
</button>
</form>
<?php endif; ?>
<?php if($booking->status == 'checked_in'): ?>
<form action="<?php echo e(route('admin.bookings.check-out', $booking->id)); ?>" method="POST" class="d-inline">
<?php echo csrf_field(); ?>
<button type="submit" class="btn btn-sm btn-warning" title="Check-out" onclick="return confirm('Penyewa check-out?')">
<i class="bi bi-box-arrow-right"></i>
</button>
</form>
<?php endif; ?>
<?php if(auth()->user()->role == 'super_admin' || $booking->status == 'pending'): ?>
<form action="<?php echo e(route('admin.bookings.destroy', $booking->id)); ?>" method="POST" class="d-inline">
<?php echo csrf_field(); ?>
<?php echo method_field('DELETE'); ?>
<button type="submit" class="btn btn-sm btn-danger" title="Hapus" onclick="return confirm('Yakin ingin menghapus booking ini? Tindakan ini tidak dapat dibatalkan!')">
<i class="bi bi-trash"></i>
</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<tr>
<td colspan="8" class="text-center py-5 text-muted">
<i class="bi bi-calendar-x fs-1 d-block mb-2"></i>
Belum ada data booking
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if($bookings->hasPages()): ?>
<div class="card-footer bg-transparent">
<?php echo e($bookings->withQueryString()->links()); ?>
</div>
<?php endif; ?>
</div>
</div>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\TA\spk_kontrakan\resources\views/admin/bookings/index.blade.php ENDPATH**/ ?>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,224 @@
<?php $__env->startSection('title', 'Kelola User'); ?>
<?php $__env->startSection('content'); ?>
<div class="container-fluid px-4">
<style>
.header-users {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 2rem;
color: white;
margin-bottom: 2rem;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
}
.user-card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
transition: all 0.3s ease;
}
.user-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.2);
}
.badge-role {
font-size: 0.85rem;
padding: 0.35rem 0.75rem;
}
.badge-admin {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.badge-super-admin {
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
color: white;
}
.badge-user {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
</style>
<!-- Header -->
<div class="header-users">
<div class="row align-items-center">
<div class="col-md-8">
<h2 class="mb-2">
<i class="bi bi-people-fill me-3"></i>Kelola User Administrator
</h2>
<p class="mb-0 fs-6">Manage user accounts dan permissions</p>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
<a href="<?php echo e(route('admin.users.create')); ?>" class="btn btn-light fw-semibold">
<i class="bi bi-plus-circle me-2"></i>Tambah User
</a>
</div>
</div>
</div>
<!-- Alert -->
<?php if(session('success')): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i><?php echo e(session('success')); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<!-- Filter & Search -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<form action="<?php echo e(route('admin.users.index')); ?>" method="GET" class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold mb-2">Cari User</label>
<input type="text" name="search" class="form-control" placeholder="Nama atau email..." value="<?php echo e(request('search')); ?>">
</div>
<div class="col-md-3">
<label class="form-label fw-semibold mb-2">Role</label>
<select name="role" class="form-select">
<option value="">Semua Role</option>
<option value="super_admin" <?php echo e(request('role') == 'super_admin' ? 'selected' : ''); ?>>Super Admin</option>
<option value="admin" <?php echo e(request('role') == 'admin' ? 'selected' : ''); ?>>Admin</option>
<option value="user" <?php echo e(request('role') == 'user' ? 'selected' : ''); ?>>Mahasiswa</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold mb-2">Status</label>
<select name="status" class="form-select">
<option value="">Semua</option>
<option value="active" <?php echo e(request('status') == 'active' ? 'selected' : ''); ?>>Aktif</option>
<option value="inactive" <?php echo e(request('status') == 'inactive' ? 'selected' : ''); ?>>Tidak Aktif</option>
</select>
</div>
<div class="col-12 d-flex gap-2">
<button type="submit" class="btn" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<i class="bi bi-search me-2"></i>Cari
</button>
<a href="<?php echo e(route('admin.users.index')); ?>" class="btn btn-outline-secondary">
<i class="bi bi-arrow-clockwise me-2"></i>Reset
</a>
</div>
</form>
</div>
</div>
<!-- Users Table -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 py-3">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-list-ul me-2"></i>Daftar User (<?php echo e($users->total()); ?>)
</h5>
</div>
</div>
<div class="card-body p-0">
<?php if($users->isEmpty()): ?>
<div class="text-center py-5">
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
<h5 class="text-muted mt-3">Tidak ada user ditemukan</h5>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Nama</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Dibuat</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
<?php $__currentLoopData = $users; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<tr>
<td>
<div class="d-flex align-items-center gap-3">
<div class="avatar-sm rounded-circle" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;">
<?php echo e(strtoupper(substr($user->name, 0, 1))); ?>
</div>
<strong><?php echo e($user->name); ?></strong>
</div>
</td>
<td>
<small class="text-muted"><?php echo e($user->email); ?></small>
</td>
<td>
<?php if($user->role === 'super_admin'): ?>
<span class="badge badge-super-admin">Super Admin</span>
<?php elseif($user->role === 'admin'): ?>
<span class="badge badge-admin">Admin</span>
<?php elseif($user->role === 'user'): ?>
<span class="badge badge-user">Mahasiswa</span>
<?php else: ?>
<span class="badge bg-secondary"><?php echo e($user->role); ?></span>
<?php endif; ?>
</td>
<td>
<?php if($user->deleted_at): ?>
<span class="badge bg-danger">Tidak Aktif</span>
<?php else: ?>
<span class="badge bg-success">Aktif</span>
<?php endif; ?>
</td>
<td>
<small class="text-muted"><?php echo e($user->created_at->format('d M Y')); ?></small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<?php if($user->deleted_at): ?>
<form action="<?php echo e(route('admin.users.restore', $user->id)); ?>" method="POST" style="display: inline;">
<?php echo csrf_field(); ?>
<button type="submit" class="btn btn-warning btn-sm" title="Restore">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
</form>
<?php else: ?>
<a href="<?php echo e(route('admin.users.edit', $user->id)); ?>" class="btn btn-primary btn-sm" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<?php endif; ?>
<?php if($user->id !== auth()->id()): ?>
<form action="<?php echo e(route('admin.users.destroy', $user->id)); ?>" method="POST" style="display: inline;" onsubmit="return confirm('Yakin hapus user ini?');">
<?php echo csrf_field(); ?>
<?php echo method_field('DELETE'); ?>
<button type="submit" class="btn btn-danger btn-sm" title="Hapus">
<i class="bi bi-trash"></i>
</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Pagination -->
<div class="d-flex justify-content-center mt-4">
<?php echo e($users->links()); ?>
</div>
</div>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\TA\spk_kontrakan\resources\views/admin/users/index.blade.php ENDPATH**/ ?>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,821 @@
<?php $__env->startSection('title', 'Metode SAW'); ?>
<?php $__env->startSection('content'); ?>
<div class="container-fluid px-4">
<style>
.breadcrumb {
background: transparent;
border-radius: 8px;
padding: 0;
margin-bottom: 2rem;
}
.breadcrumb-item a {
color: #667eea;
font-weight: 500;
}
.breadcrumb-item.active {
color: #764ba2;
font-weight: 600;
}
.header-saw {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 2rem;
color: white;
margin-bottom: 2rem;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
}
.header-saw h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.header-saw p {
opacity: 0.95;
margin-bottom: 0;
}
.section-header {
color: #667eea;
font-size: 1.1rem;
font-weight: 700;
padding-bottom: 1rem;
border-bottom: 2px solid #667eea;
margin-bottom: 1.5rem;
}
.form-card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
overflow: hidden;
}
.btn-submit {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
font-weight: 600;
padding: 0.75rem 1.75rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
color: white;
}
</style>
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="<?php echo e(route('dashboard')); ?>">Dashboard</a></li>
<li class="breadcrumb-item active">Metode SAW</li>
</ol>
</nav>
<!-- Header -->
<div class="header-saw mb-4">
<h2 class="mb-0">🧮 Metode SAW</h2>
<p class="mb-0 opacity-95">Sistem Pendukung Keputusan untuk rekomendasi terbaik</p>
</div>
<!-- Alert -->
<?php if(session('error')): ?>
<div class="alert alert-danger alert-dismissible fade show shadow-sm mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<span><?php echo e(session('error')); ?></span>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<!-- Info Alert -->
<div class="alert alert-info border-0 shadow-sm mb-4" style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border-left: 4px solid #667eea;">
<div class="d-flex">
<div class="me-3">
<i class="bi bi-info-circle fs-5" style="color: #667eea;"></i>
</div>
<div>
<h6 class="fw-bold mb-2" style="color: #667eea;">Tentang Metode SAW</h6>
<p class="mb-0">
Metode SAW digunakan untuk menentukan ranking alternatif terbaik berdasarkan kriteria yang telah ditentukan.
Sistem akan menghitung nilai normalisasi dan bobot untuk setiap kriteria, kemudian menghasilkan peringkat rekomendasi.
</p>
</div>
</div>
</div>
<!-- Main Content -->
<div class="row">
<div class="col-12">
<!-- Choose Type Card -->
<div class="card form-card mb-4">
<div class="card-body p-4">
<h5 class="section-header mb-0">
<i class="bi bi-list-ul me-2" style="color: #667eea;"></i>Pilih Jenis Pencarian
</h5>
<form action="<?php echo e(route('saw.proses')); ?>" method="POST" id="sawForm">
<?php echo csrf_field(); ?>
<!-- Hidden inputs untuk koordinat user (HANYA UNTUK LAUNDRY) -->
<input type="hidden" name="user_lat" id="user_lat">
<input type="hidden" name="user_lng" id="user_lng">
<div class="row g-2 g-md-3 mb-3 mb-md-4">
<!-- Pilih Kontrakan -->
<div class="col-12 col-md-6">
<input type="radio" class="btn-check" name="tipe" id="tipe_kontrakan" value="kontrakan" checked>
<label class="btn btn-outline-warning w-100 py-3 py-md-4" for="tipe_kontrakan" style="border-color: #667eea; color: #667eea;">
<i class="bi bi-building fs-2 fs-md-1 d-block mb-2 mb-md-3"></i>
<h5 class="mb-1 mb-md-2 fs-6 fs-md-5">Kontrakan</h5>
<small class="text-muted">Cari kontrakan deket kampus</small>
</label>
</div>
<!-- Pilih Laundry -->
<div class="col-12 col-md-6">
<input type="radio" class="btn-check" name="tipe" id="tipe_laundry" value="laundry">
<label class="btn btn-outline-warning w-100 py-3 py-md-4" for="tipe_laundry" style="border-color: #667eea; color: #667eea;">
<i class="bi bi-basket3 fs-2 fs-md-1 d-block mb-2 mb-md-3"></i>
<h5 class="mb-1 mb-md-2 fs-6 fs-md-5">Laundry</h5>
<small class="text-muted">Cari laundry deket lokasi Anda</small>
</label>
</div>
</div>
<!-- INFO JARAK KONTRAKAN -->
<div class="alert border-0 mb-3 mb-md-4" id="infoKontrakan" style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border-left: 4px solid #667eea !important;">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle-fill fs-5 me-2 mt-1" style="color: #667eea;"></i>
<div>
<strong class="small" style="color: #667eea;">Jarak Kontrakan</strong>
<p class="mb-0 small mt-1">
Jarak dihitung otomatis dari <strong>Kampus Polije</strong>.
Sistem akan merekomendasikan kontrakan terdekat dari kampus.
</p>
</div>
</div>
</div>
<!-- DETEKSI LOKASI UNTUK LAUNDRY (OPSIONAL) -->
<div class="card form-card mb-4" id="deteksiLokasiCard" style="display: none;">
<div class="card-body p-4">
<div class="d-flex align-items-start align-items-md-center mb-3">
<i class="bi bi-geo-alt-fill fs-4 fs-md-3 me-2 me-md-3 mt-1 mt-md-0" style="color: #667eea;"></i>
<div class="flex-grow-1">
<h6 class="fw-bold mb-1 fs-6">Referensi Jarak Laundry</h6>
<small class="text-muted d-block" style="font-size: 0.8rem;">
Pilih titik referensi untuk menghitung jarak ke laundry
</small>
</div>
</div>
<!-- Pilihan Referensi Jarak -->
<div class="row g-2 mb-3">
<div class="col-6">
<input type="radio" class="btn-check" name="referensi_jarak" id="referensi_kampus" value="kampus" checked>
<label class="btn btn-outline-warning w-100 py-2" for="referensi_kampus" style="border-color: #667eea; color: #667eea;">
<i class="bi bi-building d-block fs-4 mb-1"></i>
<strong class="small d-block">Dari Kampus</strong>
<small class="text-muted" style="font-size: 0.7rem;">Default</small>
</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="referensi_jarak" id="referensi_user" value="user">
<label class="btn btn-outline-warning w-100 py-2" for="referensi_user" style="border-color: #667eea; color: #667eea;">
<i class="bi bi-person-pin d-block fs-4 mb-1"></i>
<strong class="small d-block">Dari Lokasi Saya</strong>
<small class="text-muted" style="font-size: 0.7rem;">GPS</small>
</label>
</div>
</div>
<!-- Info Default (Dari Kampus) -->
<div id="infoDefaultKampus" class="alert alert-info border-0 mb-0 p-2 small">
<i class="bi bi-info-circle me-2"></i>
Jarak laundry akan dihitung dari <strong>Kampus Polije</strong>.
</div>
<!-- Tombol Deteksi Lokasi (muncul ketika pilih "Dari Lokasi Saya") -->
<div id="deteksiLokasiWrapper" style="display: none;">
<button type="button" id="detectLocationBtn" class="btn btn-success w-100 mb-2">
<i class="bi bi-crosshair me-2"></i>Deteksi Lokasi Saya Sekarang
</button>
<div id="locationStatus" class="mt-2" style="display: none;">
<div class="alert alert-success mb-0 p-2 small">
<i class="bi bi-check-circle-fill me-2"></i>
<strong>Lokasi terdeteksi!</strong>
<div class="mt-1" style="font-size: 0.75rem;" id="locationCoords"></div>
</div>
</div>
<div id="locationWarning" class="alert alert-warning mb-0 p-2 small">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Klik tombol di atas</strong> untuk mendeteksi lokasi Anda.
Jika tidak diklik, jarak akan dihitung dari kampus.
</div>
</div>
</div>
</div>
<!-- Pilihan Jenis Layanan (khusus Laundry) -->
<div class="card form-card mb-4" id="jenisLayananCard" style="display: none;">
<div class="card-body p-4">
<h6 class="section-header mb-0">
<i class="bi bi-speedometer2 me-2" style="color: #667eea;"></i>Pilih Jenis Layanan
</h6>
<div class="row g-2 g-md-3">
<div class="col-12 col-md-4">
<input type="radio" class="btn-check" name="jenis_layanan" id="layanan_reguler" value="reguler">
<label class="btn btn-outline-warning w-100 py-2 py-md-3" for="layanan_reguler" style="border-color: #667eea; color: #667eea;">
<i class="bi bi-clock fs-5 fs-md-4 d-block mb-2"></i>
<strong class="small">Reguler</strong>
<small class="d-block text-muted" style="font-size: 0.75rem;">Normal</small>
</label>
</div>
<div class="col-12 col-md-4">
<input type="radio" class="btn-check" name="jenis_layanan" id="layanan_express" value="express">
<label class="btn btn-outline-warning w-100 py-2 py-md-3" for="layanan_express" style="border-color: #667eea; color: #667eea;">
<i class="bi bi-lightning-charge fs-5 fs-md-4 d-block mb-2"></i>
<strong class="small">Express</strong>
<small class="d-block text-muted" style="font-size: 0.75rem;">Cepat</small>
</label>
</div>
<div class="col-12 col-md-4">
<input type="radio" class="btn-check" name="jenis_layanan" id="layanan_kilat" value="kilat">
<label class="btn btn-outline-warning w-100 py-2 py-md-3" for="layanan_kilat" style="border-color: #667eea; color: #667eea;">
<i class="bi bi-rocket-takeoff fs-5 fs-md-4 d-block mb-2"></i>
<strong class="small">Kilat</strong>
<small class="d-block text-muted" style="font-size: 0.75rem;">Super Cepat</small>
</label>
</div>
</div>
</div>
</div>
<!-- Kriteria Info -->
<div class="card form-card mb-4">
<div class="card-body p-4">
<h6 class="section-header mb-0">
<i class="bi bi-star me-2" style="color: #667eea;"></i>Kriteria yang Digunakan
</h6>
<div class="table-responsive">
<table class="table table-sm mb-0 small" id="kriteriaTable">
<thead>
<tr>
<th>Kriteria</th>
<th class="text-center">Bobot</th>
<th class="text-center">Tipe</th>
</tr>
</thead>
<tbody>
<?php if(isset($kriteria) && $kriteria->count() > 0): ?>
<?php $__currentLoopData = $kriteria; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $k): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<tr data-tipe-bisnis="<?php echo e($k->tipe_bisnis ?? 'kontrakan'); ?>" data-bobot="<?php echo e($k->bobot ?? 0); ?>">
<td><?php echo e($k->nama_kriteria ?? 'N/A'); ?></td>
<td class="text-center">
<span class="badge" style="background-color: #667eea;"><?php echo e($k->bobot ?? 0); ?></span>
</td>
<td class="text-center">
<?php if(isset($k->tipe) && strtolower($k->tipe) == 'benefit'): ?>
<span class="badge bg-success">
<i class="bi bi-arrow-up-circle me-1"></i>Benefit
</span>
<?php else: ?>
<span class="badge bg-danger">
<i class="bi bi-arrow-down-circle me-1"></i>Cost
</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php else: ?>
<tr>
<td colspan="3" class="text-center text-muted">Tidak ada kriteria yang tersedia</td>
</tr>
<?php endif; ?>
</tbody>
<tfoot class="fw-bold">
<tr>
<td>Total Bobot</td>
<td class="text-center">
<span class="badge" id="totalBobotDisplay" style="background-color: #667eea;">
<?php echo e(isset($kriteria) ? $kriteria->where('tipe_bisnis', 'kontrakan')->sum('bobot') : 0); ?>
</span>
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-lg" id="btnProsesSAW" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 600;">
<span id="btnText">
<i class="bi bi-calculator me-2"></i>Proses Perhitungan SAW
</span>
<span id="btnLoading" style="display: none;">
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
Memproses data...
</span>
</button>
</div>
<!-- Loading Overlay -->
<div id="loadingOverlay" style="display: none;">
<div class="loading-content">
<div class="spinner-grow text-primary mb-3" style="width: 3rem; height: 3rem;" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h5 class="text-white mb-2">Memproses Perhitungan SAW</h5>
<p class="text-white-50 small">Mohon tunggu sebentar...</p>
<div class="progress mt-3" style="height: 4px; width: 250px; max-width: 90vw;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%"></div>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- Info Cards -->
<div class="row g-2 g-md-3">
<div class="col-12 col-md-6">
<div class="card border-0 h-100" style="background: rgba(102, 126, 234, 0.1);">
<div class="card-body p-3">
<h6 class="fw-bold mb-2 fs-6" style="color: #667eea;">
<i class="bi bi-check-circle me-2"></i>Kelebihan SAW
</h6>
<ul class="small mb-0" style="font-size: 0.85rem;">
<li>Mudah dipahami</li>
<li>Perhitungan sederhana</li>
<li>Hasil objektif</li>
<li>Jarak dihitung otomatis dari GPS</li>
</ul>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card border-0 h-100" style="background: rgba(102, 126, 234, 0.1);">
<div class="card-body p-3">
<h6 class="fw-bold mb-2 fs-6" style="color: #667eea;">
<i class="bi bi-lightbulb me-2"></i>Cara Kerja
</h6>
<ol class="small mb-0" style="font-size: 0.85rem;">
<li><strong>Kontrakan:</strong> Jarak dari Kampus Polije</li>
<li><strong>Laundry:</strong> Jarak dari Kampus <em>atau</em> Lokasi Anda</li>
<li>Normalisasi nilai kriteria</li>
<li>Kalikan dengan bobot</li>
<li>Urutkan berdasarkan nilai tertinggi</li>
</ol>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Toast Container -->
<div id="toastContainer" style="position: fixed; top: 10px; right: 10px; z-index: 10000; max-width: 90vw;"></div>
<style>
/* Purple Theme for Radio Buttons */
.btn-check:checked + .btn-outline-warning {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border-color: #667eea !important;
color: white !important;
}
.btn-check:checked + .btn-outline-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border-color: #667eea !important;
color: white !important;
}
.btn-check:checked + .btn-outline-success {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border-color: #667eea !important;
color: white !important;
}
.btn-check:checked + .btn-outline-danger {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border-color: #667eea !important;
color: white !important;
}
.btn-outline-warning:hover,
.btn-outline-primary:hover,
.btn-outline-success:hover,
.btn-outline-danger:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.2);
transition: all 0.3s ease;
}
#kriteriaTable tbody tr {
transition: all 0.3s ease;
}
.bg-gradient {
position: relative;
overflow: hidden;
}
.bg-gradient::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: pulse 3s ease-in-out infinite;
pointer-events: none;
z-index: 0;
}
.bg-gradient .btn,
.bg-gradient .card-body > * {
position: relative;
z-index: 1;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.1); opacity: 0.8; }
}
/* Loading Overlay */
#loadingOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(5px);
}
.loading-content {
text-align: center;
animation: fadeInUp 0.5s ease;
padding: 0 1rem;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Toast Notification */
.custom-toast {
min-width: 250px;
max-width: 350px;
margin-bottom: 10px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: slideInRight 0.3s ease;
font-size: 0.9rem;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.custom-toast.hiding {
animation: slideOutRight 0.3s ease;
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
/* Mobile optimizations */
@media (max-width: 767px) {
.btn-lg {
padding: 0.75rem 1rem;
font-size: 1rem;
}
.badge {
font-size: 0.75rem;
padding: 0.25em 0.5em;
}
#toastContainer {
right: 5px;
top: 5px;
}
.custom-toast {
min-width: auto;
width: calc(100vw - 20px);
max-width: calc(100vw - 20px);
}
}
</style>
<script>
// Toast Notification Function
function showToast(type, message) {
const container = document.getElementById('toastContainer');
const toastId = 'toast_' + Date.now();
const iconMap = {
success: 'check-circle-fill',
error: 'exclamation-triangle-fill',
warning: 'exclamation-circle-fill',
info: 'info-circle-fill'
};
const bgMap = {
success: 'bg-success',
error: 'bg-danger',
warning: 'bg-warning',
info: 'bg-info'
};
const toast = document.createElement('div');
toast.id = toastId;
toast.className = `custom-toast alert ${bgMap[type]} alert-dismissible fade show text-white`;
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<div class="d-flex align-items-center">
<i class="bi bi-${iconMap[type]} me-2"></i>
<div class="flex-grow-1 small">${message}</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
container.appendChild(toast);
// Auto remove after 4 seconds
setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 4000);
}
// Main Script
window.addEventListener('DOMContentLoaded', function() {
console.log('Script SAW loaded!');
const radioKontrakan = document.getElementById('tipe_kontrakan');
const radioLaundry = document.getElementById('tipe_laundry');
const kriteriaTable = document.getElementById('kriteriaTable');
const jenisLayananCard = document.getElementById('jenisLayananCard');
const referensiJarakCard = document.getElementById('referensiJarakCard');
const deteksiLokasiCard = document.getElementById('deteksiLokasiCard');
const infoKontrakan = document.getElementById('infoKontrakan');
const sawForm = document.getElementById('sawForm');
const detectLocationBtn = document.getElementById('detectLocationBtn');
const locationStatus = document.getElementById('locationStatus');
const locationCoords = document.getElementById('locationCoords');
const userLatInput = document.getElementById('user_lat');
const userLngInput = document.getElementById('user_lng');
const radioReferensiUser = document.getElementById('referensi_user');
const radioReferensiKampus = document.getElementById('referensi_kampus');
const deteksiLokasiWrapper = document.getElementById('deteksiLokasiWrapper');
const infoDefaultKampus = document.getElementById('infoDefaultKampus');
const locationWarning = document.getElementById('locationWarning');
let locationDetected = false;
// ========== TOGGLE REFERENSI JARAK (KAMPUS/USER) ==========
function toggleReferensiJarak() {
if (radioReferensiUser && radioReferensiUser.checked) {
// User memilih "Dari Lokasi Saya"
if (deteksiLokasiWrapper) deteksiLokasiWrapper.style.display = 'block';
if (infoDefaultKampus) infoDefaultKampus.style.display = 'none';
if (locationWarning) locationWarning.style.display = locationDetected ? 'none' : 'block';
} else {
// Default: "Dari Kampus"
if (deteksiLokasiWrapper) deteksiLokasiWrapper.style.display = 'none';
if (infoDefaultKampus) infoDefaultKampus.style.display = 'block';
// Reset user location ketika pilih kampus
if (userLatInput) userLatInput.value = '';
if (userLngInput) userLngInput.value = '';
}
}
// ========== TOGGLE DETEKSI LOKASI BERDASARKAN TIPE ==========
function toggleDeteksiLokasi() {
if (radioLaundry.checked) {
// LAUNDRY: Tampilkan jenis layanan dan pilihan referensi jarak
jenisLayananCard.style.display = 'block';
deteksiLokasiCard.style.display = 'block';
infoKontrakan.style.display = 'none';
toggleReferensiJarak();
} else {
// KONTRAKAN: Sembunyikan jenis layanan dan deteksi lokasi
jenisLayananCard.style.display = 'none';
deteksiLokasiCard.style.display = 'none';
infoKontrakan.style.display = 'block';
// Reset user location untuk kontrakan
if (userLatInput) userLatInput.value = '';
if (userLngInput) userLngInput.value = '';
}
}
// ========== EVENT LISTENER UNTUK PILIHAN REFERENSI JARAK ==========
if (radioReferensiUser) {
radioReferensiUser.addEventListener('change', toggleReferensiJarak);
}
if (radioReferensiKampus) {
radioReferensiKampus.addEventListener('change', toggleReferensiJarak);
}
// ========== DETEKSI LOKASI (HANYA UNTUK LAUNDRY) ==========
if (detectLocationBtn) {
detectLocationBtn.addEventListener('click', function(e) {
e.preventDefault();
console.log('Tombol deteksi lokasi diklik!');
const button = this;
const originalHTML = button.innerHTML;
if (!navigator.geolocation) {
showToast('error', 'Browser Anda tidak mendukung Geolocation.');
return;
}
button.disabled = true;
button.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Mendeteksi lokasi...';
navigator.geolocation.getCurrentPosition(
function(position) {
console.log('Lokasi berhasil terdeteksi!', position);
const lat = position.coords.latitude;
const lng = position.coords.longitude;
userLatInput.value = lat;
userLngInput.value = lng;
locationDetected = true;
console.log('Lat:', lat, 'Lng:', lng);
locationCoords.innerHTML = `
<strong>Latitude:</strong> ${lat.toFixed(6)}<br>
<strong>Longitude:</strong> ${lng.toFixed(6)}
`;
locationStatus.style.display = 'block';
if (locationWarning) locationWarning.style.display = 'none';
button.disabled = false;
button.innerHTML = '<i class="bi bi-check-circle-fill me-2"></i>Lokasi Terdeteksi!';
button.classList.remove('btn-success');
button.classList.add('btn-outline-success');
showToast('success', '📍 Lokasi Anda berhasil terdeteksi! Jarak laundry akan dihitung dari posisi Anda.');
},
function(error) {
console.error('Error geolocation:', error);
button.disabled = false;
button.innerHTML = originalHTML;
let errorMsg = '';
switch(error.code) {
case error.PERMISSION_DENIED:
errorMsg = 'Izinkan akses lokasi di browser Anda.';
break;
case error.POSITION_UNAVAILABLE:
errorMsg = 'Informasi lokasi tidak tersedia.';
break;
case error.TIMEOUT:
errorMsg = 'Waktu permintaan habis. Coba lagi.';
break;
default:
errorMsg = 'Terjadi kesalahan saat mendeteksi lokasi.';
}
showToast('error', errorMsg);
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
});
}
// ========== FILTER KRITERIA ==========
function filterKriteria() {
if (!kriteriaTable) return;
const selectedTipe = radioKontrakan.checked ? 'kontrakan' : 'laundry';
const rows = kriteriaTable.querySelectorAll('tbody tr');
let totalBobot = 0;
rows.forEach(row => {
const tipeBisnis = row.getAttribute('data-tipe-bisnis');
const bobot = parseFloat(row.getAttribute('data-bobot')) || 0;
if (tipeBisnis === selectedTipe) {
row.style.display = '';
totalBobot += bobot;
} else {
row.style.display = 'none';
}
});
const totalBobotDisplay = document.getElementById('totalBobotDisplay');
if (totalBobotDisplay) {
totalBobotDisplay.textContent = totalBobot.toFixed(2);
}
}
// ========== VALIDASI FORM ==========
if (sawForm) {
sawForm.addEventListener('submit', function(e) {
// Validasi jenis layanan untuk laundry
if (radioLaundry.checked) {
const jenisLayananChecked = document.querySelector('input[name="jenis_layanan"]:checked');
if (!jenisLayananChecked) {
e.preventDefault();
showToast('warning', '⚠️ Pilih jenis layanan terlebih dahulu!');
return false;
}
}
// Tampilkan loading
const btnProsesSAW = document.getElementById('btnProsesSAW');
const btnText = document.getElementById('btnText');
const btnLoading = document.getElementById('btnLoading');
const loadingOverlay = document.getElementById('loadingOverlay');
btnProsesSAW.disabled = true;
btnText.style.display = 'none';
btnLoading.style.display = 'inline-block';
loadingOverlay.style.display = 'flex';
});
}
// Event listeners
if (radioKontrakan) {
radioKontrakan.addEventListener('change', function() {
toggleDeteksiLokasi();
filterKriteria();
});
}
if (radioLaundry) {
radioLaundry.addEventListener('change', function() {
toggleDeteksiLokasi();
filterKriteria();
});
}
// Initialize
toggleDeteksiLokasi();
filterKriteria();
});
</script>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\TA\spk_kontrakan\resources\views/saw/index.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,109 @@
<?php if($paginator->hasPages()): ?>
<nav role="navigation" aria-label="<?php echo e(__('Pagination Navigation')); ?>" class="flex items-center justify-between">
<div class="flex justify-between flex-1 sm:hidden">
<?php if($paginator->onFirstPage()): ?>
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
<?php echo __('pagination.previous'); ?>
</span>
<?php else: ?>
<a href="<?php echo e($paginator->previousPageUrl()); ?>" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
<?php echo __('pagination.previous'); ?>
</a>
<?php endif; ?>
<?php if($paginator->hasMorePages()): ?>
<a href="<?php echo e($paginator->nextPageUrl()); ?>" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
<?php echo __('pagination.next'); ?>
</a>
<?php else: ?>
<span class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
<?php echo __('pagination.next'); ?>
</span>
<?php endif; ?>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700 leading-5 dark:text-gray-400">
<?php echo __('Showing'); ?>
<?php if($paginator->firstItem()): ?>
<span class="font-medium"><?php echo e($paginator->firstItem()); ?></span>
<?php echo __('to'); ?>
<span class="font-medium"><?php echo e($paginator->lastItem()); ?></span>
<?php else: ?>
<?php echo e($paginator->count()); ?>
<?php endif; ?>
<?php echo __('of'); ?>
<span class="font-medium"><?php echo e($paginator->total()); ?></span>
<?php echo __('results'); ?>
</p>
</div>
<div>
<span class="relative z-0 inline-flex rtl:flex-row-reverse shadow-sm rounded-md">
<?php if($paginator->onFirstPage()): ?>
<span aria-disabled="true" aria-label="<?php echo e(__('pagination.previous')); ?>">
<span class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-l-md leading-5 dark:bg-gray-800 dark:border-gray-600">
Prev
</span>
</span>
<?php else: ?>
<a href="<?php echo e($paginator->previousPageUrl()); ?>" rel="prev" class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-l-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="<?php echo e(__('pagination.previous')); ?>">
Prev
</a>
<?php endif; ?>
<?php $__currentLoopData = $elements; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $element): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php if(is_string($element)): ?>
<span aria-disabled="true">
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600"><?php echo e($element); ?></span>
</span>
<?php endif; ?>
<?php if(is_array($element)): ?>
<?php $__currentLoopData = $element; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $page => $url): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php if($page == $paginator->currentPage()): ?>
<span aria-current="page">
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600"><?php echo e($page); ?></span>
</span>
<?php else: ?>
<a href="<?php echo e($url); ?>" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:text-gray-300 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="<?php echo e(__('Go to page :page', ['page' => $page])); ?>">
<?php echo e($page); ?>
</a>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php if($paginator->hasMorePages()): ?>
<a href="<?php echo e($paginator->nextPageUrl()); ?>" rel="next" class="relative inline-flex items-center px-3 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-r-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="<?php echo e(__('pagination.next')); ?>">
Next
</a>
<?php else: ?>
<span aria-disabled="true" aria-label="<?php echo e(__('pagination.next')); ?>">
<span class="relative inline-flex items-center px-3 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-r-md leading-5 dark:bg-gray-800 dark:border-gray-600">
Next
</span>
</span>
<?php endif; ?>
</span>
</div>
</div>
</nav>
<?php endif; ?>
<?php /**PATH C:\laragon\www\TA\spk_kontrakan\resources\views/vendor/pagination/tailwind.blade.php ENDPATH**/ ?>

View File

@ -4,6 +4,8 @@ class User {
final String email;
final String? phone;
final String role;
final String? roleLabel;
final String? userType;
final DateTime? createdAt;
User({
@ -12,6 +14,8 @@ class User {
required this.email,
this.phone,
required this.role,
this.roleLabel,
this.userType,
this.createdAt,
});
@ -22,12 +26,41 @@ class User {
email: json['email'] ?? '',
phone: json['phone'],
role: json['role'] ?? 'user',
roleLabel: json['role_label'],
userType: json['user_type'],
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'])
: null,
);
}
/// Get user-friendly role label
String getRoleLabel() {
if (roleLabel != null && roleLabel!.isNotEmpty) {
return roleLabel!;
}
switch (role) {
case 'super_admin':
return 'Super Admin';
case 'admin':
return 'Admin';
case 'user':
return 'Mahasiswa';
default:
return role;
}
}
/// Check if user is mahasiswa
bool isMahasiswa() => role == 'user';
/// Check if user is admin
bool isAdmin() => role == 'admin';
/// Check if user is super admin
bool isSuperAdmin() => role == 'super_admin';
Map<String, dynamic> toJson() {
return {
'id': id,
@ -35,6 +68,8 @@ class User {
'email': email,
'phone': phone,
'role': role,
'role_label': roleLabel,
'user_type': userType,
'created_at': createdAt?.toIso8601String(),
};
}

View File

@ -58,12 +58,15 @@ class _FavoritesScreenState extends State<FavoritesScreen>
: <Laundry>[];
_isLoading = false;
});
debugPrint('[FAV_SCREEN] Loaded ${_kontrakanFavorites.length} kontrakan, ${_laundryFavorites.length} laundry');
debugPrint(
'[FAV_SCREEN] Loaded ${_kontrakanFavorites.length} kontrakan, ${_laundryFavorites.length} laundry',
);
} else {
setState(() {
_kontrakanFavorites = [];
_laundryFavorites = [];
_errorMessage = result['message']?.toString() ?? 'Gagal memuat favorit';
_errorMessage =
result['message']?.toString() ?? 'Gagal memuat favorit';
_isLoading = false;
});
debugPrint('[FAV_SCREEN] ❌ Load failed: $_errorMessage');
@ -128,27 +131,27 @@ class _FavoritesScreenState extends State<FavoritesScreen>
if (!mounted) return;
if (result['success'] == true) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(
Icons.check_circle_rounded,
color: Colors.white,
size: 20,
),
const SizedBox(width: 10),
Text(result['message'] ?? 'Dihapus dari favorit'),
],
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(
Icons.check_circle_rounded,
color: Colors.white,
size: 20,
),
const SizedBox(width: 10),
Text(result['message'] ?? 'Dihapus dari favorit'),
],
),
backgroundColor: const Color(0xFF2E7D32),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
),
backgroundColor: const Color(0xFF2E7D32),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
),
);
);
_loadFavorites();
}
} catch (e) {
@ -158,7 +161,9 @@ class _FavoritesScreenState extends State<FavoritesScreen>
content: const Text('Gagal menghapus favorit'),
backgroundColor: Colors.red.shade600,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
),
);
@ -301,44 +306,47 @@ class _FavoritesScreenState extends State<FavoritesScreen>
),
)
: _errorMessage != null
? Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline_rounded,
size: 56, color: Colors.red.shade300),
const SizedBox(height: 16),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Colors.grey[600],
),
),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: _loadFavorites,
icon: const Icon(Icons.refresh_rounded),
label: const Text('Coba Lagi'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1565C0),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
],
? Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline_rounded,
size: 56,
color: Colors.red.shade300,
),
),
)
: TabBarView(
controller: _tabController,
children: [_buildKontrakanList(), _buildLaundryList()],
const SizedBox(height: 16),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Colors.grey[600],
),
),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: _loadFavorites,
icon: const Icon(Icons.refresh_rounded),
label: const Text('Coba Lagi'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1565C0),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
),
)
: TabBarView(
controller: _tabController,
children: [_buildKontrakanList(), _buildLaundryList()],
),
),
],
),

View File

@ -73,7 +73,10 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
if (!_authService.isAuthenticated) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Silakan login terlebih dahulu'), backgroundColor: Colors.orange),
const SnackBar(
content: Text('Silakan login terlebih dahulu'),
backgroundColor: Colors.orange,
),
);
}
return;
@ -106,10 +109,16 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
} catch (e) {
if (mounted) {
setState(() {
if (wasFav) _favKontrakanIds.add(id); else _favKontrakanIds.remove(id);
if (wasFav)
_favKontrakanIds.add(id);
else
_favKontrakanIds.remove(id);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red.shade600),
SnackBar(
content: Text('Error: $e'),
backgroundColor: Colors.red.shade600,
),
);
}
}
@ -119,7 +128,10 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
if (!_authService.isAuthenticated) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Silakan login terlebih dahulu'), backgroundColor: Colors.orange),
const SnackBar(
content: Text('Silakan login terlebih dahulu'),
backgroundColor: Colors.orange,
),
);
}
return;
@ -152,10 +164,16 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
} catch (e) {
if (mounted) {
setState(() {
if (wasFav) _favLaundryIds.add(id); else _favLaundryIds.remove(id);
if (wasFav)
_favLaundryIds.add(id);
else
_favLaundryIds.remove(id);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red.shade600),
SnackBar(
content: Text('Error: $e'),
backgroundColor: Colors.red.shade600,
),
);
}
}

View File

@ -31,7 +31,9 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
Future<void> _checkFavorite() async {
try {
final result = await _favoriteService.isLaundryFavorite(widget.laundry.id);
final result = await _favoriteService.isLaundryFavorite(
widget.laundry.id,
);
if (mounted) setState(() => _isFavorite = result);
} catch (e) {
debugPrint('Check favorite error: $e');
@ -82,7 +84,9 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
content: Text(result['message'] ?? 'Gagal mengubah favorit'),
backgroundColor: Colors.red[700],
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
),
);
@ -734,7 +738,9 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
Future<void> _launchWhatsApp(String phone) async {
final cleaned = phone.replaceAll(RegExp(r'[^0-9]'), '');
final formatted = cleaned.startsWith('0') ? '62${cleaned.substring(1)}' : cleaned;
final formatted = cleaned.startsWith('0')
? '62${cleaned.substring(1)}'
: cleaned;
final url = 'https://wa.me/$formatted';
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);

View File

@ -509,7 +509,7 @@ class _ProfileScreenState extends State<ProfileScreen>
_buildStatItem(
icon: Icons.verified_user_rounded,
color: const Color(0xFF1565C0),
value: _currentUser?.role == 'admin' ? 'Admin' : 'User',
value: _currentUser?.getRoleLabel() ?? 'Mahasiswa',
label: 'Status',
),
],

View File

@ -338,9 +338,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Izin lokasi ditolak permanen. Ubah di pengaturan.',
),
content: Text('Izin lokasi ditolak permanen. Ubah di pengaturan.'),
backgroundColor: Colors.red,
),
);
@ -348,12 +346,13 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
return;
}
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
).timeout(
const Duration(seconds: 15),
onTimeout: () => throw TimeoutException('Deteksi lokasi timeout'),
);
Position position =
await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
).timeout(
const Duration(seconds: 15),
onTimeout: () => throw TimeoutException('Deteksi lokasi timeout'),
);
if (!mounted) return;
setState(() {
@ -373,7 +372,9 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal mendeteksi lokasi: ${e.toString().replaceAll('Exception: ', '')}'),
content: Text(
'Gagal mendeteksi lokasi: ${e.toString().replaceAll('Exception: ', '')}',
),
backgroundColor: Colors.red,
),
);
@ -449,19 +450,22 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
final initials = name.isEmpty
? 'U'
: name.trim().split(' ').length >= 2
? '${name.trim().split(' ')[0][0]}${name.trim().split(' ')[1][0]}'.toUpperCase()
: name.trim()[0].toUpperCase();
? '${name.trim().split(' ')[0][0]}${name.trim().split(' ')[1][0]}'
.toUpperCase()
: name.trim()[0].toUpperCase();
final hour = DateTime.now().hour;
final greeting = hour < 11
? 'Selamat Pagi'
: hour < 15
? 'Selamat Siang'
: hour < 18
? 'Selamat Sore'
: 'Selamat Malam';
? 'Selamat Siang'
: hour < 18
? 'Selamat Sore'
: 'Selamat Malam';
final categoryLabel = widget.category == 'kontrakan' ? 'kontrakan' : 'laundry';
final categoryLabel = widget.category == 'kontrakan'
? 'kontrakan'
: 'laundry';
return Container(
padding: const EdgeInsets.all(16),
@ -537,10 +541,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
const SizedBox(height: 2),
Text(
email,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade400,
),
style: TextStyle(fontSize: 12, color: Colors.grey.shade400),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@ -1091,11 +1092,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
Icons.refresh,
size: 18,
color: _categoryColor,
),
child: Icon(Icons.refresh, size: 18, color: _categoryColor),
),
),
],
@ -1121,10 +1118,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 10),
Text(
'Mendeteksi lokasi...',
style: TextStyle(fontSize: 12),
),
Text('Mendeteksi lokasi...', style: TextStyle(fontSize: 12)),
],
),
)
@ -1138,19 +1132,12 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
),
child: Row(
children: [
const Icon(
Icons.check_circle,
size: 16,
color: Colors.green,
),
const Icon(Icons.check_circle, size: 16, color: Colors.green),
const SizedBox(width: 8),
Expanded(
child: Text(
'Lokasi terdeteksi: ${_userLatitude!.toStringAsFixed(6)}, ${_userLongitude!.toStringAsFixed(6)}',
style: TextStyle(
fontSize: 11,
color: Colors.green[700],
),
style: TextStyle(fontSize: 11, color: Colors.green[700]),
),
),
],
@ -1251,8 +1238,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
return Column(
children: [
_buildBobotSummary(),
if (widget.category == 'laundry' &&
_userLatitude != null)
if (widget.category == 'laundry' && _userLatitude != null)
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),

View File

@ -85,22 +85,34 @@ class _SearchScreenState extends State<SearchScreen> {
Future<void> _toggleKontrakanFav(int id) async {
final wasFav = _favKontrakanIds.contains(id);
setState(() {
if (wasFav) _favKontrakanIds.remove(id); else _favKontrakanIds.add(id);
if (wasFav)
_favKontrakanIds.remove(id);
else
_favKontrakanIds.add(id);
});
try {
final result = await _favoriteService.toggleKontrakanFavorite(id);
if (result['success'] != true && mounted) {
setState(() {
if (wasFav) _favKontrakanIds.add(id); else _favKontrakanIds.remove(id);
if (wasFav)
_favKontrakanIds.add(id);
else
_favKontrakanIds.remove(id);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(result['message'] ?? 'Gagal'), backgroundColor: Colors.red.shade600),
SnackBar(
content: Text(result['message'] ?? 'Gagal'),
backgroundColor: Colors.red.shade600,
),
);
}
} catch (e) {
if (mounted) {
setState(() {
if (wasFav) _favKontrakanIds.add(id); else _favKontrakanIds.remove(id);
if (wasFav)
_favKontrakanIds.add(id);
else
_favKontrakanIds.remove(id);
});
}
}
@ -109,22 +121,34 @@ class _SearchScreenState extends State<SearchScreen> {
Future<void> _toggleLaundryFav(int id) async {
final wasFav = _favLaundryIds.contains(id);
setState(() {
if (wasFav) _favLaundryIds.remove(id); else _favLaundryIds.add(id);
if (wasFav)
_favLaundryIds.remove(id);
else
_favLaundryIds.add(id);
});
try {
final result = await _favoriteService.toggleLaundryFavorite(id);
if (result['success'] != true && mounted) {
setState(() {
if (wasFav) _favLaundryIds.add(id); else _favLaundryIds.remove(id);
if (wasFav)
_favLaundryIds.add(id);
else
_favLaundryIds.remove(id);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(result['message'] ?? 'Gagal'), backgroundColor: Colors.red.shade600),
SnackBar(
content: Text(result['message'] ?? 'Gagal'),
backgroundColor: Colors.red.shade600,
),
);
}
} catch (e) {
if (mounted) {
setState(() {
if (wasFav) _favLaundryIds.add(id); else _favLaundryIds.remove(id);
if (wasFav)
_favLaundryIds.add(id);
else
_favLaundryIds.remove(id);
});
}
}
@ -207,7 +231,8 @@ class _SearchScreenState extends State<SearchScreen> {
// Filter by price (laundry hargaKiloan is per-kg, no multiplication needed)
filtered = filtered.where((l) {
return l.hargaKiloan >= _priceRange.start && l.hargaKiloan <= _priceRange.end;
return l.hargaKiloan >= _priceRange.start &&
l.hargaKiloan <= _priceRange.end;
}).toList();
setState(() => _filteredLaundry = filtered);
@ -291,8 +316,8 @@ class _SearchScreenState extends State<SearchScreen> {
Expanded(
child: GestureDetector(
onTap: () {
setState(() => _selectedCategory = 'Kontrakan');
_applyFilters();
setState(() => _selectedCategory = 'Kontrakan');
_applyFilters();
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
@ -342,8 +367,8 @@ class _SearchScreenState extends State<SearchScreen> {
Expanded(
child: GestureDetector(
onTap: () {
setState(() => _selectedCategory = 'Laundry');
_applyLaundryFilters();
setState(() => _selectedCategory = 'Laundry');
_applyLaundryFilters();
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),