From b192557bb8835ee7064d9169172157be7e60ae26 Mon Sep 17 00:00:00 2001 From: micko samawa Date: Sat, 14 Mar 2026 11:11:28 +0700 Subject: [PATCH] Update user management, booking model, and mobile screens --- spk_kontrakan/BOOKING_AUTO_VERIFY_DOCS.md | 168 ++ .../Console/Commands/AutoVerifyBookings.php | 94 + spk_kontrakan/app/Console/Kernel.php | 42 + .../Controllers/UserManagementController.php | 4 +- spk_kontrakan/app/Models/Booking.php | 13 + spk_kontrakan/app/Models/User.php | 53 +- ...d_owner_verification_to_bookings_table.php | 43 + .../views/admin/users/create.blade.php | 5 +- .../views/admin/users/edit.blade.php | 7 +- .../views/admin/users/index.blade.php | 14 +- .../15adb941f057c688df2ceb4a9c21ef52.php | 163 ++ .../2487b2a216139ea38383fd97d51e2280.php | 231 ++ .../750ed3e845a2f0e93cc431f7924de6f1.php | 1383 ++++++++++ .../84dd4efe61f3c815daff24c589049140.php | 2309 +++++++++++++++++ .../997e6940f3b97384fb2e68c445e4a212.php | 224 ++ .../b013a8c110c7b48f95c189a44c72a9f6.php | 1763 +++++++++++++ .../ee70804036bd06aaaf7aa7c82c92bb1c.php | 821 ++++++ .../f9a1a87513eabf56a496d6aacefe5611.php | 109 + spk_mobile/lib/models/user.dart | 35 + spk_mobile/lib/screens/favorites_screen.dart | 126 +- .../lib/screens/improved_home_screen.dart | 30 +- .../lib/screens/laundry_detail_screen.dart | 12 +- spk_mobile/lib/screens/profile_screen.dart | 2 +- .../lib/screens/recommendation_screen.dart | 68 +- spk_mobile/lib/screens/search_screen.dart | 51 +- 25 files changed, 7639 insertions(+), 131 deletions(-) create mode 100644 spk_kontrakan/BOOKING_AUTO_VERIFY_DOCS.md create mode 100644 spk_kontrakan/app/Console/Commands/AutoVerifyBookings.php create mode 100644 spk_kontrakan/app/Console/Kernel.php create mode 100644 spk_kontrakan/database/migrations/2026_03_07_000001_add_owner_verification_to_bookings_table.php create mode 100644 spk_kontrakan/storage/framework/views/15adb941f057c688df2ceb4a9c21ef52.php create mode 100644 spk_kontrakan/storage/framework/views/2487b2a216139ea38383fd97d51e2280.php create mode 100644 spk_kontrakan/storage/framework/views/750ed3e845a2f0e93cc431f7924de6f1.php create mode 100644 spk_kontrakan/storage/framework/views/84dd4efe61f3c815daff24c589049140.php create mode 100644 spk_kontrakan/storage/framework/views/997e6940f3b97384fb2e68c445e4a212.php create mode 100644 spk_kontrakan/storage/framework/views/b013a8c110c7b48f95c189a44c72a9f6.php create mode 100644 spk_kontrakan/storage/framework/views/ee70804036bd06aaaf7aa7c82c92bb1c.php create mode 100644 spk_kontrakan/storage/framework/views/f9a1a87513eabf56a496d6aacefe5611.php diff --git a/spk_kontrakan/BOOKING_AUTO_VERIFY_DOCS.md b/spk_kontrakan/BOOKING_AUTO_VERIFY_DOCS.md new file mode 100644 index 0000000..05c14ab --- /dev/null +++ b/spk_kontrakan/BOOKING_AUTO_VERIFY_DOCS.md @@ -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 diff --git a/spk_kontrakan/app/Console/Commands/AutoVerifyBookings.php b/spk_kontrakan/app/Console/Commands/AutoVerifyBookings.php new file mode 100644 index 0000000..e7cff7a --- /dev/null +++ b/spk_kontrakan/app/Console/Commands/AutoVerifyBookings.php @@ -0,0 +1,94 @@ +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; + } + } +} diff --git a/spk_kontrakan/app/Console/Kernel.php b/spk_kontrakan/app/Console/Kernel.php new file mode 100644 index 0000000..a7effab --- /dev/null +++ b/spk_kontrakan/app/Console/Kernel.php @@ -0,0 +1,42 @@ +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'); + } +} diff --git a/spk_kontrakan/app/Http/Controllers/UserManagementController.php b/spk_kontrakan/app/Http/Controllers/UserManagementController.php index 68ed274..0d61724 100644 --- a/spk_kontrakan/app/Http/Controllers/UserManagementController.php +++ b/spk_kontrakan/app/Http/Controllers/UserManagementController.php @@ -53,7 +53,7 @@ public function store(Request $request) 'name' => 'required|string|min:3|max:255', 'email' => 'required|email|unique:users,email', 'password' => 'required|string|min:8|confirmed', - 'role' => 'required|in:admin,super_admin', + 'role' => 'required|in:admin,super_admin,user', ], [ 'name.required' => 'Nama harus diisi', 'email.required' => 'Email harus diisi', @@ -85,7 +85,7 @@ public function update(Request $request, User $user) $validated = $request->validate([ 'name' => 'required|string|min:3|max:255', 'email' => 'required|email|unique:users,email,' . $user->id, - 'role' => 'required|in:admin,super_admin', + 'role' => 'required|in:admin,super_admin,user', 'password' => 'nullable|string|min:8|confirmed', ]); diff --git a/spk_kontrakan/app/Models/Booking.php b/spk_kontrakan/app/Models/Booking.php index ba7c112..32c794b 100644 --- a/spk_kontrakan/app/Models/Booking.php +++ b/spk_kontrakan/app/Models/Booking.php @@ -182,6 +182,19 @@ public function scopeCurrentlyActive(Builder $query): Builder ->where('end_date', '>=', $today); } + /** + * Scope: Booking yang siap untuk auto-verify + * (pembayaran sudah, belum checked_in, check-in date sudah tiba) + */ + public function scopeReadyForAutoVerify(Builder $query): Builder + { + $today = now()->toDateString(); + return $query->where('payment_status', self::PAYMENT_PAID) + ->whereIn('status', [self::STATUS_PENDING, self::STATUS_CONFIRMED]) + ->where('start_date', '<=', $today) + ->whereNull('checked_in_at'); + } + // ========== HELPERS ========== /** diff --git a/spk_kontrakan/app/Models/User.php b/spk_kontrakan/app/Models/User.php index ec4c1cb..de89690 100644 --- a/spk_kontrakan/app/Models/User.php +++ b/spk_kontrakan/app/Models/User.php @@ -50,6 +50,32 @@ protected function casts(): array ]; } + /** + * Append role_label dan user_type ke JSON response + */ + protected $appends = ['role_label', 'user_type']; + + /** + * Get role label accessor + */ + public function getRoleLabelAttribute(): string + { + return match($this->role) { + 'super_admin' => 'Super Admin', + 'admin' => 'Admin', + 'user' => 'Mahasiswa', + default => $this->role, + }; + } + + /** + * Get user type accessor (untuk kompatibilitas API) + */ + public function getUserTypeAttribute(): string + { + return $this->role === 'user' ? 'mahasiswa' : $this->role; + } + /** * Helper function untuk cek apakah user adalah Super Admin * @@ -71,7 +97,7 @@ public function isAdmin() } /** - * Helper function untuk cek apakah user adalah User biasa + * Helper function untuk cek apakah user adalah Mahasiswa (User biasa) * * @return bool */ @@ -80,6 +106,31 @@ public function isUser() return $this->role === 'user'; } + /** + * Helper function untuk cek apakah user adalah Mahasiswa + * + * @return bool + */ + public function isMahasiswa() + { + return $this->role === 'user'; + } + + /** + * Get role label yang user-friendly + * + * @return string + */ + public function getRoleLabel() + { + return match($this->role) { + 'super_admin' => 'Super Admin', + 'admin' => 'Admin', + 'user' => 'Mahasiswa', + default => $this->role, + }; + } + /** * Helper function untuk cek role * diff --git a/spk_kontrakan/database/migrations/2026_03_07_000001_add_owner_verification_to_bookings_table.php b/spk_kontrakan/database/migrations/2026_03_07_000001_add_owner_verification_to_bookings_table.php new file mode 100644 index 0000000..3a67343 --- /dev/null +++ b/spk_kontrakan/database/migrations/2026_03_07_000001_add_owner_verification_to_bookings_table.php @@ -0,0 +1,43 @@ +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', + ]); + }); + } +}; diff --git a/spk_kontrakan/resources/views/admin/users/create.blade.php b/spk_kontrakan/resources/views/admin/users/create.blade.php index 88876ae..49b971e 100644 --- a/spk_kontrakan/resources/views/admin/users/create.blade.php +++ b/spk_kontrakan/resources/views/admin/users/create.blade.php @@ -67,15 +67,16 @@ class="form-control @error('email') is-invalid @enderror" @error('role')
{{ $message }}
@enderror - Super Admin memiliki akses penuh ke semua fitur sistem + Super Admin: Akses penuh ke semua fitur | Admin: Kelola konten | Mahasiswa: User umum aplikasi mobile diff --git a/spk_kontrakan/resources/views/admin/users/edit.blade.php b/spk_kontrakan/resources/views/admin/users/edit.blade.php index a4e6c4c..4c7ac74 100644 --- a/spk_kontrakan/resources/views/admin/users/edit.blade.php +++ b/spk_kontrakan/resources/views/admin/users/edit.blade.php @@ -67,12 +67,17 @@ class="form-control @error('email') is-invalid @enderror" Role @error('role')
{{ $message }}
@enderror + + + Super Admin: Akses penuh | Admin: Kelola konten | Mahasiswa: User aplikasi mobile +
diff --git a/spk_kontrakan/resources/views/admin/users/index.blade.php b/spk_kontrakan/resources/views/admin/users/index.blade.php index fb321af..61300de 100644 --- a/spk_kontrakan/resources/views/admin/users/index.blade.php +++ b/spk_kontrakan/resources/views/admin/users/index.blade.php @@ -40,6 +40,11 @@ background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%); color: white; } + + .badge-user { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + } @@ -80,8 +85,9 @@
@@ -152,8 +158,12 @@ @if($user->role === 'super_admin') Super Admin - @else + @elseif($user->role === 'admin') Admin + @elseif($user->role === 'user') + Mahasiswa + @else + {{ $user->role }} @endif diff --git a/spk_kontrakan/storage/framework/views/15adb941f057c688df2ceb4a9c21ef52.php b/spk_kontrakan/storage/framework/views/15adb941f057c688df2ceb4a9c21ef52.php new file mode 100644 index 0000000..2c2727c --- /dev/null +++ b/spk_kontrakan/storage/framework/views/15adb941f057c688df2ceb4a9c21ef52.php @@ -0,0 +1,163 @@ + + +startSection('title', 'Activity Log'); ?> + +startSection('content'); ?> +
+ + + +
+

+ Activity Log +

+

Riwayat semua aktivitas user dalam sistem

+
+ + +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + Reset + +
+
+
+
+ + +
+
+
+
+ Daftar Aktivitas (total()); ?>) +
+ + Export CSV + +
+
+ +
+ isEmpty()): ?> +
+ +
Tidak ada aktivitas ditemukan
+
+ +
+ + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $log): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
WaktuUserActionDeskripsiModelIP Address
+ created_at->format('d M Y H:i')); ?> + + user->name ?? 'Unknown'); ?>
+ user->email ?? ''); ?> +
+ + action)); ?> + + + + description); ?> + + + model_type): ?> + + model_type); ?> + + model_id): ?> + #model_id); ?> + + + + + - + + + ip_address); ?> +
+
+ +
+
+ + +
+ links()); ?> + +
+
+stopSection(); ?> + +make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/spk_kontrakan/storage/framework/views/2487b2a216139ea38383fd97d51e2280.php b/spk_kontrakan/storage/framework/views/2487b2a216139ea38383fd97d51e2280.php new file mode 100644 index 0000000..6d90947 --- /dev/null +++ b/spk_kontrakan/storage/framework/views/2487b2a216139ea38383fd97d51e2280.php @@ -0,0 +1,231 @@ + + +startSection('title', 'Kelola Booking'); ?> + +startSection('content'); ?> +
+ +
+
+

+ Kelola Booking +

+ +
+ + Booking Baru + +
+ + + + + + + + + + + +
+
+
+
+ + +
+
+ + +
+
+ + + + +
+
+
+
+ + +
+ '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'], + ]; + ?> + addLoop($__currentLoopData); foreach($__currentLoopData as $stat): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+
+ +

+ count()); ?> + +

+ +
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+ + +
+
+
+ + + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $booking): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> + + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> + + + + + +
IDKontrakanPenyewaPeriodeBiayaStatusPembayaranAksi
+ #id); ?> + + + kontrakan->nama ?? '-'); ?> + + + +
tenant_name); ?>
+ tenant_phone); ?> +
+
start_date->format('d M Y')); ?>
+ s/d end_date->format('d M Y')); ?> +
+ duration_days); ?> hari +
+ Rp amount, 0, ',', '.')); ?> + + + status_label); ?> + + + + + payment_status_label); ?> + + + payment_proof): ?> + + + + + booking_source == 'user'): ?> + + + + + +
+ + + + status == 'pending'): ?> +
+ + +
+ + status == 'confirmed'): ?> +
+ + +
+ + status == 'checked_in'): ?> +
+ + +
+ + user()->role == 'super_admin' || $booking->status == 'pending'): ?> +
+ + + +
+ +
+
+ + Belum ada data booking +
+
+
+ hasPages()): ?> + + +
+
+stopSection(); ?> + +make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/spk_kontrakan/storage/framework/views/750ed3e845a2f0e93cc431f7924de6f1.php b/spk_kontrakan/storage/framework/views/750ed3e845a2f0e93cc431f7924de6f1.php new file mode 100644 index 0000000..7a13def --- /dev/null +++ b/spk_kontrakan/storage/framework/views/750ed3e845a2f0e93cc431f7924de6f1.php @@ -0,0 +1,1383 @@ + + +startSection('title', 'Kelola Kriteria SAW'); ?> + +startSection('content'); ?> +
+ + + + +
+
+
+ + +
+
+
+

+ + 📊 Kelola Kriteria SAW + 📊 Kriteria +

+

+ Atur dan kelola kriteria penilaian untuk metode Simple Additive Weighting + Kelola kriteria SAW +

+
+ +
+
+ + + + + + + + + + + + + + + + + +
+ +
+
+ + + + +
+ +
+ +
+ + + + +
+
+ + +
+ + +
+ + +
+
+ + 🏠 Kontrakan: where('tipe_bisnis', 'kontrakan')->count() : 0); ?> + + + 👕 Laundry: where('tipe_bisnis', 'laundry')->count() : 0); ?> + +
+
+
+
+
+ + +
+ isEmpty()): ?> + +
+
+
+ +
+
+
Belum ada kriteria
+

Mulai tambahkan kriteria untuk penilaian SAW (Simple Additive Weighting) agar dapat melakukan perhitungan rekomendasi yang akurat.

+ + Tambah Kriteria Pertama + +
+ + +
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $index => $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+ +
+
+
+ tipe_bisnis == 'kontrakan'): ?> + + 🏠 Kontrakan + + + + 👕 Laundry + + +
+
+ + +
nama_kriteria); ?>
+ + +
+
+
+ BOBOT +
+ bobot); ?> + (bobot * 100, 1)); ?>%) +
+
+
+
+
+ TIPE + tipe) == 'benefit'): ?> + + 📈 tipe)); ?> + + + + + 📉 tipe)); ?> + + + +
+
+
+ + + keterangan): ?> +
+ KETERANGAN +

keterangan); ?>

+
+ + + +
+ + Edit + + +
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+ + +
+
+ + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $index => $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + + + + + + + + + +
+ No + + + Tipe Bisnis + + + + + Nama Kriteria + + + + + Bobot + + + + + Tipe + + + + Keterangan + + Aksi +
+
+
+ tipe_bisnis == 'kontrakan'): ?> + + 🏠 Kontrakan + + + + 👕 Laundry + + + +
+
+ +
+
+
nama_kriteria); ?>
+ Kriteria +
+
+
+
+ bobot); ?> + (bobot * 100, 1)); ?>%) +
+
+ tipe) == 'benefit'): ?> + + 📈 Benefit + + + + 📉 Cost + + + +
+ + keterangan ? Str::limit($item->keterangan, 60) : '-'); ?> + + +
+
+
+ + + + + user()->role == 'super_admin'): ?> + + + + +
+
+
+
📊 Validasi Total Bobot
+
+ + + 🏠 + Kontrakan: + + + + + + 👕 + Laundry: + + +
+
+
+
+
+ + + Catatan: Total bobot untuk setiap tipe bisnis harus = 1.00 (100%)
+ agar perhitungan SAW (Simple Additive Weighting) memberikan hasil yang akurat. +
+
+
+
+
+
+ Total Bobot Kontrakan: + + + + + + + + + + + + + + + + Total Bobot Laundry: + + + + + + + + + + + + +
+ + + + Total: sum('bobot'), 2)); ?> + + + + + + + +
+
+ + + count() > 0): ?> +
+
+
+
+
+
+ +
+
📋 Informasi Kriteria SAW
+
+
+
    +
  • 📈 Benefit: Semakin tinggi nilai, semakin baik
    (contoh: Fasilitas, Kualitas Layanan)
  • +
  • 📉 Cost: Semakin rendah nilai, semakin baik
    (contoh: Harga, Jarak, Biaya)
  • +
  • âš–ī¸ Total Bobot: Harus = 1.00 (100%) untuk setiap tipe bisnis
    agar perhitungan SAW akurat dan valid
  • +
+
+
+
+
+
+
+
+
+
+ +
+
📊 Statistik Kriteria
+
+
+
+
+
count()); ?>
+ Total Kriteria +
+
+
+
+
+ Benefit +
+
+
+
+
+ Cost +
+
+
+
+
+
+
+ + + + + + + + + +stopSection(); ?> + +make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/spk_kontrakan/storage/framework/views/84dd4efe61f3c815daff24c589049140.php b/spk_kontrakan/storage/framework/views/84dd4efe61f3c815daff24c589049140.php new file mode 100644 index 0000000..1ab64ab --- /dev/null +++ b/spk_kontrakan/storage/framework/views/84dd4efe61f3c815daff24c589049140.php @@ -0,0 +1,2309 @@ + + +startSection('title', 'Daftar Kontrakan'); ?> + +startSection('content'); ?> +
+ + + +
+
+
+ + +
+
+
+

+ Kelola KontrakanKontrakan +

+

Informasi lengkap dan manajemen data kontrakan yang tersediaManajemen data kontrakan

+
+ +
+
+ + +
+
+
+ +
+
total() ?? 0); ?>
+
Total Kontrakan
+
+
+
+ +
+
+
Harga Terendah
+
+
+
+ +
+
+
Jarak Terjauh
+
+
+
+ +
+
+
Kamar Terbanyak
+
+
+ + + + + + + + + + + + +
+
+
+
+
+ + Pencarian & Filter Kontrakan +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+ +
+
+ + + + + + + + +
+ +
+ +
+ +
+
+ + + 💡 Ketik "WiFi", "Dapur", "AC" atau kata kunci lainnya untuk hasil lebih spesifik + 💡 Coba "WiFi", "Dapur", "AC" + +
+ + +
+
+
+ + Filter Lanjutan +
+ +
+ +
+ +
+ +
+
+
+
+ Min + +
+
+
+
+ Max + +
+
+
+
+
+ + Geser untuk minimum +
+
+ + Geser untuk maksimum +
+
+
+
+ + Range tersedia: Rp - Rp + Rp - +
+
+ + +
+ +
+
+ km +
+ + Geser untuk menyesuaikan +
+
+ + Jarak terjauh tersedia: km + Max: km +
+
+ + +
+ +
+
+
+
+ Min + kmr +
+
+
+
+ Max + kmr +
+
+
+
+
+ + Minimum kamar +
+
+ + Maksimum kamar +
+
+
+
+ + Range: - kamar +
+
+ + + count() > 0): ?> +
+ +
+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $fasilitas): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+ + + style="accent-color: var(--primary-color);" + > + +
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+
+ + + Pilih satu atau lebih fasilitas (hasil akan menampilkan kontrakan yang memiliki SEMUA fasilitas terpilih) + Pilih fasilitas yang diinginkan + +
+ + + +
+ +
+
+ > + +
+
+ > + +
+
+ > + +
+
+ > + +
+
+
+
+ + +
+ + + + Reset Filter + Reset + +
+ +
+
+
+ + +
+
+
+
+ + + + + + dari + + kontrakan + + + + + + + "" + 🔍 + + + + + + + + Rp - Rp + + + 💰 + + + + + + + Max km + 📍km + + + + + + + - kamar + 🏠- + + + + + + + fasilitas + ✨ + + +
+ + + 0): ?> + query(); + $queryString = !empty($queryParams) ? '?' . http_build_query($queryParams) : ''; + } catch (\Exception $e) { + $queryString = ''; + } + ?> + + +
+
+
+ +
+ +
+
+
+ + + count()); ?> kontrakan + + + + user()->role == 'super_admin' && $kontrakan->count() > 0): ?> + + +
+ + + + total()); ?> Data + +
+
+ +
+ isEmpty()): ?> + +
+
+ +
+
+ Tidak ada data kontrakan ditemukan + Data tidak ditemukan +
+

+ hasAny(['search', 'harga_min', 'harga_max', 'jarak_max', 'luas_min', 'luas_max', 'fasilitas_filter'])): ?> + Tidak ada kontrakan yang sesuai dengan kriteria filter Anda.
Coba ubah atau reset filter untuk melihat hasil lain.
+ Filter tidak menemukan hasil. Coba ubah atau reset filter. + + Belum ada data kontrakan. Mulai tambahkan data kontrakan pertama Anda! + Belum ada data. Tambahkan kontrakan pertama! + +

+
+ hasAny(['search', 'harga_min', 'harga_max', 'jarak_max', 'luas_min', 'luas_max', 'fasilitas_filter'])): ?> + + + Reset Filter + Reset + + + + + Tambah Kontrakan + Tambah + +
+
+ + +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $index => $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+
+
+ user()->role == 'super_admin'): ?> + + +
+
+ + nama); ?> + + status_label); ?> +
+

+ alamat, 45)); ?> + +

+
+
+ + #firstItem() + $index); ?> + + +
+ + +
+
+ 💰 Harga/Tahun + + Rp harga, 0, ',', '.')); ?> + + +
+
+ 📏 Jarak + + jarak / 1000, 1)); ?> km + +
+
+ đŸšĒ Kamar + + jumlah_kamar); ?> Kamar + +
+
+ + + fasilitas): ?> +
+ ✨ Fasilitas: +
+ fasilitas)), 0, 3); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $f): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + popLoop(); $loop = $__env->getLastLoop(); ?> + fasilitas)) > 3): ?> + +fasilitas)) - 3); ?> + +
+
+ + + + hasWhatsapp()): ?> + + Hubungi Pemilik via WhatsApp + + + + +
+ latitude && $item->longitude): ?> + + + + + + + + + + + + user()->role == 'super_admin'): ?> + +
+ + +
+ +
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+ + +
+ + + + user()->role == 'super_admin'): ?> + + + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $index => $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + user()->role == 'super_admin'): ?> + + + + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
+
+ +
+
+ No + + Nama Kontrakan + + Alamat + + Harga/Tahun + + Jarak + + Fasilitas + + Kamar + + Aksi +
+ + + firstItem() + $index); ?> + +
+
+ +
+
+
+ nama); ?> + + status_label); ?> +
+ hasWhatsapp()): ?> + + Ada kontak WhatsApp + + +
+
+
+ + alamat, 50)); ?> + + + + + + Rp harga, 0, ',', '.')); ?> + + + + + jarak / 1000, 1)); ?> km + + + fasilitas): ?> +
+ fasilitas)), 0, 2); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $f): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + popLoop(); $loop = $__env->getLastLoop(); ?> + fasilitas)) > 2): ?> + +fasilitas)) - 2); ?> + +
+ + - + +
+ + jumlah_kamar); ?> Kamar + + +
+ latitude && $item->longitude): ?> + + + + + + + + + + + + user()->role == 'super_admin'): ?> + + +
+ + +
+ + + +
+
+
+ +
+ + + hasPages()): ?> + + +
+
+ + +
+ + + + + + + + +
+ + +
+ + + + + +stopSection(); ?> + +startSection('scripts'); ?> + +stopSection(); ?> + +make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/spk_kontrakan/storage/framework/views/997e6940f3b97384fb2e68c445e4a212.php b/spk_kontrakan/storage/framework/views/997e6940f3b97384fb2e68c445e4a212.php new file mode 100644 index 0000000..accf853 --- /dev/null +++ b/spk_kontrakan/storage/framework/views/997e6940f3b97384fb2e68c445e4a212.php @@ -0,0 +1,224 @@ + + +startSection('title', 'Kelola User'); ?> + +startSection('content'); ?> +
+ + + +
+
+
+

+ Kelola User Administrator +

+

Manage user accounts dan permissions

+
+ +
+
+ + + + + + + +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + Reset + +
+
+
+
+ + +
+
+
+
+ Daftar User (total()); ?>) +
+
+
+ +
+ isEmpty()): ?> +
+ +
Tidak ada user ditemukan
+
+ +
+ + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
NamaEmailRoleStatusDibuatAksi
+
+
+ name, 0, 1))); ?> + +
+ name); ?> +
+
+ email); ?> + + role === 'super_admin'): ?> + Super Admin + role === 'admin'): ?> + Admin + role === 'user'): ?> + Mahasiswa + + role); ?> + + + deleted_at): ?> + Tidak Aktif + + Aktif + + + created_at->format('d M Y')); ?> + +
+ deleted_at): ?> +
+ + +
+ + + + + + + id !== auth()->id()): ?> +
+ + + +
+ +
+
+
+ +
+
+ + +
+ links()); ?> + +
+
+stopSection(); ?> + +make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/spk_kontrakan/storage/framework/views/b013a8c110c7b48f95c189a44c72a9f6.php b/spk_kontrakan/storage/framework/views/b013a8c110c7b48f95c189a44c72a9f6.php new file mode 100644 index 0000000..a26911e --- /dev/null +++ b/spk_kontrakan/storage/framework/views/b013a8c110c7b48f95c189a44c72a9f6.php @@ -0,0 +1,1763 @@ + + +startSection('title', 'Daftar Laundry'); ?> + +startSection('content'); ?> +
+ + + + +
+
+
+ + +
+
+
+

+ + đŸ§ē Daftar Laundry + đŸ§ē Laundry +

+

+ Kelola dan pantau data layanan laundry yang tersedia +

+
+ +
+
+ + + + + + + + + + + + +
+
+
+
+
+ + Pencarian & Filter Laundry +
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+ + + + + +
+ + + 💡 Coba "WiFi", "Antar Jemput", "24 Jam" untuk hasil lebih spesifik + 💡 Coba "WiFi", "Antar Jemput" + +
+ + +
+
+
+ + Filter Lanjutan +
+ +
+ +
+ +
+ +
+
+
+
+ Min + +
+
+
+
+ Max + +
+
+
+
+
+ + Geser untuk minimum +
+
+ + Geser untuk maksimum +
+
+
+
+ + Range: Rp - Rp + Rp - +
+
+ + +
+ +
+
+ km +
+ + Geser untuk menyesuaikan +
+
+ + Jarak terjauh: km + Max: km +
+
+ + +
+ +
+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $jenis): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+ + + style="accent-color: #667eea;" + > + +
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+
+
+ + + count() > 0): ?> +
+ +
+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $fasilitas): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+ + + style="accent-color: #ffc107;" + > + +
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+
+ + + Pilih fasilitas (hasil akan menampilkan laundry yang memiliki SEMUA fasilitas terpilih) + Pilih fasilitas yang diinginkan + +
+ + + +
+ +
+
+ > + +
+
+ > + +
+
+ > + +
+
+ > + +
+
+
+
+ + +
+ + + + Reset Filter + Reset + +
+
+
+
+
+
+ + +
+
+
+
+ + + + + + dari + + laundry + + + + + + + "" + 🔍 + + + + + + + + Rp - Rp + + + 💰 + + + + + + + Max km + 📍km + + + + + + + layanan + ⚡ + + + + + + + fasilitas + ✨ + + +
+ + + 0): ?> + +
+
+
+ + + +
+ +
+
+
+ + + count()); ?> laundry + + + user()->role == 'super_admin' && $laundry->count() > 0): ?> + + +
+ + + count()); ?> Data + +
+
+ +
+ isEmpty()): ?> + +
+
+ +
+
+ Tidak ada data laundry ditemukan + Data tidak ditemukan +
+

+ hasAny(['search', 'harga_min', 'harga_max', 'jarak', 'jenis_layanan'])): ?> + Tidak ada laundry yang sesuai dengan kriteria filter. Coba ubah atau reset filter. + Filter tidak menemukan hasil. Coba ubah filter. + + Belum ada data laundry. Mulai tambahkan layanan laundry pertama Anda! + Belum ada data. Tambahkan laundry pertama! + +

+
+ hasAny(['search', 'harga_min', 'harga_max', 'jarak', 'jenis_layanan'])): ?> + + + Reset Filter + Reset + + + + + Tambah Laundry + Tambah + + +
+
+ + +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $index => $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + layanan; + if (!empty($filters['jenis_layanan'])) { + $filteredLayanan = $item->layanan->where('jenis_layanan', $filters['jenis_layanan']); + } + ?> +
+
+
+
+ user()->role == 'super_admin'): ?> + + +
+
+ + nama); ?> + +
+

+ alamat, 45)); ?> + +

+
+
+ + # + + +
+ + + isNotEmpty()): ?> +
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $layanan): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + +
+
+ Harga +
+ Rp harga, 0, ',', '.')); ?> + +
+
+
+ + +
+
+ Jenis Layanan +
+ jenis_layanan)); ?> + +
+
+
+ + +
+
+ Estimasi Selesai +
+ estimasi_selesai); ?> Jam +
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+ + + +
+
+
+
+ Jarak dari Kampus +
+ jarak / 1000, 1)); ?> km +
+
+
+
+
+ + +
+ latitude && $item->longitude): ?> + + + + + + + + + + + + user()->role == 'super_admin'): ?> + +
+ + +
+ +
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+ + +
+ + + + user()->role == 'super_admin'): ?> + + + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $index => $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + layanan; + if (!empty($filters['jenis_layanan'])) { + $filteredLayanan = $item->layanan->where('jenis_layanan', $filters['jenis_layanan']); + } + ?> + + user()->role == 'super_admin'): ?> + + + + + + + + + + + + + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
+
+ +
+
+ No + + Nama Laundry + + Alamat + + Harga + + Jarak + + Jenis Layanan + + Estimasi + + Aksi +
+ + + firstItem() + $index); ?> + +
+
+ +
+
+
nama); ?>
+
+
+
+ + alamat, 50)); ?> + + + + isNotEmpty()): ?> +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $layanan): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + Rp harga, 0, ',', '.')); ?> + + + popLoop(); $loop = $__env->getLastLoop(); ?> +
+ + - + +
+ + jarak / 1000, 1)); ?> km + + + isNotEmpty()): ?> +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $layanan): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + jenis_layanan)); ?> + + + popLoop(); $loop = $__env->getLastLoop(); ?> +
+ + - + +
+ isNotEmpty()): ?> +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $layanan): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + estimasi_selesai); ?> jam + + popLoop(); $loop = $__env->getLastLoop(); ?> +
+ + - + +
+
+ latitude && $item->longitude): ?> + + + + + + + + + + + + user()->role == 'super_admin'): ?> + +
+ + +
+ +
+
+
+ +
+ + + hasPages()): ?> + + +
+
+ + + + + + +
+ + +
+ + + + + + + +stopSection(); ?> + +make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/spk_kontrakan/storage/framework/views/ee70804036bd06aaaf7aa7c82c92bb1c.php b/spk_kontrakan/storage/framework/views/ee70804036bd06aaaf7aa7c82c92bb1c.php new file mode 100644 index 0000000..89ae979 --- /dev/null +++ b/spk_kontrakan/storage/framework/views/ee70804036bd06aaaf7aa7c82c92bb1c.php @@ -0,0 +1,821 @@ + + +startSection('title', 'Metode SAW'); ?> + +startSection('content'); ?> +
+ + + + + + +
+

🧮 Metode SAW

+

Sistem Pendukung Keputusan untuk rekomendasi terbaik

+
+ + + + + + + +
+
+
+ +
+
+
Tentang Metode SAW
+

+ 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. +

+
+
+
+ + +
+
+ +
+
+
+ Pilih Jenis Pencarian +
+ +
+ + + + + + +
+ +
+ + +
+ + +
+ + +
+
+ + +
+
+ +
+ Jarak Kontrakan +

+ Jarak dihitung otomatis dari Kampus Polije. + Sistem akan merekomendasikan kontrakan terdekat dari kampus. +

+
+
+
+ + + + + + + + +
+
+
+ Kriteria yang Digunakan +
+
+ + + + + + + + + + count() > 0): ?> + addLoop($__currentLoopData); foreach($__currentLoopData as $k): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + + + + + + + + + + + + + +
KriteriaBobotTipe
nama_kriteria ?? 'N/A'); ?> + bobot ?? 0); ?> + + tipe) && strtolower($k->tipe) == 'benefit'): ?> + + Benefit + + + + Cost + + +
Tidak ada kriteria yang tersedia
Total Bobot + + where('tipe_bisnis', 'kontrakan')->sum('bobot') : 0); ?> + + +
+
+
+
+ + +
+ +
+ + + +
+
+
+ + +
+
+
+
+
+ Kelebihan SAW +
+
    +
  • Mudah dipahami
  • +
  • Perhitungan sederhana
  • +
  • Hasil objektif
  • +
  • Jarak dihitung otomatis dari GPS
  • +
+
+
+
+
+
+
+
+ Cara Kerja +
+
    +
  1. Kontrakan: Jarak dari Kampus Polije
  2. +
  3. Laundry: Jarak dari Kampus atau Lokasi Anda
  4. +
  5. Normalisasi nilai kriteria
  6. +
  7. Kalikan dengan bobot
  8. +
  9. Urutkan berdasarkan nilai tertinggi
  10. +
+
+
+
+
+
+
+
+ + +
+ + +stopSection(); ?> + +make('layouts.admin', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/spk_kontrakan/storage/framework/views/f9a1a87513eabf56a496d6aacefe5611.php b/spk_kontrakan/storage/framework/views/f9a1a87513eabf56a496d6aacefe5611.php new file mode 100644 index 0000000..c32b73c --- /dev/null +++ b/spk_kontrakan/storage/framework/views/f9a1a87513eabf56a496d6aacefe5611.php @@ -0,0 +1,109 @@ +hasPages()): ?> + + + \ No newline at end of file diff --git a/spk_mobile/lib/models/user.dart b/spk_mobile/lib/models/user.dart index 68da05a..aa80109 100644 --- a/spk_mobile/lib/models/user.dart +++ b/spk_mobile/lib/models/user.dart @@ -4,6 +4,8 @@ class User { final String email; final String? phone; final String role; + final String? roleLabel; + final String? userType; final DateTime? createdAt; User({ @@ -12,6 +14,8 @@ class User { required this.email, this.phone, required this.role, + this.roleLabel, + this.userType, this.createdAt, }); @@ -22,12 +26,41 @@ class User { email: json['email'] ?? '', phone: json['phone'], role: json['role'] ?? 'user', + roleLabel: json['role_label'], + userType: json['user_type'], createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null, ); } + /// Get user-friendly role label + String getRoleLabel() { + if (roleLabel != null && roleLabel!.isNotEmpty) { + return roleLabel!; + } + + switch (role) { + case 'super_admin': + return 'Super Admin'; + case 'admin': + return 'Admin'; + case 'user': + return 'Mahasiswa'; + default: + return role; + } + } + + /// Check if user is mahasiswa + bool isMahasiswa() => role == 'user'; + + /// Check if user is admin + bool isAdmin() => role == 'admin'; + + /// Check if user is super admin + bool isSuperAdmin() => role == 'super_admin'; + Map toJson() { return { 'id': id, @@ -35,6 +68,8 @@ class User { 'email': email, 'phone': phone, 'role': role, + 'role_label': roleLabel, + 'user_type': userType, 'created_at': createdAt?.toIso8601String(), }; } diff --git a/spk_mobile/lib/screens/favorites_screen.dart b/spk_mobile/lib/screens/favorites_screen.dart index 0cf2f45..8ea4c34 100644 --- a/spk_mobile/lib/screens/favorites_screen.dart +++ b/spk_mobile/lib/screens/favorites_screen.dart @@ -58,12 +58,15 @@ class _FavoritesScreenState extends State : []; _isLoading = false; }); - debugPrint('[FAV_SCREEN] Loaded ${_kontrakanFavorites.length} kontrakan, ${_laundryFavorites.length} laundry'); + debugPrint( + '[FAV_SCREEN] Loaded ${_kontrakanFavorites.length} kontrakan, ${_laundryFavorites.length} laundry', + ); } else { setState(() { _kontrakanFavorites = []; _laundryFavorites = []; - _errorMessage = result['message']?.toString() ?? 'Gagal memuat favorit'; + _errorMessage = + result['message']?.toString() ?? 'Gagal memuat favorit'; _isLoading = false; }); debugPrint('[FAV_SCREEN] ❌ Load failed: $_errorMessage'); @@ -128,27 +131,27 @@ class _FavoritesScreenState extends State if (!mounted) return; if (result['success'] == true) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon( - Icons.check_circle_rounded, - color: Colors.white, - size: 20, - ), - const SizedBox(width: 10), - Text(result['message'] ?? 'Dihapus dari favorit'), - ], + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon( + Icons.check_circle_rounded, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 10), + Text(result['message'] ?? 'Dihapus dari favorit'), + ], + ), + backgroundColor: const Color(0xFF2E7D32), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.all(16), ), - backgroundColor: const Color(0xFF2E7D32), - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - margin: const EdgeInsets.all(16), - ), - ); + ); _loadFavorites(); } } catch (e) { @@ -158,7 +161,9 @@ class _FavoritesScreenState extends State content: const Text('Gagal menghapus favorit'), backgroundColor: Colors.red.shade600, behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), margin: const EdgeInsets.all(16), ), ); @@ -301,44 +306,47 @@ class _FavoritesScreenState extends State ), ) : _errorMessage != null - ? Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline_rounded, - size: 56, color: Colors.red.shade300), - const SizedBox(height: 16), - Text( - _errorMessage!, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - color: Colors.grey[600], - ), - ), - const SizedBox(height: 20), - ElevatedButton.icon( - onPressed: _loadFavorites, - icon: const Icon(Icons.refresh_rounded), - label: const Text('Coba Lagi'), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF1565C0), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ], + ? Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline_rounded, + size: 56, + color: Colors.red.shade300, ), - ), - ) - : TabBarView( - controller: _tabController, - children: [_buildKontrakanList(), _buildLaundryList()], + const SizedBox(height: 16), + Text( + _errorMessage!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: _loadFavorites, + icon: const Icon(Icons.refresh_rounded), + label: const Text('Coba Lagi'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1565C0), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], ), + ), + ) + : TabBarView( + controller: _tabController, + children: [_buildKontrakanList(), _buildLaundryList()], + ), ), ], ), diff --git a/spk_mobile/lib/screens/improved_home_screen.dart b/spk_mobile/lib/screens/improved_home_screen.dart index 72a592c..d955804 100644 --- a/spk_mobile/lib/screens/improved_home_screen.dart +++ b/spk_mobile/lib/screens/improved_home_screen.dart @@ -73,7 +73,10 @@ class _ImprovedHomeScreenState extends State { if (!_authService.isAuthenticated) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Silakan login terlebih dahulu'), backgroundColor: Colors.orange), + const SnackBar( + content: Text('Silakan login terlebih dahulu'), + backgroundColor: Colors.orange, + ), ); } return; @@ -106,10 +109,16 @@ class _ImprovedHomeScreenState extends State { } catch (e) { if (mounted) { setState(() { - if (wasFav) _favKontrakanIds.add(id); else _favKontrakanIds.remove(id); + if (wasFav) + _favKontrakanIds.add(id); + else + _favKontrakanIds.remove(id); }); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red.shade600), + SnackBar( + content: Text('Error: $e'), + backgroundColor: Colors.red.shade600, + ), ); } } @@ -119,7 +128,10 @@ class _ImprovedHomeScreenState extends State { if (!_authService.isAuthenticated) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Silakan login terlebih dahulu'), backgroundColor: Colors.orange), + const SnackBar( + content: Text('Silakan login terlebih dahulu'), + backgroundColor: Colors.orange, + ), ); } return; @@ -152,10 +164,16 @@ class _ImprovedHomeScreenState extends State { } catch (e) { if (mounted) { setState(() { - if (wasFav) _favLaundryIds.add(id); else _favLaundryIds.remove(id); + if (wasFav) + _favLaundryIds.add(id); + else + _favLaundryIds.remove(id); }); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red.shade600), + SnackBar( + content: Text('Error: $e'), + backgroundColor: Colors.red.shade600, + ), ); } } diff --git a/spk_mobile/lib/screens/laundry_detail_screen.dart b/spk_mobile/lib/screens/laundry_detail_screen.dart index 2951c9f..9e7da55 100644 --- a/spk_mobile/lib/screens/laundry_detail_screen.dart +++ b/spk_mobile/lib/screens/laundry_detail_screen.dart @@ -31,7 +31,9 @@ class _LaundryDetailScreenState extends State { Future _checkFavorite() async { try { - final result = await _favoriteService.isLaundryFavorite(widget.laundry.id); + final result = await _favoriteService.isLaundryFavorite( + widget.laundry.id, + ); if (mounted) setState(() => _isFavorite = result); } catch (e) { debugPrint('Check favorite error: $e'); @@ -82,7 +84,9 @@ class _LaundryDetailScreenState extends State { content: Text(result['message'] ?? 'Gagal mengubah favorit'), backgroundColor: Colors.red[700], behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), margin: const EdgeInsets.all(16), ), ); @@ -734,7 +738,9 @@ class _LaundryDetailScreenState extends State { Future _launchWhatsApp(String phone) async { final cleaned = phone.replaceAll(RegExp(r'[^0-9]'), ''); - final formatted = cleaned.startsWith('0') ? '62${cleaned.substring(1)}' : cleaned; + final formatted = cleaned.startsWith('0') + ? '62${cleaned.substring(1)}' + : cleaned; final url = 'https://wa.me/$formatted'; if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); diff --git a/spk_mobile/lib/screens/profile_screen.dart b/spk_mobile/lib/screens/profile_screen.dart index 38d4edc..a0fd3ee 100644 --- a/spk_mobile/lib/screens/profile_screen.dart +++ b/spk_mobile/lib/screens/profile_screen.dart @@ -509,7 +509,7 @@ class _ProfileScreenState extends State _buildStatItem( icon: Icons.verified_user_rounded, color: const Color(0xFF1565C0), - value: _currentUser?.role == 'admin' ? 'Admin' : 'User', + value: _currentUser?.getRoleLabel() ?? 'Mahasiswa', label: 'Status', ), ], diff --git a/spk_mobile/lib/screens/recommendation_screen.dart b/spk_mobile/lib/screens/recommendation_screen.dart index b715536..be13930 100644 --- a/spk_mobile/lib/screens/recommendation_screen.dart +++ b/spk_mobile/lib/screens/recommendation_screen.dart @@ -338,9 +338,7 @@ class _RecommendationScreenState extends State { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text( - 'Izin lokasi ditolak permanen. Ubah di pengaturan.', - ), + content: Text('Izin lokasi ditolak permanen. Ubah di pengaturan.'), backgroundColor: Colors.red, ), ); @@ -348,12 +346,13 @@ class _RecommendationScreenState extends State { return; } - Position position = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.high, - ).timeout( - const Duration(seconds: 15), - onTimeout: () => throw TimeoutException('Deteksi lokasi timeout'), - ); + Position position = + await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + ).timeout( + const Duration(seconds: 15), + onTimeout: () => throw TimeoutException('Deteksi lokasi timeout'), + ); if (!mounted) return; setState(() { @@ -373,7 +372,9 @@ class _RecommendationScreenState extends State { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Gagal mendeteksi lokasi: ${e.toString().replaceAll('Exception: ', '')}'), + content: Text( + 'Gagal mendeteksi lokasi: ${e.toString().replaceAll('Exception: ', '')}', + ), backgroundColor: Colors.red, ), ); @@ -449,19 +450,22 @@ class _RecommendationScreenState extends State { final initials = name.isEmpty ? 'U' : name.trim().split(' ').length >= 2 - ? '${name.trim().split(' ')[0][0]}${name.trim().split(' ')[1][0]}'.toUpperCase() - : name.trim()[0].toUpperCase(); + ? '${name.trim().split(' ')[0][0]}${name.trim().split(' ')[1][0]}' + .toUpperCase() + : name.trim()[0].toUpperCase(); final hour = DateTime.now().hour; final greeting = hour < 11 ? 'Selamat Pagi' : hour < 15 - ? 'Selamat Siang' - : hour < 18 - ? 'Selamat Sore' - : 'Selamat Malam'; + ? 'Selamat Siang' + : hour < 18 + ? 'Selamat Sore' + : 'Selamat Malam'; - final categoryLabel = widget.category == 'kontrakan' ? 'kontrakan' : 'laundry'; + final categoryLabel = widget.category == 'kontrakan' + ? 'kontrakan' + : 'laundry'; return Container( padding: const EdgeInsets.all(16), @@ -537,10 +541,7 @@ class _RecommendationScreenState extends State { const SizedBox(height: 2), Text( email, - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade400, - ), + style: TextStyle(fontSize: 12, color: Colors.grey.shade400), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -1091,11 +1092,7 @@ class _RecommendationScreenState extends State { borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(4), - child: Icon( - Icons.refresh, - size: 18, - color: _categoryColor, - ), + child: Icon(Icons.refresh, size: 18, color: _categoryColor), ), ), ], @@ -1121,10 +1118,7 @@ class _RecommendationScreenState extends State { child: CircularProgressIndicator(strokeWidth: 2), ), SizedBox(width: 10), - Text( - 'Mendeteksi lokasi...', - style: TextStyle(fontSize: 12), - ), + Text('Mendeteksi lokasi...', style: TextStyle(fontSize: 12)), ], ), ) @@ -1138,19 +1132,12 @@ class _RecommendationScreenState extends State { ), child: Row( children: [ - const Icon( - Icons.check_circle, - size: 16, - color: Colors.green, - ), + const Icon(Icons.check_circle, size: 16, color: Colors.green), const SizedBox(width: 8), Expanded( child: Text( 'Lokasi terdeteksi: ${_userLatitude!.toStringAsFixed(6)}, ${_userLongitude!.toStringAsFixed(6)}', - style: TextStyle( - fontSize: 11, - color: Colors.green[700], - ), + style: TextStyle(fontSize: 11, color: Colors.green[700]), ), ), ], @@ -1251,8 +1238,7 @@ class _RecommendationScreenState extends State { return Column( children: [ _buildBobotSummary(), - if (widget.category == 'laundry' && - _userLatitude != null) + if (widget.category == 'laundry' && _userLatitude != null) Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), diff --git a/spk_mobile/lib/screens/search_screen.dart b/spk_mobile/lib/screens/search_screen.dart index 193a53c..840d851 100644 --- a/spk_mobile/lib/screens/search_screen.dart +++ b/spk_mobile/lib/screens/search_screen.dart @@ -85,22 +85,34 @@ class _SearchScreenState extends State { Future _toggleKontrakanFav(int id) async { final wasFav = _favKontrakanIds.contains(id); setState(() { - if (wasFav) _favKontrakanIds.remove(id); else _favKontrakanIds.add(id); + if (wasFav) + _favKontrakanIds.remove(id); + else + _favKontrakanIds.add(id); }); try { final result = await _favoriteService.toggleKontrakanFavorite(id); if (result['success'] != true && mounted) { setState(() { - if (wasFav) _favKontrakanIds.add(id); else _favKontrakanIds.remove(id); + if (wasFav) + _favKontrakanIds.add(id); + else + _favKontrakanIds.remove(id); }); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(result['message'] ?? 'Gagal'), backgroundColor: Colors.red.shade600), + SnackBar( + content: Text(result['message'] ?? 'Gagal'), + backgroundColor: Colors.red.shade600, + ), ); } } catch (e) { if (mounted) { setState(() { - if (wasFav) _favKontrakanIds.add(id); else _favKontrakanIds.remove(id); + if (wasFav) + _favKontrakanIds.add(id); + else + _favKontrakanIds.remove(id); }); } } @@ -109,22 +121,34 @@ class _SearchScreenState extends State { Future _toggleLaundryFav(int id) async { final wasFav = _favLaundryIds.contains(id); setState(() { - if (wasFav) _favLaundryIds.remove(id); else _favLaundryIds.add(id); + if (wasFav) + _favLaundryIds.remove(id); + else + _favLaundryIds.add(id); }); try { final result = await _favoriteService.toggleLaundryFavorite(id); if (result['success'] != true && mounted) { setState(() { - if (wasFav) _favLaundryIds.add(id); else _favLaundryIds.remove(id); + if (wasFav) + _favLaundryIds.add(id); + else + _favLaundryIds.remove(id); }); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(result['message'] ?? 'Gagal'), backgroundColor: Colors.red.shade600), + SnackBar( + content: Text(result['message'] ?? 'Gagal'), + backgroundColor: Colors.red.shade600, + ), ); } } catch (e) { if (mounted) { setState(() { - if (wasFav) _favLaundryIds.add(id); else _favLaundryIds.remove(id); + if (wasFav) + _favLaundryIds.add(id); + else + _favLaundryIds.remove(id); }); } } @@ -207,7 +231,8 @@ class _SearchScreenState extends State { // Filter by price (laundry hargaKiloan is per-kg, no multiplication needed) filtered = filtered.where((l) { - return l.hargaKiloan >= _priceRange.start && l.hargaKiloan <= _priceRange.end; + return l.hargaKiloan >= _priceRange.start && + l.hargaKiloan <= _priceRange.end; }).toList(); setState(() => _filteredLaundry = filtered); @@ -291,8 +316,8 @@ class _SearchScreenState extends State { Expanded( child: GestureDetector( onTap: () { - setState(() => _selectedCategory = 'Kontrakan'); - _applyFilters(); + setState(() => _selectedCategory = 'Kontrakan'); + _applyFilters(); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -342,8 +367,8 @@ class _SearchScreenState extends State { Expanded( child: GestureDetector( onTap: () { - setState(() => _selectedCategory = 'Laundry'); - _applyLaundryFilters(); + setState(() => _selectedCategory = 'Laundry'); + _applyLaundryFilters(); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200),