Update user management, booking model, and mobile screens
This commit is contained in:
parent
1a277067f4
commit
b192557bb8
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ public function store(Request $request)
|
||||||
'name' => 'required|string|min:3|max:255',
|
'name' => 'required|string|min:3|max:255',
|
||||||
'email' => 'required|email|unique:users,email',
|
'email' => 'required|email|unique:users,email',
|
||||||
'password' => 'required|string|min:8|confirmed',
|
'password' => 'required|string|min:8|confirmed',
|
||||||
'role' => 'required|in:admin,super_admin',
|
'role' => 'required|in:admin,super_admin,user',
|
||||||
], [
|
], [
|
||||||
'name.required' => 'Nama harus diisi',
|
'name.required' => 'Nama harus diisi',
|
||||||
'email.required' => 'Email harus diisi',
|
'email.required' => 'Email harus diisi',
|
||||||
|
|
@ -85,7 +85,7 @@ public function update(Request $request, User $user)
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => 'required|string|min:3|max:255',
|
'name' => 'required|string|min:3|max:255',
|
||||||
'email' => 'required|email|unique:users,email,' . $user->id,
|
'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',
|
'password' => 'nullable|string|min:8|confirmed',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,19 @@ public function scopeCurrentlyActive(Builder $query): Builder
|
||||||
->where('end_date', '>=', $today);
|
->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 ==========
|
// ========== HELPERS ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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
|
* 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
|
* @return bool
|
||||||
*/
|
*/
|
||||||
|
|
@ -80,6 +106,31 @@ public function isUser()
|
||||||
return $this->role === 'user';
|
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
|
* Helper function untuk cek role
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -67,15 +67,16 @@ class="form-control @error('email') is-invalid @enderror"
|
||||||
</label>
|
</label>
|
||||||
<select class="form-select @error('role') is-invalid @enderror" id="role" name="role">
|
<select class="form-select @error('role') is-invalid @enderror" id="role" name="role">
|
||||||
<option value="">-- Pilih Role --</option>
|
<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="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>
|
</select>
|
||||||
@error('role')
|
@error('role')
|
||||||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||||
@enderror
|
@enderror
|
||||||
<small class="text-muted d-block mt-2">
|
<small class="text-muted d-block mt-2">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<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>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,17 @@ class="form-control @error('email') is-invalid @enderror"
|
||||||
<i class="bi bi-shield me-2"></i>Role
|
<i class="bi bi-shield me-2"></i>Role
|
||||||
</label>
|
</label>
|
||||||
<select class="form-select @error('role') is-invalid @enderror" id="role" name="role">
|
<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="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>
|
</select>
|
||||||
@error('role')
|
@error('role')
|
||||||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||||
@enderror
|
@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>
|
||||||
|
|
||||||
<div class="alert alert-info border-0 rounded-3 mb-4">
|
<div class="alert alert-info border-0 rounded-3 mb-4">
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@
|
||||||
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
|
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-user {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
@ -80,8 +85,9 @@
|
||||||
<label class="form-label fw-semibold mb-2">Role</label>
|
<label class="form-label fw-semibold mb-2">Role</label>
|
||||||
<select name="role" class="form-select">
|
<select name="role" class="form-select">
|
||||||
<option value="">Semua Role</option>
|
<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="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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -152,8 +158,12 @@
|
||||||
<td>
|
<td>
|
||||||
@if($user->role === 'super_admin')
|
@if($user->role === 'super_admin')
|
||||||
<span class="badge badge-super-admin">Super Admin</span>
|
<span class="badge badge-super-admin">Super Admin</span>
|
||||||
@else
|
@elseif($user->role === 'admin')
|
||||||
<span class="badge badge-admin">Admin</span>
|
<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
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -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**/ ?>
|
||||||
|
|
@ -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
|
|
@ -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
|
|
@ -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**/ ?>
|
||||||
|
|
@ -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**/ ?>
|
||||||
|
|
@ -4,6 +4,8 @@ class User {
|
||||||
final String email;
|
final String email;
|
||||||
final String? phone;
|
final String? phone;
|
||||||
final String role;
|
final String role;
|
||||||
|
final String? roleLabel;
|
||||||
|
final String? userType;
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
|
|
||||||
User({
|
User({
|
||||||
|
|
@ -12,6 +14,8 @@ class User {
|
||||||
required this.email,
|
required this.email,
|
||||||
this.phone,
|
this.phone,
|
||||||
required this.role,
|
required this.role,
|
||||||
|
this.roleLabel,
|
||||||
|
this.userType,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -22,12 +26,41 @@ class User {
|
||||||
email: json['email'] ?? '',
|
email: json['email'] ?? '',
|
||||||
phone: json['phone'],
|
phone: json['phone'],
|
||||||
role: json['role'] ?? 'user',
|
role: json['role'] ?? 'user',
|
||||||
|
roleLabel: json['role_label'],
|
||||||
|
userType: json['user_type'],
|
||||||
createdAt: json['created_at'] != null
|
createdAt: json['created_at'] != null
|
||||||
? DateTime.parse(json['created_at'])
|
? DateTime.parse(json['created_at'])
|
||||||
: null,
|
: 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() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'id': id,
|
'id': id,
|
||||||
|
|
@ -35,6 +68,8 @@ class User {
|
||||||
'email': email,
|
'email': email,
|
||||||
'phone': phone,
|
'phone': phone,
|
||||||
'role': role,
|
'role': role,
|
||||||
|
'role_label': roleLabel,
|
||||||
|
'user_type': userType,
|
||||||
'created_at': createdAt?.toIso8601String(),
|
'created_at': createdAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,15 @@ class _FavoritesScreenState extends State<FavoritesScreen>
|
||||||
: <Laundry>[];
|
: <Laundry>[];
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
debugPrint('[FAV_SCREEN] Loaded ${_kontrakanFavorites.length} kontrakan, ${_laundryFavorites.length} laundry');
|
debugPrint(
|
||||||
|
'[FAV_SCREEN] Loaded ${_kontrakanFavorites.length} kontrakan, ${_laundryFavorites.length} laundry',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_kontrakanFavorites = [];
|
_kontrakanFavorites = [];
|
||||||
_laundryFavorites = [];
|
_laundryFavorites = [];
|
||||||
_errorMessage = result['message']?.toString() ?? 'Gagal memuat favorit';
|
_errorMessage =
|
||||||
|
result['message']?.toString() ?? 'Gagal memuat favorit';
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
debugPrint('[FAV_SCREEN] ❌ Load failed: $_errorMessage');
|
debugPrint('[FAV_SCREEN] ❌ Load failed: $_errorMessage');
|
||||||
|
|
@ -128,27 +131,27 @@ class _FavoritesScreenState extends State<FavoritesScreen>
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (result['success'] == true) {
|
if (result['success'] == true) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons.check_circle_rounded,
|
Icons.check_circle_rounded,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(result['message'] ?? 'Dihapus dari favorit'),
|
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();
|
_loadFavorites();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -158,7 +161,9 @@ class _FavoritesScreenState extends State<FavoritesScreen>
|
||||||
content: const Text('Gagal menghapus favorit'),
|
content: const Text('Gagal menghapus favorit'),
|
||||||
backgroundColor: Colors.red.shade600,
|
backgroundColor: Colors.red.shade600,
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -301,44 +306,47 @@ class _FavoritesScreenState extends State<FavoritesScreen>
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _errorMessage != null
|
: _errorMessage != null
|
||||||
? Center(
|
? Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.error_outline_rounded,
|
Icon(
|
||||||
size: 56, color: Colors.red.shade300),
|
Icons.error_outline_rounded,
|
||||||
const SizedBox(height: 16),
|
size: 56,
|
||||||
Text(
|
color: Colors.red.shade300,
|
||||||
_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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
)
|
Text(
|
||||||
: TabBarView(
|
_errorMessage!,
|
||||||
controller: _tabController,
|
textAlign: TextAlign.center,
|
||||||
children: [_buildKontrakanList(), _buildLaundryList()],
|
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()],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,10 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
|
||||||
if (!_authService.isAuthenticated) {
|
if (!_authService.isAuthenticated) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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;
|
return;
|
||||||
|
|
@ -106,10 +109,16 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (wasFav) _favKontrakanIds.add(id); else _favKontrakanIds.remove(id);
|
if (wasFav)
|
||||||
|
_favKontrakanIds.add(id);
|
||||||
|
else
|
||||||
|
_favKontrakanIds.remove(id);
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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 (!_authService.isAuthenticated) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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;
|
return;
|
||||||
|
|
@ -152,10 +164,16 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (wasFav) _favLaundryIds.add(id); else _favLaundryIds.remove(id);
|
if (wasFav)
|
||||||
|
_favLaundryIds.add(id);
|
||||||
|
else
|
||||||
|
_favLaundryIds.remove(id);
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red.shade600),
|
SnackBar(
|
||||||
|
content: Text('Error: $e'),
|
||||||
|
backgroundColor: Colors.red.shade600,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
|
||||||
|
|
||||||
Future<void> _checkFavorite() async {
|
Future<void> _checkFavorite() async {
|
||||||
try {
|
try {
|
||||||
final result = await _favoriteService.isLaundryFavorite(widget.laundry.id);
|
final result = await _favoriteService.isLaundryFavorite(
|
||||||
|
widget.laundry.id,
|
||||||
|
);
|
||||||
if (mounted) setState(() => _isFavorite = result);
|
if (mounted) setState(() => _isFavorite = result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Check favorite error: $e');
|
debugPrint('Check favorite error: $e');
|
||||||
|
|
@ -82,7 +84,9 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
|
||||||
content: Text(result['message'] ?? 'Gagal mengubah favorit'),
|
content: Text(result['message'] ?? 'Gagal mengubah favorit'),
|
||||||
backgroundColor: Colors.red[700],
|
backgroundColor: Colors.red[700],
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -734,7 +738,9 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
|
||||||
|
|
||||||
Future<void> _launchWhatsApp(String phone) async {
|
Future<void> _launchWhatsApp(String phone) async {
|
||||||
final cleaned = phone.replaceAll(RegExp(r'[^0-9]'), '');
|
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';
|
final url = 'https://wa.me/$formatted';
|
||||||
if (await canLaunchUrl(Uri.parse(url))) {
|
if (await canLaunchUrl(Uri.parse(url))) {
|
||||||
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
icon: Icons.verified_user_rounded,
|
icon: Icons.verified_user_rounded,
|
||||||
color: const Color(0xFF1565C0),
|
color: const Color(0xFF1565C0),
|
||||||
value: _currentUser?.role == 'admin' ? 'Admin' : 'User',
|
value: _currentUser?.getRoleLabel() ?? 'Mahasiswa',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -338,9 +338,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text(
|
content: Text('Izin lokasi ditolak permanen. Ubah di pengaturan.'),
|
||||||
'Izin lokasi ditolak permanen. Ubah di pengaturan.',
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -348,12 +346,13 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Position position = await Geolocator.getCurrentPosition(
|
Position position =
|
||||||
desiredAccuracy: LocationAccuracy.high,
|
await Geolocator.getCurrentPosition(
|
||||||
).timeout(
|
desiredAccuracy: LocationAccuracy.high,
|
||||||
const Duration(seconds: 15),
|
).timeout(
|
||||||
onTimeout: () => throw TimeoutException('Deteksi lokasi timeout'),
|
const Duration(seconds: 15),
|
||||||
);
|
onTimeout: () => throw TimeoutException('Deteksi lokasi timeout'),
|
||||||
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -373,7 +372,9 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Gagal mendeteksi lokasi: ${e.toString().replaceAll('Exception: ', '')}'),
|
content: Text(
|
||||||
|
'Gagal mendeteksi lokasi: ${e.toString().replaceAll('Exception: ', '')}',
|
||||||
|
),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -449,19 +450,22 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
final initials = name.isEmpty
|
final initials = name.isEmpty
|
||||||
? 'U'
|
? 'U'
|
||||||
: name.trim().split(' ').length >= 2
|
: name.trim().split(' ').length >= 2
|
||||||
? '${name.trim().split(' ')[0][0]}${name.trim().split(' ')[1][0]}'.toUpperCase()
|
? '${name.trim().split(' ')[0][0]}${name.trim().split(' ')[1][0]}'
|
||||||
: name.trim()[0].toUpperCase();
|
.toUpperCase()
|
||||||
|
: name.trim()[0].toUpperCase();
|
||||||
|
|
||||||
final hour = DateTime.now().hour;
|
final hour = DateTime.now().hour;
|
||||||
final greeting = hour < 11
|
final greeting = hour < 11
|
||||||
? 'Selamat Pagi'
|
? 'Selamat Pagi'
|
||||||
: hour < 15
|
: hour < 15
|
||||||
? 'Selamat Siang'
|
? 'Selamat Siang'
|
||||||
: hour < 18
|
: hour < 18
|
||||||
? 'Selamat Sore'
|
? 'Selamat Sore'
|
||||||
: 'Selamat Malam';
|
: 'Selamat Malam';
|
||||||
|
|
||||||
final categoryLabel = widget.category == 'kontrakan' ? 'kontrakan' : 'laundry';
|
final categoryLabel = widget.category == 'kontrakan'
|
||||||
|
? 'kontrakan'
|
||||||
|
: 'laundry';
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -537,10 +541,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
email,
|
email,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade400),
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey.shade400,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|
@ -1091,11 +1092,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
child: Icon(
|
child: Icon(Icons.refresh, size: 18, color: _categoryColor),
|
||||||
Icons.refresh,
|
|
||||||
size: 18,
|
|
||||||
color: _categoryColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -1121,10 +1118,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
),
|
),
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Text(
|
Text('Mendeteksi lokasi...', style: TextStyle(fontSize: 12)),
|
||||||
'Mendeteksi lokasi...',
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1138,19 +1132,12 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(Icons.check_circle, size: 16, color: Colors.green),
|
||||||
Icons.check_circle,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Lokasi terdeteksi: ${_userLatitude!.toStringAsFixed(6)}, ${_userLongitude!.toStringAsFixed(6)}',
|
'Lokasi terdeteksi: ${_userLatitude!.toStringAsFixed(6)}, ${_userLongitude!.toStringAsFixed(6)}',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 11, color: Colors.green[700]),
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.green[700],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -1251,8 +1238,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
_buildBobotSummary(),
|
_buildBobotSummary(),
|
||||||
if (widget.category == 'laundry' &&
|
if (widget.category == 'laundry' && _userLatitude != null)
|
||||||
_userLatitude != null)
|
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
|
|
||||||
|
|
@ -85,22 +85,34 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
Future<void> _toggleKontrakanFav(int id) async {
|
Future<void> _toggleKontrakanFav(int id) async {
|
||||||
final wasFav = _favKontrakanIds.contains(id);
|
final wasFav = _favKontrakanIds.contains(id);
|
||||||
setState(() {
|
setState(() {
|
||||||
if (wasFav) _favKontrakanIds.remove(id); else _favKontrakanIds.add(id);
|
if (wasFav)
|
||||||
|
_favKontrakanIds.remove(id);
|
||||||
|
else
|
||||||
|
_favKontrakanIds.add(id);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
final result = await _favoriteService.toggleKontrakanFavorite(id);
|
final result = await _favoriteService.toggleKontrakanFavorite(id);
|
||||||
if (result['success'] != true && mounted) {
|
if (result['success'] != true && mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (wasFav) _favKontrakanIds.add(id); else _favKontrakanIds.remove(id);
|
if (wasFav)
|
||||||
|
_favKontrakanIds.add(id);
|
||||||
|
else
|
||||||
|
_favKontrakanIds.remove(id);
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
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 {
|
Future<void> _toggleLaundryFav(int id) async {
|
||||||
final wasFav = _favLaundryIds.contains(id);
|
final wasFav = _favLaundryIds.contains(id);
|
||||||
setState(() {
|
setState(() {
|
||||||
if (wasFav) _favLaundryIds.remove(id); else _favLaundryIds.add(id);
|
if (wasFav)
|
||||||
|
_favLaundryIds.remove(id);
|
||||||
|
else
|
||||||
|
_favLaundryIds.add(id);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
final result = await _favoriteService.toggleLaundryFavorite(id);
|
final result = await _favoriteService.toggleLaundryFavorite(id);
|
||||||
if (result['success'] != true && mounted) {
|
if (result['success'] != true && mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (wasFav) _favLaundryIds.add(id); else _favLaundryIds.remove(id);
|
if (wasFav)
|
||||||
|
_favLaundryIds.add(id);
|
||||||
|
else
|
||||||
|
_favLaundryIds.remove(id);
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
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)
|
// Filter by price (laundry hargaKiloan is per-kg, no multiplication needed)
|
||||||
filtered = filtered.where((l) {
|
filtered = filtered.where((l) {
|
||||||
return l.hargaKiloan >= _priceRange.start && l.hargaKiloan <= _priceRange.end;
|
return l.hargaKiloan >= _priceRange.start &&
|
||||||
|
l.hargaKiloan <= _priceRange.end;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
setState(() => _filteredLaundry = filtered);
|
setState(() => _filteredLaundry = filtered);
|
||||||
|
|
@ -291,8 +316,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() => _selectedCategory = 'Kontrakan');
|
setState(() => _selectedCategory = 'Kontrakan');
|
||||||
_applyFilters();
|
_applyFilters();
|
||||||
},
|
},
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
|
|
@ -342,8 +367,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() => _selectedCategory = 'Laundry');
|
setState(() => _selectedCategory = 'Laundry');
|
||||||
_applyLaundryFilters();
|
_applyLaundryFilters();
|
||||||
},
|
},
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue