Update mobile config, add admin reminder notifications, and apply migrations
This commit is contained in:
parent
8359e2db4a
commit
c0fe039e91
|
|
@ -0,0 +1,255 @@
|
|||
# BAB 4 - PERANCANGAN SISTEM INFORMASI
|
||||
## 4.2 Struktur Database
|
||||
|
||||
Dalam tahap perancangan model, salah satu aspek penting yang perlu dipersiapkan adalah struktur database. Struktur ini dirancang untuk memastikan bahwa penyimpanan dan pengelolaan data dalam aplikasi dapat dilakukan secara efisien dan terorganisir. Dengan adanya struktur database yang jelas, setiap tabel dan atribut yang digunakan dapat dipahami dengan lebih mudah, sehingga memudahkan pengembang dalam proses implementasi dan pengelolaan data di dalam aplikasi.
|
||||
|
||||
### 4.2.1 Entity Relationship Diagram (ERD)
|
||||
|
||||
[INSERT SCREENSHOT DIAGRAM DI SINI]
|
||||
|
||||
Diagram di atas menunjukkan Entity Relationship Diagram (ERD) dari sistem basis data SPK Kontrakan & Laundry. Diagram ini menampilkan 8 tabel inti dengan relasi antar tabel yang menggambarkan bagaimana data saling terhubung dalam sistem.
|
||||
|
||||
---
|
||||
|
||||
### 4.2.2 Penjelasan Tabel dan Atribut
|
||||
|
||||
#### a. Tabel users
|
||||
|
||||
Tabel users digunakan untuk menyimpan data pengguna dalam sistem. Setiap pengguna memiliki informasi dasar seperti id, name, email, dan password, yang digunakan untuk autentikasi dan identifikasi. Selain itu, tabel ini juga mencatat alamat pengguna serta tipe_pengguna, yang menentukan peran dalam sistem, yaitu sebagai user (mahasiswa), admin, atau super admin.
|
||||
|
||||
Untuk keamanan, tabel ini menyediakan kolom verification_token, email_verified_at, dan is_verified, yang digunakan dalam proses verifikasi akun. Selain itu, terdapat remember_token untuk mendukung fitur "ingat saya" dalam sistem login.
|
||||
|
||||
Tabel ini juga menyimpan kolom reset_password_token dan reset_password_expires_at, yang digunakan dalam proses pemulihan kata sandi. reset_password_token menyimpan token unik yang dikirim ke pengguna untuk mereset kata sandi, sementara reset_password_expires_at mencatat batas waktu kedaluwarsa token tersebut.
|
||||
|
||||
Terakhir, tabel ini menyimpan waktu pembuatan dan perubahan akun melalui kolom created_at dan updated_at, sehingga memudahkan pengelolaan data pengguna secara terstruktur dan aman. Struktur tabel users dapat dilihat seperti pada Tabel 4.1.
|
||||
|
||||
**Tabel 4.1 Tabel users**
|
||||
|
||||
| No | Field | Type | Constraint |
|
||||
|----|-------|------|-----------|
|
||||
| 1 | id | bigint | Primary Key |
|
||||
| 2 | name | varchar(255) | Not Null |
|
||||
| 3 | email | varchar(255) | Unique, Not Null |
|
||||
| 4 | password | varchar(255) | Not Null |
|
||||
| 5 | phone | varchar(20) | - |
|
||||
| 6 | role | varchar(50) | Not Null (super_admin, admin, user) |
|
||||
| 7 | email_verified_at | timestamp | Nullable |
|
||||
| 8 | created_at | timestamp | Not Null |
|
||||
| 9 | updated_at | timestamp | Not Null |
|
||||
| 10 | deleted_at | timestamp | Nullable (Soft Delete) |
|
||||
|
||||
---
|
||||
|
||||
#### b. Tabel kontrakans
|
||||
|
||||
Tabel kontrakans digunakan untuk menyimpan data kontrakan (tempat tinggal) yang ditawarkan kepada pengguna. Setiap kontrakan memiliki informasi dasar seperti nama, alamat lengkap, dan kontak pemilik melalui no_whatsapp. Selain itu, tabel ini juga mencatat lokasi geografis kontrakan dengan latitude dan longitude untuk fitur pencarian berbasis lokasi.
|
||||
|
||||
Tabel ini juga menyimpan informasi harga sewa per bulan dalam kolom harga, serta jarak kontrakan dari kampus dalam kolom jarak untuk membantu pengguna dalam pencarian. Tabel ini mencatat fasilitas yang tersedia, jumlah kamar, dan jumlah kamar mandi.
|
||||
|
||||
Tabel ini juga mencatat status kontrakan, yang dapat berupa "tersedia" (available), "dipesan" (booked), atau "terisi" (occupied), untuk memudahkan pemantauan dan pengelolaan ketersediaan kontrakan oleh pihak terkait. Dengan adanya tabel ini, sistem dapat mengelola data kontrakan secara lebih terstruktur dan efisien. Struktur tabel kontrakans dapat dilihat seperti pada Tabel 4.2.
|
||||
|
||||
**Tabel 4.2 Tabel kontrakans**
|
||||
|
||||
| No | Field | Type | Constraint |
|
||||
|----|-------|------|-----------|
|
||||
| 1 | id | bigint | Primary Key |
|
||||
| 2 | nama | varchar(255) | Not Null |
|
||||
| 3 | alamat | text | Not Null |
|
||||
| 4 | no_whatsapp | varchar(20) | - |
|
||||
| 5 | latitude | decimal(10,8) | - |
|
||||
| 6 | longitude | decimal(10,8) | - |
|
||||
| 7 | harga | int | Not Null |
|
||||
| 8 | jarak | decimal(5,2) | - |
|
||||
| 9 | fasilitas | text | - |
|
||||
| 10 | jumlah_kamar | int | - |
|
||||
| 11 | bathroom_count | int | - |
|
||||
| 12 | foto | varchar(255) | - |
|
||||
| 13 | status | varchar(50) | Not Null (available, booked, occupied) |
|
||||
| 14 | occupied_until | date | Nullable |
|
||||
| 15 | created_at | timestamp | Not Null |
|
||||
| 16 | updated_at | timestamp | Not Null |
|
||||
|
||||
---
|
||||
|
||||
#### c. Tabel bookings
|
||||
|
||||
Tabel bookings digunakan untuk menyimpan data pemesanan kontrakan yang dilakukan oleh pengguna. Setiap pemesanan memiliki relasi dengan tabel users melalui user_id dan tabel kontrakans melalui kontrakan_id, untuk mencatat siapa yang melakukan pemesanan dan kontrakan mana yang dipesan.
|
||||
|
||||
Tabel ini mencatat tanggal check-in melalui kolom start_date dan tanggal check-out melalui kolom end_date. Status pemesanan dapat berupa "pending", "confirmed", "checked_in", "completed", atau "cancelled", untuk mencerminkan tahap-tahap proses pemesanan.
|
||||
|
||||
Tabel ini juga menyimpan jumlah pembayaran dalam kolom amount dan status pembayaran dalam kolom payment_status, yang dapat berupa "unpaid", "paid", atau "refunded". Selain itu, tabel ini juga mencatat metode pembayaran yang digunakan, bukti pembayaran, serta waktu pembayaran melalui kolom payment_method, payment_proof, dan paid_at.
|
||||
|
||||
Tabel ini juga menyimpan waktu konfirmasi, check-in, check-out, dan pembatalan melalui kolom confirmed_at, checked_in_at, checked_out_at, dan cancelled_at, untuk audit trail yang lebih lengkap. Struktur tabel bookings dapat dilihat seperti pada Tabel 4.3.
|
||||
|
||||
**Tabel 4.3 Tabel bookings**
|
||||
|
||||
| No | Field | Type | Constraint |
|
||||
|----|-------|------|-----------|
|
||||
| 1 | id | bigint | Primary Key |
|
||||
| 2 | kontrakan_id | bigint | Foreign Key → kontrakans.id |
|
||||
| 3 | user_id | bigint | Foreign Key → users.id |
|
||||
| 4 | start_date | date | Not Null |
|
||||
| 5 | end_date | date | Not Null |
|
||||
| 6 | status | varchar(50) | Not Null |
|
||||
| 7 | amount | decimal(12,2) | Not Null |
|
||||
| 8 | payment_status | varchar(50) | Not Null |
|
||||
| 9 | payment_method | varchar(50) | - |
|
||||
| 10 | payment_proof | varchar(255) | Nullable |
|
||||
| 11 | paid_at | timestamp | Nullable |
|
||||
| 12 | confirmed_at | timestamp | Nullable |
|
||||
| 13 | checked_in_at | timestamp | Nullable |
|
||||
| 14 | checked_out_at | timestamp | Nullable |
|
||||
| 15 | cancelled_at | timestamp | Nullable |
|
||||
| 16 | notes | text | Nullable |
|
||||
| 17 | created_at | timestamp | Not Null |
|
||||
| 18 | updated_at | timestamp | Not Null |
|
||||
|
||||
---
|
||||
|
||||
#### d. Tabel laundry
|
||||
|
||||
Tabel laundry digunakan untuk menyimpan data layanan laundry yang tersedia. Setiap layanan laundry memiliki informasi dasar seperti nama, alamat lengkap, dan kontak melalui no_whatsapp. Selain itu, tabel ini juga mencatat lokasi geografis dengan latitude dan longitude untuk fitur pencarian berbasis lokasi.
|
||||
|
||||
Tabel ini mencatat jarak dari kampus dalam kolom jarak, serta fasilitas yang tersedia melalui kolom fasilitas. Tabel ini juga menyimpan jam operasional layanan laundry melalui kolom jam_buka dan jam_tutup.
|
||||
|
||||
Tabel ini juga mencatat status layanan laundry, yang dapat berupa "active" atau "inactive", untuk memudahkan pengelolaan ketersediaan layanan. Struktur tabel laundry dapat dilihat seperti pada Tabel 4.4.
|
||||
|
||||
**Tabel 4.4 Tabel laundry**
|
||||
|
||||
| No | Field | Type | Constraint |
|
||||
|----|-------|------|-----------|
|
||||
| 1 | id | bigint | Primary Key |
|
||||
| 2 | nama | varchar(255) | Not Null |
|
||||
| 3 | alamat | text | Not Null |
|
||||
| 4 | no_whatsapp | varchar(20) | - |
|
||||
| 5 | latitude | decimal(10,8) | - |
|
||||
| 6 | longitude | decimal(10,8) | - |
|
||||
| 7 | jarak | decimal(5,2) | - |
|
||||
| 8 | fasilitas | text | - |
|
||||
| 9 | jam_buka | time | - |
|
||||
| 10 | jam_tutup | time | - |
|
||||
| 11 | status | varchar(50) | Not Null (active, inactive) |
|
||||
| 12 | foto | varchar(255) | - |
|
||||
| 13 | created_at | timestamp | Not Null |
|
||||
| 14 | updated_at | timestamp | Not Null |
|
||||
|
||||
---
|
||||
|
||||
#### e. Tabel layanan_laundry
|
||||
|
||||
Tabel layanan_laundry digunakan untuk menyimpan data jenis dan paket layanan yang ditawarkan oleh setiap layanan laundry. Setiap layanan memiliki relasi dengan tabel laundry melalui laundry_id.
|
||||
|
||||
Tabel ini mencatat jenis layanan melalui kolom jenis_layanan, nama paket melalui kolom nama_paket, harga melalui kolom harga, dan estimasi waktu penyelesaian melalui kolom estimasi_selesai. Tabel ini juga menyimpan deskripsi layanan dan status ketersediaan layanan melalui kolom deskripsi dan status.
|
||||
|
||||
Struktur tabel layanan_laundry dapat dilihat seperti pada Tabel 4.5.
|
||||
|
||||
**Tabel 4.5 Tabel layanan_laundry**
|
||||
|
||||
| No | Field | Type | Constraint |
|
||||
|----|-------|------|-----------|
|
||||
| 1 | id | bigint | Primary Key |
|
||||
| 2 | laundry_id | bigint | Foreign Key → laundry.id |
|
||||
| 3 | jenis_layanan | varchar(50) | Not Null |
|
||||
| 4 | nama_paket | varchar(255) | Not Null |
|
||||
| 5 | harga | decimal(10,2) | Not Null |
|
||||
| 6 | estimasi_selesai | varchar(50) | - |
|
||||
| 7 | deskripsi | text | - |
|
||||
| 8 | status | varchar(50) | Not Null (active, inactive) |
|
||||
| 9 | rating | decimal(3,2) | Nullable |
|
||||
| 10 | waktu_proses | int | - |
|
||||
| 11 | created_at | timestamp | Not Null |
|
||||
| 12 | updated_at | timestamp | Not Null |
|
||||
|
||||
---
|
||||
|
||||
#### f. Tabel reviews
|
||||
|
||||
Tabel reviews digunakan untuk menyimpan data review dan rating yang diberikan oleh pengguna terhadap kontrakan atau layanan laundry. Setiap review memiliki relasi dengan tabel users melalui user_id.
|
||||
|
||||
Tabel ini menggunakan pendekatan polymorphic relationships, di mana kolom type menentukan jenis item yang di-review (kontrakan atau laundry), dan kolom item_id menyimpan ID dari item yang di-review. Tabel ini juga menyimpan rating dalam skala 1-5 melalui kolom rating, serta isi review melalui kolom review.
|
||||
|
||||
Struktur tabel reviews dapat dilihat seperti pada Tabel 4.6.
|
||||
|
||||
**Tabel 4.6 Tabel reviews**
|
||||
|
||||
| No | Field | Type | Constraint |
|
||||
|----|-------|------|-----------|
|
||||
| 1 | id | bigint | Primary Key |
|
||||
| 2 | user_id | bigint | Foreign Key → users.id |
|
||||
| 3 | type | varchar(50) | Not Null (kontrakan, laundry) |
|
||||
| 4 | item_id | int | Not Null |
|
||||
| 5 | rating | int | Not Null (1-5) |
|
||||
| 6 | review | text | - |
|
||||
| 7 | created_at | timestamp | Not Null |
|
||||
| 8 | updated_at | timestamp | Not Null |
|
||||
|
||||
---
|
||||
|
||||
#### g. Tabel galeri
|
||||
|
||||
Tabel galeri digunakan untuk menyimpan data foto yang terkait dengan kontrakan atau layanan laundry. Setiap foto memiliki relasi dengan item yang di-referensikan melalui pendekatan polymorphic relationships.
|
||||
|
||||
Tabel ini menggunakan kolom type untuk menentukan jenis item (kontrakan atau laundry) dan kolom item_id untuk menyimpan ID item. Kolom foto menyimpan nama file atau path foto, kolom urutan menentukan urutan tampilan foto, dan kolom is_primary menandakan apakah foto tersebut adalah foto utama.
|
||||
|
||||
Struktur tabel galeri dapat dilihat seperti pada Tabel 4.7.
|
||||
|
||||
**Tabel 4.7 Tabel galeri**
|
||||
|
||||
| No | Field | Type | Constraint |
|
||||
|----|-------|------|-----------|
|
||||
| 1 | id | bigint | Primary Key |
|
||||
| 2 | type | varchar(50) | Not Null (kontrakan, laundry) |
|
||||
| 3 | item_id | int | Not Null |
|
||||
| 4 | foto | varchar(255) | Not Null |
|
||||
| 5 | urutan | int | Not Null |
|
||||
| 6 | is_primary | boolean | Not Null |
|
||||
| 7 | caption | text | Nullable |
|
||||
| 8 | created_at | timestamp | Not Null |
|
||||
| 9 | updated_at | timestamp | Not Null |
|
||||
|
||||
---
|
||||
|
||||
### 4.2.3 Penjelasan Relasi Antar Tabel
|
||||
|
||||
Berikut adalah penjelasan mengenai relasi antar tabel dalam sistem basis data:
|
||||
|
||||
| Relasi | Jenis | Keterangan |
|
||||
|--------|-------|-----------|
|
||||
| USERS → BOOKINGS | 1:M | Seorang pengguna dapat melakukan banyak pemesanan kontrakan |
|
||||
| KONTRAKANS → BOOKINGS | 1:M | Satu kontrakan dapat di-booking multiple times oleh berbagai pengguna |
|
||||
| USERS → REVIEWS | 1:M | Seorang pengguna dapat memberikan banyak review |
|
||||
| REVIEWS → KONTRAKANS/LAUNDRY | M:1 (Polymorphic) | Review dapat diberikan untuk kontrakan atau layanan laundry |
|
||||
| REVIEWS → USERS | M:1 | Banyak review dapat berasal dari satu pengguna |
|
||||
| LAUNDRY → LAYANAN_LAUNDRY | 1:M | Satu layanan laundry dapat menawarkan berbagai jenis/paket layanan |
|
||||
| GALERI → KONTRAKANS/LAUNDRY | M:1 (Polymorphic) | Foto dapat terkait dengan kontrakan atau layanan laundry |
|
||||
|
||||
---
|
||||
|
||||
### 4.2.4 Normalisasi Database
|
||||
|
||||
Database yang dirancang telah dinormalisasi hingga bentuk normal ketiga (3NF) dengan karakteristik sebagai berikut:
|
||||
|
||||
1. **Tidak ada redundansi data** - Setiap data hanya disimpan di satu tempat
|
||||
2. **Setiap atribut non-key bergantung pada key** - Tidak ada ketergantungan pada atribut non-key
|
||||
3. **Tidak ada transitive dependency** - Setiap atribut hanya bergantung pada primary key
|
||||
4. **Efficient indexing** - Foreign keys dan kolom pencarian telah diindeks untuk performa query yang optimal
|
||||
|
||||
---
|
||||
|
||||
### 4.2.5 Fitur Khusus Database
|
||||
|
||||
#### Polymorphic Relationships
|
||||
Sistem menggunakan polymorphic relationships pada tabel reviews dan galeri, memungkinkan kedua tabel tersebut terhubung dengan multiple entities (kontrakan dan laundry) menggunakan kombinasi kolom type dan item_id, sehingga lebih fleksibel dan efficient dalam mengelola data.
|
||||
|
||||
#### Auto-Sync Status
|
||||
Sistem secara otomatis melakukan sinkronisasi status kontrakan berdasarkan status booking. Ketika booking dihapus atau dibatalkan, sistem akan memperbarui status kontrakan secara otomatis:
|
||||
- Jika ada booking dengan status checked_in → status kontrakan = "occupied"
|
||||
- Jika ada booking dengan status confirmed/pending → status kontrakan = "booked"
|
||||
- Jika tidak ada booking aktif → status kontrakan = "available"
|
||||
|
||||
#### Soft Delete
|
||||
Tabel users menggunakan soft delete mechanism, di mana data pengguna tidak benar-benar dihapus dari database melainkan ditandai dengan deleted_at timestamp, sehingga data tetap tersimpan untuk audit trail dan history.
|
||||
|
||||
---
|
||||
|
||||
Dengan struktur database yang telah dirancang dengan baik ini, sistem dapat mengelola data dengan efisien, terorganisir, dan aman sesuai dengan kebutuhan aplikasi SPK Kontrakan & Laundry.
|
||||
|
|
@ -0,0 +1,467 @@
|
|||
# ER Diagram Database - SPK Kontrakan & Laundry
|
||||
|
||||
## 1. Diagram Entity Relationship (Mermaid) - Core Tables
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS ||--o{ BOOKINGS : "1:M"
|
||||
USERS ||--o{ REVIEWS : "1:M"
|
||||
USERS ||--o{ FAVORITES : "1:M"
|
||||
KONTRAKANS ||--o{ BOOKINGS : "1:M"
|
||||
KONTRAKANS ||--o{ REVIEWS : "1:M"
|
||||
KONTRAKANS ||--o{ FAVORITES : "1:M"
|
||||
KONTRAKANS ||--o{ GALERI : "1:M"
|
||||
LAUNDRY ||--o{ LAYANAN_LAUNDRY : "1:M"
|
||||
LAUNDRY ||--o{ REVIEWS : "1:M"
|
||||
LAUNDRY ||--o{ FAVORITES : "1:M"
|
||||
LAUNDRY ||--o{ GALERI : "1:M"
|
||||
BOOKINGS ||--|| USERS : "M:1"
|
||||
BOOKINGS ||--|| KONTRAKANS : "M:1"
|
||||
|
||||
USERS : PK id
|
||||
USERS : string name
|
||||
USERS : string email
|
||||
USERS : enum role
|
||||
|
||||
KONTRAKANS : PK id
|
||||
KONTRAKANS : string nama
|
||||
KONTRAKANS : string alamat
|
||||
KONTRAKANS : int harga
|
||||
KONTRAKANS : decimal jarak
|
||||
KONTRAKANS : enum status
|
||||
|
||||
BOOKINGS : PK id
|
||||
BOOKINGS : FK kontrakan_id
|
||||
BOOKINGS : FK user_id
|
||||
BOOKINGS : date start_date
|
||||
BOOKINGS : date end_date
|
||||
BOOKINGS : enum status
|
||||
|
||||
LAUNDRY : PK id
|
||||
LAUNDRY : string nama
|
||||
LAUNDRY : string alamat
|
||||
LAUNDRY : int harga
|
||||
|
||||
LAYANAN_LAUNDRY : PK id
|
||||
LAYANAN_LAUNDRY : FK laundry_id
|
||||
LAYANAN_LAUNDRY : string nama_paket
|
||||
|
||||
REVIEWS : PK id
|
||||
REVIEWS : FK user_id
|
||||
REVIEWS : string type
|
||||
REVIEWS : int item_id
|
||||
REVIEWS : int rating
|
||||
|
||||
FAVORITES : PK id
|
||||
FAVORITES : FK user_id
|
||||
FAVORITES : string type
|
||||
FAVORITES : int item_id
|
||||
|
||||
GALERI : PK id
|
||||
GALERI : string type
|
||||
GALERI : int item_id
|
||||
GALERI : string foto
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Daftar Tabel & Penjelasan
|
||||
|
||||
### **USERS** - Tabel Pengguna
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID pengguna unik |
|
||||
| name | varchar(255) | Nama lengkap |
|
||||
| email | varchar(255) | Email (unique) |
|
||||
| password | varchar(255) | Password terenkripsi |
|
||||
| phone | varchar(20) | Nomor telepon |
|
||||
| role | enum | super_admin, admin, user |
|
||||
| email_verified_at | timestamp | Verifikasi email |
|
||||
| deleted_at | timestamp | Soft delete |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Relasi:**
|
||||
- `1:M` → BOOKINGS (seorang user bisa booking multiple kontrakan)
|
||||
- `1:M` → REVIEWS (seorang user bisa review multiple item)
|
||||
- `1:M` → FAVORITES (seorang user bisa favorite multiple item)
|
||||
- `1:M` → ACTIVITY_LOGS (log semua aksi user)
|
||||
- `1:M` → PERSONAL_ACCESS_TOKENS (API auth tokens)
|
||||
|
||||
---
|
||||
|
||||
### **KONTRAKANS** - Tabel Kontrakan
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID kontrakan unik |
|
||||
| nama | varchar(255) | Nama/jenis kontrakan |
|
||||
| alamat | text | Alamat lengkap |
|
||||
| no_whatsapp | varchar(20) | WhatsApp owner |
|
||||
| latitude, longitude | decimal(10,8) | Koordinat GPS |
|
||||
| harga | int | Harga per bulan (Rp) |
|
||||
| jarak | decimal(5,2) | Jarak dari kampus (km) |
|
||||
| fasilitas | text | Daftar fasilitas |
|
||||
| jumlah_kamar | int | Jumlah kamar |
|
||||
| bathroom_count | int | Jumlah kamar mandi |
|
||||
| foto | varchar(255) | Foto primary |
|
||||
| status | enum | available, booked, occupied |
|
||||
| occupied_until | date | Tanggal kontrakan berakhir |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Relasi:**
|
||||
- `1:M` → BOOKINGS (1 kontrakan bisa di-booking multiple times)
|
||||
- `1:M` → REVIEWS (banyak review untuk 1 kontrakan)
|
||||
- `1:M` → FAVORITES (banyak user bisa favorite 1 kontrakan)
|
||||
- `1:M` → GALERI (banyak foto untuk 1 kontrakan)
|
||||
- `1:M` → KRITERIA (difilter berdasarkan kriteria)
|
||||
|
||||
**Status Flow:**
|
||||
```
|
||||
available → booked (ada booking confirmed/pending)
|
||||
→ occupied (ada booking checked_in)
|
||||
→ available (booking dibatalkan/selesai)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **BOOKINGS** - Tabel Pemesanan (Core Business Logic)
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID booking unik |
|
||||
| kontrakan_id | bigint (FK) | Referensi kontrakan |
|
||||
| user_id | bigint (FK) | Referensi penyewa |
|
||||
| start_date | date | Tanggal check-in |
|
||||
| end_date | date | Tanggal check-out |
|
||||
| status | enum | pending, confirmed, checked_in, completed, cancelled |
|
||||
| amount | decimal(12,2) | Total harga booking (Rp) |
|
||||
| payment_status | enum | unpaid, paid, refunded |
|
||||
| payment_method | varchar(50) | transfer, cod, dll |
|
||||
| payment_proof | varchar(255) | Bukti transfer (file path) |
|
||||
| paid_at | timestamp | Kapan pembayaran |
|
||||
| notes | text | Catatan khusus |
|
||||
| booking_source | varchar(50) | api, web, mobile |
|
||||
| tenant_name | varchar(255) | Nama penyewa (dari API) |
|
||||
| tenant_phone | varchar(20) | Telepon penyewa |
|
||||
| confirmed_at, checked_in_at, checked_out_at | timestamp | Audit timestamps |
|
||||
| cancelled_at | timestamp | Kapan dibatalkan |
|
||||
| cancellation_reason | text | Alasan pembatalan |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Relasi:**
|
||||
- `M:1` → USERS (banyak booking dari 1 user)
|
||||
- `M:1` → KONTRAKANS (banyak booking untuk 1 kontrakan)
|
||||
|
||||
**Auto-Sync Status Kontrakan:**
|
||||
```
|
||||
Saat booking dihapus/dibatalkan:
|
||||
↓
|
||||
Cek booking lain untuk kontrakan yang sama
|
||||
↓
|
||||
- Ada booking checked_in? → status = "occupied"
|
||||
- Ada booking confirmed/pending? → status = "booked"
|
||||
- Tidak ada? → status = "available" + clear occupied_until
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **REVIEWS** (Polymorphic) - Tabel Review/Rating
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID review unik |
|
||||
| type | varchar(50) | "kontrakan" atau "laundry" |
|
||||
| item_id | int | ID kontrakan atau laundry |
|
||||
| user_id | bigint (FK) | Siapa yang review |
|
||||
| rating | int | 1-5 stars |
|
||||
| review | text | Isi review |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Relasi:**
|
||||
- `M:1` → USERS (banyak review dari 1 user)
|
||||
- `M:1→` KONTRAKANS (via polymorphic: type='kontrakan' + item_id)
|
||||
- `M:1→` LAUNDRY (via polymorphic: type='laundry' + item_id)
|
||||
|
||||
**Contoh Data:**
|
||||
```
|
||||
| id | type | item_id | user_id | rating | review |
|
||||
| 1 | kontrakan | 5 | 3 | 5 | Bagus banget! |
|
||||
| 2 | laundry | 2 | 4 | 4 | Cepat tapi mahal |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **FAVORITES** (Polymorphic) - Tabel Favorit
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID favorite unik |
|
||||
| type | varchar(50) | "kontrakan" atau "laundry" |
|
||||
| item_id | int | ID kontrakan atau laundry |
|
||||
| user_id | bigint (FK) | User yang favorite |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Relasi:**
|
||||
- `M:1` → USERS (banyak favorite dari 1 user)
|
||||
- `M:1→` KONTRAKANS (via polymorphic)
|
||||
- `M:1→` LAUNDRY (via polymorphic)
|
||||
|
||||
**Constraint:** UNIQUE(user_id, type, item_id) - 1 user tidak bisa favorite item yang sama 2x
|
||||
|
||||
---
|
||||
|
||||
### **GALERI** (Polymorphic) - Tabel Foto/Gallery
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID foto unik |
|
||||
| type | varchar(50) | "kontrakan" atau "laundry" |
|
||||
| item_id | int | ID kontrakan atau laundry |
|
||||
| foto | varchar(255) | File path foto |
|
||||
| urutan | int | Urutan display (1, 2, 3, ...) |
|
||||
| is_primary | boolean | Apakah foto utama |
|
||||
| caption | text | Deskripsi foto |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Relasi:**
|
||||
- `M:1→` KONTRAKANS (via polymorphic)
|
||||
- `M:1→` LAUNDRY (via polymorphic)
|
||||
|
||||
---
|
||||
|
||||
### **LAUNDRY** - Tabel Laundry
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID laundry unik |
|
||||
| nama | varchar(255) | Nama laundry |
|
||||
| alamat | text | Alamat |
|
||||
| no_whatsapp | varchar(20) | WhatsApp laundry |
|
||||
| latitude, longitude | decimal(10,8) | Koordinat GPS |
|
||||
| jarak | decimal(5,2) | Jarak dari kampus (km) |
|
||||
| fasilitas | text | Daftar fasilitas |
|
||||
| jam_buka | time | Jam buka (HH:mm) |
|
||||
| jam_tutup | time | Jam tutup (HH:mm) |
|
||||
| status | enum | active, inactive |
|
||||
| foto | varchar(255) | Foto primary |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Relasi:**
|
||||
- `1:M` → LAYANAN_LAUNDRY (1 laundry punya banyak layanan)
|
||||
- `1:M` → REVIEWS (banyak review)
|
||||
- `1:M` → FAVORITES (banyak user favorite)
|
||||
- `1:M` → GALERI (banyak foto)
|
||||
|
||||
---
|
||||
|
||||
### **LAYANAN_LAUNDRY** - Tabel Jenis/Paket Layanan
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID layanan unik |
|
||||
| laundry_id | bigint (FK) | Laundry mana |
|
||||
| jenis_layanan | enum | regular, express, kilat |
|
||||
| nama_paket | varchar(255) | Nama paket (e.g., "Reguler 5kg") |
|
||||
| harga | decimal(10,2) | Harga layanan (Rp) |
|
||||
| estimasi_selesai | varchar(50) | Estimasi waktu ("2 hari", "24 jam") |
|
||||
| deskripsi | text | Detail layanan |
|
||||
| status | enum | active, inactive |
|
||||
| rating | decimal(3,2) | Rating layanan |
|
||||
| waktu_proses | int | Waktu dalam jam |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Relasi:**
|
||||
- `M:1` → LAUNDRY (banyak layanan dari 1 laundry)
|
||||
|
||||
---
|
||||
|
||||
### **KRITERIA** - Tabel Filter/SPK Kriteria
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID kriteria unik |
|
||||
| tipe_bisnis | enum | "kontrakan", "laundry" |
|
||||
| nama_kriteria | varchar(255) | Nama kriteria filter |
|
||||
| bobot | int | Bobot/priority (untuk SPK) |
|
||||
| tipe | enum | numeric, categorical, boolean |
|
||||
| keterangan | text | Penjelasan kriteria |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Contoh untuk Kontrakan:**
|
||||
```
|
||||
| id | tipe_bisnis | nama_kriteria | bobot | tipe | keterangan |
|
||||
| 1 | kontrakan | Harga | 30 | numeric | Harga sewa per bulan |
|
||||
| 2 | kontrakan | Jarak Kampus | 25 | numeric | Jarak dalam KM |
|
||||
| 3 | kontrakan | Jumlah Kamar | 20 | numeric | Banyak kamar |
|
||||
| 4 | kontrakan | Fasilitas | 25 | categorical | Kelengkapan fasilitas |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **ACTIVITY_LOGS** - Tabel Audit Log
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID log unik |
|
||||
| user_id | bigint (FK) | User yang melakukan aksi |
|
||||
| action | varchar(50) | create, update, delete, login, dll |
|
||||
| description | text | Deskripsi aksi |
|
||||
| model_type | varchar(50) | Model apa yang di-akses (User, Booking, dll) |
|
||||
| model_id | int | ID model yang di-akses |
|
||||
| old_values | json | Nilai lama (untuk update/delete) |
|
||||
| new_values | json | Nilai baru (untuk create/update) |
|
||||
| ip_address | varchar(45) | IP address user |
|
||||
| user_agent | text | Browser info |
|
||||
| created_at | timestamp | Kapan terjadi |
|
||||
|
||||
**Relasi:**
|
||||
- `M:1` → USERS (audit log user)
|
||||
- `Polymorphic` → Berbagai models (via model_type + model_id)
|
||||
|
||||
**Contoh Data:**
|
||||
```
|
||||
{
|
||||
"action": "update",
|
||||
"description": "Update harga kontrakan",
|
||||
"model_type": "Kontrakan",
|
||||
"model_id": 5,
|
||||
"old_values": {"harga": 2000000},
|
||||
"new_values": {"harga": 2500000}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **PERSONAL_ACCESS_TOKENS** - Tabel API Tokens
|
||||
| Field | Tipe | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint (PK) | ID token unik |
|
||||
| user_id | bigint (FK) | User pemilik token |
|
||||
| name | varchar(255) | Nama token (e.g., "Mobile App") |
|
||||
| token | varchar(80) | Hash token |
|
||||
| abilities | json | Izin apa saja (e.g., ["*"]) |
|
||||
| last_used_at | timestamp | Terakhir dipakai |
|
||||
| expires_at | timestamp | Kapan expired |
|
||||
| created_at, updated_at | timestamp | Audit trail |
|
||||
|
||||
**Relasi:**
|
||||
- `M:1` → USERS (banyak token per user)
|
||||
|
||||
**Gunanya:**
|
||||
- Mobile app authentication
|
||||
- Third-party API access
|
||||
- Stateless authentication dengan Laravel Sanctum
|
||||
|
||||
---
|
||||
|
||||
## 3. Summary Relasi
|
||||
|
||||
| Relasi | From | To | Tipe | Keterangan |
|
||||
|--------|------|-----|------|-----------|
|
||||
| User → Booking | 1 | M | Parent | Seorang user bisa booking multiple kontrakan |
|
||||
| Kontrakan → Booking | 1 | M | Parent | 1 kontrakan bisa di-booking multiple times |
|
||||
| User → Review | 1 | M | Parent | Seorang user bisa review multiple item |
|
||||
| Review → Kontrakan/Laundry | M | 1 | Polymorphic | Review bisa untuk kontrakan atau laundry |
|
||||
| User → Favorite | 1 | M | Parent | Seorang user bisa favorite multiple item |
|
||||
| Favorite → Kontrakan/Laundry | M | 1 | Polymorphic | Favorite bisa untuk kontrakan atau laundry |
|
||||
| Kontrakan → Galeri | 1 | M | Parent | 1 kontrakan punya banyak foto |
|
||||
| Laundry → Galeri | 1 | M | Parent | 1 laundry punya banyak foto |
|
||||
| Galeri → Kontrakan/Laundry | M | 1 | Polymorphic | Foto bisa untuk kontrakan atau laundry |
|
||||
| Laundry → Layanan | 1 | M | Parent | 1 laundry punya banyak jenis layanan |
|
||||
| User → ActivityLog | 1 | M | Parent | Setiap aksi user dicatat |
|
||||
| ActivityLog → Models | M | 1 | Polymorphic | Log bisa untuk berbagai model |
|
||||
| User → PersonalAccessToken | 1 | M | Parent | User bisa punya multiple API tokens |
|
||||
|
||||
---
|
||||
|
||||
## 4. Key Features
|
||||
|
||||
### **Auto-Sync Booking → Kontrakan Status**
|
||||
```php
|
||||
// Saat booking dihapus/dibatalkan:
|
||||
$booking = Booking::find(1);
|
||||
$kontrakanId = $booking->kontrakan_id;
|
||||
$booking->delete(); // Trigger: Booking::syncKontrakanStatus($kontrakanId)
|
||||
|
||||
// Otomatis mengupdate status kontrakan berdasarkan booking aktif
|
||||
```
|
||||
|
||||
### **Polymorphic Relationships**
|
||||
```php
|
||||
// Review bisa untuk kontrakan atau laundry:
|
||||
$review = Review::create([
|
||||
'type' => 'kontrakan', // atau 'laundry'
|
||||
'item_id' => 5,
|
||||
'user_id' => 1,
|
||||
'rating' => 5,
|
||||
'review' => 'Bagus!'
|
||||
]);
|
||||
|
||||
// Query dengan polymorphic:
|
||||
$reviews = Review::forItem('kontrakan', 5)->get();
|
||||
```
|
||||
|
||||
### **GPS Distance Calculation**
|
||||
```php
|
||||
// Hitung jarak dari koordinat user menggunakan Haversine formula
|
||||
$kontrakan->calculateDistance($userLat, $userLng);
|
||||
// Return: 2.35 (km)
|
||||
```
|
||||
|
||||
### **Activity Logging**
|
||||
```php
|
||||
// Semua aksi user terekam di database untuk audit trail
|
||||
ActivityLog::log(
|
||||
action: 'update',
|
||||
description: 'Update booking status',
|
||||
modelType: 'Booking',
|
||||
modelId: 1,
|
||||
oldValues: ['status' => 'pending'],
|
||||
newValues: ['status' => 'confirmed']
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Constraints & Rules
|
||||
|
||||
### **Primary Keys**
|
||||
- Semua tabel memiliki `id` bigint unsigned sebagai PK
|
||||
- Auto-increment
|
||||
|
||||
### **Foreign Keys**
|
||||
- BOOKINGS.kontrakan_id → KONTRAKANS.id
|
||||
- BOOKINGS.user_id → USERS.id
|
||||
- REVIEWS.user_id → USERS.id
|
||||
- FAVORITES.user_id → USERS.id
|
||||
- LAYANAN_LAUNDRY.laundry_id → LAUNDRY.id
|
||||
- ACTIVITY_LOGS.user_id → USERS.id (nullable)
|
||||
- PERSONAL_ACCESS_TOKENS.user_id → USERS.id
|
||||
|
||||
### **Unique Constraints**
|
||||
- USERS.email - unique
|
||||
- FAVORITES(user_id, type, item_id) - unique (1 user tidak bisa favorite item sama 2x)
|
||||
|
||||
### **Enums**
|
||||
- USERS.role: super_admin, admin, user
|
||||
- BOOKINGS.status: pending, confirmed, checked_in, completed, cancelled
|
||||
- BOOKINGS.payment_status: unpaid, paid, refunded
|
||||
- KONTRAKANS.status: available, booked, occupied
|
||||
- REVIEWS.type, FAVORITES.type, GALERI.type: kontrakan, laundry
|
||||
- LAUNDRY.status: active, inactive
|
||||
- KRITERIA.tipe_bisnis: kontrakan, laundry
|
||||
- LAYANAN_LAUNDRY.jenis_layanan: regular, express, kilat
|
||||
|
||||
### **Soft Deletes**
|
||||
- USERS - menggunakan soft delete (deleted_at)
|
||||
|
||||
### **Timestamps**
|
||||
- created_at, updated_at otomatis di semua tabel
|
||||
|
||||
---
|
||||
|
||||
## 6. Normalisasi Database
|
||||
|
||||
✅ **Fully Normalized to 3NF:**
|
||||
- Tidak ada redundansi data
|
||||
- Setiap atribut non-key bergantung pada key
|
||||
- Tidak ada transitive dependency
|
||||
|
||||
✅ **Efficient Indexing:**
|
||||
- Foreign keys sudah di-index
|
||||
- search columns (nama, alamat) direkomendasikan di-index
|
||||
|
||||
---
|
||||
|
||||
Dokumentasi ini dapat digunakan langsung untuk laporan sidang! 📊
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
Table users {
|
||||
id bigint [pk, increment]
|
||||
name varchar(255)
|
||||
email varchar(255) [unique]
|
||||
password varchar(255)
|
||||
role varchar(50)
|
||||
created_at timestamp
|
||||
updated_at timestamp
|
||||
}
|
||||
|
||||
Table kontrakans {
|
||||
id bigint [pk, increment]
|
||||
nama varchar(255)
|
||||
alamat text
|
||||
harga int
|
||||
jarak decimal(5,2)
|
||||
status varchar(50)
|
||||
foto varchar(255)
|
||||
created_at timestamp
|
||||
updated_at timestamp
|
||||
}
|
||||
|
||||
Table bookings {
|
||||
id bigint [pk, increment]
|
||||
kontrakan_id bigint [ref: > kontrakans.id]
|
||||
user_id bigint [ref: > users.id]
|
||||
start_date date
|
||||
end_date date
|
||||
status varchar(50)
|
||||
amount decimal(12,2)
|
||||
payment_status varchar(50)
|
||||
created_at timestamp
|
||||
updated_at timestamp
|
||||
}
|
||||
|
||||
Table laundry {
|
||||
id bigint [pk, increment]
|
||||
nama varchar(255)
|
||||
alamat text
|
||||
status varchar(50)
|
||||
foto varchar(255)
|
||||
created_at timestamp
|
||||
updated_at timestamp
|
||||
}
|
||||
|
||||
Table layanan_laundry {
|
||||
id bigint [pk, increment]
|
||||
laundry_id bigint [ref: > laundry.id]
|
||||
nama_paket varchar(255)
|
||||
harga decimal(10,2)
|
||||
status varchar(50)
|
||||
created_at timestamp
|
||||
updated_at timestamp
|
||||
}
|
||||
|
||||
Table reviews {
|
||||
id bigint [pk, increment]
|
||||
user_id bigint [ref: > users.id]
|
||||
type varchar(50)
|
||||
item_id int
|
||||
rating int
|
||||
review text
|
||||
created_at timestamp
|
||||
updated_at timestamp
|
||||
}
|
||||
|
||||
Table galeri {
|
||||
id bigint [pk, increment]
|
||||
type varchar(50)
|
||||
item_id int
|
||||
foto varchar(255)
|
||||
is_primary boolean
|
||||
created_at timestamp
|
||||
updated_at timestamp
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use ZipArchive;
|
||||
|
||||
class DatabaseBackup extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'database:backup';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Create a database backup ZIP file in storage/backups';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
try {
|
||||
$backupPath = storage_path('backups');
|
||||
|
||||
if (!File::isDirectory($backupPath)) {
|
||||
File::makeDirectory($backupPath, 0755, true);
|
||||
}
|
||||
|
||||
$timestamp = now()->format('Y-m-d-H-i-s');
|
||||
$backupFile = $backupPath . "/backup_{$timestamp}.sql";
|
||||
$zipFile = $backupPath . "/backup_{$timestamp}.zip";
|
||||
|
||||
$database = config('database.connections.mysql.database');
|
||||
$username = config('database.connections.mysql.username');
|
||||
$password = config('database.connections.mysql.password');
|
||||
$host = config('database.connections.mysql.host');
|
||||
|
||||
$dumpCommand = sprintf(
|
||||
'mysqldump --user=%s --password=%s --host=%s %s > "%s"',
|
||||
escapeshellarg($username),
|
||||
escapeshellarg($password),
|
||||
escapeshellarg($host),
|
||||
escapeshellarg($database),
|
||||
$backupFile
|
||||
);
|
||||
|
||||
$output = null;
|
||||
$exitCode = null;
|
||||
exec($dumpCommand, $output, $exitCode);
|
||||
|
||||
if ($exitCode !== 0 || !File::exists($backupFile)) {
|
||||
$this->error('Gagal membuat dump database.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFile, ZipArchive::CREATE) !== true) {
|
||||
File::delete($backupFile);
|
||||
$this->error('Gagal membuat file ZIP backup.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$zip->addFile($backupFile, 'database.sql');
|
||||
$zip->close();
|
||||
|
||||
File::delete($backupFile);
|
||||
|
||||
$this->info("Backup berhasil dibuat: backup_{$timestamp}.zip");
|
||||
|
||||
\Log::info('Database backup command executed', [
|
||||
'file' => "backup_{$timestamp}.zip",
|
||||
'executed_at' => now(),
|
||||
]);
|
||||
|
||||
return self::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error backup database: ' . $e->getMessage());
|
||||
|
||||
\Log::error('Database backup command failed', [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AdminDeviceToken;
|
||||
use App\Models\Kontrakan;
|
||||
use App\Services\FcmService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SendKontrakanAvailabilityReminder extends Command
|
||||
{
|
||||
protected $signature = 'kontrakan:weekly-availability-reminder';
|
||||
protected $description = 'Kirim notifikasi mingguan ke admin untuk konfirmasi ketersediaan kontrakan';
|
||||
|
||||
public function handle(FcmService $fcm): int
|
||||
{
|
||||
$threshold = now()->subDays(7);
|
||||
|
||||
$needsCheckCount = Kontrakan::where('status', 'available')
|
||||
->where(function ($query) use ($threshold) {
|
||||
$query->whereNull('availability_confirmed_at')
|
||||
->orWhere('availability_confirmed_at', '<=', $threshold);
|
||||
})
|
||||
->count();
|
||||
|
||||
if ($needsCheckCount === 0) {
|
||||
$this->info('Tidak ada kontrakan yang perlu konfirmasi.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$tokens = AdminDeviceToken::pluck('token')->toArray();
|
||||
|
||||
if (empty($tokens)) {
|
||||
$this->warn('Tidak ada device token admin.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$title = 'Cek ketersediaan kontrakan';
|
||||
$body = "Ada {$needsCheckCount} kontrakan yang perlu dikonfirmasi minggu ini.";
|
||||
|
||||
$fcm->sendToTokens($tokens, $title, $body, [
|
||||
'type' => 'availability_reminder',
|
||||
'count' => (string) $needsCheckCount,
|
||||
]);
|
||||
|
||||
Log::info('Weekly availability reminder sent', [
|
||||
'count' => $needsCheckCount,
|
||||
'tokens' => count($tokens),
|
||||
]);
|
||||
|
||||
$this->info('Notifikasi terkirim.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,22 +12,7 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
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');
|
||||
});
|
||||
// Scheduler didaftarkan di routes/console.php (Laravel 11 style).
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AdminProfileController extends Controller
|
||||
{
|
||||
public function edit(Request $request)
|
||||
{
|
||||
return view('admin.profile', [
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|min:3|max:255',
|
||||
'email' => 'required|email|unique:users,email,' . $user->id,
|
||||
'phone' => 'nullable|string|min:8|max:20',
|
||||
'password' => 'nullable|string|min:8|confirmed',
|
||||
], [
|
||||
'name.required' => 'Nama harus diisi',
|
||||
'email.required' => 'Email harus diisi',
|
||||
'email.unique' => 'Email sudah digunakan',
|
||||
'password.min' => 'Password minimal 8 karakter',
|
||||
'password.confirmed' => 'Konfirmasi password tidak cocok',
|
||||
]);
|
||||
|
||||
$updateData = [
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'] ?? $user->phone,
|
||||
];
|
||||
|
||||
if (!empty($validated['password'])) {
|
||||
$updateData['password'] = Hash::make($validated['password']);
|
||||
}
|
||||
|
||||
$user->update($updateData);
|
||||
|
||||
return redirect()->route('admin.profile')->with('success', 'Profil berhasil diperbarui');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AdminDeviceToken;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DeviceTokenController extends Controller
|
||||
{
|
||||
public function store(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!in_array($user->role, ['admin', 'super_admin'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Akses ditolak',
|
||||
'error_code' => 'FORBIDDEN',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'token' => 'required|string',
|
||||
'platform' => 'nullable|string|max:32',
|
||||
]);
|
||||
|
||||
$record = AdminDeviceToken::updateOrCreate(
|
||||
['token' => $data['token']],
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'platform' => $data['platform'] ?? null,
|
||||
'last_used_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Device token tersimpan',
|
||||
'data' => [
|
||||
'id' => $record->id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$data = $request->validate([
|
||||
'token' => 'required|string',
|
||||
]);
|
||||
|
||||
AdminDeviceToken::where('token', $data['token'])
|
||||
->where('user_id', $user->id)
|
||||
->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Device token dihapus',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use ZipArchive;
|
||||
use Exception;
|
||||
|
|
@ -55,46 +54,15 @@ public function index(Request $request)
|
|||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$timestamp = now()->format('Y-m-d-H-i-s');
|
||||
$backupFile = $this->backupPath . "/backup_{$timestamp}.sql";
|
||||
$exitCode = Artisan::call('database:backup');
|
||||
|
||||
// Get database credentials (use config() instead of env() for config:cache compatibility)
|
||||
$database = config('database.connections.mysql.database');
|
||||
$username = config('database.connections.mysql.username');
|
||||
$password = config('database.connections.mysql.password');
|
||||
$host = config('database.connections.mysql.host');
|
||||
|
||||
// Create SQL dump (Windows MySQL)
|
||||
$command = sprintf(
|
||||
'mysqldump --user=%s --password=%s --host=%s %s > "%s"',
|
||||
escapeshellarg($username),
|
||||
escapeshellarg($password),
|
||||
escapeshellarg($host),
|
||||
escapeshellarg($database),
|
||||
$backupFile
|
||||
);
|
||||
|
||||
$output = null;
|
||||
$exitCode = null;
|
||||
exec($command, $output, $exitCode);
|
||||
|
||||
if ($exitCode === 0 && File::exists($backupFile)) {
|
||||
// Create zip file
|
||||
$zipFile = $this->backupPath . "/backup_{$timestamp}.zip";
|
||||
$zip = new ZipArchive();
|
||||
|
||||
if ($zip->open($zipFile, ZipArchive::CREATE) === true) {
|
||||
$zip->addFile($backupFile, 'database.sql');
|
||||
$zip->close();
|
||||
|
||||
// Delete original SQL file
|
||||
File::delete($backupFile);
|
||||
|
||||
return redirect()->back()->with('success', "Backup berhasil dibuat: backup_{$timestamp}.zip");
|
||||
}
|
||||
if ($exitCode === 0) {
|
||||
$output = trim(Artisan::output());
|
||||
return redirect()->back()->with('success', $output !== '' ? $output : 'Backup berhasil dibuat');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'Gagal membuat backup');
|
||||
$output = trim(Artisan::output());
|
||||
return redirect()->back()->with('error', $output !== '' ? $output : 'Gagal membuat backup');
|
||||
} catch (Exception $e) {
|
||||
return redirect()->back()->with('error', 'Error: ' . $e->getMessage());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,18 +358,10 @@ public function togglePaymentStatus(Request $request, Booking $booking)
|
|||
}
|
||||
|
||||
/**
|
||||
* Hapus booking (super admin bisa hapus semua, admin biasa hanya pending/cancelled)
|
||||
* Hapus booking
|
||||
*/
|
||||
public function destroy(Booking $booking)
|
||||
{
|
||||
// Super admin bisa hapus booking apapun
|
||||
if (auth()->user()->role !== 'super_admin') {
|
||||
// Admin biasa hanya bisa hapus pending atau cancelled
|
||||
if (!in_array($booking->status, [Booking::STATUS_PENDING, Booking::STATUS_CANCELLED])) {
|
||||
return back()->with('error', 'Hanya booking pending atau yang dibatalkan yang bisa dihapus.');
|
||||
}
|
||||
}
|
||||
|
||||
// delete() akan trigger boot()->deleted() yang auto sync status kontrakan
|
||||
$booking->delete();
|
||||
|
||||
|
|
@ -415,30 +407,13 @@ public function bulkDestroy(Request $request)
|
|||
return back()->with('error', 'Tidak ada data booking yang bisa dihapus.');
|
||||
}
|
||||
|
||||
$isSuperAdmin = auth()->user()->role === 'super_admin';
|
||||
$deletedCount = 0;
|
||||
$skippedCount = 0;
|
||||
|
||||
foreach ($bookings as $booking) {
|
||||
$canDelete = $isSuperAdmin || in_array($booking->status, [Booking::STATUS_PENDING, Booking::STATUS_CANCELLED]);
|
||||
|
||||
if (!$canDelete) {
|
||||
$skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$booking->delete();
|
||||
$deletedCount++;
|
||||
}
|
||||
|
||||
if ($deletedCount === 0) {
|
||||
return back()->with('error', 'Tidak ada booking yang dihapus. Admin biasa hanya boleh menghapus status pending/dibatalkan.');
|
||||
}
|
||||
|
||||
if ($skippedCount > 0) {
|
||||
return back()->with('success', "Berhasil hapus {$deletedCount} booking. {$skippedCount} booking dilewati karena tidak memiliki izin hapus.");
|
||||
}
|
||||
|
||||
return back()->with('success', "Berhasil hapus {$deletedCount} booking.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
use App\Models\Kontrakan;
|
||||
use App\Models\Laundry;
|
||||
use App\Models\Kriteria;
|
||||
use App\Models\Booking;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
|
|
@ -146,8 +147,8 @@ public function index()
|
|||
|
||||
// ========== TAMBAHAN DATA YANG HILANG ==========
|
||||
$additionalData = [
|
||||
// Data review
|
||||
'totalReviews' => \App\Models\Review::count() ?? 0,
|
||||
// Data booking
|
||||
'totalBookings' => Booking::count() ?? 0,
|
||||
|
||||
// Data admin
|
||||
'totalAdmins' => \App\Models\User::where('role', 'admin')->count() ?? 1,
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ public function store(Request $request)
|
|||
'jumlah_kamar' => $request->jumlah_kamar,
|
||||
'bathroom_count' => $request->bathroom_count ?? 1,
|
||||
'foto' => $filename,
|
||||
'availability_confirmed_at' => now(),
|
||||
]);
|
||||
|
||||
// Log activity
|
||||
|
|
@ -335,6 +336,7 @@ public function update(Request $request, $id)
|
|||
'jumlah_kamar' => $request->jumlah_kamar,
|
||||
'bathroom_count' => $request->bathroom_count ?? $kontrakan->bathroom_count ?? 1,
|
||||
'foto' => $filename,
|
||||
'availability_confirmed_at' => now(),
|
||||
]);
|
||||
|
||||
// Log activity
|
||||
|
|
@ -449,6 +451,7 @@ public function updateStatus(Request $request, Kontrakan $kontrakan)
|
|||
'status' => $newStatus,
|
||||
// Reset occupied_until jika status jadi available
|
||||
'occupied_until' => $newStatus === 'available' ? null : $kontrakan->occupied_until,
|
||||
'availability_confirmed_at' => now(),
|
||||
]);
|
||||
|
||||
// Log activity
|
||||
|
|
|
|||
|
|
@ -7,6 +7,15 @@
|
|||
|
||||
class KriteriaController extends Controller
|
||||
{
|
||||
private function ensureSuperAdmin()
|
||||
{
|
||||
if (auth()->user()->role !== 'super_admin') {
|
||||
return redirect()->back()->with('error', 'Hanya super admin yang dapat mengubah kriteria.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
// Ambil filter dan sorting dari request
|
||||
|
|
@ -100,11 +109,19 @@ public function index(Request $request)
|
|||
|
||||
public function create()
|
||||
{
|
||||
if ($response = $this->ensureSuperAdmin()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return view('kriteria.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
if ($response = $this->ensureSuperAdmin()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'tipe_bisnis' => 'required|in:kontrakan,laundry',
|
||||
'nama_kriteria' => 'required|string|max:255',
|
||||
|
|
@ -127,11 +144,19 @@ public function show(Kriteria $kriterium)
|
|||
|
||||
public function edit(Kriteria $kriterium)
|
||||
{
|
||||
if ($response = $this->ensureSuperAdmin()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return view('kriteria.edit', ['kriteria' => $kriterium]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Kriteria $kriterium)
|
||||
{
|
||||
if ($response = $this->ensureSuperAdmin()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'tipe_bisnis' => 'required|in:kontrakan,laundry',
|
||||
'nama_kriteria' => 'required|string|max:255',
|
||||
|
|
@ -149,9 +174,8 @@ public function update(Request $request, Kriteria $kriterium)
|
|||
|
||||
public function destroy(Kriteria $kriterium)
|
||||
{
|
||||
// CEK ROLE - HANYA ADMIN DAN SUPER ADMIN YANG BOLEH HAPUS
|
||||
if (!in_array(auth()->user()->role, ['admin', 'super_admin'])) {
|
||||
return redirect()->back()->with('error', 'Anda tidak memiliki akses untuk menghapus data!');
|
||||
if ($response = $this->ensureSuperAdmin()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$tipeBisnis = $kriterium->tipe_bisnis; // Simpan sebelum dihapus
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AdminDeviceToken extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'token',
|
||||
'platform',
|
||||
'last_used_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'last_used_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,10 +23,12 @@ class Kontrakan extends Model
|
|||
'foto',
|
||||
'status',
|
||||
'occupied_until',
|
||||
'availability_confirmed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'occupied_until' => 'date',
|
||||
'availability_confirmed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -172,4 +172,12 @@ public function favorites()
|
|||
{
|
||||
return $this->hasMany(Favorite::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Device tokens untuk push notification admin.
|
||||
*/
|
||||
public function adminDeviceTokens()
|
||||
{
|
||||
return $this->hasMany(AdminDeviceToken::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FcmService
|
||||
{
|
||||
public function sendToTokens(array $tokens, string $title, string $body, array $data = []): void
|
||||
{
|
||||
$serverKey = config('services.fcm.server_key');
|
||||
|
||||
if (!$serverKey) {
|
||||
Log::warning('FCM server key belum diatur. Notifikasi dilewati.');
|
||||
return;
|
||||
}
|
||||
|
||||
$chunks = array_chunk(array_values(array_unique($tokens)), 500);
|
||||
foreach ($chunks as $chunk) {
|
||||
$payload = [
|
||||
'registration_ids' => $chunk,
|
||||
'notification' => [
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
],
|
||||
'data' => $data,
|
||||
'priority' => 'high',
|
||||
];
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'key=' . $serverKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->post('https://fcm.googleapis.com/fcm/send', $payload);
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::warning('FCM request gagal', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,4 +35,8 @@
|
|||
],
|
||||
],
|
||||
|
||||
'fcm' => [
|
||||
'server_key' => env('FCM_SERVER_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_127.0.0.1 FALSE / FALSE 1777218241 laravel_session eyJpdiI6InFHcGdxbHg0eFVLeVdVU0lQamFnbmc9PSIsInZhbHVlIjoiMGllMHpYaVR3aUo3YWdrZjNxMWgwcFlZMXBDaGhCL2xxenZ3cGx2Y2hTOVpLS2hNOGdkUjd5SXNYMXRUcDhoVmw2NjZWeHFYdUs3b0MvQlo1SzMzYlg3MWNjQjlyM0IrY1ZxYkdnbWJsWlJKSXdaYy96YXcxeDB1ZTBkUXNPSFoiLCJtYWMiOiJlNWRmNjEwNTY2YzQ2MmY3ODYzNTU4NTNkMWJkNDdiNTQ3OGE5Mzg2NjJhMTBkYjJmMWEzNTAyYzVmODdlZTQ2IiwidGFnIjoiIn0%3D
|
||||
127.0.0.1 FALSE / FALSE 1777218241 XSRF-TOKEN eyJpdiI6IkdBUzhwMWtDUG1xOU8xSzdGQVpsNnc9PSIsInZhbHVlIjoiMTNpS1dHWldibGc3UXhLNGJvd1JwSW12NkNpeURwaTJaQ3pXRThtcktlS2F6ekgrTnlXVmgxK1pTZWw5Q0RoZGdqeVF6VnJUVlZBZVpTVnk5ZHlhakM3NUIraGNFYmpvbE5XVWw4bTNKK1hWTDBrMW9BL2ZlKysrS3VlZGV5QlIiLCJtYWMiOiIyMGI3ZWZjZTcxMjAzMjY4ODc1Y2EzMjdlZTNkOTJmMjk1NTA1YWQwNWJlMGI3OTkzYzdlZGE3NzdiNjg4MmM1IiwidGFnIjoiIn0%3D
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('kontrakans', function (Blueprint $table) {
|
||||
$table->timestamp('availability_confirmed_at')->nullable()->after('occupied_until');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('kontrakans', function (Blueprint $table) {
|
||||
$table->dropColumn('availability_confirmed_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('admin_device_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('token')->unique();
|
||||
$table->string('platform', 32)->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('admin_device_tokens');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
HTTP/1.1 200 OK
|
||||
Host: 127.0.0.1:8000
|
||||
Date: Sun, 26 Apr 2026 13:44:01 GMT
|
||||
Connection: close
|
||||
X-Powered-By: PHP/8.2.6
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Cache-Control: no-cache, private
|
||||
Date: Sun, 26 Apr 2026 13:44:01 GMT
|
||||
Set-Cookie: XSRF-TOKEN=eyJpdiI6IkdBUzhwMWtDUG1xOU8xSzdGQVpsNnc9PSIsInZhbHVlIjoiMTNpS1dHWldibGc3UXhLNGJvd1JwSW12NkNpeURwaTJaQ3pXRThtcktlS2F6ekgrTnlXVmgxK1pTZWw5Q0RoZGdqeVF6VnJUVlZBZVpTVnk5ZHlhakM3NUIraGNFYmpvbE5XVWw4bTNKK1hWTDBrMW9BL2ZlKysrS3VlZGV5QlIiLCJtYWMiOiIyMGI3ZWZjZTcxMjAzMjY4ODc1Y2EzMjdlZTNkOTJmMjk1NTA1YWQwNWJlMGI3OTkzYzdlZGE3NzdiNjg4MmM1IiwidGFnIjoiIn0%3D; expires=Sun, 26 Apr 2026 15:44:01 GMT; Max-Age=7200; path=/; samesite=lax
|
||||
Set-Cookie: laravel_session=eyJpdiI6InFHcGdxbHg0eFVLeVdVU0lQamFnbmc9PSIsInZhbHVlIjoiMGllMHpYaVR3aUo3YWdrZjNxMWgwcFlZMXBDaGhCL2xxenZ3cGx2Y2hTOVpLS2hNOGdkUjd5SXNYMXRUcDhoVmw2NjZWeHFYdUs3b0MvQlo1SzMzYlg3MWNjQjlyM0IrY1ZxYkdnbWJsWlJKSXdaYy96YXcxeDB1ZTBkUXNPSFoiLCJtYWMiOiJlNWRmNjEwNTY2YzQ2MmY3ODYzNTU4NTNkMWJkNDdiNTQ3OGE5Mzg2NjJhMTBkYjJmMWEzNTAyYzVmODdlZTQ2IiwidGFnIjoiIn0%3D; expires=Sun, 26 Apr 2026 15:44:01 GMT; Max-Age=7200; path=/; httponly; samesite=lax
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login Pemilik - SPK Kontrakan & Laundry</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-header h2 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
margin: 8px 0 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-body {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
padding: 20px 30px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.auth-footer p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.auth-footer a:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h2><i class="fas fa-shield-alt"></i> Login Pemilik</h2>
|
||||
<p>Khusus Pemilik Kontrakan & Laundry</p>
|
||||
</div>
|
||||
|
||||
<!-- Warning Box -->
|
||||
<div class="alert alert-warning mx-3 mt-3 mb-0">
|
||||
<h6 class="fw-bold mb-2">⚠️ PERHATIAN - KHUSUS PEMILIK BISNIS</h6>
|
||||
<p class="small mb-2">💼 <strong>Admin:</strong> Pemilik kontrakan/laundry - kelola data bisnis Anda</p>
|
||||
<p class="small mb-0">👑 <strong>Super Admin:</strong> Pengelola website - kelola seluruh sistem</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-body">
|
||||
|
||||
<form method="POST" action="http://127.0.0.1:8000/admin/login">
|
||||
<input type="hidden" name="_token" value="5bay1BzJsZfarvuHrnBTsj1jmejPRbY8l3dLG1tD" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label for="email" class="form-label"><i class="fas fa-envelope"></i> Email</label>
|
||||
<input type="email" id="email" name="email" class="form-control" placeholder="Masukkan email Anda" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password" class="form-label"><i class="fas fa-key"></i> Password</label>
|
||||
<input type="password" id="password" name="password" class="form-control" placeholder="Masukkan password Anda" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-login">
|
||||
<i class="fas fa-sign-in-alt me-2"></i>Masuk
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>Belum punya akun bisnis? Hubungi super admin untuk pembuatan akun.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 870 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 637 KiB |
|
|
@ -699,7 +699,6 @@
|
|||
<div class="fw-bold">{{ Auth::user()->name }}</div>
|
||||
<small class="text-muted">{{ Auth::user()->email }}</small>
|
||||
</li>
|
||||
<li><a class="dropdown-item py-2" href="{{ route('welcome') }}"><i class="bi bi-house me-2"></i>Lihat Halaman User</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form method="POST" action="{{ route('admin.logout') }}">
|
||||
|
|
@ -726,6 +725,14 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="menu-section">
|
||||
<div class="menu-section-title">Akun</div>
|
||||
<a href="{{ route('admin.profile') }}" class="menu-item {{ request()->routeIs('admin.profile') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
<span>Profil</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Bisnis Management -->
|
||||
<div class="menu-section">
|
||||
<div class="menu-section-title">Kelola Bisnis</div>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
<!-- Info Card -->
|
||||
<div class="alert alert-info border-0 rounded-3 mb-4">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<strong>Tips:</strong> Lakukan backup secara berkala untuk mengamankan data. Backup dibuat dalam format ZIP dan dapat di-download.
|
||||
<strong>Tips:</strong> Backup otomatis dijalankan 1x per minggu (Minggu pukul 01:00), dan Anda tetap bisa membuat backup manual kapan saja.
|
||||
</div>
|
||||
|
||||
<!-- Backups List -->
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
<i class="bi bi-exclamation-triangle me-2"></i>Reminder
|
||||
</h6>
|
||||
<small class="text-muted d-block">
|
||||
⚠️ Backup database secara rutin (minimal 1x sehari) untuk mencegah kehilangan data yang tidak dapat dipulihkan.
|
||||
Backup otomatis mingguan aktif, namun backup manual tetap disarankan sebelum perubahan data besar.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,234 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Profil Admin')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid px-2 px-md-4">
|
||||
<style>
|
||||
.profile-hero {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.95) 0%, rgba(118, 75, 162, 0.95) 100%);
|
||||
color: white;
|
||||
border-radius: 18px;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 16px 32px rgba(102, 126, 234, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.profile-hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: -40px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 40px;
|
||||
transform: rotate(18deg);
|
||||
}
|
||||
|
||||
.profile-hero-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.profile-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.85rem;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 999px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.profile-card .card-header {
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
border-bottom: 2px solid rgba(102, 126, 234, 0.2);
|
||||
font-weight: 700;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.profile-list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.profile-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.85rem 1rem;
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.profile-row span {
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-row strong {
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.profile-note {
|
||||
background: rgba(14, 116, 144, 0.08);
|
||||
border-left: 4px solid #0ea5e9;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 12px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-hero {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.profile-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.profile-row strong {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="profile-hero">
|
||||
<div class="profile-hero-main">
|
||||
<div class="profile-avatar">{{ strtoupper(substr($user->name, 0, 1)) }}</div>
|
||||
<div>
|
||||
<h3 class="mb-1 fw-bold">{{ $user->name }}</h3>
|
||||
<div class="opacity-75">{{ $user->email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-meta">
|
||||
<div class="profile-chip"><i class="bi bi-briefcase"></i>{{ $user->getRoleLabel() }}</div>
|
||||
<div class="profile-chip"><i class="bi bi-calendar-event"></i>{{ now()->format('d M Y') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="card profile-card h-100">
|
||||
<div class="card-header py-3 px-4">Perbarui Profil</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="{{ route('admin.profile.update') }}" class="d-grid gap-3">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Nama Lengkap</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ old('name', $user->name) }}" required>
|
||||
@error('name')
|
||||
<small class="text-danger">{{ $message }}</small>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Email</label>
|
||||
<input type="email" name="email" class="form-control" value="{{ old('email', $user->email) }}" required>
|
||||
@error('email')
|
||||
<small class="text-danger">{{ $message }}</small>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Nomor Telepon</label>
|
||||
<input type="text" name="phone" class="form-control" value="{{ old('phone', $user->phone) }}" placeholder="Opsional">
|
||||
@error('phone')
|
||||
<small class="text-danger">{{ $message }}</small>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Password Baru</label>
|
||||
<input type="password" name="password" class="form-control" placeholder="Kosongkan jika tidak mengganti">
|
||||
@error('password')
|
||||
<small class="text-danger">{{ $message }}</small>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Konfirmasi Password</label>
|
||||
<input type="password" name="password_confirmation" class="form-control" placeholder="Ulangi password baru">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-admin-solid w-100 py-3">Simpan Perubahan</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="card profile-card h-100">
|
||||
<div class="card-header py-3 px-4">Ringkasan</div>
|
||||
<div class="card-body p-4 d-flex flex-column gap-3">
|
||||
<div class="profile-note">
|
||||
Kelola data kontrakan dan booking dengan cepat melalui menu sidebar.
|
||||
</div>
|
||||
<div class="profile-note">
|
||||
Gunakan menu dropdown kanan atas untuk logout atau kembali ke halaman user.
|
||||
</div>
|
||||
<div class="profile-row">
|
||||
<span>Role</span>
|
||||
<strong>{{ $user->getRoleLabel() }}</strong>
|
||||
</div>
|
||||
<div class="profile-row">
|
||||
<span>Status</span>
|
||||
<strong>Aktif</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-width: 720px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
|
|
@ -125,6 +125,23 @@
|
|||
.auth-footer a:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.btn-register-link {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
padding: 10px 16px;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 8px;
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-register-link:hover {
|
||||
background: #667eea;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
@ -145,6 +162,13 @@
|
|||
</div>
|
||||
|
||||
<div class="auth-body">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
|
||||
|
|
@ -172,7 +196,8 @@
|
|||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>Belum punya akun bisnis? Hubungi super admin untuk pembuatan akun.</p>
|
||||
<p>Belum punya akun bisnis?</p>
|
||||
<a class="btn-register-link" href="/admin/register" onclick="window.location.href='/admin/register'; return false;">Daftar di sini</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-width: 720px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,17 @@
|
|||
<!-- Load Skeleton Loader Script -->
|
||||
@include('components.skeleton-script')
|
||||
|
||||
<div class="container-fluid px-2 px-md-4">
|
||||
@php
|
||||
$isSuperAdmin = Auth::user()->role === 'super_admin';
|
||||
@endphp
|
||||
|
||||
<div class="container-fluid px-2 px-md-4 dashboard-shell">
|
||||
<style>
|
||||
.dashboard-shell {
|
||||
background: radial-gradient(1200px 400px at 10% -10%, rgba(102, 126, 234, 0.12), transparent 60%),
|
||||
radial-gradient(1000px 360px at 90% -15%, rgba(118, 75, 162, 0.12), transparent 60%);
|
||||
}
|
||||
|
||||
.header-dashboard {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
|
|
@ -52,6 +61,58 @@
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.25rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.meta-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.85rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
margin-top: 1.25rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.action-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-chip:hover {
|
||||
transform: translateY(-2px);
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
color: white;
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: none !important;
|
||||
|
|
@ -60,6 +121,138 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.mini-metric {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.12), rgba(118, 75, 162, 0.08));
|
||||
border: 1px solid rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.mini-metric span {
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-metric strong {
|
||||
font-size: 1.35rem;
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.insight-card {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.insight-card h6 {
|
||||
font-weight: 700;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.insight-list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.insight-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.insight-item i {
|
||||
color: #667eea;
|
||||
font-size: 1.1rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.metric-card h6 {
|
||||
font-weight: 700;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(248, 250, 252, 0.95);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.metric-row span {
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-row strong {
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.progress-rail {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(226, 232, 240, 0.9);
|
||||
overflow: hidden;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.rank-list {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.rank-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.7rem 0.9rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(248, 250, 252, 0.95);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.rank-item small {
|
||||
color: #64748b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
|
@ -232,12 +425,29 @@
|
|||
<div class="header-dashboard">
|
||||
<h2 class="mb-2">📊 Dashboard Analytics</h2>
|
||||
<p class="mb-0">Selamat datang, <strong>{{ Auth::user()->name }}!</strong> 👋</p>
|
||||
<div class="header-meta">
|
||||
<div class="meta-chip"><i class="bi bi-calendar-event"></i>{{ now()->format('d M Y') }}</div>
|
||||
<div class="meta-chip"><i class="bi bi-building"></i>{{ $jumlahKontrakan }} kontrakan</div>
|
||||
@if($isSuperAdmin)
|
||||
<div class="meta-chip"><i class="bi bi-droplet"></i>{{ $jumlahLaundry }} laundry</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a href="{{ route('kontrakan.index') }}" class="action-chip"><i class="bi bi-building"></i>Kontrakan</a>
|
||||
<a href="{{ route('admin.bookings.index') }}" class="action-chip"><i class="bi bi-calendar-check"></i>Booking</a>
|
||||
<a href="{{ route('admin.profile') }}" class="action-chip"><i class="bi bi-person-circle"></i>Profil</a>
|
||||
@if($isSuperAdmin)
|
||||
<a href="{{ route('laundry.index') }}" class="action-chip"><i class="bi bi-water"></i>Laundry</a>
|
||||
<a href="{{ route('kriteria.index') }}" class="action-chip"><i class="bi bi-list-check"></i>Kriteria</a>
|
||||
<a href="{{ route('saw.index') }}" class="action-chip"><i class="bi bi-calculator"></i>Analisis</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row g-3 g-md-4 mb-4 mb-md-5" id="statsContainer">
|
||||
<!-- Card Kontrakan - Purple Theme -->
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="{{ $isSuperAdmin ? 'col-6 col-lg-3' : 'col-12 col-lg-6' }}">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none !important; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.35); position: relative; z-index: 1;">
|
||||
<div class="card-body text-white p-3 p-md-4" style="position: relative; z-index: 2;">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
|
|
@ -260,6 +470,33 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@if(!$isSuperAdmin)
|
||||
<!-- Card Booking - Blue Theme -->
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #38bdf8 0%, #2563eb 100%); border: none !important; box-shadow: 0 8px 25px rgba(37, 99, 235, 0.25); position: relative; z-index: 1;">
|
||||
<div class="card-body text-white p-3 p-md-4" style="position: relative; z-index: 2;">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div class="flex-grow-1">
|
||||
<p class="mb-2 opacity-75 small fw-semibold"><i class="bi bi-calendar-check me-1"></i>Total Booking</p>
|
||||
<h2 class="mb-2 mb-md-3 fw-bold display-6">{{ $totalBookings }}</h2>
|
||||
<p class="mb-0 opacity-85 small d-none d-md-block">
|
||||
<i class="bi bi-clock me-1"></i>Update real-time
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-circle p-2 p-md-3 d-flex align-items-center justify-content-center d-none d-sm-flex" style="min-width: 50px; min-height: 50px; background: rgba(255,255,255,0.25);">
|
||||
<i class="bi bi-calendar2-week" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('admin.bookings.index') }}" class="btn btn-light btn-sm w-100 fw-semibold d-none d-md-block" style="position: relative; z-index: 3; color: #1d4ed8;">
|
||||
<i class="bi bi-arrow-right me-2"></i>Kelola Booking
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
@if($isSuperAdmin)
|
||||
<!-- Card Laundry - Purple Theme -->
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #818cf8 0%, #667eea 100%); border: none !important; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.35); position: relative; z-index: 1;">
|
||||
|
|
@ -284,6 +521,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
@if($isSuperAdmin)
|
||||
<!-- Card Kriteria -->
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 50%, #4338ca 100%); border: none !important; box-shadow: 0 8px 25px rgba(99, 102, 241, 0.3); position: relative; z-index: 1;">
|
||||
|
|
@ -307,7 +547,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($isSuperAdmin)
|
||||
<!-- Card SAW - Amber/Gold Tone -->
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 50%, #b45309 100%); border: none !important; box-shadow: 0 8px 25px rgba(217, 119, 6, 0.35); position: relative; z-index: 1;">
|
||||
|
|
@ -331,50 +573,70 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Charts Section -->
|
||||
<!-- Snapshot Section -->
|
||||
<div class="row g-3 g-md-4 mb-4 mb-md-5">
|
||||
<!-- Bar Chart: Perbandingan Harga -->
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card chart-card h-100">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-bold" style="color: #667eea;">
|
||||
<i class="bi bi-bar-chart-fill me-2"></i>
|
||||
<span class="d-none d-md-inline">Perbandingan Harga (Top 5)</span>
|
||||
<span class="d-md-none">Harga (Top 5)</span>
|
||||
</h5>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="glass-panel p-4 h-100">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="fw-bold mb-0" style="color: #4f46e5;">Ringkasan Aktivitas</h5>
|
||||
<span class="badge" style="background: rgba(102, 126, 234, 0.12); color: #4f46e5;">Update Hari Ini</span>
|
||||
</div>
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<canvas id="hargaChart" height="80"></canvas>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="mini-metric">
|
||||
<span>Kontrakan Aktif</span>
|
||||
<strong>{{ $jumlahKontrakan }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mini-metric">
|
||||
<span>Booking Masuk</span>
|
||||
<strong>{{ $totalBookings ?? 0 }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@if($isSuperAdmin)
|
||||
<div class="col-md-6">
|
||||
<div class="mini-metric">
|
||||
<span>Laundry Terdaftar</span>
|
||||
<strong>{{ $jumlahLaundry }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mini-metric">
|
||||
<span>Kriteria Aktif</span>
|
||||
<strong>{{ $jumlahKriteria }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Donut Charts: Distribusi Jarak -->
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card chart-card h-100">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-bold" style="color: #667eea;">
|
||||
<i class="bi bi-pie-chart-fill me-2"></i>
|
||||
<span class="d-none d-md-inline">Distribusi Jarak</span>
|
||||
<span class="d-md-none">Jarak</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<canvas id="jarakChart" height="200"></canvas>
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 py-2 border-bottom">
|
||||
<span class="small"><span class="badge" style="background-color: #198754;">●</span> <span class="d-none d-sm-inline">Dekat (≤500m)</span><span class="d-sm-none">Dekat</span></span>
|
||||
<strong style="color: #667eea;">{{ $jarakKontrakan['dekat'] + $jarakLaundry['dekat'] }}</strong>
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="insight-card p-4 h-100">
|
||||
<h6 class="mb-3">Insight Cepat</h6>
|
||||
<div class="insight-list">
|
||||
<div class="insight-item">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">Dominasi lokasi</div>
|
||||
<small class="text-muted">{{ $jarakKontrakan['dekat'] }} lokasi dekat kampus.</small>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 py-2 border-bottom">
|
||||
<span class="small"><span class="badge" style="background-color: #ffc107;">●</span> <span class="d-none d-sm-inline">Sedang (501-1000m)</span><span class="d-sm-none">Sedang</span></span>
|
||||
<strong style="color: #667eea;">{{ $jarakKontrakan['sedang'] + $jarakLaundry['sedang'] }}</strong>
|
||||
</div>
|
||||
<div class="insight-item">
|
||||
<i class="bi bi-cash-stack"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">Harga rata-rata</div>
|
||||
<small class="text-muted">Rp {{ number_format($avgHargaKontrakan, 0, ',', '.') }} per bulan.</small>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center py-2">
|
||||
<span class="small"><span class="badge" style="background-color: #dc3545;">●</span> <span class="d-none d-sm-inline">Jauh (>1000m)</span><span class="d-sm-none">Jauh</span></span>
|
||||
<strong style="color: #667eea;">{{ $jarakKontrakan['jauh'] + $jarakLaundry['jauh'] }}</strong>
|
||||
</div>
|
||||
<div class="insight-item">
|
||||
<i class="bi bi-people"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">Admin aktif</div>
|
||||
<small class="text-muted">{{ $totalAdmins ?? 1 }} akun admin tersedia.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -382,24 +644,146 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Line Chart: Trend Data -->
|
||||
<!-- Compact Visual Widgets -->
|
||||
<div class="row g-3 g-md-4 mb-4 mb-md-5">
|
||||
<div class="col-12">
|
||||
<div class="card chart-card">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-bold" style="color: #667eea;">
|
||||
<i class="bi bi-graph-up me-2"></i>
|
||||
<span class="d-none d-md-inline">Trend Data (6 Bulan Terakhir)</span>
|
||||
<span class="d-md-none">Trend Data</span>
|
||||
</h5>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Ringkasan Harga Kontrakan</h6>
|
||||
<div class="metric-grid">
|
||||
<div class="metric-row">
|
||||
<span>Harga Terendah</span>
|
||||
<strong>Rp {{ number_format($minHargaKontrakan, 0, ',', '.') }}</strong>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span>Harga Rata-rata</span>
|
||||
<strong>Rp {{ number_format($avgHargaKontrakan, 0, ',', '.') }}</strong>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span>Harga Tertinggi</span>
|
||||
<strong>Rp {{ number_format($maxHargaKontrakan, 0, ',', '.') }}</strong>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span>Rata-rata Jarak</span>
|
||||
<strong>{{ number_format($avgJarakKontrakan, 0, ',', '.') }} m</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<canvas id="trendChart" height="60"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Distribusi Jarak Kontrakan</h6>
|
||||
@php
|
||||
$totalJarakKontrakan = max(1, $jarakKontrakan['dekat'] + $jarakKontrakan['sedang'] + $jarakKontrakan['jauh']);
|
||||
@endphp
|
||||
<div class="metric-grid">
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Dekat (≤500m)</span>
|
||||
<strong>{{ $jarakKontrakan['dekat'] }}</strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: {{ ($jarakKontrakan['dekat'] / $totalJarakKontrakan) * 100 }}%; background: #22c55e;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Sedang (501-1000m)</span>
|
||||
<strong>{{ $jarakKontrakan['sedang'] }}</strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: {{ ($jarakKontrakan['sedang'] / $totalJarakKontrakan) * 100 }}%; background: #f59e0b;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Jauh (>1000m)</span>
|
||||
<strong>{{ $jarakKontrakan['jauh'] }}</strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: {{ ($jarakKontrakan['jauh'] / $totalJarakKontrakan) * 100 }}%; background: #ef4444;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Kontrakan Harga Tertinggi</h6>
|
||||
<div class="rank-list">
|
||||
@foreach($topKontrakan as $kontrakan)
|
||||
<div class="rank-item">
|
||||
<div>
|
||||
<div class="fw-semibold">{{ $kontrakan->nama }}</div>
|
||||
<small>{{ $kontrakan->jumlah_kamar ?? 0 }} kamar • {{ $kontrakan->jarak ?? 0 }} m</small>
|
||||
</div>
|
||||
<strong>Rp {{ number_format($kontrakan->harga, 0, ',', '.') }}</strong>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($isSuperAdmin)
|
||||
<div class="row g-3 g-md-4 mb-4 mb-md-5">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Laundry Harga Terendah</h6>
|
||||
<div class="rank-list">
|
||||
@foreach($hargaLaundry as $laundry)
|
||||
<div class="rank-item">
|
||||
<div>
|
||||
<div class="fw-semibold">{{ $laundry->nama }}</div>
|
||||
<small>Harga mulai</small>
|
||||
</div>
|
||||
<strong>Rp {{ number_format($laundry->harga, 0, ',', '.') }}</strong>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Distribusi Jarak Laundry</h6>
|
||||
@php
|
||||
$totalJarakLaundry = max(1, $jarakLaundry['dekat'] + $jarakLaundry['sedang'] + $jarakLaundry['jauh']);
|
||||
@endphp
|
||||
<div class="metric-grid">
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Dekat (≤500m)</span>
|
||||
<strong>{{ $jarakLaundry['dekat'] }}</strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: {{ ($jarakLaundry['dekat'] / $totalJarakLaundry) * 100 }}%; background: #22c55e;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Sedang (501-1000m)</span>
|
||||
<strong>{{ $jarakLaundry['sedang'] }}</strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: {{ ($jarakLaundry['sedang'] / $totalJarakLaundry) * 100 }}%; background: #f59e0b;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Jauh (>1000m)</span>
|
||||
<strong>{{ $jarakLaundry['jauh'] }}</strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: {{ ($jarakLaundry['jauh'] / $totalJarakLaundry) * 100 }}%; background: #ef4444;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
|
|
@ -410,6 +794,7 @@
|
|||
Quick Actions
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
@if($isSuperAdmin)
|
||||
<div class="col-md-3">
|
||||
<a href="{{ route('kontrakan.create') }}" class="btn btn-admin-outline w-100 py-3">
|
||||
<i class="bi bi-building me-2"></i> Tambah Kontrakan
|
||||
|
|
@ -430,6 +815,23 @@
|
|||
<i class="bi bi-calculator me-2"></i> Sistem Analisis
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="col-12 col-md-4">
|
||||
<a href="{{ route('kontrakan.create') }}" class="btn btn-admin-outline w-100 py-3">
|
||||
<i class="bi bi-building me-2"></i> Tambah Kontrakan
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<a href="{{ route('admin.bookings.index') }}" class="btn btn-admin-outline w-100 py-3">
|
||||
<i class="bi bi-calendar-check me-2"></i> Kelola Booking
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<a href="{{ route('admin.profile') }}" class="btn btn-admin-solid w-100 py-3">
|
||||
<i class="bi bi-person-circle me-2"></i> Profil Admin
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -438,21 +840,22 @@
|
|||
|
||||
<!-- Additional Stats Section -->
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- Activity Stats -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card border-0 shadow-sm" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px;">
|
||||
@if(!$isSuperAdmin)
|
||||
<!-- System Status -->
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm" style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border-radius: 12px;">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="mb-2 opacity-75 small fw-semibold">Total Review</p>
|
||||
<h3 class="fw-bold mb-0">{{ $totalReviews ?? 0 }}</h3>
|
||||
<p class="mb-2 opacity-75 small fw-semibold">System Status</p>
|
||||
<h3 class="fw-bold mb-0"><i class="bi bi-check-circle-fill"></i> Aktif</h3>
|
||||
</div>
|
||||
<div style="font-size: 2.5rem; opacity: 0.3;">⭐</div>
|
||||
<div style="font-size: 2.5rem; opacity: 0.3;">✓</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@else
|
||||
<!-- System Status -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card border-0 shadow-sm" style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border-radius: 12px;">
|
||||
|
|
@ -467,7 +870,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($isSuperAdmin)
|
||||
<!-- Last Backup -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card border-0 shadow-sm" style="background: linear-gradient(135deg, #17a2b8 0%, #20c997 100%); color: white; border-radius: 12px;">
|
||||
|
|
@ -497,12 +900,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Recent Data Tables -->
|
||||
<div class="row g-4">
|
||||
<!-- Recent Kontrakan -->
|
||||
<div class="col-lg-6">
|
||||
<div class="{{ $isSuperAdmin ? 'col-lg-6' : 'col-12' }}">
|
||||
<div class="card chart-card">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-bold" style="color: #667eea;">
|
||||
|
|
@ -557,6 +962,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@if($isSuperAdmin)
|
||||
<!-- Recent Laundry -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card chart-card">
|
||||
|
|
@ -626,6 +1032,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -706,6 +1113,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@if($isSuperAdmin)
|
||||
<!-- Modal Laundry -->
|
||||
<div class="modal fade" id="laundryModal" tabindex="-1" aria-labelledby="laundryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
||||
|
|
@ -950,9 +1358,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart.js Library -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
@endif
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
@ -990,133 +1396,6 @@
|
|||
updateCounter();
|
||||
});
|
||||
|
||||
// ========== BAR CHART: PERBANDINGAN HARGA ==========
|
||||
const hargaKontrakan = @json($hargaKontrakan);
|
||||
const hargaLaundry = @json($hargaLaundry);
|
||||
|
||||
new Chart(document.getElementById('hargaChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: hargaKontrakan.map(k => k.nama),
|
||||
datasets: [{
|
||||
label: 'Kontrakan',
|
||||
data: hargaKontrakan.map(k => k.harga),
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.8)',
|
||||
borderColor: 'rgba(102, 126, 234, 1)',
|
||||
borderWidth: 2
|
||||
}, {
|
||||
label: 'Laundry',
|
||||
data: hargaLaundry.map(l => l.harga),
|
||||
backgroundColor: 'rgba(240, 147, 251, 0.8)',
|
||||
borderColor: 'rgba(240, 147, 251, 1)',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.dataset.label + ': Rp ' + context.parsed.y.toLocaleString('id-ID');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return 'Rp ' + (value/1000) + 'k';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ========== DONUT CHART: DISTRIBUSI JARAK ==========
|
||||
const jarakData = @json($jarakKontrakan);
|
||||
const totalDekat = jarakData.dekat + @json($jarakLaundry['dekat']);
|
||||
const totalSedang = jarakData.sedang + @json($jarakLaundry['sedang']);
|
||||
const totalJauh = jarakData.jauh + @json($jarakLaundry['jauh']);
|
||||
|
||||
new Chart(document.getElementById('jarakChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Dekat (≤500m)', 'Sedang (501-1000m)', 'Jauh (>1000m)'],
|
||||
datasets: [{
|
||||
data: [totalDekat, totalSedang, totalJauh],
|
||||
backgroundColor: [
|
||||
'rgba(25, 135, 84, 0.8)',
|
||||
'rgba(255, 193, 7, 0.8)',
|
||||
'rgba(220, 53, 69, 0.8)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(25, 135, 84, 1)',
|
||||
'rgba(255, 193, 7, 1)',
|
||||
'rgba(220, 53, 69, 1)'
|
||||
],
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ========== LINE CHART: TREND DATA ==========
|
||||
const monthlyData = @json($monthlyData);
|
||||
|
||||
new Chart(document.getElementById('trendChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: monthlyData.map(m => m.month),
|
||||
datasets: [{
|
||||
label: 'Kontrakan',
|
||||
data: monthlyData.map(m => m.kontrakan),
|
||||
borderColor: 'rgba(102, 126, 234, 1)',
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}, {
|
||||
label: 'Laundry',
|
||||
data: monthlyData.map(m => m.laundry),
|
||||
borderColor: 'rgba(240, 147, 251, 1)',
|
||||
backgroundColor: 'rgba(240, 147, 251, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
use App\Http\Controllers\Api\BookingController;
|
||||
use App\Http\Controllers\Api\ReviewController;
|
||||
use App\Http\Controllers\Api\FavoriteController;
|
||||
use App\Http\Controllers\Api\DeviceTokenController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -181,6 +182,10 @@
|
|||
Route::delete('/{id}', [ReviewController::class, 'destroy']);
|
||||
});
|
||||
|
||||
// Device Token Routes (Admin push notifications)
|
||||
Route::post('/device-tokens', [DeviceTokenController::class, 'store']);
|
||||
Route::delete('/device-tokens', [DeviceTokenController::class, 'destroy']);
|
||||
|
||||
// Favorite Routes
|
||||
Route::prefix('favorites')->group(function () {
|
||||
Route::get('/', [FavoriteController::class, 'index']);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,39 @@
|
|||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::command('bookings:auto-verify')
|
||||
->dailyAt('00:00')
|
||||
->description('Auto-verify bookings yang sudah payment_paid dan check-in date tiba')
|
||||
->onSuccess(function () {
|
||||
Log::info('✅ Auto-verify bookings scheduler selesai dijalankan');
|
||||
})
|
||||
->onFailure(function () {
|
||||
Log::error('❌ Auto-verify bookings scheduler gagal');
|
||||
});
|
||||
|
||||
Schedule::command('database:backup')
|
||||
->weeklyOn(0, '01:00')
|
||||
->description('Auto backup database mingguan untuk super admin')
|
||||
->onSuccess(function () {
|
||||
Log::info('✅ Weekly database backup scheduler selesai dijalankan');
|
||||
})
|
||||
->onFailure(function () {
|
||||
Log::error('❌ Weekly database backup scheduler gagal');
|
||||
});
|
||||
|
||||
Schedule::command('kontrakan:weekly-availability-reminder')
|
||||
->weeklyOn(1, '09:00')
|
||||
->description('Reminder mingguan untuk konfirmasi ketersediaan kontrakan')
|
||||
->onSuccess(function () {
|
||||
Log::info('✅ Weekly kontrakan availability reminder selesai dijalankan');
|
||||
})
|
||||
->onFailure(function () {
|
||||
Log::error('❌ Weekly kontrakan availability reminder gagal');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@
|
|||
// -------------------------------------------------
|
||||
Route::get('/admin/login', [AdminAuthController::class, 'loginPage'])->name('admin.login');
|
||||
Route::post('/admin/login', [AdminAuthController::class, 'login'])->middleware('throttle:10,1')->name('admin.login.post');
|
||||
Route::get('/admin/register', [AdminAuthController::class, 'registerPage'])->name('admin.register');
|
||||
Route::post('/admin/register', [AdminAuthController::class, 'register'])->middleware('throttle:10,1')->name('admin.register.post');
|
||||
|
||||
// -------------------------------------------------
|
||||
// ROUTE ADMIN TERPROTEKSI LOGIN
|
||||
|
|
@ -42,6 +44,10 @@
|
|||
// Dashboard
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
// Admin Profile
|
||||
Route::get('/admin/profile', [\App\Http\Controllers\AdminProfileController::class, 'edit'])->name('admin.profile');
|
||||
Route::put('/admin/profile', [\App\Http\Controllers\AdminProfileController::class, 'update'])->name('admin.profile.update');
|
||||
|
||||
// ========== 🆕 CLEAR DASHBOARD CACHE (BARU!) ==========
|
||||
Route::post('/dashboard/clear-cache', [DashboardController::class, 'clearCache'])->name('dashboard.clear-cache');
|
||||
|
||||
|
|
@ -175,9 +181,3 @@
|
|||
|
||||
Route::post('/admin/logout', [AdminAuthController::class, 'logout'])->name('admin.logout');
|
||||
});
|
||||
|
||||
// Register admin hanya untuk super admin yang sudah login
|
||||
Route::middleware(['auth', 'role:super_admin'])->group(function () {
|
||||
Route::get('/admin/register', [AdminAuthController::class, 'registerPage'])->name('admin.register');
|
||||
Route::post('/admin/register', [AdminAuthController::class, 'register'])->name('admin.register.post');
|
||||
});
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Daftar Sebagai Pemilik - SPK Kontrakan & Laundry</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-header h2 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
margin: 8px 0 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-body {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.btn-register {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-register:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
padding: 20px 30px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.auth-footer p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.auth-footer a:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h2><i class="fas fa-store"></i> Daftar Sebagai Pemilik</h2>
|
||||
<p>Bergabung untuk mengelola bisnis Anda</p>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="alert alert-info mx-3 mt-3 mb-0">
|
||||
<h6 class="fw-bold mb-2">🏢 Keuntungan Bergabung:</h6>
|
||||
<ul class="small mb-0 ps-3">
|
||||
<li>✅ Kelola data kontrakan/laundry Anda</li>
|
||||
<li>✅ Terima pelanggan dari sistem SPK</li>
|
||||
<li>✅ Update informasi bisnis kapan saja</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="auth-body">
|
||||
<?php if($errors->any()): ?>
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
<strong>Terjadi Kesalahan:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<?php $__currentLoopData = $errors->all(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $error): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<li><?php echo e($error); ?></li>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</ul>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="<?php echo e(route('admin.register.post')); ?>">
|
||||
<?php echo csrf_field(); ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label"><i class="fas fa-user"></i> Nama Lengkap</label>
|
||||
<input type="text" id="name" name="name" class="form-control" placeholder="Masukkan nama Anda" value="<?php echo e(old('name')); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email" class="form-label"><i class="fas fa-envelope"></i> Email</label>
|
||||
<input type="email" id="email" name="email" class="form-control" placeholder="Masukkan email Anda" value="<?php echo e(old('email')); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password" class="form-label"><i class="fas fa-lock"></i> Password</label>
|
||||
<input type="password" id="password" name="password" class="form-control" placeholder="Buat password yang kuat" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password_confirmation" class="form-label"><i class="fas fa-check-circle"></i> Konfirmasi Password</label>
|
||||
<input type="password" id="password_confirmation" name="password_confirmation" class="form-control" placeholder="Ulangi password Anda" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-register">
|
||||
<i class="fas fa-check me-2"></i>Daftar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>Sudah punya akun? <a href="<?php echo e(route('admin.login')); ?>">Login di sini</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<?php /**PATH C:\laragon\www\TA\spk_kontrakan\resources\views/auth/admin-register.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
|
||||
|
||||
<?php $__env->startSection('title', 'Profil Admin'); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="container-fluid px-2 px-md-4">
|
||||
<style>
|
||||
.profile-hero {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.95) 0%, rgba(118, 75, 162, 0.95) 100%);
|
||||
color: white;
|
||||
border-radius: 18px;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 16px 32px rgba(102, 126, 234, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.profile-hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: -40px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 40px;
|
||||
transform: rotate(18deg);
|
||||
}
|
||||
|
||||
.profile-hero-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.profile-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.85rem;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 999px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.profile-card .card-header {
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
border-bottom: 2px solid rgba(102, 126, 234, 0.2);
|
||||
font-weight: 700;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.profile-list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.profile-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.85rem 1rem;
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.profile-row span {
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-row strong {
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.profile-note {
|
||||
background: rgba(14, 116, 144, 0.08);
|
||||
border-left: 4px solid #0ea5e9;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 12px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-hero {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.profile-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.profile-row strong {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="profile-hero">
|
||||
<div class="profile-hero-main">
|
||||
<div class="profile-avatar"><?php echo e(strtoupper(substr($user->name, 0, 1))); ?></div>
|
||||
<div>
|
||||
<h3 class="mb-1 fw-bold"><?php echo e($user->name); ?></h3>
|
||||
<div class="opacity-75"><?php echo e($user->email); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-meta">
|
||||
<div class="profile-chip"><i class="bi bi-briefcase"></i><?php echo e($user->getRoleLabel()); ?></div>
|
||||
<div class="profile-chip"><i class="bi bi-calendar-event"></i><?php echo e(now()->format('d M Y')); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="card profile-card h-100">
|
||||
<div class="card-header py-3 px-4">Perbarui Profil</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="<?php echo e(route('admin.profile.update')); ?>" class="d-grid gap-3">
|
||||
<?php echo csrf_field(); ?>
|
||||
<?php echo method_field('PUT'); ?>
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Nama Lengkap</label>
|
||||
<input type="text" name="name" class="form-control" value="<?php echo e(old('name', $user->name)); ?>" required>
|
||||
<?php $__errorArgs = ['name'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<small class="text-danger"><?php echo e($message); ?></small>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Email</label>
|
||||
<input type="email" name="email" class="form-control" value="<?php echo e(old('email', $user->email)); ?>" required>
|
||||
<?php $__errorArgs = ['email'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<small class="text-danger"><?php echo e($message); ?></small>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Nomor Telepon</label>
|
||||
<input type="text" name="phone" class="form-control" value="<?php echo e(old('phone', $user->phone)); ?>" placeholder="Opsional">
|
||||
<?php $__errorArgs = ['phone'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<small class="text-danger"><?php echo e($message); ?></small>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Password Baru</label>
|
||||
<input type="password" name="password" class="form-control" placeholder="Kosongkan jika tidak mengganti">
|
||||
<?php $__errorArgs = ['password'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<small class="text-danger"><?php echo e($message); ?></small>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Konfirmasi Password</label>
|
||||
<input type="password" name="password_confirmation" class="form-control" placeholder="Ulangi password baru">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-admin-solid w-100 py-3">Simpan Perubahan</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="card profile-card h-100">
|
||||
<div class="card-header py-3 px-4">Ringkasan</div>
|
||||
<div class="card-body p-4 d-flex flex-column gap-3">
|
||||
<div class="profile-note">
|
||||
Kelola data kontrakan dan booking dengan cepat melalui menu sidebar.
|
||||
</div>
|
||||
<div class="profile-note">
|
||||
Gunakan menu dropdown kanan atas untuk logout atau kembali ke halaman user.
|
||||
</div>
|
||||
<div class="profile-row">
|
||||
<span>Role</span>
|
||||
<strong><?php echo e($user->getRoleLabel()); ?></strong>
|
||||
</div>
|
||||
<div class="profile-row">
|
||||
<span>Status</span>
|
||||
<strong>Aktif</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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/profile.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
|
||||
|
||||
<?php $__env->startSection('title', 'Halaman Tidak Ditemukan'); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="container-fluid px-4">
|
||||
<div class="row align-items-center justify-content-center min-vh-100">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="text-center">
|
||||
<div class="mb-4">
|
||||
<h1 class="display-1 fw-bold" style="color: #f5576c;">404</h1>
|
||||
<p class="fs-4 fw-semibold mb-2">Halaman Tidak Ditemukan</p>
|
||||
<p class="text-muted mb-4">
|
||||
Maaf, halaman yang Anda cari tidak ditemukan.
|
||||
Mungkin URL sudah berubah atau halaman telah dihapus.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 justify-content-center">
|
||||
<a href="<?php echo e(route('dashboard')); ?>" class="btn btn-primary">
|
||||
<i class="bi bi-house me-2"></i>Kembali ke Dashboard
|
||||
</a>
|
||||
<a href="javascript:history.back()" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Halaman Sebelumnya
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-search me-1"></i>
|
||||
Path: <?php echo e(request()->getPathInfo()); ?>
|
||||
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.display-1 {
|
||||
font-size: 5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php echo $__env->make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\TA\spk_kontrakan\resources\views/errors/404.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php $__env->startSection('title', __('Page Expired')); ?>
|
||||
<?php $__env->startSection('code', '419'); ?>
|
||||
<?php $__env->startSection('message', __('Page Expired')); ?>
|
||||
|
||||
<?php echo $__env->make('errors::minimal', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\TA\spk_kontrakan\vendor\laravel\framework\src\Illuminate\Foundation\Exceptions/views/419.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
<?php $__env->startSection('title', 'Tambah User'); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="container-fluid px-4">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb bg-transparent p-0">
|
||||
<li class="breadcrumb-item"><a href="<?php echo e(route('dashboard')); ?>">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="<?php echo e(route('admin.users.index')); ?>">Kelola User</a></li>
|
||||
<li class="breadcrumb-item active">Tambah User</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Form Card -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-person-plus-fill me-2" style="color: #667eea;"></i>Tambah User Baru
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
<form action="<?php echo e(route('admin.users.store')); ?>" method="POST">
|
||||
<?php echo csrf_field(); ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="name" class="form-label fw-semibold">
|
||||
<i class="bi bi-person me-2"></i>Nama User
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control <?php $__errorArgs = ['name'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Masukkan nama lengkap"
|
||||
value="<?php echo e(old('name')); ?>"
|
||||
>
|
||||
<?php $__errorArgs = ['name'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<div class="invalid-feedback d-block"><?php echo e($message); ?></div>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="email" class="form-label fw-semibold">
|
||||
<i class="bi bi-envelope me-2"></i>Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control <?php $__errorArgs = ['email'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="user@example.com"
|
||||
value="<?php echo e(old('email')); ?>"
|
||||
>
|
||||
<?php $__errorArgs = ['email'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<div class="invalid-feedback d-block"><?php echo e($message); ?></div>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="role" class="form-label fw-semibold">
|
||||
<i class="bi bi-shield me-2"></i>Role
|
||||
</label>
|
||||
<select class="form-select <?php $__errorArgs = ['role'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>" id="role" name="role">
|
||||
<option value="">-- Pilih Role --</option>
|
||||
<option value="super_admin" <?php echo e(old('role') == 'super_admin' ? 'selected' : ''); ?>>Super Admin</option>
|
||||
<option value="admin" <?php echo e(old('role') == 'admin' ? 'selected' : ''); ?>>Admin</option>
|
||||
<option value="user" <?php echo e(old('role') == 'user' ? 'selected' : ''); ?>>Mahasiswa</option>
|
||||
</select>
|
||||
<?php $__errorArgs = ['role'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<div class="invalid-feedback d-block"><?php echo e($message); ?></div>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
<small class="text-muted d-block mt-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Super Admin:</strong> Akses penuh ke semua fitur | <strong>Admin:</strong> Kelola konten | <strong>Mahasiswa:</strong> User umum aplikasi mobile
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label fw-semibold">
|
||||
<i class="bi bi-lock me-2"></i>Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control <?php $__errorArgs = ['password'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Minimal 8 karakter"
|
||||
>
|
||||
<?php $__errorArgs = ['password'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<div class="invalid-feedback d-block"><?php echo e($message); ?></div>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password_confirmation" class="form-label fw-semibold">
|
||||
<i class="bi bi-lock-check me-2"></i>Konfirmasi Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
placeholder="Ulangi password"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary flex-fill">
|
||||
<i class="bi bi-check-circle me-2"></i>Simpan User
|
||||
</button>
|
||||
<a href="<?php echo e(route('admin.users.index')); ?>" class="btn btn-outline-secondary flex-fill">
|
||||
<i class="bi bi-arrow-left me-2"></i>Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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/create.blade.php ENDPATH**/ ?>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -6,8 +6,17 @@
|
|||
<!-- Load Skeleton Loader Script -->
|
||||
<?php echo $__env->make('components.skeleton-script', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
|
||||
|
||||
<div class="container-fluid px-2 px-md-4">
|
||||
<?php
|
||||
$isSuperAdmin = Auth::user()->role === 'super_admin';
|
||||
?>
|
||||
|
||||
<div class="container-fluid px-2 px-md-4 dashboard-shell">
|
||||
<style>
|
||||
.dashboard-shell {
|
||||
background: radial-gradient(1200px 400px at 10% -10%, rgba(102, 126, 234, 0.12), transparent 60%),
|
||||
radial-gradient(1000px 360px at 90% -15%, rgba(118, 75, 162, 0.12), transparent 60%);
|
||||
}
|
||||
|
||||
.header-dashboard {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
|
|
@ -52,6 +61,58 @@
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.25rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.meta-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.85rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
margin-top: 1.25rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.action-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-chip:hover {
|
||||
transform: translateY(-2px);
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
color: white;
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: none !important;
|
||||
|
|
@ -60,6 +121,138 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.mini-metric {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.12), rgba(118, 75, 162, 0.08));
|
||||
border: 1px solid rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.mini-metric span {
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-metric strong {
|
||||
font-size: 1.35rem;
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.insight-card {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.insight-card h6 {
|
||||
font-weight: 700;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.insight-list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.insight-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.insight-item i {
|
||||
color: #667eea;
|
||||
font-size: 1.1rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.metric-card h6 {
|
||||
font-weight: 700;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(248, 250, 252, 0.95);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.metric-row span {
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-row strong {
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.progress-rail {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(226, 232, 240, 0.9);
|
||||
overflow: hidden;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.rank-list {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.rank-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.7rem 0.9rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(248, 250, 252, 0.95);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.rank-item small {
|
||||
color: #64748b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
|
@ -232,12 +425,29 @@
|
|||
<div class="header-dashboard">
|
||||
<h2 class="mb-2">📊 Dashboard Analytics</h2>
|
||||
<p class="mb-0">Selamat datang, <strong><?php echo e(Auth::user()->name); ?>!</strong> 👋</p>
|
||||
<div class="header-meta">
|
||||
<div class="meta-chip"><i class="bi bi-calendar-event"></i><?php echo e(now()->format('d M Y')); ?></div>
|
||||
<div class="meta-chip"><i class="bi bi-building"></i><?php echo e($jumlahKontrakan); ?> kontrakan</div>
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<div class="meta-chip"><i class="bi bi-droplet"></i><?php echo e($jumlahLaundry); ?> laundry</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a href="<?php echo e(route('kontrakan.index')); ?>" class="action-chip"><i class="bi bi-building"></i>Kontrakan</a>
|
||||
<a href="<?php echo e(route('admin.bookings.index')); ?>" class="action-chip"><i class="bi bi-calendar-check"></i>Booking</a>
|
||||
<a href="<?php echo e(route('admin.profile')); ?>" class="action-chip"><i class="bi bi-person-circle"></i>Profil</a>
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<a href="<?php echo e(route('laundry.index')); ?>" class="action-chip"><i class="bi bi-water"></i>Laundry</a>
|
||||
<a href="<?php echo e(route('kriteria.index')); ?>" class="action-chip"><i class="bi bi-list-check"></i>Kriteria</a>
|
||||
<a href="<?php echo e(route('saw.index')); ?>" class="action-chip"><i class="bi bi-calculator"></i>Analisis</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row g-3 g-md-4 mb-4 mb-md-5" id="statsContainer">
|
||||
<!-- Card Kontrakan - Purple Theme -->
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="<?php echo e($isSuperAdmin ? 'col-6 col-lg-3' : 'col-12 col-lg-6'); ?>">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none !important; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.35); position: relative; z-index: 1;">
|
||||
<div class="card-body text-white p-3 p-md-4" style="position: relative; z-index: 2;">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
|
|
@ -261,6 +471,33 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<?php if(!$isSuperAdmin): ?>
|
||||
<!-- Card Booking - Blue Theme -->
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #38bdf8 0%, #2563eb 100%); border: none !important; box-shadow: 0 8px 25px rgba(37, 99, 235, 0.25); position: relative; z-index: 1;">
|
||||
<div class="card-body text-white p-3 p-md-4" style="position: relative; z-index: 2;">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div class="flex-grow-1">
|
||||
<p class="mb-2 opacity-75 small fw-semibold"><i class="bi bi-calendar-check me-1"></i>Total Booking</p>
|
||||
<h2 class="mb-2 mb-md-3 fw-bold display-6"><?php echo e($totalBookings); ?></h2>
|
||||
<p class="mb-0 opacity-85 small d-none d-md-block">
|
||||
<i class="bi bi-clock me-1"></i>Update real-time
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-circle p-2 p-md-3 d-flex align-items-center justify-content-center d-none d-sm-flex" style="min-width: 50px; min-height: 50px; background: rgba(255,255,255,0.25);">
|
||||
<i class="bi bi-calendar2-week" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<a href="<?php echo e(route('admin.bookings.index')); ?>" class="btn btn-light btn-sm w-100 fw-semibold d-none d-md-block" style="position: relative; z-index: 3; color: #1d4ed8;">
|
||||
<i class="bi bi-arrow-right me-2"></i>Kelola Booking
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<!-- Card Laundry - Purple Theme -->
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #818cf8 0%, #667eea 100%); border: none !important; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.35); position: relative; z-index: 1;">
|
||||
|
|
@ -285,6 +522,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<!-- Card Kriteria -->
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 50%, #4338ca 100%); border: none !important; box-shadow: 0 8px 25px rgba(99, 102, 241, 0.3); position: relative; z-index: 1;">
|
||||
|
|
@ -308,7 +548,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<!-- Card SAW - Amber/Gold Tone -->
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card stats-card h-100" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 50%, #b45309 100%); border: none !important; box-shadow: 0 8px 25px rgba(217, 119, 6, 0.35); position: relative; z-index: 1;">
|
||||
|
|
@ -332,50 +574,70 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Charts Section -->
|
||||
<!-- Snapshot Section -->
|
||||
<div class="row g-3 g-md-4 mb-4 mb-md-5">
|
||||
<!-- Bar Chart: Perbandingan Harga -->
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card chart-card h-100">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-bold" style="color: #667eea;">
|
||||
<i class="bi bi-bar-chart-fill me-2"></i>
|
||||
<span class="d-none d-md-inline">Perbandingan Harga (Top 5)</span>
|
||||
<span class="d-md-none">Harga (Top 5)</span>
|
||||
</h5>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="glass-panel p-4 h-100">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="fw-bold mb-0" style="color: #4f46e5;">Ringkasan Aktivitas</h5>
|
||||
<span class="badge" style="background: rgba(102, 126, 234, 0.12); color: #4f46e5;">Update Hari Ini</span>
|
||||
</div>
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<canvas id="hargaChart" height="80"></canvas>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="mini-metric">
|
||||
<span>Kontrakan Aktif</span>
|
||||
<strong><?php echo e($jumlahKontrakan); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mini-metric">
|
||||
<span>Booking Masuk</span>
|
||||
<strong><?php echo e($totalBookings ?? 0); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<div class="col-md-6">
|
||||
<div class="mini-metric">
|
||||
<span>Laundry Terdaftar</span>
|
||||
<strong><?php echo e($jumlahLaundry); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mini-metric">
|
||||
<span>Kriteria Aktif</span>
|
||||
<strong><?php echo e($jumlahKriteria); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Donut Charts: Distribusi Jarak -->
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card chart-card h-100">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-bold" style="color: #667eea;">
|
||||
<i class="bi bi-pie-chart-fill me-2"></i>
|
||||
<span class="d-none d-md-inline">Distribusi Jarak</span>
|
||||
<span class="d-md-none">Jarak</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<canvas id="jarakChart" height="200"></canvas>
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 py-2 border-bottom">
|
||||
<span class="small"><span class="badge" style="background-color: #198754;">●</span> <span class="d-none d-sm-inline">Dekat (≤500m)</span><span class="d-sm-none">Dekat</span></span>
|
||||
<strong style="color: #667eea;"><?php echo e($jarakKontrakan['dekat'] + $jarakLaundry['dekat']); ?></strong>
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="insight-card p-4 h-100">
|
||||
<h6 class="mb-3">Insight Cepat</h6>
|
||||
<div class="insight-list">
|
||||
<div class="insight-item">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">Dominasi lokasi</div>
|
||||
<small class="text-muted"><?php echo e($jarakKontrakan['dekat']); ?> lokasi dekat kampus.</small>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 py-2 border-bottom">
|
||||
<span class="small"><span class="badge" style="background-color: #ffc107;">●</span> <span class="d-none d-sm-inline">Sedang (501-1000m)</span><span class="d-sm-none">Sedang</span></span>
|
||||
<strong style="color: #667eea;"><?php echo e($jarakKontrakan['sedang'] + $jarakLaundry['sedang']); ?></strong>
|
||||
</div>
|
||||
<div class="insight-item">
|
||||
<i class="bi bi-cash-stack"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">Harga rata-rata</div>
|
||||
<small class="text-muted">Rp <?php echo e(number_format($avgHargaKontrakan, 0, ',', '.')); ?> per bulan.</small>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center py-2">
|
||||
<span class="small"><span class="badge" style="background-color: #dc3545;">●</span> <span class="d-none d-sm-inline">Jauh (>1000m)</span><span class="d-sm-none">Jauh</span></span>
|
||||
<strong style="color: #667eea;"><?php echo e($jarakKontrakan['jauh'] + $jarakLaundry['jauh']); ?></strong>
|
||||
</div>
|
||||
<div class="insight-item">
|
||||
<i class="bi bi-people"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">Admin aktif</div>
|
||||
<small class="text-muted"><?php echo e($totalAdmins ?? 1); ?> akun admin tersedia.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -383,24 +645,146 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Line Chart: Trend Data -->
|
||||
<!-- Compact Visual Widgets -->
|
||||
<div class="row g-3 g-md-4 mb-4 mb-md-5">
|
||||
<div class="col-12">
|
||||
<div class="card chart-card">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-bold" style="color: #667eea;">
|
||||
<i class="bi bi-graph-up me-2"></i>
|
||||
<span class="d-none d-md-inline">Trend Data (6 Bulan Terakhir)</span>
|
||||
<span class="d-md-none">Trend Data</span>
|
||||
</h5>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Ringkasan Harga Kontrakan</h6>
|
||||
<div class="metric-grid">
|
||||
<div class="metric-row">
|
||||
<span>Harga Terendah</span>
|
||||
<strong>Rp <?php echo e(number_format($minHargaKontrakan, 0, ',', '.')); ?></strong>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span>Harga Rata-rata</span>
|
||||
<strong>Rp <?php echo e(number_format($avgHargaKontrakan, 0, ',', '.')); ?></strong>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span>Harga Tertinggi</span>
|
||||
<strong>Rp <?php echo e(number_format($maxHargaKontrakan, 0, ',', '.')); ?></strong>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span>Rata-rata Jarak</span>
|
||||
<strong><?php echo e(number_format($avgJarakKontrakan, 0, ',', '.')); ?> m</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<canvas id="trendChart" height="60"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Distribusi Jarak Kontrakan</h6>
|
||||
<?php
|
||||
$totalJarakKontrakan = max(1, $jarakKontrakan['dekat'] + $jarakKontrakan['sedang'] + $jarakKontrakan['jauh']);
|
||||
?>
|
||||
<div class="metric-grid">
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Dekat (≤500m)</span>
|
||||
<strong><?php echo e($jarakKontrakan['dekat']); ?></strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: <?php echo e(($jarakKontrakan['dekat'] / $totalJarakKontrakan) * 100); ?>%; background: #22c55e;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Sedang (501-1000m)</span>
|
||||
<strong><?php echo e($jarakKontrakan['sedang']); ?></strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: <?php echo e(($jarakKontrakan['sedang'] / $totalJarakKontrakan) * 100); ?>%; background: #f59e0b;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Jauh (>1000m)</span>
|
||||
<strong><?php echo e($jarakKontrakan['jauh']); ?></strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: <?php echo e(($jarakKontrakan['jauh'] / $totalJarakKontrakan) * 100); ?>%; background: #ef4444;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Kontrakan Harga Tertinggi</h6>
|
||||
<div class="rank-list">
|
||||
<?php $__currentLoopData = $topKontrakan; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $kontrakan): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<div class="rank-item">
|
||||
<div>
|
||||
<div class="fw-semibold"><?php echo e($kontrakan->nama); ?></div>
|
||||
<small><?php echo e($kontrakan->jumlah_kamar ?? 0); ?> kamar • <?php echo e($kontrakan->jarak ?? 0); ?> m</small>
|
||||
</div>
|
||||
<strong>Rp <?php echo e(number_format($kontrakan->harga, 0, ',', '.')); ?></strong>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<div class="row g-3 g-md-4 mb-4 mb-md-5">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Laundry Harga Terendah</h6>
|
||||
<div class="rank-list">
|
||||
<?php $__currentLoopData = $hargaLaundry; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $laundry): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<div class="rank-item">
|
||||
<div>
|
||||
<div class="fw-semibold"><?php echo e($laundry->nama); ?></div>
|
||||
<small>Harga mulai</small>
|
||||
</div>
|
||||
<strong>Rp <?php echo e(number_format($laundry->harga, 0, ',', '.')); ?></strong>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="metric-card p-4 h-100">
|
||||
<h6 class="mb-3">Distribusi Jarak Laundry</h6>
|
||||
<?php
|
||||
$totalJarakLaundry = max(1, $jarakLaundry['dekat'] + $jarakLaundry['sedang'] + $jarakLaundry['jauh']);
|
||||
?>
|
||||
<div class="metric-grid">
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Dekat (≤500m)</span>
|
||||
<strong><?php echo e($jarakLaundry['dekat']); ?></strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: <?php echo e(($jarakLaundry['dekat'] / $totalJarakLaundry) * 100); ?>%; background: #22c55e;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Sedang (501-1000m)</span>
|
||||
<strong><?php echo e($jarakLaundry['sedang']); ?></strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: <?php echo e(($jarakLaundry['sedang'] / $totalJarakLaundry) * 100); ?>%; background: #f59e0b;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Jauh (>1000m)</span>
|
||||
<strong><?php echo e($jarakLaundry['jauh']); ?></strong>
|
||||
</div>
|
||||
<div class="progress-rail">
|
||||
<div class="progress-fill" style="width: <?php echo e(($jarakLaundry['jauh'] / $totalJarakLaundry) * 100); ?>%; background: #ef4444;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
|
|
@ -411,6 +795,7 @@
|
|||
Quick Actions
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<div class="col-md-3">
|
||||
<a href="<?php echo e(route('kontrakan.create')); ?>" class="btn btn-admin-outline w-100 py-3">
|
||||
<i class="bi bi-building me-2"></i> Tambah Kontrakan
|
||||
|
|
@ -431,6 +816,23 @@
|
|||
<i class="bi bi-calculator me-2"></i> Sistem Analisis
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="col-12 col-md-4">
|
||||
<a href="<?php echo e(route('kontrakan.create')); ?>" class="btn btn-admin-outline w-100 py-3">
|
||||
<i class="bi bi-building me-2"></i> Tambah Kontrakan
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<a href="<?php echo e(route('admin.bookings.index')); ?>" class="btn btn-admin-outline w-100 py-3">
|
||||
<i class="bi bi-calendar-check me-2"></i> Kelola Booking
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<a href="<?php echo e(route('admin.profile')); ?>" class="btn btn-admin-solid w-100 py-3">
|
||||
<i class="bi bi-person-circle me-2"></i> Profil Admin
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -439,21 +841,22 @@
|
|||
|
||||
<!-- Additional Stats Section -->
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- Activity Stats -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card border-0 shadow-sm" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px;">
|
||||
<?php if(!$isSuperAdmin): ?>
|
||||
<!-- System Status -->
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm" style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border-radius: 12px;">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="mb-2 opacity-75 small fw-semibold">Total Review</p>
|
||||
<h3 class="fw-bold mb-0"><?php echo e($totalReviews ?? 0); ?></h3>
|
||||
<p class="mb-2 opacity-75 small fw-semibold">System Status</p>
|
||||
<h3 class="fw-bold mb-0"><i class="bi bi-check-circle-fill"></i> Aktif</h3>
|
||||
</div>
|
||||
<div style="font-size: 2.5rem; opacity: 0.3;">⭐</div>
|
||||
<div style="font-size: 2.5rem; opacity: 0.3;">✓</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- System Status -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card border-0 shadow-sm" style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border-radius: 12px;">
|
||||
|
|
@ -468,7 +871,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<!-- Last Backup -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card border-0 shadow-sm" style="background: linear-gradient(135deg, #17a2b8 0%, #20c997 100%); color: white; border-radius: 12px;">
|
||||
|
|
@ -498,12 +901,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Recent Data Tables -->
|
||||
<div class="row g-4">
|
||||
<!-- Recent Kontrakan -->
|
||||
<div class="col-lg-6">
|
||||
<div class="<?php echo e($isSuperAdmin ? 'col-lg-6' : 'col-12'); ?>">
|
||||
<div class="card chart-card">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-bold" style="color: #667eea;">
|
||||
|
|
@ -560,6 +965,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<!-- Recent Laundry -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card chart-card">
|
||||
|
|
@ -631,6 +1037,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -712,6 +1119,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($isSuperAdmin): ?>
|
||||
<!-- Modal Laundry -->
|
||||
<div class="modal fade" id="laundryModal" tabindex="-1" aria-labelledby="laundryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
||||
|
|
@ -957,9 +1365,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart.js Library -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
@ -997,133 +1403,6 @@
|
|||
updateCounter();
|
||||
});
|
||||
|
||||
// ========== BAR CHART: PERBANDINGAN HARGA ==========
|
||||
const hargaKontrakan = <?php echo json_encode($hargaKontrakan, 15, 512) ?>;
|
||||
const hargaLaundry = <?php echo json_encode($hargaLaundry, 15, 512) ?>;
|
||||
|
||||
new Chart(document.getElementById('hargaChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: hargaKontrakan.map(k => k.nama),
|
||||
datasets: [{
|
||||
label: 'Kontrakan',
|
||||
data: hargaKontrakan.map(k => k.harga),
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.8)',
|
||||
borderColor: 'rgba(102, 126, 234, 1)',
|
||||
borderWidth: 2
|
||||
}, {
|
||||
label: 'Laundry',
|
||||
data: hargaLaundry.map(l => l.harga),
|
||||
backgroundColor: 'rgba(240, 147, 251, 0.8)',
|
||||
borderColor: 'rgba(240, 147, 251, 1)',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.dataset.label + ': Rp ' + context.parsed.y.toLocaleString('id-ID');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return 'Rp ' + (value/1000) + 'k';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ========== DONUT CHART: DISTRIBUSI JARAK ==========
|
||||
const jarakData = <?php echo json_encode($jarakKontrakan, 15, 512) ?>;
|
||||
const totalDekat = jarakData.dekat + <?php echo json_encode($jarakLaundry['dekat'], 15, 512) ?>;
|
||||
const totalSedang = jarakData.sedang + <?php echo json_encode($jarakLaundry['sedang'], 15, 512) ?>;
|
||||
const totalJauh = jarakData.jauh + <?php echo json_encode($jarakLaundry['jauh'], 15, 512) ?>;
|
||||
|
||||
new Chart(document.getElementById('jarakChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Dekat (≤500m)', 'Sedang (501-1000m)', 'Jauh (>1000m)'],
|
||||
datasets: [{
|
||||
data: [totalDekat, totalSedang, totalJauh],
|
||||
backgroundColor: [
|
||||
'rgba(25, 135, 84, 0.8)',
|
||||
'rgba(255, 193, 7, 0.8)',
|
||||
'rgba(220, 53, 69, 0.8)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(25, 135, 84, 1)',
|
||||
'rgba(255, 193, 7, 1)',
|
||||
'rgba(220, 53, 69, 1)'
|
||||
],
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ========== LINE CHART: TREND DATA ==========
|
||||
const monthlyData = <?php echo json_encode($monthlyData, 15, 512) ?>;
|
||||
|
||||
new Chart(document.getElementById('trendChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: monthlyData.map(m => m.month),
|
||||
datasets: [{
|
||||
label: 'Kontrakan',
|
||||
data: monthlyData.map(m => m.kontrakan),
|
||||
borderColor: 'rgba(102, 126, 234, 1)',
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}, {
|
||||
label: 'Laundry',
|
||||
data: monthlyData.map(m => m.laundry),
|
||||
borderColor: 'rgba(240, 147, 251, 1)',
|
||||
backgroundColor: 'rgba(240, 147, 251, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
<!-- Info Card -->
|
||||
<div class="alert alert-info border-0 rounded-3 mb-4">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<strong>Tips:</strong> Lakukan backup secara berkala untuk mengamankan data. Backup dibuat dalam format ZIP dan dapat di-download.
|
||||
<strong>Tips:</strong> Backup otomatis dijalankan 1x per minggu (Minggu pukul 01:00), dan Anda tetap bisa membuat backup manual kapan saja.
|
||||
</div>
|
||||
|
||||
<!-- Backups List -->
|
||||
|
|
@ -164,7 +164,7 @@
|
|||
<i class="bi bi-exclamation-triangle me-2"></i>Reminder
|
||||
</h6>
|
||||
<small class="text-muted d-block">
|
||||
⚠️ Backup database secara rutin (minimal 1x sehari) untuk mencegah kehilangan data yang tidak dapat dipulihkan.
|
||||
Backup otomatis mingguan aktif, namun backup manual tetap disarankan sebelum perubahan data besar.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -701,7 +701,6 @@
|
|||
<div class="fw-bold"><?php echo e(Auth::user()->name); ?></div>
|
||||
<small class="text-muted"><?php echo e(Auth::user()->email); ?></small>
|
||||
</li>
|
||||
<li><a class="dropdown-item py-2" href="<?php echo e(route('welcome')); ?>"><i class="bi bi-house me-2"></i>Lihat Halaman User</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form method="POST" action="<?php echo e(route('admin.logout')); ?>">
|
||||
|
|
@ -728,6 +727,14 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="menu-section">
|
||||
<div class="menu-section-title">Akun</div>
|
||||
<a href="<?php echo e(route('admin.profile')); ?>" class="menu-item <?php echo e(request()->routeIs('admin.profile') ? 'active' : ''); ?>">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
<span>Profil</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Bisnis Management -->
|
||||
<div class="menu-section">
|
||||
<div class="menu-section-title">Kelola Bisnis</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,131 @@
|
|||
<!-- Toast Notification Component -->
|
||||
<div id="toastContainer" class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 9999;">
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
pointer-events: auto;
|
||||
min-width: 300px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.hide {
|
||||
animation: slideOut 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
background: linear-gradient(135deg, #ffa502 0%, #ffb84d 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast .toast-header {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toast .toast-body {
|
||||
padding: 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function showToast(message, type = 'success', duration = 4000) {
|
||||
const container = document.getElementById('toastContainer');
|
||||
const toastId = 'toast-' + Date.now();
|
||||
|
||||
const icons = {
|
||||
'success': 'bi-check-circle-fill',
|
||||
'error': 'bi-exclamation-triangle-fill',
|
||||
'warning': 'bi-exclamation-circle-fill',
|
||||
'info': 'bi-info-circle-fill'
|
||||
};
|
||||
|
||||
const html = `
|
||||
<div id="${toastId}" class="toast show toast-${type}" role="alert">
|
||||
<div class="d-flex align-items-center p-3">
|
||||
<i class="bi ${icons[type] || icons.info} me-2"></i>
|
||||
<span class="flex-grow-1">${message}</span>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
const toastElement = document.getElementById(toastId);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
toastElement.classList.add('hide');
|
||||
setTimeout(() => toastElement.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// Close button
|
||||
toastElement.querySelector('.btn-close').addEventListener('click', () => {
|
||||
toastElement.classList.add('hide');
|
||||
setTimeout(() => toastElement.remove(), 300);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-show session toasts
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
<?php if(session('success')): ?>
|
||||
showToast("<?php echo e(session('success')); ?>", 'success');
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(session('error')): ?>
|
||||
showToast("<?php echo e(session('error')); ?>", 'error');
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(session('warning')): ?>
|
||||
showToast("<?php echo e(session('warning')); ?>", 'warning');
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(session('info')): ?>
|
||||
showToast("<?php echo e(session('info')); ?>", 'info');
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
<?php /**PATH C:\laragon\www\TA\spk_kontrakan\resources\views/components/toast-notification.blade.php ENDPATH**/ ?>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,491 @@
|
|||
|
||||
|
||||
<?php $__env->startSection('title', 'Edit Kriteria'); ?>
|
||||
|
||||
<?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;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
color: #667eea;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #667eea;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.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"><a href="<?php echo e(route('kriteria.index')); ?>">Kriteria</a></li>
|
||||
<li class="breadcrumb-item active">Edit Data</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="page-header mb-4">
|
||||
<h2 class="mb-2">
|
||||
<i class="bi bi-pencil-square me-2"></i>Edit Kriteria SAW
|
||||
</h2>
|
||||
<p class="mb-0 fs-6">Update informasi kriteria <strong><?php echo e($kriteria->nama_kriteria); ?></strong></p>
|
||||
</div>
|
||||
|
||||
<!-- Form Card -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card form-card">
|
||||
<div class="card-body p-4">
|
||||
<form action="<?php echo e(route('kriteria.update', $kriteria->id)); ?>" method="POST" id="kriteriaForm">
|
||||
<?php echo csrf_field(); ?>
|
||||
<?php echo method_field('PUT'); ?>
|
||||
|
||||
<!-- Informasi Kriteria Section -->
|
||||
<div class="mb-4">
|
||||
<h5 class="section-header">
|
||||
<i class="bi bi-star me-2"></i>Informasi Kriteria
|
||||
</h5>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Tipe Bisnis -->
|
||||
<div class="col-md-12">
|
||||
<label for="tipe_bisnis" class="form-label fw-semibold">
|
||||
Tipe Bisnis <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-building text-primary"></i>
|
||||
</span>
|
||||
<select
|
||||
name="tipe_bisnis"
|
||||
class="form-select border-start-0 <?php $__errorArgs = ['tipe_bisnis'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>"
|
||||
id="tipe_bisnis"
|
||||
required
|
||||
>
|
||||
<option value="">-- Pilih Tipe Bisnis --</option>
|
||||
<option value="kontrakan" <?php echo e(old('tipe_bisnis', $kriteria->tipe_bisnis) == 'kontrakan' ? 'selected' : ''); ?>>
|
||||
🏠 Kontrakan
|
||||
</option>
|
||||
<option value="laundry" <?php echo e(old('tipe_bisnis', $kriteria->tipe_bisnis) == 'laundry' ? 'selected' : ''); ?>>
|
||||
👕 Laundry
|
||||
</option>
|
||||
</select>
|
||||
<?php $__errorArgs = ['tipe_bisnis'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<div class="invalid-feedback"><?php echo e($message); ?></div>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
<small class="text-muted">Pilih tipe bisnis untuk kriteria ini</small>
|
||||
</div>
|
||||
|
||||
<!-- Nama Kriteria -->
|
||||
<div class="col-md-12">
|
||||
<label for="nama_kriteria" class="form-label fw-semibold">
|
||||
Nama Kriteria <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-bookmark text-primary"></i>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
name="nama_kriteria"
|
||||
class="form-control border-start-0 <?php $__errorArgs = ['nama_kriteria'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>"
|
||||
id="nama_kriteria"
|
||||
placeholder="Contoh: Harga Sewa, Jarak, Fasilitas"
|
||||
value="<?php echo e(old('nama_kriteria', $kriteria->nama_kriteria)); ?>"
|
||||
required
|
||||
>
|
||||
<?php $__errorArgs = ['nama_kriteria'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<div class="invalid-feedback"><?php echo e($message); ?></div>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
<small class="text-muted">Masukkan nama kriteria yang jelas dan spesifik</small>
|
||||
</div>
|
||||
|
||||
<!-- Bobot -->
|
||||
<div class="col-md-6">
|
||||
<label for="bobot" class="form-label fw-semibold">
|
||||
Bobot Kriteria <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-speedometer text-info"></i>
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="bobot"
|
||||
class="form-control border-start-0 <?php $__errorArgs = ['bobot'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>"
|
||||
id="bobot"
|
||||
placeholder="0.25"
|
||||
value="<?php echo e(old('bobot', $kriteria->bobot)); ?>"
|
||||
min="0"
|
||||
max="1"
|
||||
required
|
||||
>
|
||||
<?php $__errorArgs = ['bobot'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<div class="invalid-feedback"><?php echo e($message); ?></div>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
<small class="text-muted">Nilai bobot antara 0 - 1 (contoh: 0.25 atau 0.3)</small>
|
||||
</div>
|
||||
|
||||
<!-- Tipe -->
|
||||
<div class="col-md-6">
|
||||
<label for="tipe" class="form-label fw-semibold">
|
||||
Tipe Kriteria <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-tags text-warning"></i>
|
||||
</span>
|
||||
<select
|
||||
name="tipe"
|
||||
class="form-select border-start-0 <?php $__errorArgs = ['tipe'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>"
|
||||
id="tipe"
|
||||
required
|
||||
>
|
||||
<option value="">-- Pilih Tipe --</option>
|
||||
<option value="Benefit" <?php echo e(old('tipe', $kriteria->tipe) == 'Benefit' ? 'selected' : ''); ?>>
|
||||
⬆️ Benefit (Semakin tinggi semakin baik)
|
||||
</option>
|
||||
<option value="Cost" <?php echo e(old('tipe', $kriteria->tipe) == 'Cost' ? 'selected' : ''); ?>>
|
||||
⬇️ Cost (Semakin rendah semakin baik)
|
||||
</option>
|
||||
</select>
|
||||
<?php $__errorArgs = ['tipe'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<div class="invalid-feedback"><?php echo e($message); ?></div>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
<small class="text-muted" id="tipeHelp">Pilih tipe sesuai karakteristik kriteria</small>
|
||||
</div>
|
||||
|
||||
<!-- Keterangan -->
|
||||
<div class="col-md-12">
|
||||
<label for="keterangan" class="form-label fw-semibold">
|
||||
Keterangan <span class="text-muted">(Opsional)</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0 align-items-start pt-2">
|
||||
<i class="bi bi-chat-left-text text-secondary"></i>
|
||||
</span>
|
||||
<textarea
|
||||
name="keterangan"
|
||||
class="form-control border-start-0 <?php $__errorArgs = ['keterangan'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>"
|
||||
id="keterangan"
|
||||
rows="3"
|
||||
placeholder="Contoh: Kriteria untuk menilai harga sewa per bulan dalam rupiah"
|
||||
><?php echo e(old('keterangan', $kriteria->keterangan)); ?></textarea>
|
||||
<?php $__errorArgs = ['keterangan'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<div class="invalid-feedback"><?php echo e($message); ?></div>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
<small class="text-muted">Deskripsi singkat tentang kriteria ini</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Changes -->
|
||||
<div class="alert alert-info border-info mb-4">
|
||||
<h6 class="fw-bold mb-2">
|
||||
<i class="bi bi-info-circle me-2"></i>Info Perubahan
|
||||
</h6>
|
||||
<p class="mb-0 small">
|
||||
Anda sedang mengedit kriteria <strong><?php echo e($kriteria->nama_kriteria); ?></strong> untuk tipe bisnis
|
||||
<span class="badge bg-<?php echo e($kriteria->tipe_bisnis == 'kontrakan' ? 'primary' : 'success'); ?>">
|
||||
<?php echo e(ucfirst($kriteria->tipe_bisnis)); ?>
|
||||
|
||||
</span>.
|
||||
Pastikan semua perubahan sudah benar sebelum menyimpan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex gap-2 justify-content-end pt-3 border-top">
|
||||
<a href="<?php echo e(route('kriteria.index')); ?>" class="btn btn-light px-4">
|
||||
<i class="bi bi-arrow-left me-2"></i>Batal
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save me-2"></i>Simpan Perubahan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Cards -->
|
||||
<div class="row mt-3 g-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 bg-success bg-opacity-10">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-bold mb-2 text-success">
|
||||
<i class="bi bi-arrow-up-circle me-2"></i>Kriteria Benefit
|
||||
</h6>
|
||||
<p class="mb-0 small text-muted">
|
||||
Nilai yang lebih tinggi lebih disukai.
|
||||
<br>Contoh: Fasilitas, Jumlah Kamar, Kecepatan Layanan
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 bg-danger bg-opacity-10">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-bold mb-2 text-danger">
|
||||
<i class="bi bi-arrow-down-circle me-2"></i>Kriteria Cost
|
||||
</h6>
|
||||
<p class="mb-0 small text-muted">
|
||||
Nilai yang lebih rendah lebih disukai.
|
||||
<br>Contoh: Harga, Jarak, Waktu Tempuh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data History Card -->
|
||||
<div class="card border-0 bg-light mt-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i class="bi bi-clock-history text-primary" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="fw-bold mb-2">Riwayat Data:</h6>
|
||||
<p class="mb-1 small text-muted">
|
||||
<strong>Dibuat:</strong> <?php echo e($kriteria->created_at->format('d M Y, H:i')); ?> WIB
|
||||
</p>
|
||||
<?php if($kriteria->updated_at != $kriteria->created_at): ?>
|
||||
<p class="mb-0 small text-muted">
|
||||
<strong>Terakhir diupdate:</strong> <?php echo e($kriteria->updated_at->format('d M Y, H:i')); ?> WIB
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript untuk Helper & Validation -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('kriteriaForm');
|
||||
const tipeSelect = document.getElementById('tipe');
|
||||
const bobotInput = document.getElementById('bobot');
|
||||
const tipeHelp = document.getElementById('tipeHelp');
|
||||
|
||||
// Update help text based on tipe selection
|
||||
function updateTipeHelp() {
|
||||
const tipe = tipeSelect.value;
|
||||
if (tipe === 'Benefit') {
|
||||
tipeHelp.innerHTML = '<strong>Benefit:</strong> Semakin tinggi nilai semakin baik (contoh: Fasilitas, Jumlah Kamar)';
|
||||
tipeHelp.classList.remove('text-muted');
|
||||
tipeHelp.classList.add('text-success');
|
||||
} else if (tipe === 'Cost') {
|
||||
tipeHelp.innerHTML = '<strong>Cost:</strong> Semakin rendah nilai semakin baik (contoh: Harga, Jarak)';
|
||||
tipeHelp.classList.remove('text-muted');
|
||||
tipeHelp.classList.add('text-danger');
|
||||
} else {
|
||||
tipeHelp.innerHTML = 'Pilih tipe sesuai karakteristik kriteria';
|
||||
tipeHelp.classList.remove('text-success', 'text-danger');
|
||||
tipeHelp.classList.add('text-muted');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
updateTipeHelp();
|
||||
|
||||
tipeSelect.addEventListener('change', updateTipeHelp);
|
||||
|
||||
// Validate bobot range
|
||||
bobotInput.addEventListener('blur', function() {
|
||||
const val = parseFloat(this.value);
|
||||
if (val < 0) this.value = 0;
|
||||
if (val > 1) this.value = 1;
|
||||
});
|
||||
|
||||
// Form validation animation
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!form.checkValidity()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
const confirmed = confirm('Simpan perubahan kriteria ini?');
|
||||
if (!confirmed) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #86b7fe;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.15);
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus + .input-group-text,
|
||||
.form-select:focus + .input-group-text,
|
||||
.input-group:focus-within .input-group-text {
|
||||
border-color: #86b7fe;
|
||||
background-color: #e7f1ff;
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.was-validated .form-control:invalid,
|
||||
.was-validated .form-select:invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.was-validated .form-control:valid,
|
||||
.was-validated .form-select:valid {
|
||||
border-color: #198754;
|
||||
}
|
||||
</style>
|
||||
<?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/kriteria/edit.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -236,28 +236,20 @@
|
|||
<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;">
|
||||
<div class="col-12 col-md-6">
|
||||
<input type="radio" class="btn-check" name="jenis_layanan" id="layanan_harian" value="harian">
|
||||
<label class="btn btn-outline-warning w-100 py-2 py-md-3" for="layanan_harian" 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>
|
||||
<strong class="small">Harian</strong>
|
||||
<small class="d-block text-muted" style="font-size: 0.75rem;">Selesai harian</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;">
|
||||
<div class="col-12 col-md-6">
|
||||
<input type="radio" class="btn-check" name="jenis_layanan" id="layanan_jam" value="jam">
|
||||
<label class="btn btn-outline-warning w-100 py-2 py-md-3" for="layanan_jam" 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>
|
||||
<strong class="small">Jam</strong>
|
||||
<small class="d-block text-muted" style="font-size: 0.75rem;">Selesai per jam</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-width: 720px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
|
|
@ -125,6 +125,23 @@
|
|||
.auth-footer a:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.btn-register-link {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
padding: 10px 16px;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 8px;
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-register-link:hover {
|
||||
background: #667eea;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
@ -145,6 +162,14 @@
|
|||
</div>
|
||||
|
||||
<div class="auth-body">
|
||||
<?php if(session('success')): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i><?php echo e(session('success')); ?>
|
||||
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(session('error')): ?>
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-circle me-2"></i><?php echo e(session('error')); ?>
|
||||
|
|
@ -173,7 +198,8 @@
|
|||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>Belum punya akun bisnis? <a href="<?php echo e(route('admin.register')); ?>">Daftar Sebagai Pemilik</a></p>
|
||||
<p>Belum punya akun bisnis?</p>
|
||||
<a class="btn-register-link" href="/admin/register" onclick="window.location.href='/admin/register'; return false;">Daftar di sini</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,194 @@
|
|||
<mxfile host="app.diagrams.net" agent="GitHub Copilot">
|
||||
<diagram id="super-admin" name="Super Admin Mockup">
|
||||
<mxGraphModel dx="1178" dy="648" grid="1" gridSize="10" guides="1" tooltips="1" connect="0" arrows="0" fold="1" page="1" pageScale="1" pageWidth="2200" pageHeight="1200" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="frame_login" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="300" width="520" x="40" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_login" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="LOGIN" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="60" y="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="login_user" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="E-Mail" vertex="1">
|
||||
<mxGeometry height="32" width="400" x="100" y="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="login_pass" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Password" vertex="1">
|
||||
<mxGeometry height="32" width="400" x="100" y="155" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="login_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Login" vertex="1">
|
||||
<mxGeometry height="34" width="180" x="210" y="205" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="login_link" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=12;fontColor=#000000;" value="Register | Lupa Password" vertex="1">
|
||||
<mxGeometry height="20" width="400" x="100" y="250" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_register" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="300" width="520" x="600" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_register" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="REGISTER" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="620" y="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_name" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Nama" vertex="1">
|
||||
<mxGeometry height="28" width="400" x="660" y="105" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_email" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="E-Mail" vertex="1">
|
||||
<mxGeometry height="28" width="400" x="660" y="140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_phone" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Nomor HP" vertex="1">
|
||||
<mxGeometry height="28" width="400" x="660" y="175" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_pass" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Password" vertex="1">
|
||||
<mxGeometry height="28" width="190" x="660" y="210" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_confirm" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Konfirmasi" vertex="1">
|
||||
<mxGeometry height="28" width="190" x="870" y="210" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Register" vertex="1">
|
||||
<mxGeometry height="30" width="160" x="780" y="245" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_forgot" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="300" width="520" x="1160" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_forgot" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="LUPA PASSWORD" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="1180" y="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="forgot_email" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="E-Mail" vertex="1">
|
||||
<mxGeometry height="32" width="400" x="1220" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="forgot_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Kirim Link" vertex="1">
|
||||
<mxGeometry height="34" width="180" x="1330" y="170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_dashboard" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="40" y="380" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_dashboard" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="DASHBOARD" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="60" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="dash_cards" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Ringkasan: Kontrakan | Laundry | User | Booking" vertex="1">
|
||||
<mxGeometry height="70" width="440" x="80" y="450" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="dash_chart" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Grafik Aktivitas" vertex="1">
|
||||
<mxGeometry height="120" width="440" x="80" y="530" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_data_k" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="600" y="380" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_data_k" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="DATA KONTRAKAN" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="620" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data_k_toolbar" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=12;fontColor=#000000;" value="Tambah | Cari | Filter" vertex="1">
|
||||
<mxGeometry height="20" width="480" x="620" y="440" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data_k_table" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Tabel Kontrakan" vertex="1">
|
||||
<mxGeometry height="200" width="480" x="620" y="470" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_data_l" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="1160" y="380" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_data_l" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="DATA LAUNDRY" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="1180" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data_l_toolbar" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=12;fontColor=#000000;" value="Tambah | Cari | Filter" vertex="1">
|
||||
<mxGeometry height="20" width="480" x="1180" y="440" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data_l_table" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Tabel Laundry" vertex="1">
|
||||
<mxGeometry height="200" width="480" x="1180" y="470" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_data_booking" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="1740" y="380" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_data_booking" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="DATA BOOKING" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="1710" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data_booking_toolbar" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=12;fontColor=#000000;" value="Tambah | Cari | Filter | Status" vertex="1">
|
||||
<mxGeometry height="20" width="480" x="1760" y="440" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data_booking_table" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Tabel Booking" vertex="1">
|
||||
<mxGeometry height="200" width="480" x="1760" y="470" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_kriteria" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="40" y="740" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_kriteria" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="KRITERIA" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="60" y="760" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="kriteria_table" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Tabel Kriteria" vertex="1">
|
||||
<mxGeometry height="210" width="440" x="80" y="810" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_saw" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="600" y="740" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_saw" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="PROSES SAW" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="620" y="760" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="saw_steps" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Normalisasi -> Bobot -> Ranking" vertex="1">
|
||||
<mxGeometry height="80" width="480" x="620" y="820" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="saw_result" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Hasil Ranking" vertex="1">
|
||||
<mxGeometry height="110" width="480" x="620" y="910" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_users" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="1160" y="740" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_users" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="MANAJEMEN USER" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="1180" y="760" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="users_table" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Tabel User" vertex="1">
|
||||
<mxGeometry height="210" width="480" x="1180" y="810" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_logs" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="40" y="1100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_logs" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="LOGS ACTIVITY" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="60" y="1120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logs_table" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Log Table" vertex="1">
|
||||
<mxGeometry height="210" width="440" x="80" y="1170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_backup" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="600" y="1100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_backup" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="BACKUP DATABASE" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="620" y="1120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="backup_actions" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Backup Now | Restore | History" vertex="1">
|
||||
<mxGeometry height="70" width="480" x="620" y="1180" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="backup_table" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Riwayat Backup" vertex="1">
|
||||
<mxGeometry height="110" width="480" x="620" y="1260" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="frame_profile" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="1160" y="1100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_profile" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="PROFIL" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="1180" y="1120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="profile_info" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Nama | Email | Role" vertex="1">
|
||||
<mxGeometry height="80" width="480" x="1180" y="1170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="profile_actions" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Ubah Password | Logout" vertex="1">
|
||||
<mxGeometry height="70" width="480" x="1180" y="1260" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="frame_logout" parent="1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="320" width="520" x="1740" y="1100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_logout" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#000000;" value="LOGOUT" vertex="1">
|
||||
<mxGeometry height="30" width="480" x="1760" y="1120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logout_dialog" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Konfirmasi Logout" vertex="1">
|
||||
<mxGeometry height="120" width="360" x="1820" y="1180" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logout_text" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=12;fontColor=#000000;" value="Apakah Anda yakin ingin keluar?" vertex="1">
|
||||
<mxGeometry height="20" width="360" x="1820" y="1215" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logout_yes" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Ya, Logout" vertex="1">
|
||||
<mxGeometry height="32" width="140" x="1860" y="1250" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logout_no" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Batal" vertex="1">
|
||||
<mxGeometry height="32" width="140" x="2020" y="1250" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<mxfile host="app.diagrams.net" modified="2026-05-05" agent="GitHub Copilot" version="20.8.16">
|
||||
<diagram id="user-flow" name="Page-1">
|
||||
<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1920" pageHeight="1080" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="2" value="flowchart LR
 Start([Start]) --> Landing[Landing Page]
 Landing --> HasAccount{Sudah punya akun?}
 HasAccount -->|Ya| Login[Login]
 HasAccount -->|Tidak| Register[Register]
 Register --> Login

 Login --> LoginOk{Login sukses?}
 LoginOk -->|Tidak| Login
 LoginOk -->|Ya| Dashboard[Dashboard]

 Dashboard --> Menu{Pilih Menu?}
 Menu -->|Kontrakan| KontrakanList[Kontrakan]
 Menu -->|Lainnya| PickLaundry{Pilih Laundry?}
 PickLaundry -->|Ya| LaundryList[Laundry]
 PickLaundry -->|Tidak| PickRekom{Pilih Rekomendasi?}
 PickRekom -->|Ya| Recommendations[Rekomendasi]
 PickRekom -->|Tidak| PickBooking{Pilih Booking History?}
 PickBooking -->|Ya| BookingHistory[Booking History]
 PickBooking -->|Tidak| PickProfile{Pilih Profile?}
 PickProfile -->|Ya| Profile[Profile]
 PickProfile -->|Tidak| Dashboard

 KontrakanList --> KontrakanDetail[Detail Kontrakan]
 KontrakanDetail --> Booking[Booking]
 Booking --> End([End])

 LaundryList --> LaundryDetail[Detail Laundry]
 LaundryDetail --> End

 Recommendations --> End

 BookingHistory --> BookingDetail[Detail Booking]
 BookingDetail --> End

 Profile --> ProfileAction{Kelola Profil?}
 ProfileAction -->|Ya| EditProfile[Edit Profile]
 ProfileAction -->|Tidak| Logout[Logout]
 EditProfile --> End
 Logout --> Login" style="shape=mxgraph.misc.mermaid;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#111111;" vertex="1" parent="1">
|
||||
<mxGeometry x="20" y="20" width="1700" height="980" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
|
||||
# Flowchart User (Mobile)
|
||||
|
||||
Berikut flowchart user versi ringkas (simpel, decision 2 arah).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Start([Start]) --> Landing[Landing Page]
|
||||
Landing --> HasAccount{Sudah punya akun?}
|
||||
HasAccount -->|Ya| Login[Login]
|
||||
HasAccount -->|Tidak| Register[Register]
|
||||
Register --> Login
|
||||
|
||||
Login --> LoginOk{Login sukses?}
|
||||
LoginOk -->|Tidak| Login
|
||||
LoginOk -->|Ya| Dashboard[Dashboard]
|
||||
|
||||
Dashboard --> Menu{Pilih Menu?}
|
||||
Menu -->|Kontrakan| KontrakanList[Kontrakan]
|
||||
Menu -->|Lainnya| PickLaundry{Pilih Laundry?}
|
||||
PickLaundry -->|Ya| LaundryList[Laundry]
|
||||
PickLaundry -->|Tidak| PickRekom{Pilih Rekomendasi?}
|
||||
PickRekom -->|Ya| Recommendations[Rekomendasi]
|
||||
PickRekom -->|Tidak| PickBooking{Pilih Booking History?}
|
||||
PickBooking -->|Ya| BookingHistory[Booking History]
|
||||
PickBooking -->|Tidak| PickProfile{Pilih Profile?}
|
||||
PickProfile -->|Ya| Profile[Profile]
|
||||
PickProfile -->|Tidak| Dashboard
|
||||
|
||||
KontrakanList --> KontrakanDetail[Detail Kontrakan]
|
||||
KontrakanDetail --> Booking[Booking]
|
||||
Booking --> End([End])
|
||||
|
||||
LaundryList --> LaundryDetail[Detail Laundry]
|
||||
LaundryDetail --> End
|
||||
|
||||
Recommendations --> End
|
||||
|
||||
BookingHistory --> BookingDetail[Detail Booking]
|
||||
BookingDetail --> End
|
||||
|
||||
Profile --> ProfileAction{Kelola Profil?}
|
||||
ProfileAction -->|Ya| EditProfile[Edit Profile]
|
||||
ProfileAction -->|Tidak| Logout[Logout]
|
||||
EditProfile --> End
|
||||
Logout --> Login
|
||||
```
|
||||
```
|
||||
|
|
@ -2,6 +2,7 @@ plugins {
|
|||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:label="Kontrak Kampus"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ plugins {
|
|||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class AppConfig {
|
|||
// ============================================================================
|
||||
|
||||
// Default fallback (dipakai jika auto-detect gagal)
|
||||
static const String _defaultServer = 'http://192.168.18.16:8000';
|
||||
static const String _defaultServer = 'http://192.168.18.17:41197';
|
||||
|
||||
// Runtime values — diupdate otomatis oleh ServerDiscoveryService
|
||||
static String _serverUrl = _defaultServer;
|
||||
|
|
@ -30,4 +30,5 @@ class AppConfig {
|
|||
// Local Storage Keys
|
||||
static const String tokenKey = 'auth_token';
|
||||
static const String userKey = 'user_data';
|
||||
static const String deviceTokenKey = 'device_token';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ class Environment {
|
|||
// Pilih sesuai platform dan environment saat development
|
||||
static const String apiBaseUrl = String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'http://192.168.18.16:8000', // Default IP lokal
|
||||
defaultValue: 'http://192.168.18.17:41197', // Default IP lokal
|
||||
);
|
||||
|
||||
static const String storageBaseUrl = String.fromEnvironment(
|
||||
'STORAGE_BASE_URL',
|
||||
defaultValue: 'http://192.168.18.16:8000/storage',
|
||||
defaultValue: 'http://192.168.18.17:41197/storage',
|
||||
);
|
||||
|
||||
// Mode debugging
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter_spinkit/flutter_spinkit.dart';
|
|||
import 'screens/improved_home_screen.dart';
|
||||
import 'register.dart';
|
||||
import 'services/auth_service.dart';
|
||||
import 'services/notification_service.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
|
@ -220,6 +221,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
setState(() => _isLoading = false);
|
||||
|
||||
if (result['success']) {
|
||||
await NotificationService().init(_authService);
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const ImprovedHomeScreen()),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'login.dart';
|
||||
import 'screens/improved_home_screen.dart';
|
||||
import 'services/auth_service.dart';
|
||||
import 'services/notification_service.dart';
|
||||
import 'services/server_discovery_service.dart';
|
||||
|
||||
void main() {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp();
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
|
|
@ -20,7 +23,6 @@ class MyApp extends StatelessWidget {
|
|||
const MyApp({super.key});
|
||||
|
||||
static const _primary = Color(0xFF1565C0);
|
||||
static const _primaryDark = Color(0xFF0D47A1);
|
||||
static const _accent = Color(0xFF00897B);
|
||||
static const _surfaceTint = Color(0xFFF3F7FB);
|
||||
|
||||
|
|
@ -295,6 +297,9 @@ class _SplashScreenState extends State<SplashScreen>
|
|||
|
||||
if (mounted) setState(() => _statusText = 'Memeriksa sesi...');
|
||||
await _authService.loadToken();
|
||||
if (_authService.isAuthenticated) {
|
||||
await NotificationService().init(_authService);
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
if (!mounted) return;
|
||||
|
|
|
|||
|
|
@ -58,71 +58,39 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
|
|||
}
|
||||
|
||||
Future<void> _toggleFavorite() async {
|
||||
setState(() => _isFavLoading = true);
|
||||
try {
|
||||
final result = await _favoriteService.toggleKontrakanFavorite(
|
||||
widget.kontrakan.id,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (!mounted) return;
|
||||
|
||||
if (result['success'] == true) {
|
||||
setState(() {
|
||||
_isFavorite = result['isFavorite'] ?? !_isFavorite;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_isFavorite
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_border_rounded,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(result['message'] ?? 'Status favorit diubah'),
|
||||
],
|
||||
setState(() {
|
||||
_isFavorite = !_isFavorite;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_isFavorite
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_border_rounded,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
backgroundColor: _isFavorite
|
||||
? const Color(0xFF1565C0)
|
||||
: Colors.grey[700],
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(result['message'] ?? 'Gagal mengubah favorit'),
|
||||
backgroundColor: Colors.red[700],
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gagal: $e'),
|
||||
backgroundColor: Colors.red[700],
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
const SizedBox(width: 10),
|
||||
const Text('Perubahan favorit disimpan lokal (tanpa auto-save DB)'),
|
||||
],
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isFavLoading = false);
|
||||
backgroundColor: _isFavorite
|
||||
? const Color(0xFF1565C0)
|
||||
: Colors.grey[700],
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
if (_isFavLoading) {
|
||||
setState(() => _isFavLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,10 +42,21 @@ class AuthService {
|
|||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(AppConfig.tokenKey);
|
||||
await prefs.remove(AppConfig.userKey);
|
||||
await prefs.remove(AppConfig.deviceTokenKey);
|
||||
_token = null;
|
||||
_currentUser = null;
|
||||
}
|
||||
|
||||
Future<void> saveDeviceToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(AppConfig.deviceTokenKey, token);
|
||||
}
|
||||
|
||||
Future<String?> loadDeviceToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(AppConfig.deviceTokenKey);
|
||||
}
|
||||
|
||||
/// Tangani response 401 secara terpusat.
|
||||
/// Token lokal dibersihkan agar app tidak terus memakai token invalid.
|
||||
Future<bool> handleUnauthorized(int statusCode) async {
|
||||
|
|
@ -137,6 +148,11 @@ class AuthService {
|
|||
return {'success': true};
|
||||
}
|
||||
|
||||
final deviceToken = await loadDeviceToken();
|
||||
if (deviceToken != null) {
|
||||
await unregisterDeviceToken(deviceToken);
|
||||
}
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('${AppConfig.baseUrl}/logout'),
|
||||
headers: {
|
||||
|
|
@ -264,4 +280,47 @@ class AuthService {
|
|||
return {'success': false, 'message': 'Error: $e'};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerDeviceToken({
|
||||
required String token,
|
||||
String? platform,
|
||||
}) async {
|
||||
if (_token == null) return;
|
||||
|
||||
try {
|
||||
await http.post(
|
||||
Uri.parse('${AppConfig.baseUrl}/device-tokens'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer $_token',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'token': token,
|
||||
if (platform != null) 'platform': platform,
|
||||
}),
|
||||
);
|
||||
await saveDeviceToken(token);
|
||||
} catch (_) {
|
||||
// Non-blocking: ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unregisterDeviceToken(String token) async {
|
||||
if (_token == null) return;
|
||||
|
||||
try {
|
||||
await http.delete(
|
||||
Uri.parse('${AppConfig.baseUrl}/device-tokens'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer $_token',
|
||||
},
|
||||
body: jsonEncode({'token': token}),
|
||||
);
|
||||
} catch (_) {
|
||||
// Non-blocking: ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import '../models/user.dart';
|
||||
import 'auth_service.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService _instance = NotificationService._internal();
|
||||
factory NotificationService() => _instance;
|
||||
NotificationService._internal();
|
||||
|
||||
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
|
||||
final FlutterLocalNotificationsPlugin _localNotifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
static const AndroidNotificationChannel _channel = AndroidNotificationChannel(
|
||||
'admin_reminders',
|
||||
'Admin Reminders',
|
||||
description: 'Notifikasi pengingat admin mingguan',
|
||||
importance: Importance.high,
|
||||
);
|
||||
|
||||
Future<void> init(AuthService authService) async {
|
||||
await _initLocalNotifications();
|
||||
await _requestPermissions();
|
||||
|
||||
final user = authService.currentUser;
|
||||
if (_isAdminUser(user)) {
|
||||
await _registerToken(authService);
|
||||
_messaging.onTokenRefresh.listen((token) {
|
||||
authService.registerDeviceToken(token: token, platform: _platformName());
|
||||
});
|
||||
}
|
||||
|
||||
FirebaseMessaging.onMessage.listen((message) async {
|
||||
await _showLocalNotification(message);
|
||||
});
|
||||
}
|
||||
|
||||
bool _isAdminUser(User? user) {
|
||||
if (user == null) return false;
|
||||
return user.isAdmin() || user.isSuperAdmin();
|
||||
}
|
||||
|
||||
Future<void> _registerToken(AuthService authService) async {
|
||||
final token = await _messaging.getToken();
|
||||
if (token == null) return;
|
||||
|
||||
await authService.registerDeviceToken(
|
||||
token: token,
|
||||
platform: _platformName(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initLocalNotifications() async {
|
||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const iosSettings = DarwinInitializationSettings();
|
||||
const settings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
);
|
||||
|
||||
await _localNotifications.initialize(settings);
|
||||
|
||||
await _localNotifications
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(_channel);
|
||||
}
|
||||
|
||||
Future<void> _requestPermissions() async {
|
||||
await _messaging.requestPermission(alert: true, badge: true, sound: true);
|
||||
}
|
||||
|
||||
Future<void> _showLocalNotification(RemoteMessage message) async {
|
||||
final notification = message.notification;
|
||||
if (notification == null) return;
|
||||
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
_channel.id,
|
||||
_channel.name,
|
||||
channelDescription: _channel.description,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
);
|
||||
|
||||
final details = NotificationDetails(android: androidDetails);
|
||||
|
||||
await _localNotifications.show(
|
||||
notification.hashCode,
|
||||
notification.title,
|
||||
notification.body,
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
String _platformName() {
|
||||
if (Platform.isAndroid) return 'android';
|
||||
if (Platform.isIOS) return 'ios';
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import '../config/app_config.dart';
|
|||
/// 3. Localhost (desktop / iOS simulator)
|
||||
/// 4. Scan subnet lokal HP secara paralel
|
||||
class ServerDiscoveryService {
|
||||
static const int _port = 8000;
|
||||
static const int _port = 41197;
|
||||
static const String _cacheKey = 'discovered_server_url';
|
||||
static const Duration _probeTimeout = Duration(seconds: 2);
|
||||
static const int _scanBatchSize = 24;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import FlutterMacOS
|
|||
import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
import firebase_core
|
||||
import firebase_messaging
|
||||
import flutter_local_notifications
|
||||
import geolocator_apple
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
|
|
@ -13,6 +16,9 @@ import url_launcher_macos
|
|||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,22 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.59"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -97,6 +113,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.12"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -153,6 +177,54 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+5"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.15.2"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.24.1"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.10"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.10"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.10"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -182,6 +254,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "17.2.4"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -520,6 +616,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -717,6 +821,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -829,6 +941,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@ dependencies:
|
|||
# Date formatting
|
||||
intl: ^0.19.0
|
||||
|
||||
# Push notifications
|
||||
firebase_core: ^3.3.0
|
||||
firebase_messaging: ^15.1.0
|
||||
flutter_local_notifications: ^17.2.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
<mxfile host="app.diagrams.net" agent="GitHub Copilot" pages="2">
|
||||
<diagram id="mockup" name="Mobile Mockups">
|
||||
<mxGraphModel dx="1347" dy="740" grid="1" gridSize="10" guides="1" tooltips="1" connect="0" arrows="0" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1000" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="phone_login" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="40" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_login" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="LOGIN" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="60" y="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="login_email" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="E-Mail" vertex="1">
|
||||
<mxGeometry height="36" width="200" x="70" y="130" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="login_pass" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Password" vertex="1">
|
||||
<mxGeometry height="36" width="200" x="70" y="180" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="login_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Login" vertex="1">
|
||||
<mxGeometry height="36" width="140" x="100" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="login_link" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=12;fontColor=#000000;" value="Register" vertex="1">
|
||||
<mxGeometry height="20" width="160" x="90" y="290" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="phone_register" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="340" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_register" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="REGISTER" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="360" y="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_username" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Username" vertex="1">
|
||||
<mxGeometry height="34" width="200" x="370" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_email" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="E-Mail" vertex="1">
|
||||
<mxGeometry height="34" width="200" x="370" y="165" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_phone" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Nomor HP" vertex="1">
|
||||
<mxGeometry height="34" width="200" x="370" y="210" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_pass" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Password" vertex="1">
|
||||
<mxGeometry height="34" width="200" x="370" y="255" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_confirm" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Konfirmasi Password" vertex="1">
|
||||
<mxGeometry height="34" width="200" x="370" y="300" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="reg_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Register" vertex="1">
|
||||
<mxGeometry height="34" width="140" x="400" y="350" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="phone_forgot" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="640" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_forgot" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="LUPA PASSWORD" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="660" y="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="forgot_email" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="E-Mail" vertex="1">
|
||||
<mxGeometry height="36" width="200" x="670" y="140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="forgot_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Kirim Link" vertex="1">
|
||||
<mxGeometry height="36" width="140" x="700" y="200" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="phone_home" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="940" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_home" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="BERANDA" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="960" y="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="home_search" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Cari kontrakan/laundry" vertex="1">
|
||||
<mxGeometry height="34" width="200" x="970" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="home_card1" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Card Kontrakan" vertex="1">
|
||||
<mxGeometry height="80" width="200" x="970" y="170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="home_card2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Card Laundry" vertex="1">
|
||||
<mxGeometry height="80" width="200" x="970" y="260" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="phone_detail_k" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="40" y="590" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_detail_k" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="DETAIL KONTRAKAN" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="60" y="620" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="dk_image" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Gambar" vertex="1">
|
||||
<mxGeometry height="120" width="200" x="70" y="670" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="dk_info" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Harga, Fasilitas, Alamat" vertex="1">
|
||||
<mxGeometry height="100" width="200" x="70" y="800" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="dk_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Booking" vertex="1">
|
||||
<mxGeometry height="36" width="140" x="100" y="920" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="phone_detail_l" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="340" y="590" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_detail_l" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="DETAIL LAUNDRY" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="360" y="620" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="dl_image" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Gambar" vertex="1">
|
||||
<mxGeometry height="120" width="200" x="370" y="670" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="dl_info" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Harga, Layanan, Alamat" vertex="1">
|
||||
<mxGeometry height="100" width="200" x="370" y="800" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="dl_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Maps" vertex="1">
|
||||
<mxGeometry height="50" width="140" x="400" y="1010" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="phone_booking" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="640" y="590" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_booking" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="BOOKING" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="660" y="620" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bk_form1" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Tanggal" vertex="1">
|
||||
<mxGeometry height="34" width="200" x="670" y="680" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bk_form2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Catatan" vertex="1">
|
||||
<mxGeometry height="60" width="200" x="670" y="725" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bk_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Konfirmasi" vertex="1">
|
||||
<mxGeometry height="36" width="140" x="700" y="800" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="phone_fav" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="940" y="590" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_fav" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="FAVORIT" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="960" y="620" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fav_item1" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Item Favorit 1" vertex="1">
|
||||
<mxGeometry height="60" width="200" x="970" y="680" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fav_item2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Item Favorit 2" vertex="1">
|
||||
<mxGeometry height="60" width="200" x="970" y="750" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="phone_profile" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="40" y="1140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_profile" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="PROFIL" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="60" y="1170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="profile_info" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=left;spacingLeft=10;" value="Nama, Email, Nomor HP" vertex="1">
|
||||
<mxGeometry height="100" width="200" x="70" y="1230" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="profile_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Edit Profil" vertex="1">
|
||||
<mxGeometry height="36" width="140" x="100" y="1350" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logout_btn" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Logout" vertex="1">
|
||||
<mxGeometry height="36" width="140" x="100" y="1400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="phone_logout" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;" value="" vertex="1">
|
||||
<mxGeometry height="520" width="260" x="340" y="1140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="title_logout" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=16;fontColor=#000000;" value="LOGOUT" vertex="1">
|
||||
<mxGeometry height="30" width="220" x="360" y="1170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logout_dialog" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Konfirmasi Logout" vertex="1">
|
||||
<mxGeometry height="120" width="200" x="380" y="1250" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logout_text" parent="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=12;fontColor=#000000;" value="Yakin ingin keluar?" vertex="1">
|
||||
<mxGeometry height="20" width="200" x="380" y="1285" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logout_yes" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Ya" vertex="1">
|
||||
<mxGeometry height="32" width="90" x="390" y="1320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="logout_no" parent="1" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFFFFF;align=center;" value="Batal" vertex="1">
|
||||
<mxGeometry height="32" width="90" x="490" y="1320" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
<diagram id="dCEBloto_LI-AfNgj2wR" name="Page-2">
|
||||
<mxGraphModel dx="1414" dy="777" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
|
@ -7,12 +7,15 @@
|
|||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <geolocator_windows/geolocator_windows.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||
GeolocatorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
firebase_core
|
||||
geolocator_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue