final
This commit is contained in:
parent
7ce8586b8c
commit
46ded0ee5c
146
CARA_TEST.md
146
CARA_TEST.md
|
|
@ -1,146 +0,0 @@
|
|||
# 🔧 PANDUAN LENGKAP - Cara Test & Fix
|
||||
|
||||
## ⚠️ PENTING: Semua File SUDAH DIUPDATE!
|
||||
|
||||
Semua perubahan sudah tersimpan di:
|
||||
- ✅ routes/web.php
|
||||
- ✅ UserController.php
|
||||
- ✅ wali_accounts.blade.php
|
||||
- ✅ santri_accounts.blade.php
|
||||
- ✅ app_config.dart
|
||||
|
||||
**TAPI** mungkin browser/Flutter masih pakai file lama (cached).
|
||||
|
||||
---
|
||||
|
||||
## 🚀 LANGKAH TESTING (IKUTI URUTAN INI!)
|
||||
|
||||
### 1️⃣ Test dengan Debug Tool
|
||||
Buka browser dan akses:
|
||||
```
|
||||
http://localhost/TugasAkhir/debug_comprehensive.php
|
||||
```
|
||||
|
||||
Tool ini akan cek:
|
||||
- ✅ Apakah file sudah ter-update
|
||||
- ✅ Apakah route sudah benar
|
||||
- ✅ Apakah API berfungsi
|
||||
- ✅ Apakah Flutter config sudah benar
|
||||
|
||||
### 2️⃣ Clear Browser Cache
|
||||
**PENTING!** Tekan:
|
||||
- **Windows:** `Ctrl + Shift + R` atau `Ctrl + F5`
|
||||
- **Mac:** `Cmd + Shift + R`
|
||||
|
||||
Atau buka Incognito/Private Window.
|
||||
|
||||
### 3️⃣ Login ke Admin Panel
|
||||
```
|
||||
http://localhost/TugasAkhir/sim-pkpps/public/admin/login
|
||||
```
|
||||
|
||||
Login dengan akun admin Anda.
|
||||
|
||||
### 4️⃣ Test Delete & Reset di Web
|
||||
```
|
||||
http://localhost/TugasAkhir/sim-pkpps/public/admin/users/wali
|
||||
```
|
||||
|
||||
Coba:
|
||||
- Klik tombol **Hapus** → konfirmasi → lihat apakah akun terhapus
|
||||
- Klik tombol **Reset** → konfirmasi → lihat pesan sukses
|
||||
|
||||
**Jika MASIH BELUM BISA:**
|
||||
1. Tekan F12 (Developer Tools)
|
||||
2. Lihat tab **Console** → ada error?
|
||||
3. Lihat tab **Network** → klik tombol delete → lihat request yang dikirim
|
||||
4. Screenshot errornya dan kirim ke saya
|
||||
|
||||
### 5️⃣ Test Login Mobile
|
||||
|
||||
#### A. Hot Restart Flutter (BUKAN Hot Reload!)
|
||||
```bash
|
||||
cd c:\xampp\htdocs\TugasAkhir\sim_mobile
|
||||
flutter clean
|
||||
flutter run
|
||||
```
|
||||
|
||||
Atau di VS Code: klik icon 🔄 dengan tooltip "Hot Restart"
|
||||
|
||||
#### B. Test Login
|
||||
Gunakan credentials ini:
|
||||
|
||||
| Username | Password |
|
||||
|----------|----------|
|
||||
| Aydin Fauzan | s002 |
|
||||
| HELGA FAISA_1 | s001 |
|
||||
| Mifta Okta Yanti | s003 |
|
||||
|
||||
**PENTING:**
|
||||
- Username HARUS persis sama (huruf besar/kecil)
|
||||
- Password adalah NIS (lowercase untuk s001-s003)
|
||||
|
||||
#### C. Jika Masih Gagal
|
||||
1. Cek log Flutter di terminal
|
||||
2. Cek apakah muncul error "Connection refused"
|
||||
3. Pastikan XAMPP Apache sudah running
|
||||
4. Cek IP dengan: `ipconfig` (kalau pakai real device)
|
||||
|
||||
---
|
||||
|
||||
## 🐛 DEBUG TAMBAHAN
|
||||
|
||||
### Jika Delete Masih Error:
|
||||
Jalankan command ini:
|
||||
```bash
|
||||
cd c:\xampp\htdocs\TugasAkhir\sim-pkpps
|
||||
php artisan route:clear
|
||||
php artisan config:clear
|
||||
php artisan view:clear
|
||||
php artisan cache:clear
|
||||
```
|
||||
|
||||
### Jika Login Mobile Masih Gagal:
|
||||
Test API manual:
|
||||
```bash
|
||||
# Di PowerShell
|
||||
$body = '{"id_santri":"Aydin Fauzan","password":"s002"}'
|
||||
Invoke-RestMethod -Uri "http://localhost/TugasAkhir/sim-pkpps/public/api/v1/login" -Method POST -ContentType "application/json" -Body $body
|
||||
```
|
||||
|
||||
Jika ini berhasil, berarti API OK, masalahnya di Flutter config.
|
||||
|
||||
---
|
||||
|
||||
## 📞 Masih Belum Bisa?
|
||||
|
||||
Kirim screenshot:
|
||||
1. Error di browser (F12 → Console)
|
||||
2. Error di Flutter terminal
|
||||
3. Hasil dari debug_comprehensive.php
|
||||
|
||||
Atau kirim:
|
||||
- URL yang Anda buka
|
||||
- Tombol apa yang diklik
|
||||
- Error message yang muncul
|
||||
|
||||
---
|
||||
|
||||
## ✅ Expected Results
|
||||
|
||||
### Delete:
|
||||
- Klik Hapus → Dialog konfirmasi → Klik OK → Akun hilang dari list
|
||||
- Muncul pesan hijau: "Akun wali [nama] berhasil dihapus"
|
||||
|
||||
### Reset Password:
|
||||
- Klik Reset → Dialog konfirmasi → Klik OK
|
||||
- Muncul pesan hijau: "Password akun [nama] berhasil direset ke NIS: [nis]"
|
||||
|
||||
### Login Mobile:
|
||||
- Input username & password → Klik Login
|
||||
- Loading sebentar → Masuk ke Dashboard
|
||||
- Menu Profil menampilkan data santri
|
||||
|
||||
---
|
||||
|
||||
**Semua code sudah benar! Tinggal clear cache & test!** 🚀
|
||||
|
|
@ -1,341 +0,0 @@
|
|||
# 📰 DOKUMENTASI FITUR BERITA - 3 KATEGORI TARGET
|
||||
|
||||
## 🎯 Cara Kerja Fitur Berita
|
||||
|
||||
Sistem berita memiliki **3 kategori target** yang menentukan siapa yang bisa melihat berita:
|
||||
|
||||
### 1️⃣ **SEMUA SANTRI** (`target_berita = 'semua'`)
|
||||
- **Siapa yang bisa lihat?** Semua santri yang login ke mobile app
|
||||
- **Kapan digunakan?** Untuk pengumuman umum, berita penting untuk semua santri
|
||||
- **Contoh:** Pengumuman libur, jadwal ujian, informasi umum pondok
|
||||
|
||||
### 2️⃣ **KELAS TERTENTU** (`target_berita = 'kelas_tertentu'`)
|
||||
- **Siapa yang bisa lihat?** Hanya santri dari kelas yang dipilih
|
||||
- **Field yang digunakan:** `target_kelas` (JSON array, contoh: `["PB", "Lambatan"]`)
|
||||
- **Kapan digunakan?** Untuk pengumuman khusus satu atau beberapa kelas
|
||||
- **Contoh:** Jadwal kegiatan kelas PB, tugas untuk kelas Cepatan
|
||||
|
||||
### 3️⃣ **SANTRI TERTENTU** (`target_berita = 'santri_tertentu'`)
|
||||
- **Siapa yang bisa lihat?** Hanya santri yang dipilih secara spesifik
|
||||
- **Relasi:** Menggunakan pivot table `berita_santri`
|
||||
- **Fitur tambahan:** Bisa tracking status "sudah dibaca" atau "belum dibaca"
|
||||
- **Kapan digunakan?** Untuk pesan personal, reminder individual
|
||||
- **Contoh:** Panggilan khusus, informasi pembayaran tertunggak, pemberitahuan pribadi
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Struktur Database
|
||||
|
||||
### Table: `berita`
|
||||
```sql
|
||||
CREATE TABLE berita (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
id_berita VARCHAR(10) UNIQUE, -- B001, B002, ...
|
||||
judul VARCHAR(255) NOT NULL,
|
||||
konten TEXT NOT NULL,
|
||||
penulis VARCHAR(255),
|
||||
gambar VARCHAR(255), -- Path ke storage
|
||||
status ENUM('draft', 'published'), -- Draft tidak muncul di mobile
|
||||
target_berita ENUM('semua', 'kelas_tertentu', 'santri_tertentu'),
|
||||
target_kelas JSON, -- ["PB", "Lambatan", "Cepatan"]
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Table: `berita_santri` (Pivot - untuk santri_tertentu)
|
||||
```sql
|
||||
CREATE TABLE berita_santri (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
id_berita VARCHAR(10), -- FK ke berita.id_berita
|
||||
id_santri VARCHAR(10), -- FK ke santris.id_santri
|
||||
sudah_dibaca BOOLEAN DEFAULT FALSE,
|
||||
tanggal_baca TIMESTAMP NULL,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP,
|
||||
FOREIGN KEY (id_berita) REFERENCES berita(id_berita),
|
||||
FOREIGN KEY (id_santri) REFERENCES santris(id_santri)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Alur Kerja Backend API
|
||||
|
||||
### Endpoint: `GET /api/v1/berita`
|
||||
|
||||
**Filter Logic (di `ApiBeritaController.php`):**
|
||||
```php
|
||||
$query = Berita::where('status', 'published')
|
||||
->where(function($q) use ($idSantri, $santri) {
|
||||
// 1. Berita untuk SEMUA
|
||||
$q->where('target_berita', 'semua')
|
||||
|
||||
// 2. Berita untuk KELAS TERTENTU (cek kelas santri)
|
||||
->orWhere(function($subQ) use ($santri) {
|
||||
$subQ->where('target_berita', 'kelas_tertentu')
|
||||
->whereJsonContains('target_kelas', $santri->kelas);
|
||||
})
|
||||
|
||||
// 3. Berita untuk SANTRI TERTENTU (cek pivot)
|
||||
->orWhere(function($subQ) use ($idSantri) {
|
||||
$subQ->where('target_berita', 'santri_tertentu')
|
||||
->whereHas('santriTertentu', function($pivot) use ($idSantri) {
|
||||
$pivot->where('id_santri', $idSantri);
|
||||
});
|
||||
});
|
||||
})
|
||||
->orderBy('created_at', 'desc');
|
||||
```
|
||||
|
||||
**Response Format:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"id_berita": "B001",
|
||||
"judul": "Pengumuman Libur",
|
||||
"konten": "...",
|
||||
"penulis": "Admin",
|
||||
"gambar_url": "http://localhost/storage/berita/image.jpg",
|
||||
"target_berita": "semua",
|
||||
"tanggal": "05 Feb 2026",
|
||||
"tanggal_lengkap": "05 February 2026, 10:30",
|
||||
"sudah_dibaca": false,
|
||||
"tanggal_baca": null
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 3,
|
||||
"total": 25
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 Implementasi Mobile (Flutter)
|
||||
|
||||
### API Service (`api_service.dart`):
|
||||
```dart
|
||||
Future<Map<String, dynamic>> getBerita({int page = 1}) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${AppConfig.baseUrl}/berita?page=$page'),
|
||||
headers: await _headers(needsAuth: true), // ✅ Token diperlukan
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
}
|
||||
return {'success': false};
|
||||
}
|
||||
```
|
||||
|
||||
### UI (`berita_page.dart`):
|
||||
- Menampilkan list berita yang sudah di-filter oleh backend
|
||||
- Badge "BARU" untuk berita belum dibaca (khusus `santri_tertentu`)
|
||||
- Pull-to-refresh untuk update data
|
||||
- Load more pagination
|
||||
|
||||
---
|
||||
|
||||
## ✅ CHECKLIST TROUBLESHOOTING
|
||||
|
||||
### ❌ **Berita Tidak Muncul di Mobile?**
|
||||
|
||||
#### 1. **Cek Database - Ada Berita Published?**
|
||||
```sql
|
||||
SELECT id_berita, judul, status, target_berita, target_kelas
|
||||
FROM berita
|
||||
WHERE status = 'published';
|
||||
```
|
||||
- ❌ Jika kosong → **Buat berita baru dan set status 'published'**
|
||||
- ❌ Jika status 'draft' → **Berita tidak akan muncul di mobile**
|
||||
|
||||
#### 2. **Cek Target Berita**
|
||||
|
||||
**Untuk target 'semua':**
|
||||
- ✅ Otomatis muncul untuk semua santri yang login
|
||||
|
||||
**Untuk target 'kelas_tertentu':**
|
||||
```sql
|
||||
SELECT id_berita, judul, target_kelas
|
||||
FROM berita
|
||||
WHERE target_berita = 'kelas_tertentu';
|
||||
```
|
||||
- ✅ Pastikan `target_kelas` berisi JSON array: `["PB"]`, `["Lambatan", "Cepatan"]`
|
||||
- ✅ Cek kelas santri yang login cocok dengan `target_kelas`
|
||||
|
||||
**Untuk target 'santri_tertentu':**
|
||||
```sql
|
||||
SELECT bs.*, b.judul, s.nama_lengkap
|
||||
FROM berita_santri bs
|
||||
JOIN berita b ON bs.id_berita = b.id_berita
|
||||
JOIN santris s ON bs.id_santri = s.id_santri
|
||||
WHERE b.status = 'published';
|
||||
```
|
||||
- ✅ Pastikan ada data di pivot table `berita_santri`
|
||||
- ✅ Pastikan `id_santri` sesuai dengan santri yang login
|
||||
|
||||
#### 3. **Cek User Login & Role**
|
||||
```sql
|
||||
SELECT u.id, u.username, u.role, u.role_id, s.nama_lengkap, s.kelas
|
||||
FROM users u
|
||||
LEFT JOIN santris s ON u.role_id = s.id_santri
|
||||
WHERE u.role = 'wali';
|
||||
```
|
||||
- ✅ Pastikan user memiliki `role = 'wali'`
|
||||
- ✅ Pastikan `role_id` terisi dengan `id_santri` yang valid
|
||||
- ✅ Pastikan santri dengan `id_santri` tersebut ada dan statusnya 'Aktif'
|
||||
|
||||
#### 4. **Cek API Response**
|
||||
|
||||
**Test di browser/Postman:**
|
||||
```
|
||||
GET http://localhost/TugasAkhir/sim-pkpps/public/api/v1/berita
|
||||
Header: Authorization: Bearer <token_dari_login>
|
||||
```
|
||||
|
||||
Response yang benar:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [...] // Array berisi berita
|
||||
}
|
||||
```
|
||||
|
||||
Response error:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Unauthenticated." // ❌ Token tidak valid/expired
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. **Cek Mobile App (Flutter Debug Console)**
|
||||
|
||||
Setelah login dan buka halaman Berita, lihat console:
|
||||
```
|
||||
🔵 GET BERITA URL: http://...
|
||||
🔵 Berita Response Status: 200
|
||||
🔵 Berita Response Body: {"success":true,"data":[...]}
|
||||
✅ Berita berhasil dimuat: 5 item
|
||||
```
|
||||
|
||||
Error yang mungkin:
|
||||
```
|
||||
🔴 Berita SocketException → Server tidak jalan
|
||||
🔴 Berita error: 401 → Token tidak valid
|
||||
🔴 Berita Error: FormatException → Response bukan JSON valid
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ CARA MEMBUAT BERITA BARU
|
||||
|
||||
### Via Admin Web (Laravel):
|
||||
|
||||
1. **Login ke Admin Panel**
|
||||
```
|
||||
http://localhost/TugasAkhir/sim-pkpps/public/login
|
||||
```
|
||||
|
||||
2. **Buka Menu Berita → Tambah Berita**
|
||||
|
||||
3. **Isi Form:**
|
||||
- **Judul:** Judul berita yang menarik
|
||||
- **Konten:** Isi berita lengkap
|
||||
- **Penulis:** Nama penulis/admin
|
||||
- **Gambar:** (Optional) Upload gambar
|
||||
- **Status:** Pilih **"Published"** agar muncul di mobile
|
||||
- **Target Berita:**
|
||||
- **Semua Santri** → Semua bisa lihat
|
||||
- **Kelas Tertentu** → Pilih kelas (bisa lebih dari 1)
|
||||
- **Santri Tertentu** → Pilih santri spesifik (bisa lebih dari 1)
|
||||
|
||||
4. **Simpan**
|
||||
|
||||
### Via SQL (Quick Test):
|
||||
|
||||
**Berita untuk SEMUA santri:**
|
||||
```sql
|
||||
INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, created_at, updated_at)
|
||||
VALUES ('B001', 'Pengumuman Libur', 'Pondok libur tanggal 10-15 Februari 2026', 'Admin', 'published', 'semua', NOW(), NOW());
|
||||
```
|
||||
|
||||
**Berita untuk KELAS PB:**
|
||||
```sql
|
||||
INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, target_kelas, created_at, updated_at)
|
||||
VALUES ('B002', 'Jadwal Kelas PB', 'Kegiatan kelas PB dimulai jam 08:00', 'Admin', 'published', 'kelas_tertentu', '["PB"]', NOW(), NOW());
|
||||
```
|
||||
|
||||
**Berita untuk SANTRI TERTENTU (2 steps):**
|
||||
```sql
|
||||
-- Step 1: Buat berita
|
||||
INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, created_at, updated_at)
|
||||
VALUES ('B003', 'Pesan Khusus', 'Harap menemui admin', 'Admin', 'published', 'santri_tertentu', NOW(), NOW());
|
||||
|
||||
-- Step 2: Tambah ke pivot table
|
||||
INSERT INTO berita_santri (id_berita, id_santri, sudah_dibaca, created_at, updated_at)
|
||||
VALUES ('B003', 'S001', FALSE, NOW(), NOW()); -- Ganti S001 dengan id_santri yang sesuai
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 FILE TESTING
|
||||
|
||||
Gunakan file `test_api_berita.php` untuk debugging:
|
||||
```
|
||||
http://localhost/TugasAkhir/test_api_berita.php
|
||||
```
|
||||
|
||||
File ini akan menampilkan:
|
||||
1. ✅ Semua berita di database
|
||||
2. ✅ Sample data santri
|
||||
3. ✅ Pivot table berita_santri
|
||||
4. ✅ Data user/wali
|
||||
5. ✅ Simulasi filter berita untuk santri tertentu
|
||||
|
||||
---
|
||||
|
||||
## 📊 CONTOH SKENARIO
|
||||
|
||||
### Skenario 1: Pengumuman Umum
|
||||
- **Target:** Semua Santri
|
||||
- **Contoh:** "Libur Pondok 10-15 Februari"
|
||||
- **Setting:** `target_berita = 'semua'`
|
||||
- **Result:** Semua santri yang login bisa lihat
|
||||
|
||||
### Skenario 2: Info Kelas
|
||||
- **Target:** Kelas PB dan Lambatan
|
||||
- **Contoh:** "Jadwal Ujian Kelas PB & Lambatan"
|
||||
- **Setting:** `target_berita = 'kelas_tertentu'`, `target_kelas = ["PB", "Lambatan"]`
|
||||
- **Result:** Hanya santri kelas PB dan Lambatan yang bisa lihat
|
||||
|
||||
### Skenario 3: Pesan Personal
|
||||
- **Target:** Santri Ahmad (S001) dan Budi (S002)
|
||||
- **Contoh:** "Harap menemui admin untuk pengecekan kesehatan"
|
||||
- **Setting:** `target_berita = 'santri_tertentu'`, pivot table isi S001 dan S002
|
||||
- **Result:** Hanya Ahmad dan Budi yang bisa lihat, dengan badge "BARU" sampai mereka buka
|
||||
|
||||
---
|
||||
|
||||
## 🎓 KESIMPULAN
|
||||
|
||||
Fitur berita dengan 3 kategori target ini memberikan fleksibilitas:
|
||||
- **Efisien** → Tidak perlu kirim satu-satu
|
||||
- **Fleksibel** → Bisa target sesuai kebutuhan
|
||||
- **Trackable** → Bisa tracking siapa yang sudah baca (untuk santri_tertentu)
|
||||
- **Secure** → Filter di backend, mobile tidak bisa akses berita yang bukan haknya
|
||||
|
||||
**Backend sudah benar**, pastikan:
|
||||
1. ✅ Data berita ada dan status 'published'
|
||||
2. ✅ Target berita sesuai dengan santri yang login
|
||||
3. ✅ Token authentication valid
|
||||
4. ✅ Server Laravel jalan
|
||||
5. ✅ Koneksi database OK
|
||||
|
||||
Jika masih ada masalah, cek console Flutter untuk error spesifik!
|
||||
|
|
@ -1,547 +0,0 @@
|
|||
# DOKUMENTASI FITUR CMS PEMBINAAN & SANKSI
|
||||
|
||||
**Tanggal:** 9 Februari 2026
|
||||
**Status:** ✅ SELESAI - Full CMS Implementation
|
||||
|
||||
---
|
||||
|
||||
## 🎯 OVERVIEW FITUR
|
||||
|
||||
Fitur **Pembinaan & Sanksi** telah dikembangkan menjadi **Content Management System (CMS) yang fleksibel** dengan **Rich Text Editor** untuk memudahkan admin dalam membuat dan mengelola konten.
|
||||
|
||||
### ✨ Keunggulan Fitur:
|
||||
1. **Rich Text Editor** (TinyMCE) - Tidak perlu coding HTML manual
|
||||
2. **WYSIWYG** (What You See Is What You Get) - Preview langsung saat mengetik
|
||||
3. **Format Konten Fleksibel** - Bisa buat apa saja: peraturan, tata tertib, pembinaan, dll
|
||||
4. **Formatting Lengkap** - Bold, italic, heading, list, table, color, dll
|
||||
5. **Urutan Konten** - Bisa diatur urutannya
|
||||
6. **Status Aktif/Nonaktif** - Konten bisa disembunyikan tanpa dihapus
|
||||
|
||||
---
|
||||
|
||||
## 📋 FITUR YANG TERSEDIA
|
||||
|
||||
### 1. **Create (Tambah Konten)**
|
||||
- ✅ Form dengan Rich Text Editor (Quill.js)
|
||||
- ✅ Auto-generate ID (PS001, PS002, dst)
|
||||
- ✅ Toolbar lengkap untuk formatting
|
||||
- ✅ Info box dengan tips penggunaan
|
||||
- ✅ Preview langsung saat mengetik
|
||||
|
||||
**Toolbar Editor:**
|
||||
- 📋 **Header** - H1, H2, H3 untuk judul & sub judul
|
||||
- **B** Bold - Tebal
|
||||
- *I* Italic - Miring
|
||||
- <u>U</u> Underline - Garis bawah
|
||||
- <s>S</s> Strike - Coret
|
||||
- 🎨 Text Color - Warna teks
|
||||
- 🎨 Background Color - Warna latar
|
||||
- ⬅️ Align Left/Center/Right/Justify
|
||||
- 📋 Bullet List - Daftar dengan bullet
|
||||
- 🔢 Number List - Daftar bernomor
|
||||
- ↹ Indent/Outdent - Indentasi
|
||||
- 🔗 Link - Hyperlink
|
||||
- 🖼️ Image - Gambar (URL)
|
||||
- 🧹 Clean - Hapus format
|
||||
|
||||
### 2. **Read (Index & Detail)**
|
||||
**Index Page:**
|
||||
- ✅ Daftar semua konten dalam tabel
|
||||
- ✅ Preview singkat konten (100 karakter)
|
||||
- ✅ Info waktu update (difForHumans)
|
||||
- ✅ Sorting by urutan
|
||||
- ✅ Badge urutan dan status
|
||||
- ✅ Navigasi ke Master Pelanggaran
|
||||
|
||||
**Detail Page:**
|
||||
- ✅ Tampilan informasi lengkap
|
||||
- ✅ Konten ditampilkan dengan format HTML yang rapi
|
||||
- ✅ Custom CSS styling untuk konten
|
||||
- ✅ Info created/updated timestamp
|
||||
- ✅ Tombol edit & kembali
|
||||
|
||||
### 3. **Update (Edit Konten)**
|
||||
- ✅ Form dengan Rich Text Editor
|
||||
- ✅ Load konten existing ke editor
|
||||
- ✅ Toolbar sama seperti create
|
||||
- ✅ Tombol "Lihat Detail" untuk preview
|
||||
- ✅ Alert info untuk membantu user
|
||||
|
||||
### 4. **Delete (Hapus Konten)**
|
||||
- ✅ Konfirmasi dengan nama judul
|
||||
- ✅ Warning: data tidak bisa dikembalikan
|
||||
- ✅ Soft delete ready (jika diperlukan nanti)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ TEKNOLOGI YANG DIGUNAKAN
|
||||
|
||||
### Rich Text Editor: **Quill.js 1.3.6**
|
||||
```html
|
||||
<!-- CSS -->
|
||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
|
||||
<!-- JS -->
|
||||
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
|
||||
```
|
||||
|
||||
**Keunggulan Quill.js:**
|
||||
- ✅ **100% Gratis** - Tidak perlu API key atau registrasi
|
||||
- ✅ **Open Source** - MIT License
|
||||
- ✅ **Ringan** - Hanya ~50KB gzipped
|
||||
- ✅ **Modern** - API yang clean dan mudah digunakan
|
||||
- ✅ **Cross-browser** - Support semua browser modern
|
||||
- ✅ **Mobile Friendly** - Touch support
|
||||
|
||||
**Konfigurasi:**
|
||||
- Theme: Snow (clean & modern)
|
||||
- Height: Min 350px, Max 600px (scrollable)
|
||||
- Toolbar: Header, Bold, Italic, Color, List, Align, Link, Image
|
||||
- Auto-sync: Real-time sync ke textarea
|
||||
- Validation: Empty content check
|
||||
|
||||
### Database Structure:
|
||||
**Table:** `pembinaan_sanksis`
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| id | bigint unsigned | Primary key |
|
||||
| id_pembinaan | varchar(10) | Auto ID (PS001, PS002) |
|
||||
| judul | varchar(255) | Judul konten |
|
||||
| konten | text | HTML content |
|
||||
| urutan | int | Urutan tampilan (default 0) |
|
||||
| is_active | boolean | Status (default true) |
|
||||
| created_at | timestamp | Waktu dibuat |
|
||||
| updated_at | timestamp | Waktu diupdate |
|
||||
|
||||
**Indexes:**
|
||||
- id_pembinaan (unique)
|
||||
- urutan
|
||||
- is_active
|
||||
|
||||
---
|
||||
|
||||
## 📁 FILE YANG DIUPDATE
|
||||
|
||||
### 1. **Views**
|
||||
```
|
||||
resources/views/admin/pembinaan_sanksi/
|
||||
├── index.blade.php ✅ Updated dengan preview & navigasi
|
||||
├── create.blade.php ✅ Updated dengan TinyMCE
|
||||
├── edit.blade.php ✅ Updated dengan TinyMCE
|
||||
└── show.blade.php ✅ Updated dengan HTML rendering & styling
|
||||
```
|
||||
|
||||
### 2. **Controller**
|
||||
```
|
||||
app/Http/Controllers/Admin/PembinaanSanksiController.php
|
||||
```
|
||||
✅ Sudah lengkap (tidak perlu update)
|
||||
- CRUD complete
|
||||
- Validation proper
|
||||
- Route model binding
|
||||
|
||||
### 3. **Model**
|
||||
```
|
||||
app/Models/PembinaanSanksi.php
|
||||
```
|
||||
✅ Sudah lengkap
|
||||
- Auto-generate ID
|
||||
- Scopes: aktif(), byUrutan()
|
||||
- Fillable & casts proper
|
||||
|
||||
### 4. **Migration**
|
||||
```
|
||||
database/migrations/2026_02_09_071441_create_pembinaan_sanksis_table.php
|
||||
```
|
||||
✅ Sudah lengkap
|
||||
|
||||
---
|
||||
|
||||
## 🎨 CONTOH PENGGUNAAN
|
||||
|
||||
### Contoh 1: Membuat Peraturan Pondok
|
||||
**Judul:** `Tata Tertib Pondok`
|
||||
|
||||
**Konten (menggunakan editor):**
|
||||
```
|
||||
TATA TERTIB PONDOK PESANTREN
|
||||
|
||||
I. Kewajiban Santri
|
||||
Setiap santri wajib:
|
||||
1. Mengikuti seluruh kegiatan yang telah dijadwalkan
|
||||
2. Menjaga kebersihan kamar dan lingkungan pondok
|
||||
3. Berpakaian sesuai dengan ketentuan yang berlaku
|
||||
|
||||
II. Larangan Bagi Santri
|
||||
Dilarang keras:
|
||||
• Keluar pondok tanpa izin
|
||||
• Membawa handphone tanpa izin
|
||||
• Berkelahi atau berbuat kerusuhan
|
||||
```
|
||||
|
||||
### Contoh 2: Membuat Pembinaan & Sanksi
|
||||
**Judul:** `PEMBINAAN DAN SANKSI`
|
||||
|
||||
**Konten (dengan formatting):**
|
||||
- Heading 1 untuk judul utama
|
||||
- Heading 2 untuk sub judul
|
||||
- Bold untuk penekanan
|
||||
- Numbered list untuk poin-poin
|
||||
- Color untuk highlight penting
|
||||
- Table untuk jadwal
|
||||
|
||||
### Contoh 3: Membuat Peraturan Khusus
|
||||
**Judul:** `Peraturan Kepulangan Santri`
|
||||
|
||||
**Konten:**
|
||||
- Bisa pakai emoji/icon
|
||||
- Background color untuk warning box
|
||||
- Border styling untuk info penting
|
||||
- List dengan sub-list
|
||||
|
||||
---
|
||||
|
||||
## 🚀 CARA PENGGUNAAN
|
||||
|
||||
### A. Menambah Konten Baru
|
||||
|
||||
1. **Akses Menu**
|
||||
```
|
||||
Admin Menu → Master Pelanggaran → Pembinaan & Sanksi
|
||||
```
|
||||
Atau:
|
||||
```
|
||||
http://localhost/TugasAkhir/sim-pkpps/public/admin/pembinaan-sanksi
|
||||
```
|
||||
|
||||
2. **Klik "Tambah Konten"**
|
||||
|
||||
3. **Isi Form:**
|
||||
- **Judul:** Masukkan judul yang jelas (contoh: "Tata Tertib Pondok")
|
||||
- **Konten:** Gunakan editor untuk membuat konten
|
||||
- **Urutan:** Atur urutan tampilan (0 = paling atas)
|
||||
- **Status:** Centang "Aktif" agar ditampilkan
|
||||
|
||||
4. **Gunakan Toolbar:**
|
||||
- Blok teks → Bold/Italic/Underline
|
||||
- Pilih Styles → Heading 1/2/3 untuk judul
|
||||
- Klik icon list → Numbered atau Bullet list
|
||||
- Klik icon table → Insert table
|
||||
|
||||
5. **Klik "Simpan"**
|
||||
|
||||
### B. Edit Konten
|
||||
|
||||
1. **Dari index, klik tombol Edit (kuning)**
|
||||
2. **Ubah konten di editor**
|
||||
3. **Preview dengan "Lihat Detail"** (opsional)
|
||||
4. **Klik "Update"**
|
||||
|
||||
### C. Menghapus Konten
|
||||
|
||||
1. **Dari index, klik tombol Hapus (merah)**
|
||||
2. **Konfirmasi penghapusan**
|
||||
3. **Konten akan terhapus permanen**
|
||||
|
||||
### D. Mengatur Urutan
|
||||
|
||||
1. **Edit konten yang ingin diatur**
|
||||
2. **Ubah "Urutan Tampilan"**
|
||||
- 0 = Paling atas
|
||||
- 1 = Kedua
|
||||
- 2 = Ketiga, dst
|
||||
3. **Klik "Update"**
|
||||
|
||||
---
|
||||
|
||||
## 💡 TIPS & TRIK
|
||||
|
||||
### 1. **Membuat Judul yang Menarik**
|
||||
```
|
||||
Gunakan Heading 1 untuk judul utama
|
||||
Gunakan Heading 2 untuk sub judul
|
||||
Gunakan Bold untuk penekanan kata
|
||||
```
|
||||
|
||||
### 2. **Membuat Daftar Bernomor**
|
||||
```
|
||||
Pilih text → Klik icon "Numbered list"
|
||||
Tekan Enter untuk nomor berikutnya
|
||||
Tekan Tab untuk sub-list (nested)
|
||||
```
|
||||
|
||||
### 3. **Membuat Warning Box**
|
||||
```
|
||||
1. Ketik text warning
|
||||
2. Blok text
|
||||
3. Ubah background color → Kuning/Merah
|
||||
4. Tambah border dengan align center
|
||||
```
|
||||
|
||||
### 4. **Membuat Tabel**
|
||||
```
|
||||
1. Klik icon Table
|
||||
2. Pilih rows x columns
|
||||
3. Isi data di cell
|
||||
4. Right click → Table properties untuk styling
|
||||
```
|
||||
|
||||
### 5. **Copy dari Word/Excel**
|
||||
```
|
||||
⚠️ Jangan copy-paste langsung!
|
||||
1. Copy dari Word
|
||||
2. Klik "Paste as text" di editor
|
||||
3. Format ulang dengan toolbar
|
||||
```
|
||||
|
||||
### 6. **Best Practices**
|
||||
- ✅ Gunakan heading untuk struktur
|
||||
- ✅ Konsisten dalam formatting
|
||||
- ✅ Gunakan list untuk poin-poin
|
||||
- ✅ Hindari terlalu banyak warna
|
||||
- ✅ Test preview sebelum publish
|
||||
|
||||
---
|
||||
|
||||
## 📊 SAMPLE KONTEN YANG SUDAH DIBUAT
|
||||
|
||||
### 1. **PEMBINAAN DAN SANKSI**
|
||||
- Urutan: 1
|
||||
- Konten: Tujuan pembinaan, jenis sanksi, ketentuan kafaroh
|
||||
- Format: H1, H2, numbered list, text color, bold/italic
|
||||
|
||||
### 2. **Tata Tertib Pondok**
|
||||
- Urutan: 2
|
||||
- Konten: Kewajiban santri, larangan, jadwal harian
|
||||
- Format: H1, H2, bullet list, table, text color
|
||||
|
||||
### 3. **Peraturan Kepulangan Santri**
|
||||
- Urutan: 3
|
||||
- Konten: Waktu kepulangan, prosedur, hal penting
|
||||
- Format: H1, H2, emoji/icon, colored boxes, lists
|
||||
|
||||
---
|
||||
|
||||
## 🎯 KEGUNAAN KONTEN
|
||||
|
||||
### Untuk Admin:
|
||||
✅ Mudah membuat dan update peraturan
|
||||
✅ Tidak perlu coding HTML
|
||||
✅ Format konten profesional
|
||||
✅ Bisa buat berbagai jenis dokumen
|
||||
|
||||
### Untuk Santri/Wali:
|
||||
✅ Informasi jelas dan terstruktur
|
||||
✅ Mudah dibaca dengan formatting yang baik
|
||||
✅ Bisa akses kapan saja
|
||||
✅ Update otomatis jika ada perubahan
|
||||
|
||||
---<2D> DOKUMENTASI QUILL.JS
|
||||
|
||||
### Kenapa Quill.js?
|
||||
|
||||
**Sebelumnya:** TinyMCE (perlu API key, ada warning)
|
||||
**Sekarang:** Quill.js (100% gratis, no API key!)
|
||||
|
||||
**Perbandingan:**
|
||||
|
||||
| Fitur | TinyMCE | Quill.js |
|
||||
|-------|---------|----------|
|
||||
| API Key | ❌ Perlu (gratis tapi harus daftar) | ✅ Tidak perlu |
|
||||
| Warning | ⚠️ Ada | ✅ Tidak ada |
|
||||
| Size | ~500KB | ✅ ~50KB |
|
||||
| License | Freemium | ✅ MIT (Open Source) |
|
||||
| Setup | Complex | ✅ Simple |
|
||||
| Mobile | Good | ✅ Excellent |
|
||||
|
||||
### Features Quill.js:
|
||||
|
||||
✅ **WYSIWYG Editor** - What You See Is What You Get
|
||||
✅ **Semantic HTML** - Output HTML yang clean
|
||||
✅ **Custom Toolbar** - Toolbar sesuai kebutuhan
|
||||
✅ **Keyboard Shortcuts** - Ctrl+B, Ctrl+I, dll
|
||||
✅ **Paste from Word** - Copy-paste dari Word/Excel
|
||||
✅ **Cross-platform** - Windows, Mac, Linux, Mobile
|
||||
|
||||
### Official Resources:
|
||||
|
||||
- Website: https://quilljs.com/
|
||||
- Documentation: https://quilljs.com/docs/
|
||||
- GitHub: https://github.com/quilljs/quill
|
||||
- License: MIT (Free forever!)
|
||||
|
||||
---
|
||||
|
||||
## 🔗 INTEGRASI DENGAN MENU LAIN
|
||||
|
||||
### Navigasi Breadcrumb:
|
||||
```
|
||||
Master Pelanggaran → Pembinaan & Sanksi
|
||||
```
|
||||
|
||||
**Dari Pembinaan & Sanksi**, ada tombol:
|
||||
- "Master Pelanggaran" → Kembali ke kategori pelanggaran
|
||||
|
||||
**Dari Master Pelanggaran**, ada tombol:
|
||||
- "Klasifikasi Pelanggaran" → Ke klasifikasi
|
||||
- "Pembinaan & Sanksi" → Ke pembinaan & sanksi
|
||||
- "Tambah Pelanggaran" → Tambah pelanggaran
|
||||
|
||||
---
|
||||
|
||||
## 🧪 TESTING
|
||||
|
||||
### Test 1: Create Konten
|
||||
1. ✅ Buka create form
|
||||
2. ✅ Editor TinyMCE loaded
|
||||
3. ✅ Isi judul dan konten
|
||||
4. ✅ Gunakan berbagai formatting
|
||||
5. ✅ Submit → Data tersimpan
|
||||
6. ✅ HTML di database
|
||||
|
||||
### Test 2: Edit Konten
|
||||
1. ✅ Buka edit form
|
||||
2. ✅ Konten HTML di-load ke editor
|
||||
3. ✅ Edit konten
|
||||
4. ✅ Submit → Data terupdate
|
||||
|
||||
### Test 3: View Konten
|
||||
1. ✅ Buka detail page
|
||||
2. ✅ HTML di-render dengan benar
|
||||
3. ✅ Formatting tetap terjaga
|
||||
4. ✅ Styling CSS applied
|
||||
|
||||
### Test 4: Delete Konten
|
||||
1. ✅ Klik delete
|
||||
2. ✅ Konfirmasi muncul
|
||||
3. ✅ Data terhapus dari database
|
||||
|
||||
---
|
||||
|
||||
## 🎓 VIDEO TUTORIAL (Untuk User)
|
||||
|
||||
### Topik yang Bisa Dibuat:
|
||||
1. **Cara Menambah Konten Baru**
|
||||
- Login admin
|
||||
- Akses menu
|
||||
- Isi form dengan editor
|
||||
- Submit & review
|
||||
|
||||
2. **Cara Menggunakan Rich Text Editor**
|
||||
- Toolbar overview
|
||||
- Membuat heading
|
||||
- Membuat list
|
||||
- Membuat table
|
||||
- Coloring & formatting
|
||||
|
||||
3. **Tips Membuat Konten Profesional**
|
||||
- Structure content
|
||||
- Consistent formatting
|
||||
- Use of headings
|
||||
- Best practices
|
||||
|
||||
---
|
||||
|
||||
## 📱 RESPONSIVE DESIGN
|
||||
|
||||
✅ **Desktop:** Full editor dengan toolbar lengkap
|
||||
✅ **Tablet:** Editor adjustable, toolbar wrap
|
||||
✅ **Mobile:** Editor tetap usable (tapi recommend desktop)
|
||||
|
||||
**Note:** Untuk edit konten yang kompleks, sangat disarankan menggunakan desktop/laptop.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 SECURITY
|
||||
|
||||
### XSS Protection:
|
||||
- ✅ Konten disimpan sebagai HTML (sanitized by TinyMCE)
|
||||
- ✅ Output dengan `{!! !!}` untuk render HTML
|
||||
- ✅ Input validation di controller
|
||||
- ✅ CSRF protection
|
||||
|
||||
### Access Control:
|
||||
- ✅ Only admin can CRUD
|
||||
- ✅ Middleware: `auth`, `role:admin`
|
||||
- ✅ Route protection
|
||||
|
||||
---
|
||||
|
||||
## 🚀 FUTURE ENHANCEMENTS (Opsional)
|
||||
|
||||
### 1. **Image Upload**
|
||||
- Upload gambar ke server
|
||||
- Insert image di konten
|
||||
- Image gallery
|
||||
|
||||
### 2. **Template Library**
|
||||
- Pre-made templates
|
||||
- Quick insert template
|
||||
- Custom save template
|
||||
|
||||
### 3. **Version Control**
|
||||
- History perubahan konten
|
||||
- Rollback to previous version
|
||||
- Compare versions
|
||||
|
||||
### 4. **Export/Import**
|
||||
- Export konten ke PDF
|
||||
- Import dari Word
|
||||
- Backup & restore
|
||||
|
||||
### 5. **Multi-language**
|
||||
- Konten dalam bahasa Indonesia & Inggris
|
||||
- Switch language di frontend
|
||||
|
||||
---
|
||||
|
||||
## 📞 SUPPORT
|
||||
|
||||
Jika ada pertanyaan atau masalah:
|
||||
1. Cek dokumentasi ini
|
||||
2. Lihat sample konten yang sudah dibuat
|
||||
3. Test di environment development dulu
|
||||
4. Contact developer jika perlu
|
||||
|
||||
---
|
||||
|
||||
## ✅ CHECKLIST IMPLEMENTASI
|
||||
|
||||
- [x] Rich Text Editor (TinyMCE) integrated
|
||||
- [x] Create form dengan editor
|
||||
- [x] Edit form dengan editor
|
||||
- [x] Show page dengan HTML rendering
|
||||
- [x] Index page dengan preview
|
||||
- [x] Toolbar lengkap (heading, bold, italic, list, table, color, dll)
|
||||
- [x] Auto-save to database as HTML
|
||||
- [x] WYSIWYG editor
|
||||
- [x] Sample content inserted
|
||||
- [x] CSS styling untuk konten
|
||||
- [x] Navigation buttons
|
||||
- [x] Responsive design
|
||||
- [x] Security (XSS, CSRF)
|
||||
- [x] Validation proper
|
||||
- [x] User-friendly interface
|
||||
- [x] Info boxes & tips
|
||||
- [x] Dokumentasi lengkap
|
||||
|
||||
---
|
||||
|
||||
## 🎉 KESIMPULAN
|
||||
|
||||
Fitur **Pembinaan & Sanksi** telah berhasil dikembangkan menjadi **CMS yang fleksibel dan mudah digunakan**. Admin dapat dengan mudah membuat, mengedit, dan mengelola konten dengan format yang profesional tanpa perlu mengetahui coding HTML.
|
||||
|
||||
**Keunggulan Utama:**
|
||||
1. ✅ **User-Friendly** - Editor WYSIWYG yang mudah
|
||||
2. ✅ **Fleksibel** - Bisa buat konten apa saja
|
||||
3. ✅ **Profesional** - Format rapi dengan styling
|
||||
4. ✅ **Efisien** - Tidak perlu coding manual
|
||||
5. ✅ **Terintegrasi** - Part dari menu pelanggaran
|
||||
|
||||
**Ready to Use!** 🚀
|
||||
|
||||
---
|
||||
|
||||
**Dibuat oleh:** GitHub Copilot
|
||||
**Tanggal:** 9 Februari 2026
|
||||
**Verified:** ✅ All Features Working
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
# DOKUMENTASI DASHBOARD KEGIATAN SANTRI
|
||||
|
||||
## 📊 Overview
|
||||
Dashboard Kegiatan Santri adalah fitur baru yang menampilkan jadwal kegiatan hari ini dengan progress absensi real-time, mengurangi redundansi menu, dan menambahkan visualisasi yang berguna.
|
||||
|
||||
## ✅ Fitur yang Telah Diimplementasikan
|
||||
|
||||
### A. Halaman Dashboard Kegiatan Hari Ini
|
||||
**Route:** `/admin/kegiatan`
|
||||
|
||||
#### 1. KPI Cards (Key Performance Indicators)
|
||||
Dashboard menampilkan 4 kartu statistik utama:
|
||||
- **Total Kegiatan Hari Ini** - Jumlah kegiatan yang dijadwalkan untuk hari yang dipilih
|
||||
- **Kegiatan Selesai** - Jumlah kegiatan yang sudah selesai dilaksanakan
|
||||
- **Rata-rata Kehadiran** - Persentase rata-rata kehadiran santri di semua kegiatan
|
||||
- **Sedang Berlangsung** - Jumlah kegiatan yang sedang berlangsung (real-time)
|
||||
|
||||
#### 2. Filter & Quick Actions
|
||||
- **Dropdown Pilih Hari** - Filter berdasarkan hari (Senin-Ahad)
|
||||
- **Date Picker** - Filter berdasarkan tanggal spesifik
|
||||
- **Tombol "Lihat Semua Jadwal"** - Link ke halaman jadwal lengkap
|
||||
- **Tombol "Tambah Kegiatan"** - Link ke form tambah kegiatan baru
|
||||
- **Tombol "Reset"** - Reset filter ke hari ini
|
||||
|
||||
#### 3. Card Kegiatan (Timeline View)
|
||||
Setiap kegiatan ditampilkan dalam card dengan informasi:
|
||||
|
||||
**Informasi Kegiatan:**
|
||||
- Waktu (jam mulai - jam selesai)
|
||||
- Hari dengan badge berwarna
|
||||
- Nama Kegiatan dengan icon kategori
|
||||
- Kategori dengan badge berwarna sesuai kategori
|
||||
- Materi (jika ada)
|
||||
|
||||
**Status Badge:**
|
||||
- 🟢 **Sedang Berlangsung** (hijau) - Animasi pulse
|
||||
- 🔵 **Selesai** (biru)
|
||||
- ⚪ **Belum Dimulai** (abu-abu)
|
||||
|
||||
Status diupdate otomatis berdasarkan waktu real-time sistem.
|
||||
|
||||
**Progress Bar Absensi:**
|
||||
- Menampilkan: "X/Y santri hadir (Z%)"
|
||||
- Warna dinamis:
|
||||
- 🟢 Hijau: >85% hadir
|
||||
- 🟡 Kuning: 70-85% hadir
|
||||
- 🟠 Orange: 50-70% hadir
|
||||
- 🔴 Merah: <50% hadir
|
||||
- Animasi smooth transition
|
||||
|
||||
**Quick Actions per Kegiatan:**
|
||||
- **Input Absensi** → Redirect ke halaman input absensi kegiatan
|
||||
- **Lihat Detail** → Modal popup (coming soon)
|
||||
- **Rekap** → Redirect ke rekap absensi kegiatan
|
||||
- **Info** → Detail kegiatan lengkap
|
||||
|
||||
#### 4. Empty State
|
||||
Jika tidak ada kegiatan di hari yang dipilih, ditampilkan:
|
||||
- Icon kalender
|
||||
- Pesan "Tidak ada kegiatan dijadwalkan hari ini"
|
||||
- Button "Buat Kegiatan Baru"
|
||||
- Button "Lihat Semua Jadwal"
|
||||
|
||||
### B. Halaman Jadwal Lengkap
|
||||
**Route:** `/admin/kegiatan/jadwal/semua`
|
||||
|
||||
Menampilkan daftar semua jadwal kegiatan dalam tabel dengan fitur:
|
||||
- **Filter:** Hari, Kategori, Search
|
||||
- **Pagination:** 15 data per halaman
|
||||
- **Action Buttons:** Detail, Edit, Hapus
|
||||
- **Quick Access:**
|
||||
- Button ke Dashboard Kegiatan
|
||||
- Button ke Kategori Kegiatan
|
||||
- Button Tambah Kegiatan
|
||||
|
||||
**Note:** Menggunakan view yang sama dengan index lama (`index.blade.php`) untuk menghindari duplikasi.
|
||||
|
||||
### C. Struktur Menu Sidebar (Updated)
|
||||
|
||||
**Kegiatan Santri** (Parent Menu - Dropdown)
|
||||
```
|
||||
├── 📊 Dashboard Kegiatan (NEW)
|
||||
├── ✅ Absensi Kegiatan
|
||||
├── 💳 Kartu RFID
|
||||
└── 📊 Laporan & Statistik
|
||||
```
|
||||
|
||||
**Perubahan dari struktur lama:**
|
||||
- ❌ Removed: Menu "Kategori Kegiatan" (dipindah ke quick access di halaman jadwal)
|
||||
- ❌ Removed: Menu "Jadwal Kegiatan" (sekarang jadi Dashboard)
|
||||
- ✅ Added: Menu "Dashboard Kegiatan" sebagai landing page utama
|
||||
- ✅ Updated: Icon "Laporan & Statistik" dari `fa-history` ke `fa-chart-bar`
|
||||
|
||||
## 🎨 Styling & UI/UX
|
||||
|
||||
### Desain Visual
|
||||
- **Card-based layout** - Modern dan clean
|
||||
- **Gradient KPI cards** - Dengan efek radial overlay
|
||||
- **Smooth animations:**
|
||||
- Progress bar: 0.6s ease transition
|
||||
- Pulse animation untuk status "Berlangsung": 2s loop
|
||||
- Modal: fadeIn & slideUp animation 0.3s
|
||||
- Card hover: transform translateY & shadow transition
|
||||
|
||||
### Color Scheme
|
||||
- Primary: `#6FBA9D` (hijau tosca)
|
||||
- Success: `#28a745` (hijau)
|
||||
- Warning: `#ffc107` (kuning)
|
||||
- Info: `#17a2b8` (biru)
|
||||
- Danger: `#dc3545` (merah)
|
||||
|
||||
### Responsive Design
|
||||
- **Desktop:** Grid layout optimal
|
||||
- **Tablet:** Flexible grid adjustment
|
||||
- **Mobile:**
|
||||
- KPI cards: 1 kolom
|
||||
- Filter: vertical stack
|
||||
- Card kegiatan: full width
|
||||
|
||||
## 🔧 Technical Implementation
|
||||
|
||||
### Controller Updates
|
||||
**File:** `app/Http/Controllers/Admin/KegiatanController.php`
|
||||
|
||||
**Method Baru:**
|
||||
1. **`index()`** - Dashboard kegiatan hari ini
|
||||
- Query kegiatan berdasarkan hari
|
||||
- Join dengan absensis untuk hari yang dipilih
|
||||
- Hitung statistik (hadir, persentase, status)
|
||||
- Status kegiatan berdasarkan waktu real-time
|
||||
|
||||
2. **`jadwal()`** - Jadwal lengkap (moved from old index)
|
||||
- Filter hari, kategori, search
|
||||
- Pagination 15 per halaman
|
||||
|
||||
### Views Created
|
||||
1. **`resources/views/admin/kegiatan/data/dashboard.blade.php`** - Dashboard utama
|
||||
2. **`resources/views/admin/kegiatan/data/index.blade.php`** - Diupdate untuk jadwal lengkap (reuse existing view)
|
||||
|
||||
### Routes Updated
|
||||
**File:** `routes/web.php`
|
||||
|
||||
```php
|
||||
// Dashboard Kegiatan (default index)
|
||||
Route::get('kegiatan', [KegiatanController::class, 'index'])->name('kegiatan.index');
|
||||
|
||||
// Jadwal Lengkap
|
||||
Route::get('kegiatan/jadwal/semua', [KegiatanController::class, 'jadwal'])->name('kegiatan.jadwal');
|
||||
|
||||
// Resource routes lainnya tetap sama
|
||||
Route::resource('kegiatan', KegiatanController::class);
|
||||
```
|
||||
|
||||
### Database Queries Optimization
|
||||
- **Eager Loading:** `with(['kategori', 'absensis'])`
|
||||
- **Date Filtering:** `whereDate()` untuk filter tanggal spesifik
|
||||
- **Select Specific Columns:** Hanya mengambil kolom yang diperlukan
|
||||
- **No N+1 Problem:** Semua relasi dimuat di awal
|
||||
|
||||
## 📱 User Flow
|
||||
|
||||
### Flow 1: Monitoring Kegiatan Hari Ini
|
||||
```
|
||||
Sidebar > Dashboard Kegiatan
|
||||
↓
|
||||
Lihat KPI Cards (statistik overview)
|
||||
↓
|
||||
Review Timeline Kegiatan Hari Ini
|
||||
↓
|
||||
Cek Progress Bar Absensi
|
||||
↓
|
||||
Klik "Input Absensi" atau "Rekap"
|
||||
```
|
||||
|
||||
### Flow 2: Lihat Jadwal Lengkap
|
||||
```
|
||||
Dashboard Kegiatan > Button "Lihat Semua Jadwal"
|
||||
↓
|
||||
Filter (jika perlu): Hari, Kategori, Search
|
||||
↓
|
||||
Review Tabel Jadwal
|
||||
↓
|
||||
Action: Detail, Edit, atau Hapus
|
||||
```
|
||||
|
||||
### Flow 3: Input Absensi Cepat
|
||||
```
|
||||
Dashboard Kegiatan
|
||||
↓
|
||||
Scroll ke kegiatan yang sedang berlangsung
|
||||
↓
|
||||
Klik "Input Absensi"
|
||||
↓
|
||||
Form Input Absensi (dengan pre-filled kegiatan & tanggal)
|
||||
```
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
### Load Time
|
||||
- **Target:** < 1 detik
|
||||
- **Actual:** ~0.3-0.5 detik (optimal)
|
||||
|
||||
### Optimizations Applied
|
||||
- Eager loading relasi
|
||||
- Cache busting untuk query berulang
|
||||
- Minimal JavaScript (vanilla JS only)
|
||||
- CSS inline untuk komponen spesifik
|
||||
- No heavy libraries (no React/Vue/Angular)
|
||||
|
||||
## 🔐 Security
|
||||
- CSRF Protection pada semua form
|
||||
- Role-based access (admin only)
|
||||
- Input validation di controller
|
||||
- SQL injection prevention (Eloquent ORM)
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
### ✅ Functional Testing
|
||||
- [x] Dashboard load dengan data benar
|
||||
- [x] KPI cards hitung dengan akurat
|
||||
- [x] Filter hari bekerja
|
||||
- [x] Filter tanggal bekerja
|
||||
- [x] Status kegiatan update real-time
|
||||
- [x] Progress bar warna sesuai persentase
|
||||
- [x] Link "Input Absensi" benar
|
||||
- [x] Link "Rekap" benar
|
||||
- [x] Link "Info" benar
|
||||
- [x] Empty state tampil jika tidak ada kegiatan
|
||||
- [x] Sidebar menu update
|
||||
- [x] Jadwal lengkap load dengan pagination
|
||||
|
||||
### ✅ UI/UX Testing
|
||||
- [x] Responsive di mobile
|
||||
- [x] Responsive di tablet
|
||||
- [x] Responsive di desktop
|
||||
- [x] Animasi smooth
|
||||
- [x] Hover effects bekerja
|
||||
- [x] Modal open/close (prepared for future)
|
||||
|
||||
### ✅ Performance Testing
|
||||
- [x] No N+1 query
|
||||
- [x] Load time < 1 detik
|
||||
- [x] No JavaScript errors
|
||||
- [x] CSS tidak conflict
|
||||
|
||||
## 📝 Future Enhancements
|
||||
|
||||
### Modal Detail (Coming Soon)
|
||||
Fitur yang direncanakan:
|
||||
- Info kegiatan lengkap
|
||||
- Statistik absensi hari ini (Hadir, Izin, Sakit, Alpa)
|
||||
- Pie chart kecil
|
||||
- Daftar santri dengan status (scrollable)
|
||||
- Button "Download Rekap PDF"
|
||||
|
||||
### Real-time Updates (Optional)
|
||||
- Auto-refresh status kegiatan setiap menit
|
||||
- WebSocket untuk update absensi real-time
|
||||
- Push notification untuk admin
|
||||
|
||||
### Advanced Analytics
|
||||
- Grafik trend kehadiran per kegiatan
|
||||
- Perbandingan antar periode
|
||||
- Export data ke Excel/CSV
|
||||
|
||||
## 🐛 Known Issues & Fixes
|
||||
|
||||
### ✅ Fixed: Carbon Parsing Error
|
||||
**Issue:** `Could not parse '2026-02-12 2026-02-12 13:00:00': Double date specification`
|
||||
**Cause:** `waktu_mulai` dan `waktu_selesai` sudah dalam format datetime/Carbon object, bukan string waktu saja.
|
||||
**Solution:** Extract waktu dengan `format('H:i')` sebelum digabung dengan tanggal:
|
||||
```php
|
||||
$waktuMulaiStr = is_string($kegiatan->waktu_mulai)
|
||||
? $kegiatan->waktu_mulai
|
||||
: $kegiatan->waktu_mulai->format('H:i');
|
||||
$waktuMulai = Carbon::parse($selectedDate->format('Y-m-d') . ' ' . $waktuMulaiStr);
|
||||
```
|
||||
|
||||
### ✅ Fixed: Duplicate View Files
|
||||
**Issue:** `index.blade.php` dan `jadwal.blade.php` memiliki konten yang sama.
|
||||
**Solution:** Hapus `jadwal.blade.php`, reuse `index.blade.php` untuk route jadwal lengkap.
|
||||
|
||||
### Future Improvements
|
||||
- Modal detail belum fully implemented (placeholder saja)
|
||||
- Mobile landscape orientation need adjustment untuk KPI cards
|
||||
|
||||
## 📞 Support
|
||||
Untuk pertanyaan atau issue terkait fitur ini, hubungi:
|
||||
- Developer: [Your Name]
|
||||
- Email: [your@email.com]
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 12 Februari 2026
|
||||
**Version:** 1.0.0
|
||||
**Status:** ✅ Production Ready
|
||||
|
|
@ -1,307 +0,0 @@
|
|||
# DOKUMENTASI PENGEMBANGAN FITUR PEMBAYARAN SPP
|
||||
|
||||
## 📋 Overview
|
||||
Fitur Pembayaran SPP telah dikembangkan dengan sistem tab yang memisahkan antara santri yang sudah bayar dan belum bayar, dilengkapi dengan sistem filter yang komprehensif dan badge status yang jelas.
|
||||
|
||||
## ✨ Fitur yang Dikembangkan
|
||||
|
||||
### 1. **Sistem Tab "Sudah Bayar" & "Belum Bayar"**
|
||||
- **Tab Sudah Bayar**: Menampilkan daftar santri yang telah melunasi SPP periode tertentu
|
||||
- Menampilkan nominal yang dibayarkan
|
||||
- Tanggal pembayaran
|
||||
- Link ke riwayat pembayaran santri
|
||||
- Tombol cetak bukti pembayaran
|
||||
|
||||
- **Tab Belum Bayar**: Menampilkan daftar santri yang belum melunasi SPP
|
||||
- Menampilkan nominal tagihan
|
||||
- Batas waktu pembayaran
|
||||
- Jumlah hari keterlambatan (jika telat)
|
||||
- Link ke halaman tagihan santri
|
||||
- Tombol untuk membuat tagihan baru (jika belum ada)
|
||||
|
||||
### 2. **Status Pembayaran**
|
||||
Tiga status utama:
|
||||
- ✅ **Sudah Bayar (Lunas)**: Badge hijau dengan gradient
|
||||
- ⏰ **Belum Bayar (Belum Lunas)**: Badge warning dengan gradient pink
|
||||
- 🚨 **Terlambat**: Badge merah dengan animasi pulse dan highlight baris
|
||||
|
||||
### 3. **Filter Data**
|
||||
- **Filter Bulan**: Dropdown untuk memilih bulan (1-12)
|
||||
- **Filter Tahun**: Dropdown tahun berdasarkan data yang ada
|
||||
- **Filter Status** (hanya di tab Belum Bayar):
|
||||
- Semua Status
|
||||
- Belum Lunas
|
||||
- Terlambat
|
||||
- Belum Ada Tagihan
|
||||
- **Search**: Pencarian berdasarkan nama santri, NIS, atau ID Santri
|
||||
- **Default Filter**: Otomatis menampilkan bulan dan tahun saat ini
|
||||
|
||||
### 4. **Badge & Penanda Khusus**
|
||||
- Badge "TERLAMBAT" berwarna merah terang dengan animasi pulse
|
||||
- Highlight baris dengan background merah muda untuk santri yang terlambat
|
||||
- Informasi jumlah hari keterlambatan
|
||||
- Badge dengan gradient yang menarik untuk setiap status
|
||||
|
||||
### 5. **Statistik Dashboard**
|
||||
Empat card statistik dengan gradient:
|
||||
- 👥 **Total Santri**: Jumlah total santri aktif
|
||||
- ✅ **Sudah Bayar**: Jumlah santri yang sudah bayar + total nominal
|
||||
- ❌ **Belum Bayar**: Jumlah santri yang belum bayar + total tunggakan
|
||||
- ⏰ **Terlambat**: Jumlah santri yang melewati batas waktu
|
||||
|
||||
### 6. **Navigasi & UX**
|
||||
- Tab navigation dengan counter badge
|
||||
- Informasi periode yang sedang ditampilkan
|
||||
- Tombol reset filter
|
||||
- Pagination manual dengan info halaman
|
||||
- Hover effects pada tombol dan baris tabel
|
||||
- Responsive design
|
||||
|
||||
### 7. **Integrasi Form Create**
|
||||
- Pre-fill form dengan parameter dari URL
|
||||
- Otomatis memilih santri, bulan, dan tahun dari link "Buat Tagihan"
|
||||
|
||||
## 🗂️ File yang Dimodifikasi
|
||||
|
||||
### 1. **Controller** - `PembayaranSppController.php`
|
||||
```php
|
||||
// Method index() - Complete rewrite
|
||||
- Menambahkan sistem tab (sudah-bayar / belum-bayar)
|
||||
- Grouping data per santri (bukan per transaksi)
|
||||
- Filter berdasarkan bulan, tahun, search, dan status
|
||||
- Perhitungan statistik real-time
|
||||
- Manual pagination
|
||||
- Default filter ke bulan/tahun saat ini
|
||||
```
|
||||
|
||||
**Fitur Utama:**
|
||||
- Eager loading untuk optimasi query
|
||||
- Collection mapping untuk data transformation
|
||||
- Filter dinamis berdasarkan tab
|
||||
- Statistik agregasi (count & sum)
|
||||
|
||||
### 2. **View** - `index.blade.php`
|
||||
**Struktur Baru:**
|
||||
```php
|
||||
1. Alert messages (success/error)
|
||||
2. Filter section dengan label dan icon
|
||||
3. Statistics cards (4 cards dengan gradient)
|
||||
4. Tab navigation (Belum Bayar & Sudah Bayar)
|
||||
5. Action buttons (Generate, Tambah, Laporan)
|
||||
6. Periode info
|
||||
7. Data table dengan kolom dinamis
|
||||
8. Manual pagination
|
||||
9. Custom CSS untuk badge dan animasi
|
||||
```
|
||||
|
||||
**Styling:**
|
||||
- Gradient backgrounds untuk cards
|
||||
- Badge dengan animasi pulse untuk status terlambat
|
||||
- Hover effects
|
||||
- Highlight baris untuk santri terlambat
|
||||
- Responsive grid layout
|
||||
|
||||
### 3. **View** - `create.blade.php`
|
||||
**Modifikasi:**
|
||||
- Pre-fill `id_santri` dari request parameter
|
||||
- Pre-fill `bulan` dari request parameter
|
||||
- Pre-fill `tahun` dari request parameter
|
||||
- Fallback ke nilai default jika parameter tidak ada
|
||||
|
||||
## 📊 Flow Data
|
||||
|
||||
### Tab "Belum Bayar"
|
||||
```
|
||||
1. Query santri aktif dengan eager load pembayaran
|
||||
2. Filter by bulan & tahun
|
||||
3. Filter santri yang belum lunas atau belum ada tagihan
|
||||
4. Apply search filter
|
||||
5. Apply status filter (Belum Lunas/Telat/Belum Ada Tagihan)
|
||||
6. Hitung statistik
|
||||
7. Manual pagination
|
||||
8. Return view dengan data
|
||||
```
|
||||
|
||||
### Tab "Sudah Bayar"
|
||||
```
|
||||
1. Query santri aktif dengan eager load pembayaran
|
||||
2. Filter by bulan & tahun
|
||||
3. Filter santri yang status = Lunas
|
||||
4. Apply search filter
|
||||
5. Hitung statistik
|
||||
6. Manual pagination
|
||||
7. Return view dengan data
|
||||
```
|
||||
|
||||
## 🎨 Design Decisions
|
||||
|
||||
### 1. **Grouping per Santri (bukan per transaksi)**
|
||||
**Alasan:**
|
||||
- Lebih intuitif untuk monitoring pembayaran
|
||||
- Mudah melihat siapa yang sudah/belum bayar
|
||||
- Menghindari duplikasi data santri
|
||||
|
||||
### 2. **Default Filter ke Bulan/Tahun Saat Ini**
|
||||
**Alasan:**
|
||||
- Fokus pada periode aktif
|
||||
- Mengurangi clutter data
|
||||
- Admin biasanya ingin cek bulan berjalan
|
||||
|
||||
### 3. **Manual Pagination**
|
||||
**Alasan:**
|
||||
- Data sudah difilter di collection
|
||||
- Built-in paginator tidak cocok untuk collection hasil transform
|
||||
- Lebih fleksibel untuk custom logic
|
||||
|
||||
### 4. **Badge dengan Animasi Pulse**
|
||||
**Alasan:**
|
||||
- Menarik perhatian untuk santri yang telat
|
||||
- Visual feedback yang jelas
|
||||
- Meningkatkan UX
|
||||
|
||||
### 5. **Tab System**
|
||||
**Alasan:**
|
||||
- Pemisahan yang jelas antara lunas dan belum lunas
|
||||
- Mengurangi cognitive load
|
||||
- Mudah fokus pada salah satu kelompok
|
||||
|
||||
## 🔍 Query Optimization
|
||||
|
||||
### Eager Loading
|
||||
```php
|
||||
Santri::where('status', 'Aktif')
|
||||
->with(['pembayaranSpp' => function($q) use ($bulan, $tahun) {
|
||||
$q->where('bulan', $bulan)->where('tahun', $tahun);
|
||||
}])
|
||||
```
|
||||
**Benefit:**
|
||||
- Menghindari N+1 query problem
|
||||
- Load hanya data pembayaran yang relevan
|
||||
- Performa lebih cepat
|
||||
|
||||
### Collection Filtering vs Query Filtering
|
||||
- Query filtering untuk periode (bulan/tahun)
|
||||
- Collection filtering untuk status dan search
|
||||
- Lebih fleksibel untuk logic complex
|
||||
|
||||
## 🎯 Key Features Breakdown
|
||||
|
||||
### Penanda Telat
|
||||
```php
|
||||
// Check telat di Model
|
||||
public function isTelat() {
|
||||
if ($this->status === 'Lunas') return false;
|
||||
return Carbon::now()->isAfter($this->batas_bayar);
|
||||
}
|
||||
|
||||
// Highlight visual
|
||||
- Background baris: #fff5f5 (pink muda)
|
||||
- Badge: Gradient merah dengan animasi
|
||||
- Info: Jumlah hari keterlambatan
|
||||
```
|
||||
|
||||
### Filter yang Sedang Aktif
|
||||
```php
|
||||
// Preserve filter saat pindah tab
|
||||
array_merge(request()->except('tab'), ['tab' => 'sudah-bayar'])
|
||||
|
||||
// Show reset button jika ada filter
|
||||
@if(request()->hasAny(['search', 'filter_status']) || $bulan != date('n') || $tahun != date('Y'))
|
||||
```
|
||||
|
||||
### Link ke Riwayat/Tagihan
|
||||
```php
|
||||
// Riwayat pembayaran per santri
|
||||
route('admin.pembayaran-spp.riwayat', $item['id_santri'])
|
||||
|
||||
// Create dengan pre-fill
|
||||
route('admin.pembayaran-spp.create', [
|
||||
'id_santri' => $item['id_santri'],
|
||||
'bulan' => $bulan,
|
||||
'tahun' => $tahun
|
||||
])
|
||||
```
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
### Grid Layout
|
||||
```css
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
```
|
||||
**Benefit:**
|
||||
- Auto-responsive tanpa media queries manual
|
||||
- Kartu statistik menyesuaikan lebar layar
|
||||
|
||||
### Form Filter
|
||||
```css
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
```
|
||||
**Benefit:**
|
||||
- Input fields wrap ke baris baru di layar kecil
|
||||
- Tetap horizontal di layar besar
|
||||
|
||||
## ⚡ Performance Considerations
|
||||
|
||||
1. **Pagination**: 20 items per page - balance antara UX dan performa
|
||||
2. **Eager Loading**: Hindari N+1 queries
|
||||
3. **Collection Operations**: Lebih cepat daripada multiple queries
|
||||
4. **CSS Animations**: Hardware-accelerated (opacity, transform)
|
||||
|
||||
## 🚀 Testing Checklist
|
||||
|
||||
- [ ] Tab switching preserve filter
|
||||
- [ ] Filter bulan & tahun berfungsi
|
||||
- [ ] Search santri berfungsi
|
||||
- [ ] Filter status di tab Belum Bayar
|
||||
- [ ] Badge terlambat muncul untuk santri telat
|
||||
- [ ] Statistik terupdate sesuai filter
|
||||
- [ ] Pagination berfungsi
|
||||
- [ ] Link riwayat pembayaran
|
||||
- [ ] Link buat tagihan dengan pre-fill
|
||||
- [ ] Tombol reset filter
|
||||
- [ ] Cetak bukti di tab Sudah Bayar
|
||||
- [ ] Responsive di mobile
|
||||
|
||||
## 📝 Notes untuk Developer
|
||||
|
||||
### Jangan Ubah:
|
||||
- ❌ Struktur database
|
||||
- ❌ Alur bisnis (create, update, delete)
|
||||
- ❌ Routes yang sudah ada
|
||||
- ❌ Model relationships
|
||||
|
||||
### Boleh Dikustomisasi:
|
||||
- ✅ Warna gradient badge
|
||||
- ✅ Jumlah item per page
|
||||
- ✅ Default filter (jika tidak ingin ke bulan saat ini)
|
||||
- ✅ Kolom tambahan di tabel
|
||||
- ✅ Statistik tambahan
|
||||
|
||||
### Tips Maintenance:
|
||||
1. Gunakan Collection operations untuk filtering complex
|
||||
2. Keep controller logic readable dengan method extract jika perlu
|
||||
3. Cache tahunList jika data besar
|
||||
4. Monitor query performance dengan Laravel Debugbar
|
||||
|
||||
## 🐛 Known Limitations
|
||||
|
||||
1. **Manual Pagination**: Tidak kompatibel dengan Laravel Pagination Links bawaan
|
||||
2. **Collection Filtering**: Semua data santri di-load dulu sebelum filter - bisa lambat jika santri > 1000
|
||||
3. **Real-time Stats**: Dihitung setiap request - pertimbangkan caching untuk production
|
||||
|
||||
## 💡 Future Enhancements
|
||||
|
||||
1. **Export Excel**: Export data berdasarkan filter
|
||||
2. **Bulk Actions**: Tandai lunas multiple santri sekaligus
|
||||
3. **Notifications**: Email/SMS reminder untuk yang telat
|
||||
4. **Dashboard Chart**: Visualisasi trend pembayaran
|
||||
5. **Auto Reminder**: Cron job untuk reminder otomatis
|
||||
6. **Payment Gateway**: Integrasi pembayaran online
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: February 6, 2026
|
||||
**Version**: 1.0
|
||||
**Developer**: GitHub Copilot Assistant
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
# PERBAIKAN FITUR CAPAIAN SANTRI - MOBILE APP
|
||||
|
||||
**Tanggal:** 10 Februari 2026
|
||||
**Status:** ✅ **SELESAI**
|
||||
|
||||
## 🐛 Masalah yang Ditemukan
|
||||
|
||||
Fitur Capaian Santri di aplikasi mobile gagal mengambil data dari API, meskipun route API sudah terdaftar dengan benar.
|
||||
|
||||
### Root Cause
|
||||
|
||||
Kesalahan query database pada model **Semester**:
|
||||
- Migrasi database menggunakan kolom: `is_active` (boolean)
|
||||
- Kode controller menggunakan: `where('status', 'Aktif')` ❌
|
||||
- Error: `SQLSTATE[42S22]: Column not found: 1054 Unknown column 'status' in 'where clause'`
|
||||
|
||||
## ✅ Perbaikan yang Dilakukan
|
||||
|
||||
### 1. File: `ApiCapaianController.php`
|
||||
|
||||
**Lokasi:** `sim-pkpps/app/Http/Controllers/Api/ApiCapaianController.php`
|
||||
|
||||
#### Perubahan:
|
||||
|
||||
| Baris | Sebelum | Sesudah |
|
||||
|-------|---------|---------|
|
||||
| 57 | `Semester::where('status', 'Aktif')->first()` | `Semester::aktif()->first()` |
|
||||
| 115 | `$s->status === 'Aktif'` | `$s->is_active == 1` |
|
||||
| 115 | `'status'` dalam select | `'is_active'` dalam select |
|
||||
| 192 | `Semester::where('status', 'Aktif')->first()` | `Semester::aktif()->first()` |
|
||||
|
||||
**Detail Perubahan:**
|
||||
|
||||
```php
|
||||
// ❌ SEBELUM
|
||||
$semesterAktif = Semester::where('status', 'Aktif')->first();
|
||||
|
||||
$semesters = Semester::select('id_semester', 'nama_semester', 'tahun_ajaran', 'periode', 'status')
|
||||
->get()
|
||||
->map(function($s) {
|
||||
return [
|
||||
'id_semester' => $s->id_semester,
|
||||
'nama_semester' => $s->nama_semester,
|
||||
'is_aktif' => $s->status === 'Aktif',
|
||||
];
|
||||
});
|
||||
|
||||
// ✅ SESUDAH
|
||||
$semesterAktif = Semester::aktif()->first();
|
||||
|
||||
$semesters = Semester::select('id_semester', 'nama_semester', 'tahun_ajaran', 'periode', 'is_active')
|
||||
->get()
|
||||
->map(function($s) {
|
||||
return [
|
||||
'id_semester' => $s->id_semester,
|
||||
'nama_semester' => $s->nama_semester,
|
||||
'is_aktif' => $s->is_active == 1,
|
||||
];
|
||||
});
|
||||
```
|
||||
|
||||
### 2. File: `DashboardController.php`
|
||||
|
||||
**Lokasi:** `sim-pkpps/app/Http/Controllers/DashboardController.php`
|
||||
|
||||
**Baris 77:** Diperbaiki query semester
|
||||
|
||||
```php
|
||||
// ❌ SEBELUM
|
||||
$semesterAktif = Semester::where('status', 'aktif')->first();
|
||||
|
||||
// ✅ SESUDAH
|
||||
$semesterAktif = Semester::aktif()->first();
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### 1. Test Database Query
|
||||
|
||||
```bash
|
||||
php test_capaian_api.php
|
||||
```
|
||||
|
||||
**Hasil:**
|
||||
```
|
||||
✅ Santri: HELGA FAISA (ID: S001, Kelas: Lambatan)
|
||||
✅ Semester Aktif: Semester 1 2024/2025 (ID: SEM001)
|
||||
📚 Materi untuk kelas Lambatan: 1 materi
|
||||
📊 Capaian Santri: 1 capaian
|
||||
```
|
||||
|
||||
### 2. Test API Endpoint
|
||||
|
||||
```bash
|
||||
php test_capaian_endpoint.php
|
||||
```
|
||||
|
||||
**Endpoint:** `GET /api/v1/capaian/overview`
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"santri": {
|
||||
"id_santri": "S001",
|
||||
"nama_lengkap": "HELGA FAISA",
|
||||
"kelas": "Lambatan"
|
||||
},
|
||||
"semester": {
|
||||
"id_semester": "SEM001",
|
||||
"nama_semester": "Semester 1 2024/2025",
|
||||
"list_semester": [
|
||||
{
|
||||
"id_semester": "SEM002",
|
||||
"nama_semester": "Semester 2 2025/2026",
|
||||
"is_aktif": false
|
||||
},
|
||||
{
|
||||
"id_semester": "SEM001",
|
||||
"nama_semester": "Semester 1 2024/2025",
|
||||
"is_aktif": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"statistik_umum": {
|
||||
"total_materi": 1,
|
||||
"rata_rata_progress": 6,
|
||||
"materi_selesai": 0
|
||||
},
|
||||
"per_kategori": [
|
||||
{
|
||||
"kategori": "Al-Qur'an",
|
||||
"icon": "book_quran",
|
||||
"color": "#6FBAA5",
|
||||
"total_materi": 1,
|
||||
"rata_rata_progress": 6,
|
||||
"materi_selesai": 0
|
||||
},
|
||||
{
|
||||
"kategori": "Hadist",
|
||||
"icon": "scroll",
|
||||
"color": "#81C6E8",
|
||||
"total_materi": 0,
|
||||
"rata_rata_progress": 0,
|
||||
"materi_selesai": 0
|
||||
},
|
||||
{
|
||||
"kategori": "Materi Tambahan",
|
||||
"icon": "book",
|
||||
"color": "#FFD56B",
|
||||
"total_materi": 0,
|
||||
"rata_rata_progress": 0,
|
||||
"materi_selesai": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Validasi Struktur Data:**
|
||||
- ✅ Santri data exists
|
||||
- ✅ Semester data exists
|
||||
- ✅ Statistik umum exists
|
||||
- ✅ Per kategori exists
|
||||
- ✅ List semester: 2 items
|
||||
- ✅ Categories: 3 items
|
||||
|
||||
## 📱 Verifikasi Mobile App
|
||||
|
||||
### API Endpoints yang Diperbaiki
|
||||
|
||||
1. ✅ `GET /api/v1/capaian/overview` - Overview capaian dengan statistik
|
||||
2. ✅ `GET /api/v1/capaian/kategori/{kategori}` - List materi per kategori
|
||||
3. ✅ `GET /api/v1/capaian/detail/{idCapaian}` - Detail capaian per materi
|
||||
4. ✅ `GET /api/v1/capaian/grafik-progress` - Grafik progress historis
|
||||
|
||||
### Model Semester (Referensi)
|
||||
|
||||
**File:** `app/Models/Semester.php`
|
||||
|
||||
**Kolom Database:**
|
||||
- `is_active` (boolean) - Status aktif semester
|
||||
- Scope helper: `scopeAktif()` untuk query semester aktif
|
||||
|
||||
```php
|
||||
// ✅ CARA YANG BENAR
|
||||
Semester::aktif()->first()
|
||||
Semester::where('is_active', 1)->first()
|
||||
|
||||
// ❌ CARA YANG SALAH (kolom tidak ada)
|
||||
Semester::where('status', 'Aktif')->first()
|
||||
```
|
||||
|
||||
## 📝 Catatan Tambahan
|
||||
|
||||
### Data Testing
|
||||
|
||||
File `add_capaian_test_data.php` ditambahkan untuk membuat data testing dengan progress 6%.
|
||||
|
||||
### Logika Filtering
|
||||
|
||||
API hanya menghitung capaian dengan `persentase > 0` dalam statistik:
|
||||
```php
|
||||
$capaiansBerisi = $capaians->where('persentase', '>', 0);
|
||||
```
|
||||
|
||||
Ini berarti capaian dengan 0 halaman selesai tidak akan muncul di statistik.
|
||||
|
||||
## 🔍 Checklist Verifikasi
|
||||
|
||||
- [x] Semester query diperbaiki di `ApiCapaianController`
|
||||
- [x] Semester query diperbaiki di `DashboardController`
|
||||
- [x] Model `Semester` scope `aktif()` digunakan dengan benar
|
||||
- [x] API endpoint `capaian/overview` mengembalikan response 200
|
||||
- [x] Struktur JSON response sesuai dengan model Flutter
|
||||
- [x] Data testing ditambahkan dengan progress > 0%
|
||||
- [x] Field `is_aktif` dalam list_semester bernilai boolean
|
||||
|
||||
## ✨ Kesimpulan
|
||||
|
||||
Masalah **berhasil diperbaiki** dengan mengubah query dari kolom `status` yang tidak ada menjadi `is_active` yang sesuai dengan struktur database.
|
||||
|
||||
Mobile app sekarang dapat:
|
||||
- ✅ Mengambil overview capaian santri
|
||||
- ✅ Melihat statistik per kategori
|
||||
- ✅ Filter berdasarkan semester
|
||||
- ✅ Menampilkan progress capaian
|
||||
|
||||
**Status:** Siap untuk testing di aplikasi mobile Flutter! 🚀
|
||||
|
|
@ -1,502 +0,0 @@
|
|||
# DOKUMENTASI PERBAIKAN MENU PELANGGARAN
|
||||
|
||||
**Tanggal:** 9 Februari 2026
|
||||
**Status:** ✅ SELESAI
|
||||
|
||||
---
|
||||
|
||||
## 🔧 MASALAH YANG DIPERBAIKI
|
||||
|
||||
### Error Kolom Database
|
||||
**Error:**
|
||||
```
|
||||
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'is_active' in 'where clause'
|
||||
select * from `klasifikasi_pelanggarans` where `is_active` = 1 order by `urutan` asc, `nama_klasifikasi` asc
|
||||
```
|
||||
|
||||
**Lokasi Error:**
|
||||
- `RiwayatPelanggaranController::index()` - Line 80
|
||||
- `KategoriPelanggaranController::index()` - Line 27
|
||||
|
||||
**Penyebab:**
|
||||
Table `klasifikasi_pelanggarans` tidak memiliki kolom `is_active` dan `urutan` karena migration belum dijalankan.
|
||||
|
||||
---
|
||||
|
||||
## ✅ SOLUSI YANG DITERAPKAN
|
||||
|
||||
### 1. Update Migration Files
|
||||
|
||||
#### a. File: `2026_02_09_071146_create_klasifikasi_pelanggarans_table.php`
|
||||
- ✅ Ditambahkan check `Schema::hasTable()` sebelum create table
|
||||
- ✅ Mencegah error jika table sudah ada
|
||||
|
||||
#### b. File: `2026_02_09_071244_update_kategori_pelanggarans_add_klasifikasi_and_kafaroh.php`
|
||||
- ✅ Ditambahkan check `Schema::hasColumn()` untuk setiap kolom
|
||||
- ✅ Foreign key ditambahkan dengan try-catch untuk mencegah duplicate error
|
||||
- ✅ Menghindari dependency pada Doctrine DBAL
|
||||
|
||||
#### c. File: `2026_02_09_071335_update_riwayat_pelanggarans_add_kafaroh_and_parent_fields.php`
|
||||
- ✅ Ditambahkan check `Schema::hasColumn()` untuk semua kolom baru
|
||||
- ✅ Foreign key ditambahkan dengan try-catch
|
||||
- ✅ Index ditambahkan bersamaan dengan kolom
|
||||
|
||||
#### d. File: `2026_02_09_071441_create_pembinaan_sanksis_table.php`
|
||||
- ✅ Ditambahkan check `Schema::hasTable()`
|
||||
|
||||
#### e. File (BARU): `2026_02_09_080305_add_missing_columns_to_klasifikasi_pelanggarans_table.php`
|
||||
- ✅ Menambahkan kolom `deskripsi`, `is_active`, dan `urutan` yang hilang
|
||||
- ✅ Menambahkan index untuk `is_active`
|
||||
- ✅ Mencegah error jika kolom sudah ada
|
||||
|
||||
### 2. Jalankan Migration
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
**Hasil:**
|
||||
```
|
||||
✓ 2026_02_09_071146_create_klasifikasi_pelanggarans_table .......... DONE
|
||||
✓ 2026_02_09_071244_update_kategori_pelanggarans_add_klasifikasi_and_kafaroh .. DONE
|
||||
✓ 2026_02_09_071335_update_riwayat_pelanggarans_add_kafaroh_and_parent_fields . DONE
|
||||
✓ 2026_02_09_071441_create_pembinaan_sanksis_table ................. DONE
|
||||
✓ 2026_02_09_080305_add_missing_columns_to_klasifikasi_pelanggarans_table ..... DONE
|
||||
```
|
||||
|
||||
### 3. Insert Data Sample
|
||||
Sample data telah ditambahkan untuk testing:
|
||||
- ✅ 4 Klasifikasi Pelanggaran:
|
||||
- Pelanggaran Akhlaq
|
||||
- Pelanggaran Ketertiban
|
||||
- Pelanggaran Kerapian
|
||||
- Pelanggaran Akademik
|
||||
- ✅ 2 Kategori Pelanggaran sample
|
||||
|
||||
---
|
||||
|
||||
## 📊 STRUKTUR TABLE YANG DIHASILKAN
|
||||
|
||||
### Table: `klasifikasi_pelanggarans`
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| id | bigint(20) unsigned | Primary key |
|
||||
| id_klasifikasi | varchar(10) | ID format KL001, KL002, dst |
|
||||
| nama_klasifikasi | varchar(100) | Nama klasifikasi |
|
||||
| keterangan | text | Keterangan klasifikasi |
|
||||
| deskripsi | text | Deskripsi klasifikasi |
|
||||
| is_active | tinyint(1) | Status aktif/nonaktif ✅ |
|
||||
| urutan | int(11) | Urutan tampilan ✅ |
|
||||
| created_at | timestamp | - |
|
||||
| updated_at | timestamp | - |
|
||||
|
||||
### Table: `kategori_pelanggarans` (Updated)
|
||||
Added columns:
|
||||
- ✅ `id_klasifikasi` - varchar(10) - Foreign key to klasifikasi_pelanggarans
|
||||
- ✅ `kafaroh` - text - Kafaroh/Taqorrub yang harus dilakukan
|
||||
- ✅ `is_active` - tinyint(1) - Status aktif/nonaktif
|
||||
|
||||
### Table: `riwayat_pelanggarans` (Updated)
|
||||
Added columns:
|
||||
- ✅ `is_kafaroh_selesai` - boolean - Status kafaroh
|
||||
- ✅ `tanggal_kafaroh_selesai` - timestamp - Tanggal kafaroh diselesaikan
|
||||
- ✅ `admin_kafaroh_id` - unsignedBigInteger - Admin yang menyelesaikan
|
||||
- ✅ `catatan_kafaroh` - text - Catatan kafaroh
|
||||
- ✅ `poin_asli` - integer - Poin asli sebelum dilebur
|
||||
- ✅ `is_published_to_parent` - boolean - Status kirim ke wali
|
||||
- ✅ `tanggal_published` - timestamp - Tanggal dikirim ke wali
|
||||
- ✅ `admin_published_id` - unsignedBigInteger - Admin yang publish
|
||||
|
||||
### Table: `pembinaan_sanksis` (NEW)
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| id | bigint unsigned | Primary key |
|
||||
| id_pembinaan | varchar(10) | ID format PS001, PS002 |
|
||||
| judul | varchar(255) | Judul pembinaan/sanksi |
|
||||
| konten | text | Konten pembinaan (HTML supported) |
|
||||
| urutan | int | Urutan tampilan |
|
||||
| is_active | boolean | Status aktif/nonaktif |
|
||||
| created_at | timestamp | - |
|
||||
| updated_at | timestamp | - |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 FITUR YANG SUDAH LENGKAP
|
||||
|
||||
### 1. Klasifikasi Pelanggaran
|
||||
**Controller:** `KlasifikasiPelanggaranController.php` ✅
|
||||
**Routes:** `admin.klasifikasi-pelanggaran.*` ✅
|
||||
**Views:** ✅
|
||||
- [x] index.blade.php
|
||||
- [x] create.blade.php
|
||||
- [x] edit.blade.php
|
||||
- [x] show.blade.php
|
||||
|
||||
**Fitur:**
|
||||
- [x] CRUD lengkap
|
||||
- [x] Auto-generate ID (KL001, KL002, dst)
|
||||
- [x] Urutan tampilan
|
||||
- [x] Status aktif/nonaktif
|
||||
- [x] Count jumlah pelanggaran per klasifikasi
|
||||
- [x] Proteksi hapus jika masih digunakan
|
||||
|
||||
### 2. Kategori Pelanggaran
|
||||
**Controller:** `KategoriPelanggaranController.php` ✅
|
||||
**Routes:** `admin.kategori-pelanggaran.*` ✅
|
||||
**Views:** ✅
|
||||
- [x] index.blade.php
|
||||
- [x] create.blade.php
|
||||
- [x] edit.blade.php
|
||||
- [x] show.blade.php
|
||||
|
||||
**Fitur:**
|
||||
- [x] CRUD lengkap
|
||||
- [x] Auto-generate ID (KP001, KP002, dst)
|
||||
- [x] Relasi dengan Klasifikasi
|
||||
- [x] Field Kafaroh/Taqorrub
|
||||
- [x] Poin pelanggaran
|
||||
- [x] Status aktif/nonaktif
|
||||
- [x] Filter by klasifikasi & status
|
||||
- [x] Proteksi hapus jika masih digunakan
|
||||
|
||||
### 3. Riwayat Pelanggaran
|
||||
**Controller:** `RiwayatPelanggaranController.php` ✅ LENGKAP
|
||||
**Routes:** `admin.riwayat-pelanggaran.*` ✅
|
||||
**Views:** ✅
|
||||
- [x] index.blade.php
|
||||
- [x] create.blade.php
|
||||
- [x] edit.blade.php
|
||||
- [x] show.blade.php
|
||||
- [x] riwayat_santri.blade.php
|
||||
|
||||
**Fitur:**
|
||||
- [x] CRUD lengkap
|
||||
- [x] Auto-generate ID (P001, P002, dst)
|
||||
- [x] Filter by santri, kategori, klasifikasi
|
||||
- [x] Filter by status kafaroh
|
||||
- [x] Filter by status publish
|
||||
- [x] Filter by tanggal & bulan
|
||||
- [x] **Selesaikan Kafaroh** dengan catatan
|
||||
- [x] **Publish ke Wali Santri**
|
||||
- [x] **Batalkan Publish ke Wali**
|
||||
- [x] View riwayat per santri
|
||||
- [x] Statistik dashboard
|
||||
- [x] Poin dilebur jadi 0 setelah kafaroh selesai
|
||||
|
||||
**Methods Controller:**
|
||||
1. ✅ `index()` - Daftar dengan filter lengkap
|
||||
2. ✅ `create()` - Form tambah
|
||||
3. ✅ `store()` - Simpan data
|
||||
4. ✅ `show()` - Detail dengan riwayat lainnya
|
||||
5. ✅ `edit()` - Form edit
|
||||
6. ✅ `update()` - Update data
|
||||
7. ✅ `destroy()` - Hapus data
|
||||
8. ✅ `riwayatSantri()` - Riwayat per santri
|
||||
9. ✅ `selesaikanKafaroh()` - Selesaikan kafaroh & lebur poin
|
||||
10. ✅ `publishToParent()` - Kirim ke wali santri
|
||||
11. ✅ `unpublishFromParent()` - Batalkan kirim ke wali
|
||||
|
||||
### 4. Pembinaan & Sanksi (CMS Fleksibel)
|
||||
**Controller:** `PembinaanSanksiController.php` ✅
|
||||
**Routes:** `admin.pembinaan-sanksi.*` ✅
|
||||
**Views:** ✅
|
||||
- [x] index.blade.php - List dengan preview & navigation
|
||||
- [x] create.blade.php - Form dengan Quill.js Rich Text Editor
|
||||
- [x] edit.blade.php - Form edit dengan Quill.js Rich Text Editor
|
||||
- [x] show.blade.php - Display dengan HTML rendering & custom CSS
|
||||
|
||||
**🎨 Rich Text Editor: Quill.js 1.3.6**
|
||||
- ✅ 100% Gratis - Tidak perlu API key
|
||||
- ✅ Open Source (MIT License)
|
||||
- ✅ Ringan (hanya ~50KB gzipped)
|
||||
- ✅ WYSIWYG - What You See Is What You Get
|
||||
- ✅ Mobile friendly dengan touch support
|
||||
|
||||
**Toolbar Editor:**
|
||||
- Header (H1, H2, H3) untuk judul & sub judul
|
||||
- Bold, Italic, Underline, Strike untuk format teks
|
||||
- Text & Background Color untuk warna
|
||||
- Bullet & Number List untuk daftar
|
||||
- Align (Left, Center, Right, Justify)
|
||||
- Link untuk hyperlink internal/eksternal
|
||||
- Image untuk embed gambar via URL
|
||||
- Clean untuk hapus format
|
||||
|
||||
**Fitur CMS:**
|
||||
- [x] CRUD lengkap (Create, Read, Update, Delete)
|
||||
- [x] Auto-generate ID (PS001, PS002, dst)
|
||||
- [x] Konten tersimpan sebagai HTML (support rich formatting)
|
||||
- [x] Urutan tampilan (sortable)
|
||||
- [x] Status aktif/nonaktif
|
||||
- [x] Preview konten dengan styling custom
|
||||
- [x] Form validation (tidak bisa submit konten kosong)
|
||||
- [x] Info box dengan tips penggunaan editor
|
||||
|
||||
---
|
||||
|
||||
## 🔗 ROUTES YANG SUDAH TERDAFTAR
|
||||
|
||||
### Klasifikasi Pelanggaran
|
||||
```php
|
||||
Route::resource('klasifikasi-pelanggaran', KlasifikasiPelanggaranController::class);
|
||||
```
|
||||
|
||||
### Kategori Pelanggaran
|
||||
```php
|
||||
Route::resource('kategori-pelanggaran', KategoriPelanggaranController::class);
|
||||
```
|
||||
|
||||
### Riwayat Pelanggaran
|
||||
```php
|
||||
Route::resource('riwayat-pelanggaran', RiwayatPelanggaranController::class);
|
||||
|
||||
Route::prefix('riwayat-pelanggaran')->name('riwayat-pelanggaran.')->group(function () {
|
||||
Route::get('santri/{id_santri}', [RiwayatPelanggaranController::class, 'riwayatSantri'])
|
||||
->name('riwayat-santri');
|
||||
Route::post('/{riwayatPelanggaran}/selesaikan-kafaroh', [RiwayatPelanggaranController::class, 'selesaikanKafaroh'])
|
||||
->name('selesaikan-kafaroh');
|
||||
Route::post('/{riwayatPelanggaran}/publish-to-parent', [RiwayatPelanggaranController::class, 'publishToParent'])
|
||||
->name('publish-to-parent');
|
||||
Route::post('/{riwayatPelanggaran}/unpublish-from-parent', [RiwayatPelanggaranController::class, 'unpublishFromParent'])
|
||||
->name('unpublish-from-parent');
|
||||
});
|
||||
```
|
||||
|
||||
### Pembinaan & Sanksi
|
||||
```php
|
||||
Route::resource('pembinaan-sanksi', PembinaanSanksiController::class);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 CARA TESTING
|
||||
|
||||
### 1. Akses Menu Klasifikasi Pelanggaran
|
||||
```
|
||||
http://localhost/TugasAkhir/sim-pkpps/public/admin/klasifikasi-pelanggaran
|
||||
```
|
||||
✅ Harus bisa akses tanpa error
|
||||
|
||||
### 2. Akses Menu Kategori Pelanggaran
|
||||
```
|
||||
http://localhost/TugasAkhir/sim-pkpps/public/admin/kategori-pelanggaran
|
||||
```
|
||||
✅ Harus bisa akses tanpa error
|
||||
✅ Dropdown klasifikasi terisi
|
||||
|
||||
### 3. Akses Menu Riwayat Pelanggaran
|
||||
```
|
||||
http://localhost/TugasAkhir/sim-pkpps/public/admin/riwayat-pelanggaran
|
||||
```
|
||||
✅ Harus bisa akses tanpa error
|
||||
✅ Filter klasifikasi, status kafaroh, dan status publish berfungsi
|
||||
|
||||
### 4. Test Fitur Kafaroh
|
||||
1. Buat riwayat pelanggaran baru
|
||||
2. Buka detail riwayat
|
||||
3. Klik "Selesaikan Kafaroh"
|
||||
4. Isi catatan (opsional)
|
||||
5. Submit
|
||||
6. ✅ Poin harus menjadi 0
|
||||
7. ✅ Status kafaroh menjadi "Selesai"
|
||||
|
||||
### 5. Test Fitur Publish ke Wali
|
||||
1. Buka detail riwayat pelanggaran
|
||||
2. Klik "Kirim ke Wali Santri"
|
||||
3. ✅ Status publish menjadi "Terkirim"
|
||||
4. Klik "Batalkan Kirim ke Wali"
|
||||
5. ✅ Status publish kembali "Belum Terkirim"
|
||||
|
||||
### 6. Test Fitur Pembinaan & Sanksi (CMS)
|
||||
```
|
||||
http://localhost/TugasAkhir/sim-pkpps/public/admin/pembinaan-sanksi
|
||||
```
|
||||
|
||||
**Test Create:**
|
||||
1. Klik "Tambah Konten"
|
||||
2. ✅ Quill.js editor muncul tanpa API key warning
|
||||
3. Isi judul dan konten (coba bold, italic, heading, list)
|
||||
4. Klik "Simpan"
|
||||
5. ✅ Konten tersimpan dengan formatting
|
||||
|
||||
**Test Edit:**
|
||||
1. Klik "Edit" pada konten
|
||||
2. ✅ Konten muncul di editor dengan formatting utuh
|
||||
3. Ubah konten
|
||||
4. Klik "Update"
|
||||
5. ✅ Perubahan tersimpan
|
||||
|
||||
**Test View:**
|
||||
1. Klik "Lihat Detail"
|
||||
2. ✅ Konten tampil dengan HTML formatting
|
||||
3. ✅ Custom CSS styling teraplikasi (heading, list, alignment)
|
||||
|
||||
**Test Features:**
|
||||
- ✅ Bold & Italic berfungsi
|
||||
- ✅ Header H1, H2, H3 berfungsi
|
||||
- ✅ Bullet & Number list berfungsi
|
||||
- ✅ Text alignment berfungsi
|
||||
- ✅ Color picker berfungsi
|
||||
- ✅ Link & Image embed berfungsi
|
||||
|
||||
---
|
||||
|
||||
## 📝 MODEL YANG DIGUNAKAN
|
||||
|
||||
### 1. KlasifikasiPelanggaran
|
||||
- ✅ Auto-generate ID
|
||||
- ✅ `scopeAktif()` - Filter aktif
|
||||
- ✅ `scopeByUrutan()` - Sort by urutan
|
||||
- ✅ Relasi `hasMany` ke KategoriPelanggaran
|
||||
|
||||
### 2. KategoriPelanggaran
|
||||
- ✅ Auto-generate ID
|
||||
- ✅ `scopeAktif()` - Filter aktif
|
||||
- ✅ `scopeByKlasifikasi()` - Filter by klasifikasi
|
||||
- ✅ Relasi `belongsTo` ke KlasifikasiPelanggaran
|
||||
- ✅ Relasi `hasMany` ke RiwayatPelanggaran
|
||||
- ✅ Accessor `getNamaLengkapAttribute()`
|
||||
|
||||
### 3. RiwayatPelanggaran
|
||||
- ✅ Auto-generate ID
|
||||
- ✅ Auto-set `poin_asli` saat created
|
||||
- ✅ Multiple Scopes:
|
||||
- `scopeBySantri()`
|
||||
- `scopeByKategori()`
|
||||
- `scopeByTanggal()`
|
||||
- `scopeBulanIni()`
|
||||
- `scopeTerbaru()`
|
||||
- `scopeKafarohSelesai()`
|
||||
- `scopeKafarohBelumSelesai()`
|
||||
- `scopePublishedToParent()`
|
||||
- `scopeNotPublishedToParent()`
|
||||
- `scopeSearch()`
|
||||
- ✅ Relasi:
|
||||
- `belongsTo` Santri
|
||||
- `belongsTo` KategoriPelanggaran
|
||||
- `belongsTo` User (adminKafaroh)
|
||||
- `belongsTo` User (adminPublished)
|
||||
- ✅ Accessors:
|
||||
- `getTanggalFormatAttribute()`
|
||||
- `getStatusKafarohAttribute()`
|
||||
- `getStatusPublishAttribute()`
|
||||
|
||||
### 4. PembinaanSanksi
|
||||
- ✅ Auto-generate ID (PS001, PS002, dst)
|
||||
- ✅ `scopeAktif()` - Filter aktif
|
||||
- ✅ `scopeByUrutan()` - Sort by urutan
|
||||
- ✅ Support HTML content untuk rich text formatting
|
||||
- ✅ Integration dengan Quill.js Rich Text Editor
|
||||
- ✅ Custom CSS styling untuk tampilan konten
|
||||
|
||||
---
|
||||
|
||||
## 🎉 KESIMPULAN
|
||||
|
||||
### Status Perbaikan: ✅ BERHASIL
|
||||
|
||||
**Yang telah diperbaiki:**
|
||||
1. ✅ Error kolom database (`is_active`, `urutan`, `deskripsi`)
|
||||
2. ✅ Migration files updated dengan column checks
|
||||
3. ✅ Semua migration berhasil dijalankan
|
||||
4. ✅ Sample data tersedia untuk testing
|
||||
5. ✅ Semua controller lengkap dan berfungsi
|
||||
6. ✅ Semua routes terdaftar
|
||||
7. ✅ Semua views tersedia dan lengkap
|
||||
8. ✅ Fitur kafaroh berfungsi (lebur poin jadi 0)
|
||||
9. ✅ Fitur publish ke wali berfungsi
|
||||
10. ✅ Model dengan relasi dan scopes lengkap
|
||||
11. ✅ CMS Pembinaan & Sanksi dengan Quill.js Rich Text Editor
|
||||
12. ✅ No API key requirement (100% gratis)
|
||||
|
||||
**Menu Pelanggaran yang sudah lengkap:**
|
||||
1. ✅ Klasifikasi Pelanggaran (CRUD)
|
||||
2. ✅ Kategori Pelanggaran (CRUD + Kafaroh)
|
||||
3. ✅ Riwayat Pelanggaran (CRUD + Kafaroh + Publish)
|
||||
4. ✅ Pembinaan & Sanksi (CMS dengan Rich Text Editor)
|
||||
|
||||
**Teknologi yang digunakan:**
|
||||
- Laravel 10.x untuk backend framework
|
||||
- Blade Templates untuk views
|
||||
- MySQL untuk database
|
||||
- Quill.js 1.3.6 untuk Rich Text Editor (no API key!)
|
||||
- CDN-based libraries (zero installation required)
|
||||
|
||||
**Tidak ada error lagi!** 🎊
|
||||
|
||||
---
|
||||
|
||||
## 📚 DOKUMENTASI TAMBAHAN
|
||||
|
||||
### Cara Menambah Klasifikasi Baru:
|
||||
1. Login sebagai Admin
|
||||
2. Menu: Klasifikasi Pelanggaran → Tambah Klasifikasi
|
||||
3. Isi nama, deskripsi, dan urutan
|
||||
4. Sistem otomatis generate ID (KL001, KL002, dst)
|
||||
|
||||
### Cara Menambah Kategori Pelanggaran:
|
||||
1. Menu: Master Pelanggaran → Tambah Pelanggaran
|
||||
2. Pilih klasifikasi
|
||||
3. Isi nama pelanggaran, poin, dan kafaroh
|
||||
4. Sistem otomatis generate ID (KP001, KP002, dst)
|
||||
|
||||
### Cara Input Riwayat Pelanggaran:
|
||||
1. Menu: Riwayat Pelanggaran → Tambah Riwayat
|
||||
2. Pilih santri
|
||||
3. Pilih klasifikasi → kategori akan difilter otomatis
|
||||
4. Pilih kategori → poin ditarik otomatis
|
||||
5. Isi tanggal dan keterangan (opsional)
|
||||
6. Submit
|
||||
|
||||
### Cara Selesaikan Kafaroh:
|
||||
1. Buka detail riwayat pelanggaran
|
||||
2. Klik "Selesaikan Kafaroh"
|
||||
3. Isi catatan (opsional)
|
||||
4. Poin otomatis menjadi 0
|
||||
5. Admin yang menyelesaikan tercatat
|
||||
|
||||
### Cara Publish ke Wali:
|
||||
1. Buka detail riwayat pelanggaran
|
||||
2. Klik "Kirim ke Wali Santri"
|
||||
3. Konfirmasi
|
||||
4. Status berubah menjadi "Terkirim"
|
||||
5. Admin yang publish tercatat
|
||||
|
||||
### Cara Mengelola Konten Pembinaan & Sanksi:
|
||||
|
||||
**Tambah Konten Baru:**
|
||||
1. Menu: Pembinaan & Sanksi → Tambah Konten
|
||||
2. Isi judul (misal: "Tata Tertib Santri")
|
||||
3. Gunakan editor Quill.js untuk membuat konten:
|
||||
- Klik H1/H2/H3 untuk heading
|
||||
- Bold/Italic untuk penekanan
|
||||
- Klik bullet/number untuk daftar
|
||||
- Pilih warna untuk highlight
|
||||
- Gunakan align untuk rata kiri/tengah/kanan
|
||||
4. Set urutan tampilan
|
||||
5. Klik "Simpan"
|
||||
6. Sistem otomatis generate ID (PS001, PS002, dst)
|
||||
|
||||
**Edit Konten:**
|
||||
1. Klik "Edit" pada konten yang ingin diubah
|
||||
2. Konten akan muncul di editor dengan formatting utuh
|
||||
3. Ubah sesuai kebutuhan
|
||||
4. Klik "Update"
|
||||
|
||||
**Lihat Detail:**
|
||||
1. Klik "Lihat Detail"
|
||||
2. Konten tampil dengan HTML formatting lengkap
|
||||
3. Custom CSS styling teraplikasi otomatis
|
||||
|
||||
**Tips Menggunakan Editor:**
|
||||
- **Header:** Gunakan H1 untuk judul utama, H2 untuk sub judul, H3 untuk sub-sub judul
|
||||
- **List:** Gunakan bullet list untuk poin-poin, number list untuk langkah-langkah
|
||||
- **Bold/Italic:** Gunakan untuk penekanan kata penting
|
||||
- **Color:** Gunakan dengan bijak, jangan terlalu banyak warna
|
||||
- **Alignment:** Sesuaikan dengan kebutuhan layout (biasanya left)
|
||||
- **Link:** Bisa link ke halaman lain atau website eksternal
|
||||
- **Image:** Masukkan URL gambar (harus online/CDN)
|
||||
|
||||
---
|
||||
|
||||
**Dibuat oleh:** GitHub Copilot
|
||||
**Verified:** ✅ All Tests Passed
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
# Dokumentasi RBAC (Role-Based Access Control) — SIM-PKPPS
|
||||
|
||||
## 1. Ringkasan
|
||||
|
||||
Sistem RBAC telah diimplementasikan untuk memisahkan hak akses 3 jenis admin:
|
||||
|
||||
| Role | Deskripsi |
|
||||
|------|-----------|
|
||||
| **super_admin** | Akses penuh ke semua fitur, termasuk keuangan & SPP |
|
||||
| **akademik** | Fokus data akademik: santri, kelas, kegiatan, materi, pelanggaran, berita |
|
||||
| **pamong** | Fokus pengasuhan: uang saku, absensi, kesehatan, kepulangan |
|
||||
|
||||
Role lain (`santri`, `wali`) tidak terpengaruh — tetap berjalan seperti sebelumnya.
|
||||
|
||||
---
|
||||
|
||||
## 2. Akun Test
|
||||
|
||||
| Role | Username / Email | Password |
|
||||
|------|-----------------|----------|
|
||||
| super_admin | `helga.faisa06@gmail.com` | `12345678` |
|
||||
| akademik | `akademik@test.com` | `password123` |
|
||||
| pamong | `pamong@test.com` | `password123` |
|
||||
|
||||
Login di: **http://127.0.0.1:8000/admin/login**
|
||||
|
||||
> **Penting:** Jika mengalami redirect loop di browser, bersihkan cookies terlebih dahulu (Ctrl+Shift+Delete → Cookies).
|
||||
|
||||
---
|
||||
|
||||
## 3. Matriks Hak Akses
|
||||
|
||||
### Legenda
|
||||
- ✅ = Akses penuh (CRUD)
|
||||
- 👁 = Hanya lihat (Read Only)
|
||||
- ❌ = Tidak bisa akses
|
||||
|
||||
| Fitur | super_admin | akademik | pamong |
|
||||
|-------|:-----------:|:--------:|:------:|
|
||||
| **Dashboard** | ✅ (semua data) | ✅ (tanpa SPP & uang saku) | ✅ (tanpa SPP) |
|
||||
| **Data Santri** | ✅ | ✅ | 👁 |
|
||||
| **Kelas & Kelompok** | ✅ | ✅ | ❌ |
|
||||
| **Kenaikan Kelas** | ✅ | ✅ | ❌ |
|
||||
| **Kegiatan** | ✅ | ✅ | 👁 |
|
||||
| **Jadwal Kegiatan** | ✅ | ✅ | 👁 |
|
||||
| **Absensi Kegiatan** | ✅ | ✅ | ✅ |
|
||||
| **Kartu RFID** | ✅ | ✅ | ❌ |
|
||||
| **Capaian Santri** | ✅ | ✅ | ✅ |
|
||||
| **Materi & Semester** | ✅ | ✅ | ❌ |
|
||||
| **Pelanggaran** | ✅ | ✅ | ❌ |
|
||||
| **Pembinaan & Sanksi** | ✅ | ✅ | ❌ |
|
||||
| **Berita** | ✅ | ✅ | ❌ |
|
||||
| **Kategori Kegiatan** | ✅ | ✅ | ❌ |
|
||||
| **Rekap Absensi** | ✅ | ✅ | ❌ |
|
||||
| **Riwayat Kegiatan** | ✅ | ✅ | ❌ |
|
||||
| **Laporan Kegiatan** | ✅ | ✅ | ❌ |
|
||||
| **Kesehatan Santri** | ✅ | ✅ | ✅ |
|
||||
| **Kepulangan** | ✅ | ✅ | ✅ |
|
||||
| **Uang Saku** | ✅ | ❌ | ✅ |
|
||||
| **Keuangan Pondok** | ✅ | ❌ | ❌ |
|
||||
| **Pembayaran SPP** | ✅ | ❌ | ❌ |
|
||||
| **Manajemen User (santri/wali)** | ✅ | ❌ | ❌ |
|
||||
| **Manajemen Akun Admin** | ✅ | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 4. File yang Dimodifikasi / Dibuat
|
||||
|
||||
### Migration
|
||||
| File | Status |
|
||||
|------|--------|
|
||||
| `database/migrations/2026_02_24_000001_update_users_role_enum.php` | **BARU** — Migrasi enum role |
|
||||
|
||||
### Model
|
||||
| File | Perubahan |
|
||||
|------|-----------|
|
||||
| `app/Models/User.php` | Tambah method: `isSuperAdmin()`, `isAkademik()`, `isPamong()`, `hasRole(...$roles)`. Update `isAdmin()` |
|
||||
|
||||
### Middleware
|
||||
| File | Perubahan |
|
||||
|------|-----------|
|
||||
| `app/Http/Middleware/Role.php` | **FIX KRITIS**: Signature `string $roles` → `string ...$roles` (variadic). Hapus `explode()`. Redirect by role. |
|
||||
| `app/Http/Middleware/RedirectIfAuthenticated.php` | Redirect sesuai role (admin→admin.dashboard, santri→santri.dashboard) |
|
||||
| `app/Http/Middleware/Authenticate.php` | Dibersihkan (debug log dihapus) |
|
||||
| `app/Http/Kernel.php` | Disable `AuthenticateSession` dan `ClearStuckSession` dari web middleware group |
|
||||
|
||||
### Controller
|
||||
| File | Perubahan |
|
||||
|------|-----------|
|
||||
| `app/Http/Controllers/Auth/AdminAuthController.php` | Register default = `super_admin`. Hapus session invalidation sebelum login. |
|
||||
| `app/Http/Controllers/DashboardController.php` | Data dashboard kondisional per role. Fix nama kolom uang_saku. |
|
||||
| `app/Http/Controllers/Admin/UserController.php` | Tambah 6 method CRUD untuk akun admin (akademik/pamong) |
|
||||
|
||||
### Routes
|
||||
| File | Perubahan |
|
||||
|------|-----------|
|
||||
| `routes/web.php` | Restrukturisasi menjadi 5 middleware group berdasarkan role |
|
||||
|
||||
### Views
|
||||
| File | Status |
|
||||
|------|--------|
|
||||
| `resources/views/layouts/admin-sidebar.blade.php` | **REWRITE** — Menu kondisional per role |
|
||||
| `resources/views/admin/dashboardAdmin.blade.php` | Update — Seksi SPP/uang saku kondisional |
|
||||
| `resources/views/admin/dashboard/_kpi-cards.blade.php` | Update — KPI "Belum Ada Wali" hanya super_admin |
|
||||
| `resources/views/admin/dashboard/_alert-panel.blade.php` | Update — Alert SPP hanya super_admin |
|
||||
| `resources/views/layouts/app.blade.php` | Update — `isAdmin()` menggantikan `role === 'admin'` |
|
||||
| `resources/views/admin/users/admin_accounts.blade.php` | **BARU** — Daftar akun admin |
|
||||
| `resources/views/admin/users/admin_form.blade.php` | **BARU** — Form create/edit akun admin |
|
||||
|
||||
---
|
||||
|
||||
## 5. Langkah-Langkah yang Dilakukan
|
||||
|
||||
### Langkah 1: Migrasi Database
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
Migrasi mengubah enum `role` di tabel `users`:
|
||||
- **Sebelum:** `admin`, `santri`, `wali`
|
||||
- **Sesudah:** `super_admin`, `akademik`, `pamong`, `santri`, `wali`
|
||||
- Semua user yang sebelumnya `admin` otomatis menjadi `super_admin`.
|
||||
|
||||
### Langkah 2: Update Model User
|
||||
Ditambahkan helper method di `User.php`:
|
||||
```php
|
||||
public function isSuperAdmin() { return $this->role === 'super_admin'; }
|
||||
public function isAkademik() { return $this->role === 'akademik'; }
|
||||
public function isPamong() { return $this->role === 'pamong'; }
|
||||
public function isAdmin() { return in_array($this->role, ['super_admin', 'akademik', 'pamong']); }
|
||||
public function hasRole() { return in_array($this->role, func_get_args()); }
|
||||
```
|
||||
|
||||
### Langkah 3: Fix Middleware Role (Variadic Parameter)
|
||||
**Root cause** dari redirect loop: Laravel memanggil middleware `role:super_admin,akademik,pamong` dengan 3 argumen terpisah, bukan 1 string. Signature harus menggunakan **variadic** (`...`):
|
||||
|
||||
```php
|
||||
// SALAH (hanya tangkap argumen pertama):
|
||||
public function handle(Request $request, Closure $next, string $roles)
|
||||
|
||||
// BENAR (tangkap semua argumen):
|
||||
public function handle(Request $request, Closure $next, string ...$roles)
|
||||
```
|
||||
|
||||
### Langkah 4: Restrukturisasi Routes
|
||||
`routes/web.php` dibagi menjadi 5 middleware group:
|
||||
|
||||
| Group | Middleware | Isi |
|
||||
|-------|-----------|-----|
|
||||
| 1 | `role:super_admin,akademik,pamong` | Dashboard, Logout |
|
||||
| 2 | `role:super_admin` | Keuangan, SPP, Manajemen User |
|
||||
| 3 | `role:super_admin,akademik` | Santri CUD, Kelas, Kegiatan CUD, Pelanggaran, Berita, dll |
|
||||
| 4 | `role:super_admin,akademik,pamong` | Santri Read, Kegiatan Read, Absensi, Capaian, Kesehatan, Kepulangan |
|
||||
| 5 | `role:super_admin,pamong` | Uang Saku |
|
||||
|
||||
### Langkah 5: Update Sidebar & Dashboard
|
||||
- Sidebar menampilkan menu sesuai role user yang login
|
||||
- Dashboard menampilkan data sesuai hak akses role
|
||||
|
||||
### Langkah 6: CRUD Akun Admin
|
||||
Super admin dapat membuat akun akademik/pamong via UI:
|
||||
- **URL:** `/admin/users/admin`
|
||||
- Hanya super_admin yang bisa mengakses
|
||||
- Tidak bisa membuat akun super_admin baru via UI (untuk keamanan)
|
||||
|
||||
---
|
||||
|
||||
## 6. Cara Membuat Akun Admin Baru
|
||||
|
||||
### Via UI (Recommended)
|
||||
1. Login sebagai **super_admin**
|
||||
2. Buka menu **Data Master → Akun Admin**
|
||||
3. Klik **Tambah Akun Admin**
|
||||
4. Isi form (email, nama, password, pilih role akademik/pamong)
|
||||
5. Klik **Simpan**
|
||||
|
||||
### Via Tinker (Manual)
|
||||
```bash
|
||||
php artisan tinker
|
||||
```
|
||||
```php
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
User::create([
|
||||
'name' => 'Nama User',
|
||||
'email' => 'user@example.com',
|
||||
'username' => 'user@example.com',
|
||||
'password' => Hash::make('password123'),
|
||||
'role' => 'akademik', // atau 'pamong'
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Troubleshooting
|
||||
|
||||
### Redirect Loop (ERR_TOO_MANY_REDIRECTS)
|
||||
1. **Bersihkan cookies browser** (Ctrl+Shift+Delete → Cookies)
|
||||
2. Jalankan:
|
||||
```bash
|
||||
php artisan cache:clear
|
||||
php artisan config:clear
|
||||
php artisan route:clear
|
||||
```
|
||||
3. Hapus session files:
|
||||
```bash
|
||||
# Di PowerShell:
|
||||
Remove-Item storage/framework/sessions/* -Force
|
||||
```
|
||||
|
||||
### Error 500 Setelah Login
|
||||
- Periksa `storage/logs/laravel.log` untuk detail error
|
||||
- Pastikan semua migrasi sudah dijalankan: `php artisan migrate:status`
|
||||
|
||||
### User Tidak Bisa Login
|
||||
- Pastikan kolom `username` terisi (login menggunakan `username`, bukan `email`)
|
||||
- Untuk update username yang kosong:
|
||||
```bash
|
||||
php artisan tinker
|
||||
```
|
||||
```php
|
||||
User::whereNull('username')->orWhere('username', '')->get()->each(fn($u) => $u->update(['username' => $u->email]));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Arsitektur Middleware
|
||||
|
||||
```
|
||||
Request
|
||||
│
|
||||
├─ web middleware group (Kernel.php)
|
||||
│ ├─ EncryptCookies
|
||||
│ ├─ AddQueuedCookiesToResponse
|
||||
│ ├─ StartSession
|
||||
│ ├─ ShareErrorsFromSession
|
||||
│ ├─ VerifyCsrfToken
|
||||
│ └─ SubstituteBindings
|
||||
│
|
||||
├─ auth middleware (Authenticate.php)
|
||||
│ └─ Redirect ke /admin/login jika belum login
|
||||
│
|
||||
└─ role middleware (Role.php)
|
||||
└─ Cek apakah user->role termasuk dalam daftar yang diizinkan
|
||||
├─ Ya → lanjut ke controller
|
||||
└─ Tidak → redirect ke dashboard dengan pesan error
|
||||
```
|
||||
|
||||
> **Catatan:** `AuthenticateSession` dan `ClearStuckSession` telah di-disable dari web middleware group karena menyebabkan konflik session.
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
# Dokumentasi Redesign Halaman Jadwal & Absensi Kegiatan
|
||||
|
||||
## 🎯 Perubahan yang Dilakukan
|
||||
|
||||
### 1. **Halaman Jadwal Kegiatan** (`admin.kegiatan.data.index`)
|
||||
**Lokasi**: `sim-pkpps/resources/views/admin/kegiatan/data/index.blade.php`
|
||||
|
||||
#### Perubahan Tampilan:
|
||||
✅ **Dari**: Tabel flat dengan filter dropdown di atas
|
||||
✅ **Ke**: 7 tab horizontal (Senin-Ahad) dengan card grid per hari
|
||||
|
||||
#### Fitur Utama:
|
||||
- **Tab Navigation**: 7 tab horizontal untuk setiap hari dalam seminggu
|
||||
- **Auto-Select Tab**: Tab hari ini otomatis terpilih saat pertama kali membuka halaman
|
||||
- **Card Layout**: Kegiatan ditampilkan sebagai card, bukan baris tabel
|
||||
- **Filter per Tab**: Dropdown filter kelas & kategori di dalam setiap tab
|
||||
- **Tab Switching JavaScript**: Berpindah tab tanpa reload (URL state preserved dengan `pushState`)
|
||||
|
||||
#### Struktur Card Kegiatan:
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Nama Kegiatan [Badge] │
|
||||
│ 🕐 08:00 - 10:00 │
|
||||
│ [Kelas1] [Kelas2] [+2 lainnya]│
|
||||
│ 📖 Materi: ... │
|
||||
│ │
|
||||
│ [Input Absensi] [Detail] │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### CSS Responsif:
|
||||
- Grid auto-fill dengan minimum width 320px
|
||||
- Horizontal scroll pada tab navigation untuk mobile
|
||||
- Hover effects & animations (fadeIn, translateY)
|
||||
|
||||
---
|
||||
|
||||
### 2. **Halaman Input Absensi** (`admin.absensi-kegiatan.index`)
|
||||
**Lokasi**: `sim-pkpps/resources/views/admin/kegiatan/absensi/index.blade.php`
|
||||
|
||||
#### Perubahan Tampilan:
|
||||
✅ **Dari**: Filter dropdown biasa + tabel list kegiatan
|
||||
✅ **Ke**: Date picker dengan header tanggal + card grid dengan status badge
|
||||
|
||||
#### Fitur Utama:
|
||||
- **Date Picker Section**: Background gradient hijau dengan header tanggal lengkap
|
||||
- **Nama Hari Otomatis**: Menampilkan "Jumat, 8 Desember 2024" berdasarkan tanggal dipilih
|
||||
- **Filter dalam Date Picker**: Kategori & Kelas digabung dalam satu section
|
||||
- **Status Badge**: Menampilkan "Sudah Input" (hijau) atau "Belum Input" (merah)
|
||||
- **Progress Bar**: Jika sudah ada data absensi, tampilkan persentase kehadiran
|
||||
- **Query Otomatis Hari**: Sistem otomatis filter kegiatan berdasarkan hari dari tanggal dipilih
|
||||
|
||||
#### Struktur Card Kegiatan:
|
||||
```
|
||||
┌─────────────────────────────────┬──────────────┐
|
||||
│ Nama Kegiatan [Badge] │ [Status] │
|
||||
│ 🕐 08:00 - 10:00 │ │
|
||||
│ [Kelas1] [Kelas2] [Kelas3] │ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────┐│ │
|
||||
│ │ Kehadiran 15/20 (75%) ││ │
|
||||
│ │ ████████████░░░░░░░░ ││ │
|
||||
│ └─────────────────────────────┘│ │
|
||||
│ │ │
|
||||
│ [Input Absensi] [Rekap] │ │
|
||||
└─────────────────────────────────┴──────────────┘
|
||||
```
|
||||
|
||||
#### Logika Backend (View Only):
|
||||
```php
|
||||
// Map hari Indonesia ke hari sistem
|
||||
$hariDipilih = Carbon::parse($tanggal)->locale('id')->isoFormat('dddd');
|
||||
$hariMap = ['Senin' => 'Senin', 'Minggu' => 'Ahad', ...];
|
||||
$hariFilter = $hariMap[$hariDipilih] ?? 'Senin';
|
||||
|
||||
// Filter kegiatan berdasarkan hari dari tanggal dipilih
|
||||
$query = $kegiatans->where('hari', $hariFilter);
|
||||
|
||||
// Cek apakah sudah ada data absensi
|
||||
$absensiExists = AbsensiKegiatan::where('kegiatan_id', $kegiatan->kegiatan_id)
|
||||
->whereDate('tanggal', $tanggal)
|
||||
->exists();
|
||||
|
||||
// Hitung persentase kehadiran
|
||||
$absensiData = AbsensiKegiatan::where('kegiatan_id', $kegiatan->kegiatan_id)
|
||||
->whereDate('tanggal', $tanggal)
|
||||
->get();
|
||||
$hadirCount = $absensiData->where('status', 'Hadir')->count();
|
||||
$persenKehadiran = round(($hadirCount / $totalSantri) * 100);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Palette Warna
|
||||
|
||||
| Elemen | Warna | Hex Code |
|
||||
|--------|-------|----------|
|
||||
| Primary Green | Eucalyptus Green | `#6FBA9D` |
|
||||
| Dark Green | Darker Shade | `#5EA98C` |
|
||||
| Light Green | Background | `#E8F7F2` |
|
||||
| Page Background | Very Light | `#F8FBF9` |
|
||||
| Status Sudah (Green) | Success | `#D1FAE5` / `#065F46` |
|
||||
| Status Belum (Red) | Error | `#FEE2E2` / `#991B1B` |
|
||||
| Blue Button | Info | `#3B82F6` |
|
||||
|
||||
---
|
||||
|
||||
## 📦 Tidak Ada Perubahan Controller
|
||||
|
||||
✅ **Semua perubahan hanya pada VIEW layer**
|
||||
✅ Controller logic tetap sama:
|
||||
- `App\Http\Controllers\Admin\KegiatanController@jadwal` → index.blade.php
|
||||
- `App\Http\Controllers\Admin\AbsensiKegiatanController@index` → absensi/index.blade.php
|
||||
|
||||
✅ Model relationships tetap digunakan:
|
||||
- `$kegiatan->kelasKegiatan` (many-to-many via `kelas_kegiatan`)
|
||||
- `$kegiatan->kategori` (belongsTo)
|
||||
- `AbsensiKegiatan::where()` queries
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Testing Checklist
|
||||
|
||||
### Halaman Jadwal (`/admin/kegiatan/jadwal`)
|
||||
- [ ] Tab navigation berfungsi (klik untuk switch)
|
||||
- [ ] Tab hari ini otomatis terpilih
|
||||
- [ ] Filter kelas & kategori submit dengan GET parameter
|
||||
- [ ] Card menampilkan semua informasi kegiatan
|
||||
- [ ] Tombol "Input Absensi" redirect ke input page
|
||||
- [ ] Tombol "Detail" redirect ke detail page
|
||||
- [ ] Responsive di mobile (tab horizontal scroll)
|
||||
|
||||
### Halaman Absensi (`/admin/absensi-kegiatan`)
|
||||
- [ ] Date picker default ke hari ini
|
||||
- [ ] Nama hari + tanggal tampil di header
|
||||
- [ ] Filter kategori & kelas berfungsi
|
||||
- [ ] Status badge menampilkan "Sudah Input" / "Belum Input" dengan benar
|
||||
- [ ] Progress bar muncul jika sudah ada data absensi
|
||||
- [ ] Persentase kehadiran dihitung dengan benar (hadir/total)
|
||||
- [ ] Tombol "Input Absensi" membawa parameter `tanggal` dalam URL
|
||||
- [ ] Tombol "Rekap" redirect ke rekap page
|
||||
- [ ] Empty state muncul jika tidak ada kegiatan di hari tersebut
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Teknologi & Dependencies
|
||||
|
||||
**View Engine**: Laravel Blade
|
||||
**Styling**: Inline CSS (no external library)
|
||||
**JavaScript**: Vanilla JS (tab switching, no jQuery)
|
||||
**PHP Helpers**: Carbon (date formatting dengan locale Indonesia)
|
||||
**Icons**: Font Awesome 5
|
||||
|
||||
**Browser Compatibility**:
|
||||
- Chrome/Edge: ✅ Full support
|
||||
- Firefox: ✅ Full support
|
||||
- Safari: ✅ CSS Grid supported
|
||||
- Mobile: ✅ Responsive grid & horizontal scroll
|
||||
|
||||
---
|
||||
|
||||
## 📝 Catatan Teknis
|
||||
|
||||
### URL Parameters yang Digunakan:
|
||||
|
||||
**Jadwal**:
|
||||
```
|
||||
GET /admin/kegiatan/jadwal?hari=Senin&kelas_id=1&kategori_id=2
|
||||
```
|
||||
|
||||
**Absensi**:
|
||||
```
|
||||
GET /admin/absensi-kegiatan?tanggal=2024-12-06&kategori_id=1&id_kelas=2
|
||||
```
|
||||
|
||||
### Mapping Hari Minggu → Ahad:
|
||||
```php
|
||||
$hariMap = [
|
||||
'Senin' => 'Senin',
|
||||
'Selasa' => 'Selasa',
|
||||
'Rabu' => 'Rabu',
|
||||
'Kamis' => 'Kamis',
|
||||
'Jumat' => 'Jumat',
|
||||
'Sabtu' => 'Sabtu',
|
||||
'Minggu' => 'Ahad' // Penting untuk database
|
||||
];
|
||||
```
|
||||
|
||||
### Animation Classes:
|
||||
```css
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Hasil Akhir
|
||||
|
||||
- **Jadwal**: Tab-based layout dengan 7 hari, auto-select hari ini, card grid per tab
|
||||
- **Absensi**: Date picker dengan header tanggal, card dengan status badge & progress bar
|
||||
- **UI/UX**: Clean, modern, responsif, dengan animasi smooth
|
||||
- **Performance**: Lightweight CSS tanpa library eksternal
|
||||
- **Code Quality**: Clean Blade syntax, reusable CSS classes
|
||||
|
||||
**Total Files Modified**: 2 files
|
||||
**Lines Changed**: ~600 lines (redesign complete)
|
||||
**No Breaking Changes**: Semua route & controller logic tetap sama
|
||||
|
||||
---
|
||||
|
||||
Dibuat: {{ now()->format('d F Y H:i') }}
|
||||
Developer: GitHub Copilot
|
||||
Project: SIM-PKPPS (Sistem Informasi Manajemen Pesantren)
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
# 🔧 FIX: "Koneksi Gagal" di Mobile App
|
||||
|
||||
## 🎯 Masalah
|
||||
Aplikasi Flutter menampilkan error: **"Koneksi gagal, periksa internet Anda"**
|
||||
|
||||
## ✅ Solusi Sudah Diterapkan
|
||||
|
||||
File `app_config.dart` sudah diupdate dengan IP komputer Anda: **10.130.244.240**
|
||||
|
||||
---
|
||||
|
||||
## 📱 LANGKAH-LANGKAH FIX
|
||||
|
||||
### 1️⃣ Pastikan Device & Komputer di WiFi yang Sama
|
||||
|
||||
**PENTING!** HP dan komputer harus terhubung ke WiFi yang sama.
|
||||
|
||||
Cek WiFi:
|
||||
- Komputer: Lihat icon WiFi di taskbar
|
||||
- HP: Settings → WiFi → lihat nama network
|
||||
|
||||
### 2️⃣ Test Koneksi dari HP
|
||||
|
||||
**A. Buka Browser di HP, akses:**
|
||||
```
|
||||
http://10.130.244.240/TugasAkhir/test_mobile_api.html
|
||||
```
|
||||
|
||||
**B. Klik tombol:**
|
||||
- "Test Koneksi Server" → harus muncul ✅ KONEKSI BERHASIL
|
||||
- "Test Login API" → harus muncul ✅ LOGIN BERHASIL
|
||||
|
||||
**Jika halaman tidak bisa dibuka:**
|
||||
→ Lanjut ke Step 3 (Windows Firewall)
|
||||
|
||||
### 3️⃣ Fix Windows Firewall
|
||||
|
||||
Windows Firewall mungkin memblokir koneksi dari HP.
|
||||
|
||||
**Cara 1: Izinkan Apache (Recommended)**
|
||||
|
||||
1. Buka Command Prompt **as Administrator**
|
||||
2. Jalankan:
|
||||
```cmd
|
||||
netsh advfirewall firewall add rule name="Apache HTTP" dir=in action=allow protocol=TCP localport=80
|
||||
netsh advfirewall firewall add rule name="Apache HTTPS" dir=in action=allow protocol=TCP localport=443
|
||||
```
|
||||
|
||||
**Cara 2: Matikan Firewall Sementara (untuk testing)**
|
||||
|
||||
1. Windows Settings → Update & Security → Windows Security
|
||||
2. Firewall & network protection
|
||||
3. Private network → Turn off (HANYA untuk testing!)
|
||||
4. Setelah berhasil, nyalakan lagi dan gunakan Cara 1
|
||||
|
||||
### 4️⃣ Restart Flutter App
|
||||
|
||||
Setelah test koneksi berhasil:
|
||||
|
||||
```bash
|
||||
cd c:\xampp\htdocs\TugasAkhir\sim_mobile
|
||||
flutter clean
|
||||
flutter run
|
||||
```
|
||||
|
||||
**PENTING:** Harus **Hot Restart** (bukan hot reload!)
|
||||
- VS Code: Klik icon 🔄
|
||||
- Android Studio: Klik lightning bolt hijau
|
||||
|
||||
### 5️⃣ Test Login
|
||||
|
||||
**Gunakan credentials:**
|
||||
- Username: `Aydin Fauzan`
|
||||
- Password: `s002`
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Error: "Halaman test_mobile_api.html tidak bisa dibuka"
|
||||
|
||||
**Penyebab:** Firewall atau WiFi berbeda
|
||||
|
||||
**Solusi:**
|
||||
1. Ping dari HP ke komputer:
|
||||
- Install app "Network Utilities" atau "Fing"
|
||||
- Ping ke: 10.130.244.240
|
||||
- Jika timeout → WiFi berbeda atau Firewall
|
||||
|
||||
2. Cek XAMPP Apache:
|
||||
- Buka XAMPP Control Panel
|
||||
- Pastikan Apache **running** (hijau)
|
||||
|
||||
### Error: "Test koneksi berhasil, tapi login gagal"
|
||||
|
||||
**Penyebab:** API atau database bermasalah
|
||||
|
||||
**Solusi:**
|
||||
Test dari komputer dulu:
|
||||
```bash
|
||||
$body = '{"id_santri":"Aydin Fauzan","password":"s002"}'
|
||||
Invoke-RestMethod -Uri "http://localhost/TugasAkhir/sim-pkpps/public/api/v1/login" -Method POST -ContentType "application/json" -Body $body
|
||||
```
|
||||
|
||||
Jika ini gagal:
|
||||
- Cek routes: `php artisan route:list --name=login`
|
||||
- Cek database connection
|
||||
- Cek Laravel log: `sim-pkpps/storage/logs/laravel.log`
|
||||
|
||||
### Error: "Flutter masih error 'koneksi gagal'"
|
||||
|
||||
**Penyebab:** Config tidak ter-reload
|
||||
|
||||
**Solusi:**
|
||||
1. Stop Flutter app (Shift+F5)
|
||||
2. Jalankan:
|
||||
```bash
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter run
|
||||
```
|
||||
3. Atau uninstall app dari HP, install ulang
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Cek IP Komputer Berubah
|
||||
|
||||
Jika IP komputer berubah (setelah restart/ganti WiFi):
|
||||
|
||||
1. Cek IP baru:
|
||||
```bash
|
||||
ipconfig | findstr IPv4
|
||||
```
|
||||
|
||||
2. Update `app_config.dart`:
|
||||
```dart
|
||||
static const String baseUrl = 'http://[IP_BARU]/TugasAkhir/sim-pkpps/public/api/v1';
|
||||
```
|
||||
|
||||
3. Hot restart Flutter
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist Final
|
||||
|
||||
- [ ] HP dan komputer di WiFi yang sama
|
||||
- [ ] XAMPP Apache running
|
||||
- [ ] Firewall rule untuk Apache sudah dibuat
|
||||
- [ ] Test koneksi dari HP berhasil (test_mobile_api.html)
|
||||
- [ ] Flutter app sudah hot restart
|
||||
- [ ] Login dengan username & password yang benar
|
||||
|
||||
---
|
||||
|
||||
## 📞 Masih Error?
|
||||
|
||||
Kirim screenshot:
|
||||
1. Error di Flutter (terminal log)
|
||||
2. Hasil test dari test_mobile_api.html
|
||||
3. XAMPP Control Panel (Apache status)
|
||||
4. WiFi settings (HP dan komputer)
|
||||
|
||||
---
|
||||
|
||||
**IP Komputer Anda: 10.130.244.240**
|
||||
**Test Page: http://10.130.244.240/TugasAkhir/test_mobile_api.html**
|
||||
|
|
@ -1,905 +0,0 @@
|
|||
# Santri.kelas Usage Mapping
|
||||
|
||||
_Generated: 2026-02-12 16:30:36_
|
||||
|
||||
This document maps all usage of `$santri->kelas` and related patterns in the codebase to guide refactoring to the new kelas system.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Summary
|
||||
|
||||
- **Total files with kelas usage:** 40
|
||||
- **Total matches found:** 115
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Priority Levels
|
||||
|
||||
### 🔴 HIGH Priority (Break functionality)
|
||||
|
||||
- **app/Http/Controllers/Admin/CapaianController.php**
|
||||
- Issue: Query filtering by kelas column
|
||||
- Action Required: Update to use kelasSantri relationship
|
||||
|
||||
- **app/Http/Controllers/Admin/SantriController.php**
|
||||
- Issue: Query filtering by kelas column
|
||||
- Action Required: Update to use kelasSantri relationship
|
||||
|
||||
- **database/migrations/2025_09_29_033444_create_santris_table.php**
|
||||
- Issue: Database schema definition
|
||||
- Action Required: Review but DO NOT modify old migrations
|
||||
|
||||
- **database/migrations/2025_10_31_064743_create_materi_table.php**
|
||||
- Issue: Database schema definition
|
||||
- Action Required: Review but DO NOT modify old migrations
|
||||
|
||||
### 🟡 MEDIUM Priority (UI/Display)
|
||||
|
||||
- **app/Models/Materi.php**
|
||||
- Issue: Model attribute or accessor
|
||||
- Action Required: Review accessor implementation
|
||||
|
||||
- **app/Models/Santri.php**
|
||||
- Issue: Model attribute or accessor
|
||||
- Action Required: Review accessor implementation
|
||||
|
||||
- **resources/views/admin/berita/show.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/capaian/create.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/capaian/export-rapor.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/capaian/index.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/capaian/riwayat-santri.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/kegiatan/absensi/input.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/kegiatan/kartu/cetak.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/kegiatan/kartu/daftar.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/kegiatan/kartu/index.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/kegiatan/riwayat/detail-santri.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/kepulangan/create.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/kepulangan/over-limit.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/kepulangan/surat-pdf.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/kesehatan-santri/riwayat.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/pembayaran-spp/create.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/pembayaran-spp/edit.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/riwayat_pelanggaran/riwayat_santri.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/santri/form.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/santri/index.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/santri/show.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/admin/users/wali_accounts.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/santri/berita/index.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/santri/capaian/index.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
- **resources/views/santri/kegiatan/index.blade.php**
|
||||
- Issue: Display kelas in UI
|
||||
- Action Required: Change to use $santri->kelas_name accessor
|
||||
|
||||
### 🟢 LOW Priority (Backward compatible)
|
||||
|
||||
- **app/Http/Controllers/Admin/AbsensiKegiatanController.php**
|
||||
- Note: Other usage
|
||||
|
||||
- **app/Http/Controllers/Admin/BeritaController.php**
|
||||
- Note: Other usage
|
||||
|
||||
- **app/Http/Controllers/Admin/MateriController.php**
|
||||
- Note: Other usage
|
||||
|
||||
- **app/Http/Controllers/Admin/PembayaranSppController.php**
|
||||
- Note: Other usage
|
||||
|
||||
- **app/Http/Controllers/Api/ApiAuthController.php**
|
||||
- Note: Other usage
|
||||
|
||||
- **app/Http/Controllers/Api/ApiBeritaController.php**
|
||||
- Note: Other usage
|
||||
|
||||
- **app/Http/Controllers/Api/ApiCapaianController.php**
|
||||
- Note: Other usage
|
||||
|
||||
- **app/Http/Controllers/DashboardController.php**
|
||||
- Note: Other usage
|
||||
|
||||
- **app/Http/Controllers/Santri/SantriBeritaController.php**
|
||||
- Note: Other usage
|
||||
|
||||
- **database/seeders/KelasSeeder.php**
|
||||
- Note: Other usage
|
||||
|
||||
---
|
||||
|
||||
## 📂 Detailed Listing by Directory
|
||||
|
||||
### App / Http / Controllers
|
||||
|
||||
#### 📄 `app/Http/Controllers/Admin/AbsensiKegiatanController.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 179:** `'kelas' => $santri->kelas,`
|
||||
|
||||
**Pattern: `kelas_column`**
|
||||
|
||||
- **Line 179:** `'kelas' => $santri->kelas,`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/Admin/BeritaController.php`
|
||||
|
||||
**Pattern: `enum_values`**
|
||||
|
||||
- **Line 51:** `$kelasOptions = ['PB', 'Lambatan', 'Cepatan'];`
|
||||
- **Line 127:** `$kelasOptions = ['PB', 'Lambatan', 'Cepatan'];`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/Admin/CapaianController.php`
|
||||
|
||||
**Pattern: `where_kelas`**
|
||||
|
||||
- **Line 35:** `$query->where('kelas', $selectedKelas);`
|
||||
- **Line 344:** `->when($kelas, fn($q) => $q->where('kelas', $kelas))`
|
||||
- **Line 347:** `->when($kelas, fn($q) => $q->where('kelas', $kelas))`
|
||||
- **Line 352:** `->when($kelas, fn($q) => $q->whereHas('santri', fn($sq) => $sq->where('kelas', $kelas)))`
|
||||
- **Line 393:** `$kelasMateris = $materis->where('kelas', $k);`
|
||||
- **Line 463:** `$filteredMateris = $kelas ? $materis->where('kelas', $kelas) : $materis;`
|
||||
- **Line 480:** `$heatmapMateris = $kelas ? $materis->where('kelas', $kelas)->values() : $materis->take(15)->values();`
|
||||
- **Line 835:** `$q->where('kelas', $kelas);`
|
||||
- **Line 896:** `$query->where('kelas', $kelas);`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 116:** `$materis = Materi::where('kelas', $santri->kelas)`
|
||||
- **Line 123:** `'kelas' => $santri->kelas,`
|
||||
- **Line 453:** `'kelas' => $santri->kelas,`
|
||||
- **Line 484:** `$row = ['nama' => $santri->nama_lengkap, 'id_santri' => $santri->id_santri, 'kelas' => $santri->kelas];`
|
||||
|
||||
**Pattern: `kelas_column`**
|
||||
|
||||
- **Line 123:** `'kelas' => $santri->kelas,`
|
||||
- **Line 453:** `'kelas' => $santri->kelas,`
|
||||
- **Line 484:** `$row = ['nama' => $santri->nama_lengkap, 'id_santri' => $santri->id_santri, 'kelas' => $santri->kelas];`
|
||||
|
||||
**Pattern: `enum_values`**
|
||||
|
||||
- **Line 341:** `$kelasList = ['Lambatan', 'Cepatan', 'PB'];`
|
||||
- **Line 708:** `$kelas = $request->input('kelas', 'Lambatan');`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `where('kelas')` with `whereHas('kelasSantri')`
|
||||
2. Update query to use kelas ID instead of name
|
||||
3. Test filter functionality thoroughly
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/Admin/MateriController.php`
|
||||
|
||||
**Pattern: `kelas_column`**
|
||||
|
||||
- **Line 82:** `'kelas' => 'required|in:Lambatan,Cepatan,PB',`
|
||||
- **Line 156:** `'kelas' => 'required|in:Lambatan,Cepatan,PB',`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/Admin/PembayaranSppController.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 54:** `'kelas' => $santri->kelas,`
|
||||
|
||||
**Pattern: `kelas_column`**
|
||||
|
||||
- **Line 54:** `'kelas' => $santri->kelas,`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/Admin/SantriController.php`
|
||||
|
||||
**Pattern: `where_kelas`**
|
||||
|
||||
- **Line 38:** `$query->where('kelas', $request->kelas);`
|
||||
|
||||
**Pattern: `kelas_column`**
|
||||
|
||||
- **Line 86:** `'kelas' => 'required|in:PB,Lambatan,Cepatan',`
|
||||
- **Line 154:** `'kelas' => 'required|in:PB,Lambatan,Cepatan',`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `where('kelas')` with `whereHas('kelasSantri')`
|
||||
2. Update query to use kelas ID instead of name
|
||||
3. Test filter functionality thoroughly
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/Api/ApiAuthController.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 158:** `'kelas' => $santri->kelas,`
|
||||
|
||||
**Pattern: `kelas_column`**
|
||||
|
||||
- **Line 158:** `'kelas' => $santri->kelas,`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/Api/ApiBeritaController.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 42:** `->whereJsonContains('target_kelas', $santri->kelas);`
|
||||
- **Line 146:** `$bolehAkses = in_array($santri->kelas, $berita->target_kelas ?? []);`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/Api/ApiCapaianController.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 125:** `'kelas' => $santri->kelas,`
|
||||
- **Line 490:** `->where('santris.kelas', $santri->kelas)`
|
||||
- **Line 523:** `->where('santris.kelas', $santri->kelas)`
|
||||
- **Line 591:** `'kelas' => $santri->kelas,`
|
||||
|
||||
**Pattern: `kelas_column`**
|
||||
|
||||
- **Line 125:** `'kelas' => $santri->kelas,`
|
||||
- **Line 295:** `'kelas' => $capaian->materi->kelas,`
|
||||
- **Line 591:** `'kelas' => $santri->kelas,`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/DashboardController.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 204:** `'kelas' => $santri->kelas,`
|
||||
- **Line 251:** `->whereJsonContains('target_kelas', $santri->kelas);`
|
||||
|
||||
**Pattern: `kelas_column`**
|
||||
|
||||
- **Line 204:** `'kelas' => $santri->kelas,`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Http/Controllers/Santri/SantriBeritaController.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 44:** `->whereJsonContains('target_kelas', $santri->kelas);`
|
||||
- **Line 89:** `->whereJsonContains('target_kelas', $santri->kelas);`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
### App / Models
|
||||
|
||||
#### 📄 `app/Models/Materi.php`
|
||||
|
||||
**Pattern: `where_kelas`**
|
||||
|
||||
- **Line 80:** `return $query->where('kelas', $kelas);`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Review model methods and accessors
|
||||
2. Ensure backward compatibility
|
||||
3. Add tests for new relations
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `app/Models/Santri.php`
|
||||
|
||||
**Pattern: `enum_values`**
|
||||
|
||||
- **Line 177:** `'Lambatan' => 'Lambatan',`
|
||||
- **Line 178:** `'Cepatan' => 'Cepatan',`
|
||||
|
||||
**Pattern: `where_kelas`**
|
||||
|
||||
- **Line 306:** `return $query->where('kelas', $kelas);`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Review model methods and accessors
|
||||
2. Ensure backward compatibility
|
||||
3. Add tests for new relations
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Resources / views
|
||||
|
||||
#### 📄 `resources/views/admin/berita/show.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 130:** `<i class="fas fa-graduation-cap"></i> {{ $santri->kelas }}`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 130:** `<i class="fas fa-graduation-cap"></i> {{ $santri->kelas }}`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/capaian/create.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 25:** `data-kelas="{{ $santri->kelas }}"`
|
||||
- **Line 27:** `{{ $santri->nama_lengkap }} ({{ $santri->nis }}) - Kelas: {{ $santri->kelas }}`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 25:** `data-kelas="{{ $santri->kelas }}"`
|
||||
- **Line 27:** `{{ $santri->nama_lengkap }} ({{ $santri->nis }}) - Kelas: {{ $santri->kelas }}`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/capaian/export-rapor.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 96:** `<div class="info-item"><span class="label">Kelas</span> <span class="value">{{ $santri->kelas }}</span></div>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 96:** `<div class="info-item"><span class="label">Kelas</span> <span class="value">{{ $santri->kelas }}</span></div>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/capaian/index.blade.php`
|
||||
|
||||
**Pattern: `enum_values`**
|
||||
|
||||
- **Line 38:** `<a href="{{ route('admin.capaian.index', array_merge(request()->except('kelas'), ['kelas' => 'PB'])) }}"`
|
||||
- **Line 43:** `<a href="{{ route('admin.capaian.index', array_merge(request()->except('kelas'), ['kelas' => 'Lambatan'])) }}"`
|
||||
- **Line 48:** `<a href="{{ route('admin.capaian.index', array_merge(request()->except('kelas'), ['kelas' => 'Cepatan'])) }}"`
|
||||
- **Line 112:** `@if($data['santri']->kelas == 'PB')`
|
||||
- **Line 114:** `@elseif($data['santri']->kelas == 'Lambatan')`
|
||||
|
||||
**Pattern: `kelas_column`**
|
||||
|
||||
- **Line 38:** `<a href="{{ route('admin.capaian.index', array_merge(request()->except('kelas'), ['kelas' => 'PB'])) }}"`
|
||||
- **Line 43:** `<a href="{{ route('admin.capaian.index', array_merge(request()->except('kelas'), ['kelas' => 'Lambatan'])) }}"`
|
||||
- **Line 48:** `<a href="{{ route('admin.capaian.index', array_merge(request()->except('kelas'), ['kelas' => 'Cepatan'])) }}"`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/capaian/riwayat-santri.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 18:** `<strong>Kelas:</strong> <span class="badge badge-secondary">{{ $santri->kelas }}</span>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 18:** `<strong>Kelas:</strong> <span class="badge badge-secondary">{{ $santri->kelas }}</span>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/kegiatan/absensi/input.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 63:** `<td><span class="badge badge-secondary">{{ $santri->kelas }}</span></td>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 63:** `<td><span class="badge badge-secondary">{{ $santri->kelas }}</span></td>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/kegiatan/kartu/cetak.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 423:** `<span class="value">: @if(isset($santri)){{ $santri->kelas }}@else Lambatan @endif</span>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 423:** `<span class="value">: @if(isset($santri)){{ $santri->kelas }}@else Lambatan @endif</span>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/kegiatan/kartu/daftar.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 29:** `<td><span class="badge badge-secondary">{{ $santri->kelas }}</span></td>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 29:** `<td><span class="badge badge-secondary">{{ $santri->kelas }}</span></td>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/kegiatan/kartu/index.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 60:** `<td><span class="badge badge-secondary">{{ $santri->kelas }}</span></td>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 60:** `<td><span class="badge badge-secondary">{{ $santri->kelas }}</span></td>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/kegiatan/riwayat/detail-santri.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 15:** `Kelas: <strong>{{ $santri->kelas }}</strong> |`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 15:** `Kelas: <strong>{{ $santri->kelas }}</strong> |`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/kepulangan/create.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 49:** `{{ $santri->nama_lengkap }} ({{ $santri->id_santri }} - {{ $santri->kelas }})`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 49:** `{{ $santri->nama_lengkap }} ({{ $santri->id_santri }} - {{ $santri->kelas }})`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/kepulangan/over-limit.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 78:** `<td>{{ $santri->kelas }}</td>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 78:** `<td>{{ $santri->kelas }}</td>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/kepulangan/surat-pdf.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 269:** `<div class="data-value">{{ $santri->kelas }}</div>`
|
||||
- **Line 394:** `<td style="padding: 5px;">: {{ $santri->kelas }}</td>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 269:** `<div class="data-value">{{ $santri->kelas }}</div>`
|
||||
- **Line 394:** `<td style="padding: 5px;">: {{ $santri->kelas }}</td>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/kesehatan-santri/riwayat.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 21:** `<strong>Kelas:</strong> {{ $santri->kelas }}<br>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 21:** `<strong>Kelas:</strong> {{ $santri->kelas }}<br>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/pembayaran-spp/create.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 35:** `{{ $santri->id_santri }} - {{ $santri->nama_lengkap }} ({{ $santri->kelas }})`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 35:** `{{ $santri->id_santri }} - {{ $santri->nama_lengkap }} ({{ $santri->kelas }})`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/pembayaran-spp/edit.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 36:** `{{ $santri->id_santri }} - {{ $santri->nama_lengkap }} ({{ $santri->kelas }})`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 36:** `{{ $santri->id_santri }} - {{ $santri->nama_lengkap }} ({{ $santri->kelas }})`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/riwayat_pelanggaran/riwayat_santri.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 33:** `{{ $santri->id_santri }} | {{ $santri->kelas }}`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 33:** `{{ $santri->id_santri }} | {{ $santri->kelas }}`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/santri/form.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 87:** `<option value="PB" {{ old('kelas', $isEdit ? $santri->kelas : '') == 'PB' ? 'selected' : '' }}>PB (Pembinaan)</option>`
|
||||
- **Line 88:** `<option value="Lambatan" {{ old('kelas', $isEdit ? $santri->kelas : '') == 'Lambatan' ? 'selected' : '' }}>Lambatan</option>`
|
||||
- **Line 89:** `<option value="Cepatan" {{ old('kelas', $isEdit ? $santri->kelas : '') == 'Cepatan' ? 'selected' : '' }}>Cepatan</option>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/santri/index.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 89:** `<td><strong>{{ $santri->kelas }}</strong></td>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 89:** `<td><strong>{{ $santri->kelas }}</strong></td>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/santri/show.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 75:** `<strong style="color: #6FBA9D; font-size: 1.1rem;">{{ $santri->kelas }}</strong>`
|
||||
- **Line 76:** `@if($santri->kelas == 'PB')`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 75:** `<strong style="color: #6FBA9D; font-size: 1.1rem;">{{ $santri->kelas }}</strong>`
|
||||
|
||||
**Pattern: `enum_values`**
|
||||
|
||||
- **Line 76:** `@if($santri->kelas == 'PB')`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/admin/users/wali_accounts.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 95:** `<td>{{ $santri->kelas }}</td>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 95:** `<td>{{ $santri->kelas }}</td>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/santri/berita/index.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 10:** `Informasi terbaru untuk <strong>{{ $santri->kelas }}</strong>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 10:** `Informasi terbaru untuk <strong>{{ $santri->kelas }}</strong>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/santri/capaian/index.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 56:** `<div class="card-value-small">{{ $santri->kelas }}</div>`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 56:** `<div class="card-value-small">{{ $santri->kelas }}</div>`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `resources/views/santri/kegiatan/index.blade.php`
|
||||
|
||||
**Pattern: `property_access`**
|
||||
|
||||
- **Line 9:** `{{ $santri->nama_lengkap }} - Kelas {{ $santri->kelas }}`
|
||||
|
||||
**Pattern: `blade_kelas`**
|
||||
|
||||
- **Line 9:** `{{ $santri->nama_lengkap }} - Kelas {{ $santri->kelas }}`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
1. Replace `{{ $santri->kelas }}` with `{{ $santri->kelas_name }}`
|
||||
2. Test display in browser
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Database / migrations
|
||||
|
||||
#### 📄 `database/migrations/2025_09_29_033444_create_santris_table.php`
|
||||
|
||||
**Pattern: `enum_values`**
|
||||
|
||||
- **Line 25:** `$table->enum('kelas', ['PB', 'Lambatan', 'Cepatan']); // PB = Pembinaan`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
#### 📄 `database/migrations/2025_10_31_064743_create_materi_table.php`
|
||||
|
||||
**Pattern: `enum_values`**
|
||||
|
||||
- **Line 18:** `$table->enum('kelas', ['Lambatan', 'Cepatan', 'PB'])->index();`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
### Database / seeders
|
||||
|
||||
#### 📄 `database/seeders/KelasSeeder.php`
|
||||
|
||||
**Pattern: `enum_values`**
|
||||
|
||||
- **Line 29:** `'nama_kelas' => 'PB',`
|
||||
- **Line 38:** `'nama_kelas' => 'Lambatan',`
|
||||
- **Line 47:** `'nama_kelas' => 'Cepatan',`
|
||||
|
||||
**💡 Suggested Action:**
|
||||
Review usage and update as needed based on context.
|
||||
|
||||
---
|
||||
|
||||
## 📖 Refactoring Guide
|
||||
|
||||
### General Patterns
|
||||
|
||||
#### 1. Display in Views (Blade)
|
||||
```php
|
||||
// OLD:
|
||||
{{ $santri->kelas }}
|
||||
|
||||
// NEW (backward compatible):
|
||||
{{ $santri->kelas_name }}
|
||||
```
|
||||
|
||||
#### 2. Filter in Controllers
|
||||
```php
|
||||
// OLD:
|
||||
$santris = Santri::where('kelas', 'PB')->get();
|
||||
|
||||
// NEW:
|
||||
$santris = Santri::whereHas('kelasSantri', function($q) {
|
||||
$q->where('id_kelas', 1); // PB = 1
|
||||
})->get();
|
||||
```
|
||||
|
||||
#### 3. Kegiatan-Kelas Relation
|
||||
```php
|
||||
// OLD: Filter santri by kelas for kegiatan
|
||||
$santris = Santri::whereIn('kelas', ['PB', 'Lambatan'])->get();
|
||||
|
||||
// NEW: Use kegiatan relation
|
||||
$santris = $kegiatan->getEligibleSantris();
|
||||
```
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] Santri detail page displays correct kelas
|
||||
- [ ] Santri list filter by kelas works
|
||||
- [ ] Dashboard statistics by kelas accurate
|
||||
- [ ] Kegiatan filtering by kelas works
|
||||
- [ ] Absensi shows correct santri per kegiatan
|
||||
- [ ] Reports include correct kelas information
|
||||
- [ ] Mobile API returns kelas data correctly
|
||||
|
||||
|
|
@ -1,311 +0,0 @@
|
|||
# Multiple Kelas API Response Documentation
|
||||
|
||||
## Overview
|
||||
Backend API telah diupdate untuk mendukung **multiple kelas** per santri dengan sistem relasi baru (kelompok_kelas → kelas → santri_kelas).
|
||||
|
||||
## Endpoints yang Diupdate
|
||||
|
||||
### 1. POST `/api/login`
|
||||
### 2. GET `/api/profile`
|
||||
|
||||
Kedua endpoint ini sekarang return data kelas dalam struktur baru dengan backward compatibility.
|
||||
|
||||
---
|
||||
|
||||
## Response Structure (BARU)
|
||||
|
||||
### Example Response JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"token": "1|abc123...",
|
||||
"user": {
|
||||
"name": "Ahmad Santoso",
|
||||
"role": "santri",
|
||||
"role_id": "S001"
|
||||
},
|
||||
"santri": {
|
||||
"id_santri": "S001",
|
||||
"nis": "2024001",
|
||||
"nama_lengkap": "Ahmad Santoso",
|
||||
"jenis_kelamin": "Laki-laki",
|
||||
"status": "Aktif",
|
||||
"alamat_santri": "Jl. Raya No. 123, Jakarta",
|
||||
"daerah_asal": "Jakarta",
|
||||
"nama_orang_tua": "Bapak Fulan",
|
||||
"nomor_hp_ortu": "08123456789",
|
||||
"foto": "santri/S001.jpg",
|
||||
"foto_url": "http://localhost:8000/storage/santri/S001.jpg",
|
||||
|
||||
// ✅ BACKWARD COMPATIBILITY: Tetap ada field 'kelas' lama
|
||||
"kelas": "Lambatan B", // Kelas primary atau pertama
|
||||
|
||||
// 🆕 NEW: Array semua kelas yang diikuti, GROUPED BY KELOMPOK
|
||||
"kelas_list": [
|
||||
{
|
||||
"kelompok_id": "KLMPK001",
|
||||
"kelompok_name": "PB",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 1,
|
||||
"kode_kelas": "KLS001",
|
||||
"nama_kelas": "PB Putra A",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kelompok_id": "KLMPK002",
|
||||
"kelompok_name": "Lambatan",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 5,
|
||||
"kode_kelas": "KLS005",
|
||||
"nama_kelas": "Lambatan B",
|
||||
"is_primary": true // ⭐ Kelas utama
|
||||
},
|
||||
{
|
||||
"id_kelas": 6,
|
||||
"kode_kelas": "KLS006",
|
||||
"nama_kelas": "Lambatan A",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kelompok_id": "KLMPK003",
|
||||
"kelompok_name": "Cepatan",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 8,
|
||||
"kode_kelas": "KLS008",
|
||||
"nama_kelas": "Cepatan A",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kelompok_id": "KLMPK004",
|
||||
"kelompok_name": "Hadist",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 15,
|
||||
"kode_kelas": "KLS015",
|
||||
"nama_kelas": "Hadist Pemula",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"bergabung_sejak": "14 February 2026"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field Description
|
||||
|
||||
### Field Lama (Backward Compatibility)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `kelas` | string | Nama kelas utama (primary). Fallback: kelas pertama atau "Belum Ada Kelas" |
|
||||
|
||||
### Field Baru (kelas_list)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `kelas_list` | array | Array kelompok kelas yang diikuti santri |
|
||||
| `kelas_list[].kelompok_id` | string | ID kelompok (KLMPK001, KLMPK002, dst) |
|
||||
| `kelas_list[].kelompok_name` | string | Nama kelompok (PB, Lambatan, Cepatan, dst) |
|
||||
| `kelas_list[].kelas` | array | Array kelas dalam kelompok ini |
|
||||
| `kelas[].id_kelas` | int | ID kelas (primary key) |
|
||||
| `kelas[].kode_kelas` | string | Kode kelas (KLS001, KLS002, dst) |
|
||||
| `kelas[].nama_kelas` | string | Nama kelas lengkap |
|
||||
| `kelas[].is_primary` | boolean | **true** jika ini kelas utama santri, **false** untuk kelas lainnya |
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases Handling
|
||||
|
||||
### Case 1: Santri Belum Punya Kelas
|
||||
```json
|
||||
{
|
||||
"kelas": "Belum Ada Kelas",
|
||||
"kelas_list": []
|
||||
}
|
||||
```
|
||||
|
||||
### Case 2: Santri Punya 1 Kelas Saja
|
||||
```json
|
||||
{
|
||||
"kelas": "PB Putra A",
|
||||
"kelas_list": [
|
||||
{
|
||||
"kelompok_id": "KLMPK001",
|
||||
"kelompok_name": "PB",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 1,
|
||||
"kode_kelas": "KLS001",
|
||||
"nama_kelas": "PB Putra A",
|
||||
"is_primary": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Case 3: Santri Punya Banyak Kelas, Tidak Ada Primary
|
||||
```json
|
||||
{
|
||||
"kelas": "PB Putra A", // Fallback ke kelas pertama
|
||||
"kelas_list": [
|
||||
{
|
||||
"kelompok_id": "KLMPK001",
|
||||
"kelompok_name": "PB",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 1,
|
||||
"kode_kelas": "KLS001",
|
||||
"nama_kelas": "PB Putra A",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kelompok_id": "KLMPK002",
|
||||
"kelompok_name": "Lambatan",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 5,
|
||||
"kode_kelas": "KLS005",
|
||||
"nama_kelas": "Lambatan B",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend Implementation Details
|
||||
|
||||
### File: `app/Http/Controllers/Api/ApiAuthController.php`
|
||||
|
||||
**Methods Updated:**
|
||||
- `login()` - Lines ~74-120
|
||||
- `profile()` - Lines ~160-210
|
||||
- `buildKelasListGrouped()` - NEW private method (Lines ~215-270)
|
||||
|
||||
**Query Optimization:**
|
||||
```php
|
||||
$santri = Santri::with(['kelasSantri.kelas.kelompok', 'kelasPrimary.kelas'])
|
||||
->where('id_santri', $user->role_id)
|
||||
->first();
|
||||
```
|
||||
- **Eager loading** mencegah N+1 query problem
|
||||
- Query count: **2-3 queries** (optimal)
|
||||
- Response size: **< 10KB** untuk santri dengan 5-10 kelas
|
||||
|
||||
**Grouping Logic:**
|
||||
1. Ambil semua `santri_kelas` records
|
||||
2. Group by `kelompok_id`
|
||||
3. Map ke struktur JSON
|
||||
4. Sort by `is_primary DESC` (kelas primary di atas)
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Backend Testing
|
||||
|
||||
```bash
|
||||
# Test login endpoint
|
||||
curl -X POST http://localhost:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"id_santri": "S001", "password": "password123"}'
|
||||
|
||||
# Test profile endpoint (dengan token)
|
||||
curl -X GET http://localhost:8000/api/profile \
|
||||
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
||||
```
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ Response includes both `kelas` and `kelas_list`
|
||||
- ✅ `kelas_list` is array, grouped by kelompok
|
||||
- ✅ `is_primary` flag correct
|
||||
- ✅ No SQL errors in Laravel log
|
||||
- ✅ Response time < 500ms
|
||||
|
||||
### Backward Compatibility Testing
|
||||
|
||||
**Test dengan App Versi Lama:**
|
||||
1. App lama hanya baca field `kelas` (string)
|
||||
2. Field `kelas` tetap ada → ✅ App lama masih berfungsi
|
||||
3. Field `kelas_list` diabaikan oleh app lama → ✅ No crash
|
||||
|
||||
**Test dengan App Versi Baru:**
|
||||
1. App baru baca field `kelas_list` (array)
|
||||
2. Jika `kelas_list` null/empty → Fallback ke field `kelas`
|
||||
3. Tampilkan multiple kelas dengan UI baru
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: kelas_list selalu empty
|
||||
**Solution:**
|
||||
- Cek apakah santri sudah punya data di tabel `santri_kelas`
|
||||
- Jalankan migration: `php artisan migrate:santri-kelas-full`
|
||||
|
||||
### Problem: is_primary selalu false
|
||||
**Solution:**
|
||||
- Cek data di `santri_kelas`, kolom `is_primary`
|
||||
- Pastikan ada minimal 1 record dengan `is_primary = 1`
|
||||
- Update manual:
|
||||
```sql
|
||||
UPDATE santri_kelas SET is_primary = 1
|
||||
WHERE id_santri = 'S001' AND id_kelas = 5 LIMIT 1;
|
||||
```
|
||||
|
||||
### Problem: kelompok_name null
|
||||
**Solution:**
|
||||
- Cek relasi `kelas.kelompok` sudah eager loaded
|
||||
- Pastikan `id_kelompok` di tabel `kelas` valid
|
||||
- Cek tabel `kelompok_kelas` ada data
|
||||
|
||||
---
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
| Metric | Before | After | Notes |
|
||||
|--------|--------|-------|-------|
|
||||
| Query Count | 1 | 2-3 | Optimal dengan eager loading |
|
||||
| Response Size | ~2KB | ~5KB | Masih sangat ringan |
|
||||
| Response Time | 50ms | 80ms | Masih < 100ms (excellent) |
|
||||
| Memory Usage | 2MB | 3MB | Minimal |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0.0 | 2026-02-14 | Initial release: Single kelas (field 'kelas' saja) |
|
||||
| 2.0.0 | 2026-02-14 | **NEW**: Multiple kelas dengan `kelas_list`, backward compatible |
|
||||
|
||||
---
|
||||
|
||||
## Contact
|
||||
|
||||
Questions? Check:
|
||||
- Laravel log: `storage/logs/laravel.log`
|
||||
- API documentation: `/api/documentation` (if available)
|
||||
- Database: Check `santri_kelas` table structure
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
# Flutter Multiple Kelas UI Documentation
|
||||
|
||||
## Overview
|
||||
Aplikasi mobile Flutter telah diupdate untuk menampilkan **multiple kelas** per santri dengan UI yang clean, informatif, dan responsive.
|
||||
|
||||
---
|
||||
|
||||
## UI/UX Changes Summary
|
||||
|
||||
### BEFORE (Version 1.0)
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ Profil Santri │
|
||||
├────────────────────────────────────┤
|
||||
│ [Foto Avatar] │
|
||||
│ Ahmad Santoso │
|
||||
│ S001 │
|
||||
│ [Aktif] │
|
||||
├────────────────────────────────────┤
|
||||
│ 📋 Informasi Dasar │
|
||||
│ ID Santri: S001 │
|
||||
│ NIS: 2024001 │
|
||||
│ Nama Lengkap: Ahmad Santoso │
|
||||
│ Jenis Kelamin: Laki-laki │
|
||||
│ Kelas: Lambatan B ← SINGLE KELAS
|
||||
│ Status: Aktif │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### AFTER (Version 2.0)
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ Profil Santri │
|
||||
├────────────────────────────────────┤
|
||||
│ [Foto Avatar] │
|
||||
│ Ahmad Santoso │
|
||||
│ S001 │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ 📚 Lambatan B │ ← Primary badge
|
||||
│ └──────────────────────┘ │
|
||||
│ +3 kelas lainnya ↓ ← Hint │
|
||||
│ [Aktif] │
|
||||
├────────────────────────────────────┤
|
||||
│ 📋 Informasi Dasar │
|
||||
│ ID Santri: S001 │
|
||||
│ NIS: 2024001 │
|
||||
│ Nama Lengkap: Ahmad Santoso │
|
||||
│ Jenis Kelamin: Laki-laki │
|
||||
│ Status: Aktif │ ← Kelas dihapus
|
||||
├────────────────────────────────────┤
|
||||
│ 🎓 Kelas yang Diikuti ← NEW SECTION
|
||||
│ │
|
||||
│ ▼ 🔵 PB (1 kelas) ← Expanded │
|
||||
│ ├─ PB Putra A │
|
||||
│ │ KLS001 │
|
||||
│ │
|
||||
│ ▼ 🟠 Lambatan (2 kelas) ← Expanded │
|
||||
│ ├─ Lambatan B [⭐ Utama] ← Primary
|
||||
│ │ KLS005 │
|
||||
│ ├─ Lambatan A │
|
||||
│ │ KLS006 │
|
||||
│ │
|
||||
│ ▶ 🟢 Cepatan (1 kelas) ← Collapsed│
|
||||
│ │
|
||||
│ ▶ 🔴 Hadist (1 kelas) ← Collapsed│
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New Features
|
||||
|
||||
### 1. Primary Kelas Badge (Header)
|
||||
**Location:** Di header, antara ID Santri dan Status Badge
|
||||
|
||||
**Features:**
|
||||
- Menampilkan kelas utama (primary kelas)
|
||||
- Icon 📚 (sekolah)
|
||||
- Background: Semi-transparent white
|
||||
- Border: White border (subtle)
|
||||
- Hint: "+X kelas lainnya" jika total kelas > 1
|
||||
|
||||
**Code:**
|
||||
```dart
|
||||
Widget _buildPrimaryKelasBadge() { ... }
|
||||
```
|
||||
|
||||
### 2. Kelas yang Diikuti Section
|
||||
**Location:** Setelah "Informasi Dasar", sebelum "Alamat & Asal"
|
||||
|
||||
**Features:**
|
||||
- Section card dengan icon 🎓
|
||||
- ExpansionTile per kelompok (collapsible)
|
||||
- Color-coded badges per kelompok
|
||||
- Sortir: Primary kelas di atas
|
||||
- Badge "⭐ Utama" untuk primary kelas
|
||||
|
||||
**Code:**
|
||||
```dart
|
||||
Widget _buildKelasListSection() { ... }
|
||||
Widget _buildKelompokExpansionTile(String kelompokName, List kelasItems) { ... }
|
||||
```
|
||||
|
||||
### 3. Color Coding System
|
||||
Setiap kelompok memiliki warna unik:
|
||||
|
||||
| Kelompok | Color | Hex Code | Icon |
|
||||
|----------|-------|----------|------|
|
||||
| PB / Pondok | 🔵 Blue | #3b82f6 | 🏫 Icons.school |
|
||||
| Lambatan | 🟠 Orange | #fb923c | 📖 Icons.menu_book |
|
||||
| Cepatan | 🟢 Green | #10b981 | ⚡ Icons.speed |
|
||||
| Tahfidz | 🟣 Purple | #7C3AED | 📚 Icons.auto_stories |
|
||||
| Hadist | 🔵 Teal | #14b8a6 | 📗 Icons.import_contacts |
|
||||
| Default | ⚫ Gray | #6b7280 | 🎓 Icons.class_ |
|
||||
|
||||
**Code:**
|
||||
```dart
|
||||
Color _getKelompokColor(String kelompokName) { ... }
|
||||
IconData _getKelompokIcon(String kelompokName) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Component Breakdown
|
||||
|
||||
### ExpansionTile Structure
|
||||
|
||||
```dart
|
||||
Container (Border + Border Radius)
|
||||
└─ Theme (Hide default divider)
|
||||
└─ ExpansionTile
|
||||
├─ Leading: Colored icon badge
|
||||
├─ Title: Kelompok name (bold, colored)
|
||||
├─ Subtitle: "X kelas" (gray)
|
||||
└─ Children: List of kelas items
|
||||
└─ Container (Kelas item)
|
||||
├─ Left: Nama kelas + Kode kelas
|
||||
└─ Right: Badge "⭐ Utama" (if primary)
|
||||
```
|
||||
|
||||
### Primary Badge Indicator
|
||||
|
||||
**Styling:**
|
||||
- background: Gold (#fbbf24)
|
||||
- Icon: ⭐ Star (white, size 12)
|
||||
- Text: "Utama" (white, size 10, bold)
|
||||
- Padding: 8px horizontal, 4px vertical
|
||||
- Border radius: 8px
|
||||
|
||||
### Kelas Item Styling
|
||||
|
||||
**Primary Kelas:**
|
||||
- Background: Kelompok color with 10% opacity
|
||||
- Border: Kelompok color with 30% opacity, width 1.5px
|
||||
- Text: Bold, kelompok color
|
||||
- Badge: "⭐ Utama" visible
|
||||
|
||||
**Non-Primary Kelas:**
|
||||
- Background: Light gray (5% opacity)
|
||||
- Border: None
|
||||
- Text: Semi-bold, black87
|
||||
- Badge: Hidden
|
||||
|
||||
---
|
||||
|
||||
## Responsive Design
|
||||
|
||||
### Screen Sizes Supported
|
||||
- Min width: 320px (iPhone SE)
|
||||
- Max width: 800px (iPad)
|
||||
- Optimal: 360-428px (Most smartphones)
|
||||
|
||||
### Adaptive Behavior
|
||||
- ExpansionTile: Auto-adjust height
|
||||
- Text overflow: Ellipsis
|
||||
- Padding: Proportional to screen width
|
||||
- Card elevation: 2 (consistent)
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### 1. Lazy Loading
|
||||
- Section "Kelas yang Diikuti" hanya render saat visible
|
||||
- ExpansionTile default collapsed
|
||||
- Children di-render saat expanded
|
||||
|
||||
### 2. Minimal Dependencies
|
||||
- **NO EXTERNAL PACKAGES** untuk kelas display
|
||||
- Hanya Flutter built-in widgets:
|
||||
- ExpansionTile
|
||||
- Card
|
||||
- Container
|
||||
- Row, Column
|
||||
- Icon, Text
|
||||
|
||||
### 3. No Heavy Assets
|
||||
- Semua icon menggunakan `Icons.*` (Flutter built-in)
|
||||
- No image assets loaded
|
||||
- No SVG files
|
||||
|
||||
### 4. Efficient State Management
|
||||
- Single `_santriData` map
|
||||
- No redundant API calls
|
||||
- Cache-first strategy dengan SharedPreferences
|
||||
|
||||
---
|
||||
|
||||
## Code Files Modified
|
||||
|
||||
### File: `lib/features/profil/profil_page.dart`
|
||||
|
||||
**New Methods Added:**
|
||||
1. `_buildPrimaryKelasBadge()` - Lines ~305-360
|
||||
2. `_buildKelasListSection()` - Lines ~365-440
|
||||
3. `_buildKelompokExpansionTile()` - Lines ~445-570
|
||||
4. `_getKelompokColor()` - Lines ~575-595
|
||||
5. `_getKelompokIcon()` - Lines ~600-620
|
||||
|
||||
**Modified Sections:**
|
||||
1. `build()` method - Added conditional section display
|
||||
2. `_buildHeader()` - Added primary kelas badge call
|
||||
3. "Informasi Dasar" card - Removed kelas row
|
||||
|
||||
**Total Lines:** ~620 lines (dari ~300 lines sebelumnya)
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Defensive Programming
|
||||
|
||||
```dart
|
||||
// Handle null kelas_list
|
||||
if (_santriData?['kelas_list'] != null &&
|
||||
(_santriData!['kelas_list'] as List).isNotEmpty) {
|
||||
_buildKelasListSection()
|
||||
}
|
||||
|
||||
// Handle null kelompok
|
||||
final kelompokName = kelompok['kelompok_name'] ?? 'Unknown';
|
||||
final kelasItems = kelompok['kelas'] as List? ?? [];
|
||||
|
||||
// Handle null kelas properties
|
||||
final namaKelas = kelas['nama_kelas'] ?? '-';
|
||||
final kodeKelas = kelas['kode_kelas'] ?? '-';
|
||||
final isPrimary = kelas['is_primary'] == true;
|
||||
```
|
||||
|
||||
### Empty State
|
||||
|
||||
```dart
|
||||
if (kelasList.isEmpty) {
|
||||
return _buildSectionCard(
|
||||
title: 'Kelas yang Diikuti',
|
||||
icon: Icons.class_,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
'Belum mengikuti kelas apapun',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Guide
|
||||
|
||||
### Manual Testing Steps
|
||||
|
||||
#### Test 1: Display Multiple Kelas
|
||||
1. Login sebagai santri yang punya multiple kelas
|
||||
2. Navigasi ke tab "Profil"
|
||||
3. **Expected:**
|
||||
- Header menampilkan primary kelas badge
|
||||
- Hint "+X kelas lainnya" muncul
|
||||
- Section "Kelas yang Diikuti" visible
|
||||
- Kelompok di-group dengan benar
|
||||
- Primary kelas punya badge "⭐ Utama"
|
||||
|
||||
#### Test 2: Expansion/Collapse
|
||||
1. Tap kelompok yang collapsed
|
||||
2. **Expected:** ExpansionTile expand, menampilkan kelas items
|
||||
3. Tap lagi
|
||||
4. **Expected:** ExpansionTile collapse
|
||||
|
||||
#### Test 3: Primary Badge Visibility
|
||||
1. Cari kelas dengan `is_primary = true`
|
||||
2. **Expected:** Badge "⭐ Utama" muncul di kanan kelas item
|
||||
3. Cari kelas dengan `is_primary = false`
|
||||
4. **Expected:** Badge tidak muncul
|
||||
|
||||
#### Test 4: Empty State
|
||||
1. Login sebagai santri belum punya kelas
|
||||
2. **Expected:**
|
||||
- Section "Kelas yang Diikuti" TIDAK muncul
|
||||
- Field kelas di "Informasi Dasar" tidak ada
|
||||
|
||||
#### Test 5: Single Kelas
|
||||
1. Login sebagai santri dengan 1 kelas saja
|
||||
2. **Expected:**
|
||||
- Primary kelas badge muncul
|
||||
- Hint "+X kelas lainnya" TIDAK muncul (karena cuma 1)
|
||||
- Section "Kelas yang Diikuti" muncul dengan 1 kelompok
|
||||
|
||||
#### Test 6: Color Coding
|
||||
1. Cek kelompok "PB" → Blue
|
||||
2. Cek kelompok "Lambatan" → Orange
|
||||
3. Cek kelompok "Cepatan" → Green
|
||||
4. Cek kelompok "Tahfidz" → Purple
|
||||
5. Cek kelompok "Hadist" → Teal
|
||||
|
||||
#### Test 7: Responsive
|
||||
1. Test di screen 320px (iPhone SE)
|
||||
2. Test di screen 375px (iPhone 13)
|
||||
3. Test di screen 428px (iPhone 13 Pro Max)
|
||||
4. **Expected:** No horizontal overflow, text ellipsis bekerja
|
||||
|
||||
#### Test 8: Pull-to-Refresh
|
||||
1. Swipe down di profil page
|
||||
2. **Expected:** Loading indicator muncul, data refresh dari API
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Problem: Section tidak muncul
|
||||
**Check:**
|
||||
```dart
|
||||
print('kelas_list: ${_santriData?['kelas_list']}');
|
||||
print('is List: ${_santriData?['kelas_list'] is List}');
|
||||
print('isEmpty: ${(_santriData?['kelas_list'] as List?)?.isEmpty}');
|
||||
```
|
||||
|
||||
### Problem: ExpansionTile tidak expand
|
||||
**Check:**
|
||||
- Pastikan `Theme` wrapper ada (untuk hide default divider)
|
||||
- Cek console error saat tap
|
||||
|
||||
### Problem: Badge "Utama" tidak muncul
|
||||
**Check:**
|
||||
```dart
|
||||
print('isPrimary: ${kelas['is_primary']}');
|
||||
print('isPrimary type: ${kelas['is_primary'].runtimeType}');
|
||||
```
|
||||
|
||||
### Problem: Color salah
|
||||
**Check:**
|
||||
```dart
|
||||
print('kelompokName: $kelompokName');
|
||||
print('color: ${_getKelompokColor(kelompokName)}');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements (Optional)
|
||||
|
||||
### Phase 2 (Nice to Have)
|
||||
|
||||
1. **Smooth Animation**
|
||||
- Add `AnimatedSwitcher` untuk smooth transition
|
||||
- Fade animation saat expand/collapse
|
||||
|
||||
2. **Search/Filter**
|
||||
- Search box untuk cari kelas
|
||||
- Filter by kelompok
|
||||
|
||||
3. **Tap to Detail**
|
||||
- Tap kelas item → Navigate ke detail kelas page
|
||||
- Show jadwal, materi, guru, dll
|
||||
|
||||
4. **Statistics**
|
||||
- Show kehadiran per kelas
|
||||
- Show nilai rata-rata per kelas
|
||||
|
||||
### Phase 3 (Advanced)
|
||||
|
||||
1. **Tahun Ajaran**
|
||||
- Display tahun ajaran per kelas
|
||||
- Filter by tahun ajaran
|
||||
|
||||
2. **Kelas History**
|
||||
- Show riwayat kelas tahun-tahun sebelumnya
|
||||
|
||||
3. **QR Code**
|
||||
- Generate QR code untuk absensi per kelas
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0.0 | 2026-02-14 | Initial: Single kelas display |
|
||||
| 2.0.0 | 2026-02-14 | **NEW**: Multiple kelas with ExpansionTile, color coding, primary badge |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Flutter Analyze Errors
|
||||
```bash
|
||||
cd sim_mobile
|
||||
flutter analyze
|
||||
```
|
||||
|
||||
### Flutter Format
|
||||
```bash
|
||||
flutter format lib/features/profil/profil_page.dart
|
||||
```
|
||||
|
||||
### Build APK (Test)
|
||||
```bash
|
||||
flutter build apk --debug
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contact & Support
|
||||
|
||||
- File: `lib/features/profil/profil_page.dart`
|
||||
- Backup: `lib/features/profil/profil_page.dart.backup`
|
||||
- Flutter version: 3.x
|
||||
- Dart version: 3.x
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
# Panduan Perbaikan Sistem Login Mobile SIM-PKPPS
|
||||
|
||||
## ✅ Perbaikan yang Sudah Dilakukan
|
||||
|
||||
### 1. **Auto-Fill Username & Password** ✅
|
||||
- JavaScript diperbaiki dari `@push('scripts')` menjadi inline `<script>` di [create_account.blade.php](sim-pkpps/resources/views/admin/users/create_account.blade.php)
|
||||
- Saat memilih santri di dropdown, otomatis mengisi:
|
||||
- **Username**: Nama Santri
|
||||
- **Password**: NIS Santri
|
||||
- Field menjadi readonly saat sudah terisi otomatis
|
||||
- Jika santri belum punya NIS, akan muncul alert dan field bisa diisi manual
|
||||
|
||||
### 2. **Fungsi Delete Akun** ✅
|
||||
- Ditambahkan method `destroyAccount()` di [UserController.php](sim-pkpps/app/Http/Controllers/Admin/UserController.php)
|
||||
- Routes ditambahkan:
|
||||
- `DELETE /admin/users/santri/{user}` → `admin.users.santri_destroy`
|
||||
- `DELETE /admin/users/wali/{user}` → `admin.users.wali_destroy`
|
||||
- Tombol delete dengan konfirmasi di:
|
||||
- [santri_accounts.blade.php](sim-pkpps/resources/views/admin/users/santri_accounts.blade.php)
|
||||
- [wali_accounts.blade.php](sim-pkpps/resources/views/admin/users/wali_accounts.blade.php)
|
||||
|
||||
### 3. **Fungsi Reset Password** ✅
|
||||
- Ditambahkan method `resetPassword()` di [UserController.php](sim-pkpps/app/Http/Controllers/Admin/UserController.php)
|
||||
- Reset password otomatis ke NIS santri
|
||||
- Routes ditambahkan:
|
||||
- `POST /admin/users/santri/{user}/reset-password` → `admin.users.santri_reset_password`
|
||||
- `POST /admin/users/wali/{user}/reset-password` → `admin.users.wali_reset_password`
|
||||
- Tombol reset dengan konfirmasi di view akun santri/wali
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Cara Testing Login Mobile
|
||||
|
||||
### A. Test API Login Menggunakan File PHP Test
|
||||
1. **Edit file [test_login.php](test_login.php)**
|
||||
```php
|
||||
$username = "Ahmad Fauzi"; // Ganti dengan nama santri yang sudah punya akun wali
|
||||
$password = "2024001"; // Ganti dengan NIS santri tersebut
|
||||
```
|
||||
|
||||
2. **Jalankan dari terminal:**
|
||||
```bash
|
||||
php test_login.php
|
||||
```
|
||||
|
||||
3. **Hasil yang diharapkan:**
|
||||
```
|
||||
✅ LOGIN BERHASIL!
|
||||
Token: 1|xxxxxxxxxxxxx
|
||||
User: Ahmad Fauzi
|
||||
Role: wali
|
||||
```
|
||||
|
||||
### B. Cek Database Untuk Memastikan Akun Ada
|
||||
```sql
|
||||
-- Cek akun wali yang sudah dibuat
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.role,
|
||||
s.nama_lengkap,
|
||||
s.nis
|
||||
FROM users u
|
||||
JOIN santris s ON u.role_id = s.id_santri
|
||||
WHERE u.role = 'wali';
|
||||
```
|
||||
|
||||
### C. Troubleshooting Login Mobile Gagal
|
||||
|
||||
#### ❌ Error: "Username atau password salah"
|
||||
**Penyebab:**
|
||||
- Username tidak match persis dengan database (case-sensitive, spasi, typo)
|
||||
- Password salah (pastikan menggunakan NIS yang benar)
|
||||
|
||||
**Solusi:**
|
||||
1. Cek username di database:
|
||||
```sql
|
||||
SELECT username FROM users WHERE role='wali';
|
||||
```
|
||||
2. Pastikan di Flutter login menggunakan username yang **PERSIS SAMA** termasuk huruf besar/kecil dan spasi
|
||||
3. Password harus NIS santri (bisa dicek di tabel santris)
|
||||
|
||||
#### ❌ Error: "Connection refused" / "Network error"
|
||||
**Penyebab:**
|
||||
- Laravel server tidak jalan
|
||||
- Base URL salah di Flutter
|
||||
|
||||
**Solusi:**
|
||||
1. Pastikan Laravel server running:
|
||||
```bash
|
||||
cd sim-pkpps
|
||||
php artisan serve
|
||||
```
|
||||
2. Cek [app_config.dart](sim_mobile/lib/core/config/app_config.dart):
|
||||
```dart
|
||||
static const String baseUrl = 'http://10.0.2.2:8000/api/v1'; // Emulator
|
||||
// atau
|
||||
static const String baseUrl = 'http://192.168.x.x:8000/api/v1'; // Real device
|
||||
```
|
||||
|
||||
#### ❌ Error: "Akun tidak memiliki akses mobile"
|
||||
**Penyebab:**
|
||||
- User role bukan 'santri' atau 'wali'
|
||||
|
||||
**Solusi:**
|
||||
- Pastikan di database field `role` adalah 'wali', bukan 'admin' atau lainnya
|
||||
|
||||
---
|
||||
|
||||
## 📋 Checklist Testing Lengkap
|
||||
|
||||
### 1. Testing Web Admin (Buat Akun Wali)
|
||||
- [ ] Buka halaman Manajemen Akun Wali (`/admin/users/wali`)
|
||||
- [ ] Klik "Buat Akun Wali"
|
||||
- [ ] Pilih santri dari dropdown
|
||||
- [ ] **Cek:** Username otomatis terisi dengan nama santri ✅
|
||||
- [ ] **Cek:** Password otomatis terisi dengan NIS ✅
|
||||
- [ ] Klik "Simpan"
|
||||
- [ ] **Cek:** Akun muncul di daftar dengan info login ✅
|
||||
|
||||
### 2. Testing Fungsi Delete
|
||||
- [ ] Di halaman Manajemen Akun Wali
|
||||
- [ ] Klik tombol "Hapus" pada salah satu akun
|
||||
- [ ] **Cek:** Muncul konfirmasi dialog ✅
|
||||
- [ ] Klik OK
|
||||
- [ ] **Cek:** Akun terhapus dari daftar ✅
|
||||
|
||||
### 3. Testing Fungsi Reset Password
|
||||
- [ ] Di halaman Manajemen Akun Wali
|
||||
- [ ] Klik tombol "Reset" pada salah satu akun
|
||||
- [ ] **Cek:** Muncul konfirmasi dialog ✅
|
||||
- [ ] Klik OK
|
||||
- [ ] **Cek:** Muncul pesan sukses dengan info password baru (NIS) ✅
|
||||
|
||||
### 4. Testing Login Mobile
|
||||
- [ ] Jalankan Flutter app (emulator/real device)
|
||||
- [ ] Pastikan Laravel server running (`php artisan serve`)
|
||||
- [ ] Di login page, masukkan:
|
||||
- **Username**: Nama santri (persis seperti di database)
|
||||
- **Password**: NIS santri
|
||||
- [ ] Klik Login
|
||||
- [ ] **Cek:** Berhasil masuk ke dashboard ✅
|
||||
- [ ] **Cek:** Menu Profil menampilkan data santri ✅
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Debug Mode - Jika Masih Gagal
|
||||
|
||||
### 1. Tambahkan Log di ApiAuthController
|
||||
Edit [ApiAuthController.php](sim-pkpps/app/Http/Controllers/Api/ApiAuthController.php):
|
||||
|
||||
```php
|
||||
public function login(Request $request)
|
||||
{
|
||||
// Log untuk debug
|
||||
\Log::info('Login attempt', [
|
||||
'username' => $request->id_santri,
|
||||
'password_length' => strlen($request->password)
|
||||
]);
|
||||
|
||||
$user = User::where('username', $request->id_santri)->first();
|
||||
|
||||
if (!$user) {
|
||||
\Log::warning('User not found', ['username' => $request->id_santri]);
|
||||
}
|
||||
|
||||
// ... kode lainnya
|
||||
}
|
||||
```
|
||||
|
||||
Cek log di `storage/logs/laravel.log`
|
||||
|
||||
### 2. Test Manual Dengan Postman/cURL
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id_santri": "Ahmad Fauzi",
|
||||
"password": "2024001"
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. Validasi Data di Database
|
||||
|
||||
```sql
|
||||
-- Cek akun wali yang baru dibuat
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.role,
|
||||
u.role_id,
|
||||
s.nama_lengkap,
|
||||
s.nis,
|
||||
LENGTH(u.password) as password_hash_length
|
||||
FROM users u
|
||||
JOIN santris s ON u.role_id = s.id_santri
|
||||
WHERE u.role = 'wali'
|
||||
ORDER BY u.id DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
**Password hash length seharusnya 60 karakter (bcrypt)**
|
||||
|
||||
---
|
||||
|
||||
## 📱 Format Login yang Benar
|
||||
|
||||
| Field | Value | Contoh |
|
||||
|-------|-------|--------|
|
||||
| Username | Nama Santri (PERSIS seperti di database) | `Ahmad Fauzi` |
|
||||
| Password | NIS Santri | `2024001` |
|
||||
| Role | Otomatis terdeteksi dari database | `wali` |
|
||||
|
||||
**⚠️ PENTING:**
|
||||
- Username **case-sensitive**: "Ahmad Fauzi" ≠ "ahmad fauzi"
|
||||
- Spasi dihitung: "Ahmad Fauzi" ≠ "AhmadFauzi"
|
||||
- Password adalah NIS **plain text** (tidak di-hash saat input), Laravel akan auto-verify hash
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Keamanan
|
||||
|
||||
- Password di database di-hash dengan bcrypt (60 karakter)
|
||||
- Token menggunakan Laravel Sanctum
|
||||
- Setiap login, token lama dihapus (single device per account)
|
||||
- API hanya bisa diakses oleh role 'santri' dan 'wali'
|
||||
|
||||
---
|
||||
|
||||
## 📞 Troubleshooting Contact
|
||||
|
||||
Jika masih ada masalah:
|
||||
1. Cek file log Laravel: `sim-pkpps/storage/logs/laravel.log`
|
||||
2. Cek Flutter console untuk error network
|
||||
3. Pastikan username & password **100% match** dengan database
|
||||
4. Test dengan file [test_login.php](test_login.php) terlebih dahulu sebelum test di mobile
|
||||
|
||||
---
|
||||
|
||||
**Semua fungsi sudah diimplementasikan:**
|
||||
- ✅ Auto-fill username & password
|
||||
- ✅ Delete akun
|
||||
- ✅ Reset password
|
||||
- ✅ Login mobile ready (tinggal test dengan data yang benar)
|
||||
|
|
@ -1,350 +0,0 @@
|
|||
# 🎓 Multiple Kelas System - Implementation Summary
|
||||
|
||||
## ✅ COMPLETED
|
||||
|
||||
Sistem multiple kelas untuk santri telah **SELESAI DIIMPLEMENTASI** pada backend Laravel dan aplikasi mobile Flutter.
|
||||
|
||||
---
|
||||
|
||||
## 📦 What's New
|
||||
|
||||
### Backend (Laravel)
|
||||
✅ API `/api/login` dan `/api/profile` sekarang return **multiple kelas** grouped by kelompok
|
||||
✅ Field `kelas_list` (array) untuk semua kelas santri
|
||||
✅ Field `kelas` (string) tetap ada untuk **backward compatibility**
|
||||
✅ Flag `is_primary` untuk menandai kelas utama
|
||||
✅ **Eager loading** untuk optimasi query (No N+1 problem)
|
||||
|
||||
### Frontend (Flutter)
|
||||
✅ Section baru **"Kelas yang Diikuti"** di profil page
|
||||
✅ **Primary kelas badge** di header (dengan icon 📚)
|
||||
✅ **ExpansionTile** per kelompok (collapsible/expandable)
|
||||
✅ **Color-coded** badges untuk setiap kelompok kelas
|
||||
✅ Badge **"⭐ Utama"** untuk kelas primary
|
||||
✅ **Responsive design** (support 320px - 800px screen width)
|
||||
✅ **Pull-to-refresh** untuk update data
|
||||
✅ **Empty state handling** (santri tanpa kelas)
|
||||
|
||||
---
|
||||
|
||||
## 📁 Files Modified/Created
|
||||
|
||||
### Backend
|
||||
| File | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| `app/Http/Controllers/Api/ApiAuthController.php` | ✏️ **MODIFIED** | Added kelas_list support in login() & profile() |
|
||||
| | | Added buildKelasListGrouped() helper method |
|
||||
|
||||
### Frontend
|
||||
| File | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| `sim_mobile/lib/features/profil/profil_page.dart` | ✏️ **MODIFIED** | Complete rewrite with multi-kelas support |
|
||||
| `sim_mobile/lib/features/profil/profil_page.dart.backup` | 📄 **CREATED** | Backup of original file |
|
||||
|
||||
### Documentation
|
||||
| File | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| `MULTIPLE_KELAS_API_RESPONSE.md` | 📄 **CREATED** | API structure & response examples |
|
||||
| `MULTIPLE_KELAS_UI_FLUTTER.md` | 📄 **CREATED** | UI/UX design & implementation guide |
|
||||
| `TESTING_CHECKLIST_MULTIPLE_KELAS.md` | 📄 **CREATED** | Complete testing checklist (26 tests) |
|
||||
| `README_MULTIPLE_KELAS.md` | 📄 **CREATED** | This file - Quick start guide |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start Guide
|
||||
|
||||
### Step 1: Verify Backend
|
||||
|
||||
```bash
|
||||
# Navigate to Laravel project
|
||||
cd c:\xampp\htdocs\TugasAkhir\sim-pkpps
|
||||
|
||||
# Check for syntax errors
|
||||
php artisan route:list | grep api
|
||||
|
||||
# Test login endpoint (replace S001 & password)
|
||||
curl -X POST http://localhost:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"id_santri": "S001", "password": "password123"}'
|
||||
```
|
||||
|
||||
**Expected:** Response includes `kelas` (string) and `kelas_list` (array)
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Test Flutter App
|
||||
|
||||
```bash
|
||||
# Navigate to Flutter project
|
||||
cd c:\xampp\htdocs\TugasAkhir\sim_mobile
|
||||
|
||||
# Clean build
|
||||
flutter clean
|
||||
flutter pub get
|
||||
|
||||
# Run on device/emulator
|
||||
flutter run
|
||||
```
|
||||
|
||||
**Test Flow:**
|
||||
1. Login dengan santri yang punya multiple kelas
|
||||
2. Tap tab "Profil"
|
||||
3. **Expected:** Section "Kelas yang Diikuti" muncul
|
||||
4. Tap kelompok → ExpansionTile expand
|
||||
5. Verify badge "⭐ Utama" di primary kelas
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Create Test Data (Manual)
|
||||
|
||||
```sql
|
||||
-- Connect to your MySQL database
|
||||
USE sim_pkpps;
|
||||
|
||||
-- Insert sample kelas for testing
|
||||
-- Replace S001 with your test santri ID
|
||||
|
||||
-- Kelas 1: PB Putra A (not primary)
|
||||
INSERT INTO santri_kelas (id_santri, id_kelas, tahun_ajaran, is_primary)
|
||||
VALUES ('S001', 1, '2025/2026', 0);
|
||||
|
||||
-- Kelas 2: Lambatan B (PRIMARY)
|
||||
INSERT INTO santri_kelas (id_santri, id_kelas, tahun_ajaran, is_primary)
|
||||
VALUES ('S001', 5, '2025/2026', 1);
|
||||
|
||||
-- Kelas 3: Cepatan A (not primary)
|
||||
INSERT INTO santri_kelas (id_santri, id_kelas, tahun_ajaran, is_primary)
|
||||
VALUES ('S001', 8, '2025/2026', 0);
|
||||
|
||||
-- Kelas 4: Hadist Pemula (not primary)
|
||||
INSERT INTO santri_kelas (id_santri, id_kelas, tahun_ajaran, is_primary)
|
||||
VALUES ('S001', 15, '2025/2026', 0);
|
||||
|
||||
-- Verify
|
||||
SELECT * FROM santri_kelas WHERE id_santri = 'S001';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI Preview (Text Description)
|
||||
|
||||
### HEADER
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ [Avatar Foto Santri] │
|
||||
│ Ahmad Santoso │
|
||||
│ S001 │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ 📚 Lambatan B │ ← Primary badge
|
||||
│ └──────────────────────┘ │
|
||||
│ +3 kelas lainnya ↓ ← Hint │
|
||||
│ [Aktif] │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### KELAS SECTION
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ 🎓 Kelas yang Diikuti ← NEW │
|
||||
├────────────────────────────────────┤
|
||||
│ ▼ 🔵 PB (1 kelas) ← Expanded │
|
||||
│ ├─ PB Putra A │
|
||||
│ │ KLS001 │
|
||||
│ │
|
||||
│ ▼ 🟠 Lambatan (1 kelas) │
|
||||
│ ├─ Lambatan B [⭐ Utama] ← Primary
|
||||
│ │ KLS005 │
|
||||
│ │
|
||||
│ ▶ 🟢 Cepatan (1 kelas) ← Collapsed│
|
||||
│ ▶ 🟣 Hadist (1 kelas) │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 API Response Example
|
||||
|
||||
### Login/Profile Response
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id_santri": "S001",
|
||||
"nama_lengkap": "Ahmad Santoso",
|
||||
|
||||
// ✅ Backward compatibility
|
||||
"kelas": "Lambatan B",
|
||||
|
||||
// 🆕 NEW: Multiple kelas
|
||||
"kelas_list": [
|
||||
{
|
||||
"kelompok_id": "KLMPK001",
|
||||
"kelompok_name": "PB",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 1,
|
||||
"kode_kelas": "KLS001",
|
||||
"nama_kelas": "PB Putra A",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kelompok_id": "KLMPK002",
|
||||
"kelompok_name": "Lambatan",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 5,
|
||||
"kode_kelas": "KLS005",
|
||||
"nama_kelas": "Lambatan B",
|
||||
"is_primary": true // ⭐ Primary
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Color Coding Reference
|
||||
|
||||
| Kelompok | Color | Hex | Icon |
|
||||
|----------|-------|-----|------|
|
||||
| PB / Pondok | 🔵 Blue | #3b82f6 | 🏫 school |
|
||||
| Lambatan | 🟠 Orange | #fb923c | 📖 menu_book |
|
||||
| Cepatan | 🟢 Green | #10b981 | ⚡ speed |
|
||||
| Tahfidz | 🟣 Purple | #7C3AED | 📚 auto_stories |
|
||||
| Hadist | 🔵 Teal | #14b8a6 | 📗 import_contacts |
|
||||
| Default | ⚫ Gray | #6b7280 | 🎓 class_ |
|
||||
|
||||
---
|
||||
|
||||
## ✅ Testing Checklist (Quick)
|
||||
|
||||
### Backend
|
||||
- [ ] Login returns `kelas_list` array
|
||||
- [ ] Primary kelas has `is_primary: true`
|
||||
- [ ] Field `kelas` masih ada (backward compat)
|
||||
- [ ] Query count < 5 (no N+1)
|
||||
- [ ] Response time < 500ms
|
||||
|
||||
### Frontend
|
||||
- [ ] Section "Kelas yang Diikuti" muncul
|
||||
- [ ] Primary kelas badge di header
|
||||
- [ ] ExpansionTile bisa expand/collapse
|
||||
- [ ] Badge "⭐ Utama" di primary kelas
|
||||
- [ ] Color coding benar per kelompok
|
||||
- [ ] Pull-to-refresh works
|
||||
- [ ] Empty state handled (santri tanpa kelas)
|
||||
- [ ] Responsive (320px - 800px)
|
||||
|
||||
### Integration
|
||||
- [ ] Admin add kelas → Mobile refresh → Kelas baru muncul
|
||||
- [ ] Admin change primary → Mobile refresh → Primary berubah
|
||||
- [ ] Old app + New backend → No crash
|
||||
- [ ] New app + Old backend → Fallback to single kelas
|
||||
|
||||
**📋 Full Testing Checklist:** See `TESTING_CHECKLIST_MULTIPLE_KELAS.md` (26 detailed tests)
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Problem: kelas_list always empty
|
||||
**Solution:**
|
||||
1. Check `santri_kelas` table has data
|
||||
2. Run: `SELECT * FROM santri_kelas WHERE id_santri = 'YOUR_SANTRI_ID';`
|
||||
3. If empty, insert sample data (see Step 3 above)
|
||||
|
||||
### Problem: Primary badge not showing in Flutter
|
||||
**Solution:**
|
||||
1. Check `is_primary` column in database
|
||||
2. Ensure at least 1 record has `is_primary = 1`
|
||||
3. Pull-to-refresh in app
|
||||
|
||||
### Problem: ExpansionTile not expanding
|
||||
**Solution:**
|
||||
1. Check Flutter console for errors
|
||||
2. Ensure `kelas_list` is properly parsed as List
|
||||
3. Debug: `print(_santriData?['kelas_list']);`
|
||||
|
||||
### Problem: API returns 500 error
|
||||
**Solution:**
|
||||
1. Check Laravel log: `storage/logs/laravel.log`
|
||||
2. Verify database relationships (kelompok, kelas, santri_kelas)
|
||||
3. Test query manually in MySQL
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Reference
|
||||
|
||||
| Document | Description | When to Read |
|
||||
|----------|-------------|--------------|
|
||||
| **MULTIPLE_KELAS_API_RESPONSE.md** | API structure, response examples, edge cases | Backend development/testing |
|
||||
| **MULTIPLE_KELAS_UI_FLUTTER.md** | UI design, widget breakdown, code explanation | Frontend development/customization |
|
||||
| **TESTING_CHECKLIST_MULTIPLE_KELAS.md** | Complete test scenarios (26 tests) | Quality assurance/testing |
|
||||
| **README_MULTIPLE_KELAS.md** | This file - Quick start & overview | Getting started |
|
||||
|
||||
---
|
||||
|
||||
## 🔜 Next Steps (Optional Enhancements)
|
||||
|
||||
### Phase 2 (Nice to Have)
|
||||
- [ ] Smooth expand/collapse animation
|
||||
- [ ] Search/filter kelas by name
|
||||
- [ ] Tap kelas → Navigate to detail page
|
||||
|
||||
### Phase 3 (Advanced)
|
||||
- [ ] Display tahun_ajaran per kelas
|
||||
- [ ] Kelas history (riwayat tahun sebelumnya)
|
||||
- [ ] Statistics per kelas (kehadiran, nilai)
|
||||
- [ ] QR code for absensi per kelas
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support & Contact
|
||||
|
||||
**Created by:** GitHub Copilot (Claude Sonnet 4.5)
|
||||
**Date:** February 14, 2026
|
||||
**Version:** 2.0.0
|
||||
|
||||
**Files to Check:**
|
||||
- Laravel Log: `sim-pkpps/storage/logs/laravel.log`
|
||||
- Database: `sim_pkpps` → `santri_kelas` table
|
||||
- Flutter Console: Run `flutter run` to see real-time logs
|
||||
|
||||
**Backup Files:**
|
||||
- `sim_mobile/lib/features/profil/profil_page.dart.backup` (original version)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Key Features Summary
|
||||
|
||||
1. **Multiple Kelas per Santri** - 1 santri bisa ikut banyak kelas dari berbagai kelompok
|
||||
2. **Primary Kelas Flag** - Tandai kelas utama dengan `is_primary`
|
||||
3. **Backward Compatible** - Field `kelas` lama tetap ada
|
||||
4. **Optimized Queries** - Eager loading, no N+1 problem
|
||||
5. **Clean UI** - ExpansionTile, color-coded, responsive
|
||||
6. **Lightweight** - No heavy libraries, pure Flutter widgets
|
||||
7. **Well Documented** - 3 comprehensive docs + testing checklist
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Success Criteria
|
||||
|
||||
✅ Backend API returns `kelas_list` in proper structure
|
||||
✅ Flutter app displays multiple kelas grouped by kelompok
|
||||
✅ Primary kelas clearly indicated with badge
|
||||
✅ App responsive on all screen sizes
|
||||
✅ No performance degradation (< 500ms API, 60 FPS UI)
|
||||
✅ Backward compatible with old app versions
|
||||
✅ Comprehensive documentation created
|
||||
✅ Testing checklist provided
|
||||
|
||||
---
|
||||
|
||||
**STATUS: READY FOR TESTING** 🚀
|
||||
|
||||
Start with **Step 1** above and follow the testing checklist!
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
# REFACTORING QUICK REFERENCE
|
||||
# ============================
|
||||
|
||||
## 1. MIGRATE DATA
|
||||
```bash
|
||||
# Test migration (dry-run)
|
||||
php artisan migrate:santri-kelas --dry-run
|
||||
|
||||
# Run actual migration
|
||||
php artisan migrate:santri-kelas
|
||||
|
||||
# Force overwrite existing data
|
||||
php artisan migrate:santri-kelas --force
|
||||
```
|
||||
|
||||
## 2. SCAN CODEBASE
|
||||
```bash
|
||||
# Generate usage report
|
||||
php scan_kelas_usage.php
|
||||
|
||||
# View report
|
||||
cat KELAS_USAGE_MAP.md
|
||||
# or open in editor
|
||||
code KELAS_USAGE_MAP.md
|
||||
```
|
||||
|
||||
## 3. REFACTORING PATTERNS
|
||||
|
||||
### Pattern 1: Display in Views (MEDIUM Priority)
|
||||
```blade
|
||||
<!-- OLD -->
|
||||
{{ $santri->kelas }}
|
||||
|
||||
<!-- NEW (Backward Compatible) -->
|
||||
{{ $santri->kelas_name }}
|
||||
```
|
||||
|
||||
### Pattern 2: Filter in Controllers (HIGH Priority)
|
||||
```php
|
||||
// OLD
|
||||
$santris = Santri::where('kelas', 'PB')->get();
|
||||
|
||||
// NEW
|
||||
$santris = Santri::whereHas('kelasSantri', function($q) {
|
||||
$q->where('id_kelas', 1); // PB = 1
|
||||
})->get();
|
||||
|
||||
// OR using kelas relation
|
||||
$kelas = Kelas::where('nama_kelas', 'PB')->first();
|
||||
$santris = $kelas->santris;
|
||||
```
|
||||
|
||||
### Pattern 3: Multiple Kelas Filter (HIGH Priority)
|
||||
```php
|
||||
// OLD
|
||||
$santris = Santri::whereIn('kelas', ['PB', 'Lambatan'])->get();
|
||||
|
||||
// NEW
|
||||
$kelasIds = Kelas::whereIn('nama_kelas', ['PB', 'Lambatan'])->pluck('id');
|
||||
$santris = Santri::whereHas('kelasSantri', function($q) use ($kelasIds) {
|
||||
$q->whereIn('id_kelas', $kelasIds);
|
||||
})->get();
|
||||
```
|
||||
|
||||
### Pattern 4: Kegiatan Eligible Santris (HIGH Priority)
|
||||
```php
|
||||
// OLD: Manual filter by kelas
|
||||
$santris = Santri::whereIn('kelas', ['PB', 'Lambatan'])->get();
|
||||
|
||||
// NEW: Use helper method
|
||||
$santris = $kegiatan->getEligibleSantris();
|
||||
// This automatically handles:
|
||||
// - Umum (all santri)
|
||||
// - Specific kelas (filtered)
|
||||
```
|
||||
|
||||
### Pattern 5: Check Santri Kelas
|
||||
```php
|
||||
// NEW: Check if santri in specific kelas
|
||||
if ($santri->hasKelas($id_kelas)) {
|
||||
// Do something
|
||||
}
|
||||
|
||||
// Get all kelas for santri in current year
|
||||
$kelasList = $santri->getKelasByTahun('2024/2025');
|
||||
```
|
||||
|
||||
## 4. TESTING CHECKLIST
|
||||
|
||||
```bash
|
||||
# After each refactor, test:
|
||||
□ Display: Santri detail page shows correct kelas
|
||||
□ Filter: Santri list filter by kelas works
|
||||
□ Stats: Dashboard statistics by kelas accurate
|
||||
□ Kegiatan: Filtering by kelas works
|
||||
□ Absensi: Shows correct santri per kegiatan
|
||||
□ Reports: Include correct kelas information
|
||||
□ Mobile: API returns kelas data correctly
|
||||
```
|
||||
|
||||
## 5. KELAS ID MAPPING
|
||||
|
||||
```
|
||||
PB -> ID: 1 (KLS001)
|
||||
Lambatan -> ID: 2 (KLS002)
|
||||
Cepatan -> ID: 3 (KLS003)
|
||||
```
|
||||
|
||||
## 6. COMMON ISSUES & FIXES
|
||||
|
||||
### Issue: Query too slow
|
||||
```php
|
||||
// Add eager loading
|
||||
$santris = Santri::with(['kelasPrimary.kelas'])->get();
|
||||
```
|
||||
|
||||
### Issue: Kelas not showing
|
||||
```php
|
||||
// Make sure santri has been migrated
|
||||
php artisan migrate:santri-kelas
|
||||
|
||||
// Check in database
|
||||
SELECT * FROM santri_kelas WHERE id_santri = 'S001';
|
||||
```
|
||||
|
||||
### Issue: Multiple kelas showing
|
||||
```php
|
||||
// Get only primary kelas
|
||||
$primaryKelas = $santri->kelasPrimary->kelas->nama_kelas;
|
||||
```
|
||||
|
||||
## 7. ROLLBACK (If needed)
|
||||
|
||||
```php
|
||||
// If something goes wrong, you can:
|
||||
// 1. Delete migrated data
|
||||
DELETE FROM santri_kelas WHERE tahun_ajaran = '2024/2025';
|
||||
|
||||
// 2. Re-run migration
|
||||
php artisan migrate:santri-kelas --force
|
||||
|
||||
// 3. Old column 'kelas' is still there for fallback
|
||||
```
|
||||
|
||||
## 8. FILES TO PRIORITIZE
|
||||
|
||||
### HIGH (Do First):
|
||||
1. app/Http/Controllers/Admin/CapaianController.php
|
||||
2. app/Http/Controllers/Admin/SantriController.php
|
||||
3. Any controller with where('kelas') or whereIn('kelas')
|
||||
|
||||
### MEDIUM (Do After High):
|
||||
1. All blade view files (24 files)
|
||||
2. Change {{ $santri->kelas }} to {{ $santri->kelas_name }}
|
||||
|
||||
### LOW (Do Last):
|
||||
1. API controllers (already may work with accessor)
|
||||
2. Other controllers without direct kelas query
|
||||
|
||||
## 9. USEFUL COMMANDS
|
||||
|
||||
```bash
|
||||
# Check migration status
|
||||
php artisan migrate:status
|
||||
|
||||
# Rollback last migration (if needed)
|
||||
php artisan migrate:rollback
|
||||
|
||||
# Seed kelas data
|
||||
php artisan db:seed --class=KelompokKelasSeeder
|
||||
php artisan db:seed --class=KelasSeeder
|
||||
|
||||
# Check current data
|
||||
php artisan tinker
|
||||
>>> Santri::with('kelasPrimary.kelas')->first()->kelas_name
|
||||
>>> SantriKelas::count()
|
||||
```
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
# RINGKASAN PERBAIKAN SISTEM LOGIN MOBILE
|
||||
|
||||
## ✅ SUDAH DIPERBAIKI
|
||||
|
||||
### 1. **Auto-Fill Username & Password** ✅
|
||||
- File: `sim-pkpps/resources/views/admin/users/create_account.blade.php`
|
||||
- JavaScript diperbaiki (tidak lagi menggunakan @push)
|
||||
- Saat pilih santri → otomatis isi username (nama) & password (NIS)
|
||||
- Field readonly otomatis untuk wali
|
||||
|
||||
### 2. **Fungsi Delete Akun** ✅
|
||||
- File: `sim-pkpps/app/Http/Controllers/Admin/UserController.php`
|
||||
- Method baru: `destroyAccount()`
|
||||
- Routes:
|
||||
- DELETE `/admin/users/santri/{user}`
|
||||
- DELETE `/admin/users/wali/{user}`
|
||||
- Tombol delete ada di view santri_accounts dan wali_accounts
|
||||
|
||||
### 3. **Fungsi Reset Password** ✅
|
||||
- File: `sim-pkpps/app/Http/Controllers/Admin/UserController.php`
|
||||
- Method baru: `resetPassword()`
|
||||
- Auto-reset password ke NIS santri
|
||||
- Routes:
|
||||
- POST `/admin/users/santri/{user}/reset-password`
|
||||
- POST `/admin/users/wali/{user}/reset-password`
|
||||
- Tombol reset ada di view santri_accounts dan wali_accounts
|
||||
|
||||
---
|
||||
|
||||
## 🔍 CARA TEST LOGIN MOBILE
|
||||
|
||||
### Step 1: Pastikan Server Running
|
||||
```bash
|
||||
cd c:\xampp\htdocs\TugasAkhir\sim-pkpps
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
### Step 2: Buat Akun Wali (Jika Belum Ada)
|
||||
1. Buka browser: http://localhost:8000/admin/users/wali
|
||||
2. Login sebagai admin
|
||||
3. Klik "Buat Akun Wali"
|
||||
4. Pilih santri dari dropdown
|
||||
5. **PERHATIKAN**: Username dan password akan terisi otomatis
|
||||
6. Klik Simpan
|
||||
|
||||
### Step 3: Catat Username & Password
|
||||
- **Username**: Nama santri (misal: "Ahmad Fauzi")
|
||||
- **Password**: NIS santri (misal: "2024001")
|
||||
|
||||
### Step 4: Test API dengan PHP Script
|
||||
```bash
|
||||
php c:\xampp\htdocs\TugasAkhir\test_login.php
|
||||
```
|
||||
Edit dulu file test_login.php, ganti username dan password sesuai akun yang dibuat.
|
||||
|
||||
### Step 5: Test di Flutter Mobile App
|
||||
1. Pastikan base URL di Flutter sudah benar:
|
||||
- Emulator: `http://10.0.2.2:8000/api/v1`
|
||||
- Real device: `http://192.168.x.x:8000/api/v1`
|
||||
|
||||
2. Run Flutter app:
|
||||
```bash
|
||||
cd c:\xampp\htdocs\TugasAkhir\sim_mobile
|
||||
flutter run
|
||||
```
|
||||
|
||||
3. Di login page, masukkan:
|
||||
- Username: **PERSIS** seperti nama santri di database
|
||||
- Password: NIS santri
|
||||
|
||||
4. Klik Login
|
||||
|
||||
---
|
||||
|
||||
## ❓ TROUBLESHOOTING
|
||||
|
||||
### ❌ "Username atau password salah"
|
||||
**Penyebab**: Username tidak match persis dengan database
|
||||
|
||||
**Solusi**:
|
||||
1. Cek username di database:
|
||||
```sql
|
||||
SELECT username FROM users WHERE role='wali';
|
||||
```
|
||||
2. Pastikan huruf besar/kecil dan spasi PERSIS SAMA
|
||||
|
||||
### ❌ "Connection refused"
|
||||
**Penyebab**: Server Laravel tidak running atau base URL salah
|
||||
|
||||
**Solusi**:
|
||||
1. Jalankan: `php artisan serve`
|
||||
2. Cek base URL di Flutter (app_config.dart)
|
||||
|
||||
### ❌ Auto-fill tidak jalan
|
||||
**Sudah diperbaiki**: JavaScript sekarang inline di file create_account.blade.php
|
||||
|
||||
### ❌ Tombol Delete/Reset tidak ada
|
||||
**Sudah diperbaiki**: Tombol sudah ditambahkan di view santri_accounts dan wali_accounts
|
||||
|
||||
---
|
||||
|
||||
## 📁 FILE YANG DIUBAH
|
||||
|
||||
1. ✅ `sim-pkpps/app/Http/Controllers/Admin/UserController.php`
|
||||
- Method: destroyAccount(), resetPassword()
|
||||
|
||||
2. ✅ `sim-pkpps/routes/web.php`
|
||||
- Routes baru untuk delete & reset password
|
||||
|
||||
3. ✅ `sim-pkpps/resources/views/admin/users/create_account.blade.php`
|
||||
- JavaScript auto-fill diperbaiki
|
||||
|
||||
4. ✅ `sim-pkpps/resources/views/admin/users/wali_accounts.blade.php`
|
||||
- Tombol delete & reset ditambahkan
|
||||
|
||||
5. ✅ `sim-pkpps/resources/views/admin/users/santri_accounts.blade.php`
|
||||
- Tombol delete & reset ditambahkan
|
||||
|
||||
---
|
||||
|
||||
## 🚀 LANGKAH SELANJUTNYA
|
||||
|
||||
1. **Test auto-fill di web admin**
|
||||
- Buka halaman buat akun wali
|
||||
- Pilih santri
|
||||
- Pastikan username & password terisi otomatis
|
||||
|
||||
2. **Test delete & reset**
|
||||
- Coba hapus akun
|
||||
- Coba reset password
|
||||
- Pastikan ada konfirmasi dialog
|
||||
|
||||
3. **Test login mobile**
|
||||
- Gunakan username & password yang PERSIS dari database
|
||||
- Test dengan emulator atau real device
|
||||
- Pastikan server Laravel running
|
||||
|
||||
---
|
||||
|
||||
**SEMUA FUNGSI SUDAH SELESAI! TINGGAL TESTING!** ✅
|
||||
|
|
@ -1,722 +0,0 @@
|
|||
# Testing Checklist - Multiple Kelas System
|
||||
|
||||
## Overview
|
||||
Checklist lengkap untuk testing fitur Multiple Kelas pada backend Laravel dan aplikasi mobile Flutter.
|
||||
|
||||
---
|
||||
|
||||
## 📋 PHASE 1: Backend API Testing
|
||||
|
||||
### A. Persiapan Data Testing
|
||||
|
||||
#### ✅ Step 1: Cek Database Structure
|
||||
```sql
|
||||
-- Cek tabel santri_kelas
|
||||
DESC santri_kelas;
|
||||
|
||||
-- Expected columns:
|
||||
-- id, id_santri, id_kelas, tahun_ajaran, is_primary, created_at, updated_at
|
||||
```
|
||||
|
||||
#### ✅ Step 2: Insert Sample Data (Manual)
|
||||
```sql
|
||||
-- Insert santri dengan multiple kelas
|
||||
-- Contoh: Santri S001 masuk 4 kelas
|
||||
|
||||
-- Kelas 1: PB Putra A (bukan primary)
|
||||
INSERT INTO santri_kelas (id_santri, id_kelas, tahun_ajaran, is_primary)
|
||||
VALUES ('S001', 1, '2025/2026', 0);
|
||||
|
||||
-- Kelas 2: Lambatan B (PRIMARY)
|
||||
INSERT INTO santri_kelas (id_santri, id_kelas, tahun_ajaran, is_primary)
|
||||
VALUES ('S001', 5, '2025/2026', 1);
|
||||
|
||||
-- Kelas 3: Cepatan A (bukan primary)
|
||||
INSERT INTO santri_kelas (id_santri, id_kelas, tahun_ajaran, is_primary)
|
||||
VALUES ('S001', 8, '2025/2026', 0);
|
||||
|
||||
-- Kelas 4: Hadist Pemula (bukan primary)
|
||||
INSERT INTO santri_kelas (id_santri, id_kelas, tahun_ajaran, is_primary)
|
||||
VALUES ('S001', 15, '2025/2026', 0);
|
||||
```
|
||||
|
||||
#### ✅ Step 3: Verify Sample Data
|
||||
```sql
|
||||
-- Cek data santri S001
|
||||
SELECT sk.*, k.nama_kelas, kk.nama_kelompok
|
||||
FROM santri_kelas sk
|
||||
JOIN kelas k ON sk.id_kelas = k.id
|
||||
JOIN kelompok_kelas kk ON k.id_kelompok = kk.id_kelompok
|
||||
WHERE sk.id_santri = 'S001';
|
||||
|
||||
-- Expected: 4 rows, 1 dengan is_primary = 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### B. Testing Login Endpoint
|
||||
|
||||
#### ✅ Test 1: Login Berhasil
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id_santri": "S001",
|
||||
"password": "password123"
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Login berhasil",
|
||||
"token": "1|abc123xyz...",
|
||||
"user": {
|
||||
"name": "Ahmad Santoso",
|
||||
"role": "santri",
|
||||
"role_id": "S001"
|
||||
},
|
||||
"santri": {
|
||||
"id_santri": "S001",
|
||||
...
|
||||
"kelas": "Lambatan B",
|
||||
"kelas_list": [ ... ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Response status: 200 OK
|
||||
- [ ] Field `token` ada dan valid
|
||||
- [ ] Field `santri.kelas` = "Lambatan B" (primary)
|
||||
- [ ] Field `santri.kelas_list` adalah array
|
||||
- [ ] `kelas_list` punya 4 kelompok (atau sesuai data)
|
||||
- [ ] Ada 1 kelas dengan `is_primary = true`
|
||||
|
||||
#### ✅ Test 2: Login Gagal (Password Salah)
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id_santri": "S001",
|
||||
"password": "wrongpassword"
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected Response:**
|
||||
```json
|
||||
{
|
||||
"message": "ID Santri atau password salah.",
|
||||
"errors": {
|
||||
"id_santri": ["ID Santri atau password salah."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Response status: 422 Unprocessable Entity
|
||||
- [ ] Error message jelas
|
||||
|
||||
---
|
||||
|
||||
### C. Testing Profile Endpoint
|
||||
|
||||
#### ✅ Test 3: Get Profile (With Valid Token)
|
||||
```bash
|
||||
# Ganti YOUR_TOKEN dengan token dari login
|
||||
curl -X GET http://localhost:8000/api/profile \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
**Expected Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id_santri": "S001",
|
||||
"nis": "2024001",
|
||||
"nama_lengkap": "Ahmad Santoso",
|
||||
"jenis_kelamin": "Laki-laki",
|
||||
"status": "Aktif",
|
||||
"kelas": "Lambatan B",
|
||||
"kelas_list": [
|
||||
{
|
||||
"kelompok_id": "KLMPK001",
|
||||
"kelompok_name": "PB",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 1,
|
||||
"kode_kelas": "KLS001",
|
||||
"nama_kelas": "PB Putra A",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kelompok_id": "KLMPK002",
|
||||
"kelompok_name": "Lambatan",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 5,
|
||||
"kode_kelas": "KLS005",
|
||||
"nama_kelas": "Lambatan B",
|
||||
"is_primary": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kelompok_id": "KLMPK003",
|
||||
"kelompok_name": "Cepatan",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 8,
|
||||
"kode_kelas": "KLS008",
|
||||
"nama_kelas": "Cepatan A",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kelompok_id": "KLMPK004",
|
||||
"kelompok_name": "Hadist",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 15,
|
||||
"kode_kelas": "KLS015",
|
||||
"nama_kelas": "Hadist Pemula",
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Response status: 200 OK
|
||||
- [ ] Field `kelas` ada (string)
|
||||
- [ ] Field `kelas_list` ada (array)
|
||||
- [ ] Setiap kelompok punya struktur benar
|
||||
- [ ] Primary kelas punya `is_primary: true`
|
||||
- [ ] Kelompok di-group dengan benar
|
||||
|
||||
#### ✅ Test 4: Get Profile (Without Token)
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/api/profile
|
||||
```
|
||||
|
||||
**Expected Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Unauthenticated."
|
||||
}
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Response status: 401 Unauthorized
|
||||
|
||||
---
|
||||
|
||||
### D. Edge Case Testing
|
||||
|
||||
#### ✅ Test 5: Santri Tanpa Kelas
|
||||
```sql
|
||||
-- Hapus semua kelas santri S002 (untuk testing)
|
||||
DELETE FROM santri_kelas WHERE id_santri = 'S002';
|
||||
```
|
||||
|
||||
```bash
|
||||
# Login sebagai S002
|
||||
curl -X POST http://localhost:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"id_santri": "S002", "password": "password123"}'
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
```json
|
||||
{
|
||||
"santri": {
|
||||
"kelas": "Belum Ada Kelas",
|
||||
"kelas_list": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Field `kelas` = "Belum Ada Kelas"
|
||||
- [ ] Field `kelas_list` = [] (empty array)
|
||||
- [ ] No error/crash
|
||||
|
||||
#### ✅ Test 6: Santri dengan 1 Kelas Saja
|
||||
```sql
|
||||
-- Insert 1 kelas untuk S003
|
||||
INSERT INTO santri_kelas (id_santri, id_kelas, tahun_ajaran, is_primary)
|
||||
VALUES ('S003', 1, '2025/2026', 1);
|
||||
```
|
||||
|
||||
```bash
|
||||
# Login sebagai S003
|
||||
curl -X POST http://localhost:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"id_santri": "S003", "password": "password123"}'
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
```json
|
||||
{
|
||||
"santri": {
|
||||
"kelas": "PB Putra A",
|
||||
"kelas_list": [
|
||||
{
|
||||
"kelompok_id": "KLMPK001",
|
||||
"kelompok_name": "PB",
|
||||
"kelas": [
|
||||
{
|
||||
"id_kelas": 1,
|
||||
"nama_kelas": "PB Putra A",
|
||||
"is_primary": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Field `kelas` = nama kelas
|
||||
- [ ] `kelas_list` punya 1 item
|
||||
- [ ] `is_primary` = true
|
||||
|
||||
#### ✅ Test 7: Santri Tanpa Primary (Semua is_primary = false)
|
||||
```sql
|
||||
-- Update S004: semua kelas jadi non-primary
|
||||
UPDATE santri_kelas SET is_primary = 0 WHERE id_santri = 'S004';
|
||||
```
|
||||
|
||||
```bash
|
||||
# Login sebagai S004
|
||||
curl -X POST http://localhost:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"id_santri": "S004", "password": "password123"}'
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
```json
|
||||
{
|
||||
"santri": {
|
||||
"kelas": "PB Putra A", // Fallback ke kelas pertama
|
||||
"kelas_list": [ ... ], // Semua is_primary: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Field `kelas` = nama kelas pertama (fallback)
|
||||
- [ ] Semua item di `kelas_list` punya `is_primary: false`
|
||||
- [ ] No error/crash
|
||||
|
||||
---
|
||||
|
||||
### E. Performance Testing
|
||||
|
||||
#### ✅ Test 8: Query Count (N+1 Problem Check)
|
||||
```php
|
||||
// Tambahkan di ApiAuthController.php (temporary)
|
||||
\DB::enableQueryLog();
|
||||
|
||||
// ... existing code ...
|
||||
|
||||
\Log::info('Query count: ' . count(\DB::getQueryLog()));
|
||||
\Log::info('Queries:', \DB::getQueryLog());
|
||||
```
|
||||
|
||||
**Test:**
|
||||
1. Login sebagai santri dengan 5 kelas
|
||||
2. Cek `storage/logs/laravel.log`
|
||||
|
||||
**Expected:**
|
||||
- Query count: 2-3 queries (optimal)
|
||||
- **NO** N+1 problem (banyak query loop)
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Query count < 5
|
||||
- [ ] Eager loading bekerja (`with()` clause)
|
||||
|
||||
#### ✅ Test 9: Response Time
|
||||
```bash
|
||||
# Test response time (run 5x)
|
||||
time curl -X POST http://localhost:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"id_santri": "S001", "password": "password123"}'
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- Average response time: < 500ms
|
||||
- Max response time: < 1000ms
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Response time acceptable
|
||||
- [ ] No timeout
|
||||
|
||||
#### ✅ Test 10: Response Size
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"id_santri": "S001", "password": "password123"}' \
|
||||
--compressed | wc -c
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- Response size: < 10KB
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Response size reasonable
|
||||
- [ ] No unnecessary data
|
||||
|
||||
---
|
||||
|
||||
## 📱 PHASE 2: Flutter Mobile App Testing
|
||||
|
||||
### F. Setup Testing Environment
|
||||
|
||||
#### ✅ Step 1: Update API Base URL
|
||||
```dart
|
||||
// lib/core/api/api_service.dart
|
||||
static const String _baseUrl = 'http://YOUR_LOCAL_IP:8000/api';
|
||||
|
||||
// Contoh:
|
||||
// Windows: 'http://192.168.1.100:8000/api'
|
||||
// Mac: 'http://192.168.1.100:8000/api'
|
||||
```
|
||||
|
||||
#### ✅ Step 2: Build & Run App
|
||||
```bash
|
||||
cd sim_mobile
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter run
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
- [ ] App build success
|
||||
- [ ] No compile errors
|
||||
- [ ] App launched on device/emulator
|
||||
|
||||
---
|
||||
|
||||
### G. UI Display Testing
|
||||
|
||||
#### ✅ Test 11: Login & Display Profile
|
||||
1. Launch app
|
||||
2. Login dengan S001 (yang punya 4 kelas)
|
||||
3. Tap tab "Profil"
|
||||
|
||||
**Expected:**
|
||||
- Header:
|
||||
- [ ] Avatar displayed
|
||||
- [ ] Nama lengkap displayed
|
||||
- [ ] ID Santri displayed
|
||||
- [ ] **Primary kelas badge** displayed ("📚 Lambatan B")
|
||||
- [ ] **Hint "+3 kelas lainnya"** displayed
|
||||
- [ ] Status badge displayed ("Aktif")
|
||||
|
||||
- Informasi Dasar Card:
|
||||
- [ ] No "Kelas" row (removed)
|
||||
- [ ] All other fields displayed
|
||||
|
||||
- **Kelas yang Diikuti Section:**
|
||||
- [ ] Section card displayed
|
||||
- [ ] 4 kelompok displayed (PB, Lambatan, Cepatan, Hadist)
|
||||
- [ ] Each kelompok has correct color & icon
|
||||
|
||||
#### ✅ Test 12: ExpansionTile Interaction
|
||||
1. Tap "PB" kelompok (saat collapsed)
|
||||
2. **Expected:** ExpansionTile expands, show "PB Putra A"
|
||||
3. Tap "PB" lagi
|
||||
4. **Expected:** ExpansionTile collapses
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Expand animation smooth
|
||||
- [ ] Collapse animation smooth
|
||||
- [ ] No lag/jank
|
||||
|
||||
#### ✅ Test 13: Primary Badge Display
|
||||
1. Expand "Lambatan" kelompok
|
||||
2. **Expected:** "Lambatan B" punya badge "⭐ Utama"
|
||||
3. Expand "PB" kelompok
|
||||
4. **Expected:** "PB Putra A" TIDAK punya badge (is_primary: false)
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Badge "⭐ Utama" muncul HANYA di primary kelas
|
||||
- [ ] Badge styling correct (gold background, white icon/text)
|
||||
|
||||
#### ✅ Test 14: Color Coding
|
||||
1. Cek warna per kelompok:
|
||||
- [ ] PB → Blue (#3b82f6)
|
||||
- [ ] Lambatan → Orange (#fb923c)
|
||||
- [ ] Cepatan → Green (#10b981)
|
||||
- [ ] Hadist → Teal (#14b8a6)
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Icon badge color match
|
||||
- [ ] Border color match
|
||||
- [ ] Primary kelas highlight match
|
||||
|
||||
---
|
||||
|
||||
### H. Edge Case UI Testing
|
||||
|
||||
#### ✅ Test 15: Santri Tanpa Kelas
|
||||
1. Login sebagai S002 (tanpa kelas)
|
||||
2. Tap tab "Profil"
|
||||
|
||||
**Expected:**
|
||||
- [ ] Header: Primary badge show "-" atau "Belum Ada Kelas"
|
||||
- [ ] NO hint "+X kelas lainnya"
|
||||
- [ ] Section "Kelas yang Diikuti" **TIDAK MUNCUL**
|
||||
- [ ] Informasi Dasar, Alamat, Orang Tua tetap displayed
|
||||
|
||||
#### ✅ Test 16: Santri dengan 1 Kelas
|
||||
1. Login sebagai S003 (1 kelas: PB Putra A)
|
||||
2. Tap tab "Profil"
|
||||
|
||||
**Expected:**
|
||||
- [ ] Header: Primary badge show "PB Putra A"
|
||||
- [ ] NO hint "+X kelas lainnya" (karena cuma 1)
|
||||
- [ ] Section "Kelas yang Diikuti" displayed
|
||||
- [ ] 1 kelompok saja (PB)
|
||||
- [ ] Badge "⭐ Utama" muncul
|
||||
|
||||
#### ✅ Test 17: Network Error Handling
|
||||
1. Disconnect internet/WiFi
|
||||
2. Login atau pull-to-refresh
|
||||
|
||||
**Expected:**
|
||||
- [ ] Error message displayed
|
||||
- [ ] App tidak crash
|
||||
- [ ] Cached data (jika ada) tetap displayed
|
||||
|
||||
---
|
||||
|
||||
### I. Responsive & Performance Testing
|
||||
|
||||
#### ✅ Test 18: Small Screen (iPhone SE, 320px)
|
||||
1. Test di iPhone SE atau emulator 320px width
|
||||
2. Scroll semua section
|
||||
|
||||
**Expected:**
|
||||
- [ ] No horizontal overflow
|
||||
- [ ] Text tidak terpotong (ellipsis bekerja)
|
||||
- [ ] Padding proporsional
|
||||
- [ ] Badge "Utama" tidak keluar container
|
||||
|
||||
#### ✅ Test 19: Large Screen (iPad, 800px)
|
||||
1. Test di iPad atau emulator tablet
|
||||
2. Scroll semua section
|
||||
|
||||
**Expected:**
|
||||
- [ ] Layout rapi
|
||||
- [ ] Padding tidak terlalu besar/kecil
|
||||
- [ ] ExpansionTile width reasonable
|
||||
|
||||
#### ✅ Test 20: Pull-to-Refresh
|
||||
1. Di profil page, swipe down
|
||||
2. **Expected:** Loading indicator muncul
|
||||
3. Wait 1-2 detik
|
||||
4. **Expected:** Data refresh dari API
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Loading indicator displayed
|
||||
- [ ] Data updated
|
||||
- [ ] No crash
|
||||
|
||||
#### ✅ Test 21: Memory & Performance
|
||||
1. Open Developer Tools / Profiler
|
||||
2. Navigate antara tab (Beranda → Profil → dll)
|
||||
3. Repeat 5x
|
||||
|
||||
**Expected:**
|
||||
- [ ] Memory usage stable (< 100MB)
|
||||
- [ ] No memory leak
|
||||
- [ ] Frame rate: 60 FPS
|
||||
- [ ] No dropped frames
|
||||
|
||||
---
|
||||
|
||||
## 📊 PHASE 3: Integration Testing
|
||||
|
||||
### J. End-to-End Scenario
|
||||
|
||||
#### ✅ Test 22: Complete User Flow
|
||||
**Scenario:** Ahmad seorang santri baru, didaftarkan oleh admin, lalu login pertama kali.
|
||||
|
||||
1. **Admin:** Create santri S010
|
||||
2. **Admin:** Assign 3 kelas (PB, Lambatan primary, Cepatan)
|
||||
3. **Admin:** Create user account untuk S010
|
||||
4. **Mobile:** Login dengan S010
|
||||
5. **Mobile:** Tap tab Profil
|
||||
|
||||
**Expected:**
|
||||
- [ ] Login berhasil
|
||||
- [ ] Profil displayed dengan 3 kelas
|
||||
- [ ] Lambatan sebagai primary (badge "Utama")
|
||||
- [ ] Primary kelas badge di header
|
||||
- [ ] Hint "+2 kelas lainnya"
|
||||
|
||||
#### ✅ Test 23: Admin Update Kelas → Mobile Refresh
|
||||
**Scenario:** Admin menambah kelas baru untuk santri.
|
||||
|
||||
1. **Mobile:** Login S001, lihat profil (4 kelas)
|
||||
2. **Admin/Web:** Add kelas baru untuk S001 (Tahfidz)
|
||||
3. **Mobile:** Pull-to-refresh di profil page
|
||||
|
||||
**Expected:**
|
||||
- [ ] Data refresh dari API
|
||||
- [ ] 5 kelas sekarang displayed
|
||||
- [ ] Hint "+4 kelas lainnya"
|
||||
- [ ] Primary kelas tetap sama
|
||||
|
||||
#### ✅ Test 24: Change Primary Kelas
|
||||
**Scenario:** Admin mengubah primary kelas santri.
|
||||
|
||||
1. **Mobile:** Login S001, primary = "Lambatan B"
|
||||
2. **Admin/Web:** Update primary ke "Cepatan A"
|
||||
3. **Mobile:** Pull-to-refresh
|
||||
|
||||
**Expected:**
|
||||
- [ ] Primary kelas badge di header = "Cepatan A"
|
||||
- [ ] Badge "⭐ Utama" pindah ke "Cepatan A"
|
||||
- [ ] "Lambatan B" tidak punya badge lagi
|
||||
|
||||
---
|
||||
|
||||
## ✅ PHASE 4: Backward Compatibility Testing
|
||||
|
||||
### K. Compatibility Testing
|
||||
|
||||
#### ✅ Test 25: Old App + New Backend
|
||||
**Scenario:** User belum update app, tapi backend sudah update.
|
||||
|
||||
**Setup:**
|
||||
1. Deploy backend baru (dengan kelas_list)
|
||||
2. Use app versi lama (hanya baca field 'kelas')
|
||||
|
||||
**Expected:**
|
||||
- [ ] Login berhasil
|
||||
- [ ] Field 'kelas' masih ada di response
|
||||
- [ ] App lama display kelas primary (single)
|
||||
- [ ] No crash on app lama
|
||||
|
||||
#### ✅ Test 26: New App + Old Backend
|
||||
**Scenario:** User update app, tapi backend belum update.
|
||||
|
||||
**Setup:**
|
||||
1. Use backend lama (belum ada kelas_list)
|
||||
2. Use app versi baru
|
||||
|
||||
**Expected:**
|
||||
- [ ] Login berhasil
|
||||
- [ ] `kelas_list` = null atau tidak ada
|
||||
- [ ] App baru fallback ke field 'kelas'
|
||||
- [ ] Section "Kelas yang Diikuti" tidak muncul
|
||||
- [ ] No crash
|
||||
|
||||
---
|
||||
|
||||
## 📈 Results Summary
|
||||
|
||||
### Backend Testing Results
|
||||
| Test | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Login Endpoint | ☐ Pass ☐ Fail | |
|
||||
| Profile Endpoint | ☐ Pass ☐ Fail | |
|
||||
| Empty Kelas | ☐ Pass ☐ Fail | |
|
||||
| Single Kelas | ☐ Pass ☐ Fail | |
|
||||
| No Primary | ☐ Pass ☐ Fail | |
|
||||
| Query Performance | ☐ Pass ☐ Fail | |
|
||||
| Response Time | ☐ Pass ☐ Fail | |
|
||||
|
||||
### Frontend Testing Results
|
||||
| Test | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| UI Display | ☐ Pass ☐ Fail | |
|
||||
| Expansion Tile | ☐ Pass ☐ Fail | |
|
||||
| Primary Badge | ☐ Pass ☐ Fail | |
|
||||
| Color Coding | ☐ Pass ☐ Fail | |
|
||||
| Empty State | ☐ Pass ☐ Fail | |
|
||||
| Pull-to-Refresh | ☐ Pass ☐ Fail | |
|
||||
| Responsive | ☐ Pass ☐ Fail | |
|
||||
|
||||
### Integration Testing Results
|
||||
| Test | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| End-to-End Flow | ☐ Pass ☐ Fail | |
|
||||
| Admin Update | ☐ Pass ☐ Fail | |
|
||||
| Change Primary | ☐ Pass ☐ Fail | |
|
||||
| Backward Compat | ☐ Pass ☐ Fail | |
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug Report Template
|
||||
|
||||
```markdown
|
||||
### Bug Title
|
||||
[Singkat dan jelas]
|
||||
|
||||
### Environment
|
||||
- OS: [Windows/Mac/Linux]
|
||||
- Backend: Laravel 10.x
|
||||
- Frontend: Flutter 3.x
|
||||
- Device: [iPhone 13, Android Pixel, etc.]
|
||||
|
||||
### Steps to Reproduce
|
||||
1. Login sebagai S001
|
||||
2. Tap tab Profil
|
||||
3. Expand kelompok "PB"
|
||||
4. ...
|
||||
|
||||
### Expected Behavior
|
||||
[Apa yang seharusnya terjadi]
|
||||
|
||||
### Actual Behavior
|
||||
[Apa yang benar-benar terjadi]
|
||||
|
||||
### Screenshots
|
||||
[Attach screenshots jika ada]
|
||||
|
||||
### Logs
|
||||
[Laravel log, Flutter console log]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Sign-off
|
||||
|
||||
### Backend Testing
|
||||
- Tester: _______________
|
||||
- Date: _______________
|
||||
- Signature: _______________
|
||||
|
||||
### Frontend Testing
|
||||
- Tester: _______________
|
||||
- Date: _______________
|
||||
- Signature: _______________
|
||||
|
||||
### Integration Testing
|
||||
- Tester: _______________
|
||||
- Date: _______________
|
||||
- Signature: _______________
|
||||
|
||||
---
|
||||
|
||||
## 📞 Contact
|
||||
|
||||
Jika ada masalah, refer ke:
|
||||
- `MULTIPLE_KELAS_API_RESPONSE.md` - API documentation
|
||||
- `MULTIPLE_KELAS_UI_FLUTTER.md` - UI documentation
|
||||
- `storage/logs/laravel.log` - Backend errors
|
||||
- Flutter DevTools - Frontend debugging
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
<?php
|
||||
// add_capaian_test_data.php - Add sample capaian data with progress
|
||||
|
||||
require __DIR__ . '/sim-pkpps/vendor/autoload.php';
|
||||
$app = require __DIR__ . '/sim-pkpps/bootstrap/app.php';
|
||||
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
|
||||
|
||||
echo "=== ADDING CAPAIAN TEST DATA ===\n\n";
|
||||
|
||||
$santri = \App\Models\Santri::where('id_santri', 'S001')->first();
|
||||
$semester = \App\Models\Semester::aktif()->first();
|
||||
|
||||
if (!$santri || !$semester) {
|
||||
echo "❌ Missing santri or semester\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "Santri: {$santri->nama_lengkap}\n";
|
||||
echo "Semester: {$semester->nama_semester}\n\n";
|
||||
|
||||
// Get or create materis
|
||||
$materis = \App\Models\Materi::where('kelas', $santri->kelas)->get();
|
||||
|
||||
if ($materis->isEmpty()) {
|
||||
echo "Creating sample materi...\n";
|
||||
|
||||
$materiData = [
|
||||
[
|
||||
'nama_kitab' => 'Al-Baqarah',
|
||||
'kategori' => 'Al-Qur\'an',
|
||||
'kelas' => 'Lambatan',
|
||||
'halaman_mulai' => 1,
|
||||
'halaman_akhir' => 100,
|
||||
],
|
||||
[
|
||||
'nama_kitab' => 'Shahih Bukhari Juz 1',
|
||||
'kategori' => 'Hadist',
|
||||
'kelas' => 'Lambatan',
|
||||
'halaman_mulai' => 1,
|
||||
'halaman_akhir' => 150,
|
||||
],
|
||||
[
|
||||
'nama_kitab' => 'Tafsir Jalalain',
|
||||
'kategori' => 'Materi Tambahan',
|
||||
'kelas' => 'Lambatan',
|
||||
'halaman_mulai' => 1,
|
||||
'halaman_akhir' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($materiData as $data) {
|
||||
$m = \App\Models\Materi::create($data);
|
||||
echo " ✅ Created: {$m->nama_kitab}\n";
|
||||
}
|
||||
|
||||
$materis = \App\Models\Materi::where('kelas', $santri->kelas)->get();
|
||||
}
|
||||
|
||||
echo "\nAdding capaian with progress...\n";
|
||||
|
||||
// Delete existing capaians
|
||||
\App\Models\Capaian::where('id_santri', $santri->id_santri)->delete();
|
||||
|
||||
foreach ($materis as $index => $materi) {
|
||||
// Create different progress levels
|
||||
$progressLevels = [
|
||||
['halaman' => '1-15', 'persentase' => 15],
|
||||
['halaman' => '1-25,30-40', 'persentase' => 40],
|
||||
['halaman' => '1-50,60-80', 'persentase' => 70],
|
||||
];
|
||||
|
||||
$progress = $progressLevels[$index % 3];
|
||||
|
||||
$capaian = \App\Models\Capaian::create([
|
||||
'id_santri' => $santri->id_santri,
|
||||
'id_materi' => $materi->id_materi,
|
||||
'id_semester' => $semester->id_semester,
|
||||
'halaman_selesai' => $progress['halaman'],
|
||||
'tanggal_input' => now(),
|
||||
]);
|
||||
|
||||
echo " ✅ {$materi->nama_kitab}: {$capaian->persentase}%\n";
|
||||
}
|
||||
|
||||
echo "\n=== DONE ===\n";
|
||||
echo "Now try the API again!\n";
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
<?php
|
||||
// Quick check: apakah file gambar benar-benar ada?
|
||||
|
||||
$imagePath = __DIR__ . '/sim-pkpps/storage/app/public/berita/cxPh4dGaR6qpyPeshxvSQAjgaY7xef9t180dgShU.jpg';
|
||||
$publicPath = __DIR__ . '/sim-pkpps/public/storage/berita/cxPh4dGaR6qpyPeshxvSQAjgaY7xef9t180dgShU.jpg';
|
||||
|
||||
echo "<h1>🔍 Check Gambar Berita</h1>";
|
||||
|
||||
echo "<h2>1. File di storage/app/public/berita/</h2>";
|
||||
if (file_exists($imagePath)) {
|
||||
echo "✅ File ADA di: <code>$imagePath</code><br>";
|
||||
echo "Ukuran: " . filesize($imagePath) . " bytes<br>";
|
||||
} else {
|
||||
echo "❌ File TIDAK ADA di: <code>$imagePath</code><br>";
|
||||
}
|
||||
|
||||
echo "<h2>2. File di public/storage/berita/ (symlink)</h2>";
|
||||
if (file_exists($publicPath)) {
|
||||
echo "✅ File ACCESSIBLE di: <code>$publicPath</code><br>";
|
||||
echo "Ukuran: " . filesize($publicPath) . " bytes<br>";
|
||||
} else {
|
||||
echo "❌ File TIDAK ACCESSIBLE di: <code>$publicPath</code><br>";
|
||||
}
|
||||
|
||||
echo "<h2>3. Test URL</h2>";
|
||||
$url = 'http://localhost/TugasAkhir/sim-pkpps/public/storage/berita/cxPh4dGaR6qpyPeshxvSQAjgaY7xef9t180dgShU.jpg';
|
||||
echo "URL: <a href='$url' target='_blank'>$url</a><br><br>";
|
||||
|
||||
if (file_exists($publicPath)) {
|
||||
echo "<img src='$url' style='max-width: 400px; border: 2px solid green;' onerror='this.style.border=\"2px solid red\";'><br>";
|
||||
echo "<p>Jika gambar di atas tidak muncul, berarti ada masalah CORS atau server config.</p>";
|
||||
}
|
||||
|
||||
echo "<h2>4. Symlink Check</h2>";
|
||||
$symlinkPath = __DIR__ . '/sim-pkpps/public/storage';
|
||||
if (is_link($symlinkPath)) {
|
||||
echo "✅ Symlink EXISTS<br>";
|
||||
echo "Target: " . readlink($symlinkPath) . "<br>";
|
||||
} else if (is_dir($symlinkPath)) {
|
||||
echo "✅ Directory EXISTS (bukan symlink)<br>";
|
||||
} else {
|
||||
echo "❌ Storage link TIDAK ADA!<br>";
|
||||
echo "<p><strong>Solusi:</strong> Jalankan <code>php artisan storage:link</code></p>";
|
||||
}
|
||||
?>
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
require __DIR__ . '/sim-pkpps/vendor/autoload.php';
|
||||
$app = require __DIR__ . '/sim-pkpps/bootstrap/app.php';
|
||||
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
|
||||
|
||||
$santri = \App\Models\Santri::where('id_santri', 'S001')->first();
|
||||
if ($santri) {
|
||||
echo "Santri: {$santri->nama_lengkap}\n";
|
||||
echo "ID: {$santri->id_santri}\n";
|
||||
echo "NIS: {$santri->nis}\n";
|
||||
|
||||
// Check users
|
||||
$users = \App\Models\User::where('role_id', 'S001')->get();
|
||||
echo "\nUsers for this santri:\n";
|
||||
foreach ($users as $user) {
|
||||
echo " - Username: {$user->username}, Role: {$user->role}\n";
|
||||
|
||||
// Test password
|
||||
$testPasswords = ['S001', $santri->nis, '123456', 'password'];
|
||||
foreach ($testPasswords as $pass) {
|
||||
if (\Illuminate\Support\Facades\Hash::check($pass, $user->password)) {
|
||||
echo " ✅ Password: $pass\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
@echo off
|
||||
echo ============================================
|
||||
echo SIM-PKPPS Login Test Script
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
echo [1/4] Checking Laravel server...
|
||||
curl -s http://localhost:8000 > nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo ❌ Laravel server NOT running!
|
||||
echo Please run: cd sim-pkpps ^&^& php artisan serve
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo ✅ Laravel server is running
|
||||
|
||||
echo.
|
||||
echo [2/4] Testing API health...
|
||||
curl -s http://localhost:8000/api/v1/login > nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo ❌ API endpoint not accessible
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo ✅ API endpoint accessible
|
||||
|
||||
echo.
|
||||
echo [3/4] Checking database connection...
|
||||
cd sim-pkpps
|
||||
php artisan tinker --execute="echo 'DB Connected: ' . (DB::connection()->getPdo() ? 'Yes' : 'No');"
|
||||
if %errorlevel% neq 0 (
|
||||
echo ❌ Database connection failed
|
||||
echo Check .env file configuration
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo ✅ Database connected
|
||||
|
||||
echo.
|
||||
echo [4/4] Listing wali accounts...
|
||||
php artisan tinker --execute="$users = App\Models\User::where('role', 'wali')->with('santri')->get(); foreach($users as $u) { echo 'Username: ' . $u->username . ' | Santri: ' . ($u->santri ? $u->santri->nama_lengkap : 'N/A') . ' | NIS: ' . ($u->santri ? $u->santri->nis : 'N/A') . PHP_EOL; }"
|
||||
|
||||
echo.
|
||||
echo ============================================
|
||||
echo All checks passed! ✅
|
||||
echo ============================================
|
||||
echo.
|
||||
echo Now you can test login with:
|
||||
echo - Username: [nama_lengkap_santri]
|
||||
echo - Password: [nis_santri]
|
||||
echo.
|
||||
pause
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
require __DIR__ . '/sim-pkpps/vendor/autoload.php';
|
||||
$app = require __DIR__ . '/sim-pkpps/bootstrap/app.php';
|
||||
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
|
||||
|
||||
echo "=== USERS IN DATABASE ===\n\n";
|
||||
|
||||
$users = \App\Models\User::whereIn('role', ['santri', 'wali'])->get();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
echo "❌ No santri/wali users found\n";
|
||||
echo "\nCreating test user...\n";
|
||||
|
||||
// Create a test wali user
|
||||
$santri = \App\Models\Santri::first();
|
||||
if ($santri) {
|
||||
$user = \App\Models\User::create([
|
||||
'username' => 'wali001',
|
||||
'name' => 'Wali ' . $santri->nama_lengkap,
|
||||
'email' => 'wali001@test.com',
|
||||
'password' => bcrypt('S001'),
|
||||
'role' => 'wali',
|
||||
'role_id' => $santri->id_santri,
|
||||
]);
|
||||
echo "✅ Created user: {$user->username} (password: S001)\n";
|
||||
echo " Role: {$user->role}\n";
|
||||
echo " Role ID: {$user->role_id}\n";
|
||||
}
|
||||
} else {
|
||||
foreach ($users as $user) {
|
||||
echo "Username: {$user->username}\n";
|
||||
echo "Role: {$user->role}\n";
|
||||
echo "Role ID: {$user->role_id}\n";
|
||||
echo "---\n";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Comprehensive Debug Script
|
||||
* Akses: http://localhost/TugasAkhir/debug_comprehensive.php
|
||||
*/
|
||||
|
||||
echo "<html><head><title>Debug Comprehensive</title>";
|
||||
echo "<style>body{font-family:Arial;padding:20px;} .ok{color:green;} .error{color:red;} .section{border:1px solid #ddd;padding:15px;margin:10px 0;} pre{background:#f4f4f4;padding:10px;}</style>";
|
||||
echo "</head><body>";
|
||||
|
||||
echo "<h1>🔍 Comprehensive Debug - SIM-PKPPS</h1>";
|
||||
echo "<p>Waktu: " . date('Y-m-d H:i:s') . "</p><hr>";
|
||||
|
||||
// Test 1: Laravel Files
|
||||
echo "<div class='section'>";
|
||||
echo "<h2>1. File Existence Check</h2>";
|
||||
|
||||
$files = [
|
||||
'Controller' => __DIR__ . '/sim-pkpps/app/Http/Controllers/Admin/UserController.php',
|
||||
'Routes' => __DIR__ . '/sim-pkpps/routes/web.php',
|
||||
'View Wali' => __DIR__ . '/sim-pkpps/resources/views/admin/users/wali_accounts.blade.php',
|
||||
'API Controller' => __DIR__ . '/sim-pkpps/app/Http/Controllers/Api/ApiAuthController.php',
|
||||
'Flutter Config' => __DIR__ . '/sim_mobile/lib/core/config/app_config.dart',
|
||||
];
|
||||
|
||||
foreach ($files as $name => $path) {
|
||||
if (file_exists($path)) {
|
||||
echo "✅ <span class='ok'>{$name}: EXISTS</span> - Modified: " . date('Y-m-d H:i:s', filemtime($path)) . "<br>";
|
||||
} else {
|
||||
echo "❌ <span class='error'>{$name}: NOT FOUND</span><br>";
|
||||
}
|
||||
}
|
||||
echo "</div>";
|
||||
|
||||
// Test 2: Routes Content
|
||||
echo "<div class='section'>";
|
||||
echo "<h2>2. Routes Check</h2>";
|
||||
$routesFile = __DIR__ . '/sim-pkpps/routes/web.php';
|
||||
if (file_exists($routesFile)) {
|
||||
$content = file_get_contents($routesFile);
|
||||
|
||||
$checks = [
|
||||
'wali_destroy route' => "name('wali_destroy')",
|
||||
'wali_reset_password route' => "name('wali_reset_password')",
|
||||
'POST delete method' => "post('wali/{userId}/delete'",
|
||||
'POST reset method' => "post('wali/{userId}/reset-password'"
|
||||
];
|
||||
|
||||
foreach ($checks as $desc => $needle) {
|
||||
if (strpos($content, $needle) !== false) {
|
||||
echo "✅ <span class='ok'>{$desc}: FOUND</span><br>";
|
||||
} else {
|
||||
echo "❌ <span class='error'>{$desc}: NOT FOUND</span><br>";
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "</div>";
|
||||
|
||||
// Test 3: View File Check
|
||||
echo "<div class='section'>";
|
||||
echo "<h2>3. View File Check (wali_accounts.blade.php)</h2>";
|
||||
$viewFile = __DIR__ . '/sim-pkpps/resources/views/admin/users/wali_accounts.blade.php';
|
||||
if (file_exists($viewFile)) {
|
||||
$content = file_get_contents($viewFile);
|
||||
|
||||
$checks = [
|
||||
'Delete button' => "route('admin.users.wali_destroy'",
|
||||
'Reset button' => "route('admin.users.wali_reset_password'",
|
||||
'CSRF token' => '@csrf',
|
||||
'User ID parameter' => '$user->id',
|
||||
];
|
||||
|
||||
foreach ($checks as $desc => $needle) {
|
||||
if (strpos($content, $needle) !== false) {
|
||||
echo "✅ <span class='ok'>{$desc}: FOUND</span><br>";
|
||||
} else {
|
||||
echo "❌ <span class='error'>{$desc}: NOT FOUND</span><br>";
|
||||
}
|
||||
}
|
||||
|
||||
echo "<br><strong>Last modified:</strong> " . date('Y-m-d H:i:s', filemtime($viewFile));
|
||||
}
|
||||
echo "</div>";
|
||||
|
||||
// Test 4: Controller Methods
|
||||
echo "<div class='section'>";
|
||||
echo "<h2>4. Controller Methods Check</h2>";
|
||||
$controllerFile = __DIR__ . '/sim-pkpps/app/Http/Controllers/Admin/UserController.php';
|
||||
if (file_exists($controllerFile)) {
|
||||
$content = file_get_contents($controllerFile);
|
||||
|
||||
$checks = [
|
||||
'destroyAccount method' => 'public function destroyAccount(string $role, string $userId)',
|
||||
'resetPassword method' => 'public function resetPassword(string $role, string $userId)',
|
||||
'User::findOrFail in destroy' => 'User::findOrFail($userId)',
|
||||
];
|
||||
|
||||
foreach ($checks as $desc => $needle) {
|
||||
if (strpos($content, $needle) !== false) {
|
||||
echo "✅ <span class='ok'>{$desc}: FOUND</span><br>";
|
||||
} else {
|
||||
echo "❌ <span class='error'>{$desc}: NOT FOUND</span><br>";
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "</div>";
|
||||
|
||||
// Test 5: Flutter Config
|
||||
echo "<div class='section'>";
|
||||
echo "<h2>5. Flutter Configuration</h2>";
|
||||
$flutterConfig = __DIR__ . '/sim_mobile/lib/core/config/app_config.dart';
|
||||
if (file_exists($flutterConfig)) {
|
||||
$content = file_get_contents($flutterConfig);
|
||||
|
||||
if (strpos($content, 'TugasAkhir/sim-pkpps/public/api/v1') !== false) {
|
||||
echo "✅ <span class='ok'>Base URL: CORRECT (includes TugasAkhir path)</span><br>";
|
||||
} else {
|
||||
echo "❌ <span class='error'>Base URL: INCORRECT (missing TugasAkhir path)</span><br>";
|
||||
}
|
||||
|
||||
echo "<br><strong>Current URL in file:</strong><br>";
|
||||
preg_match('/baseUrl = \'(.+?)\'/s', $content, $matches);
|
||||
if (isset($matches[1])) {
|
||||
echo "<pre>" . htmlspecialchars($matches[1]) . "</pre>";
|
||||
}
|
||||
}
|
||||
echo "</div>";
|
||||
|
||||
// Test 6: API Test
|
||||
echo "<div class='section'>";
|
||||
echo "<h2>6. API Login Test</h2>";
|
||||
$apiUrl = 'http://localhost/TugasAkhir/sim-pkpps/public/api/v1/login';
|
||||
$data = json_encode([
|
||||
'id_santri' => 'Aydin Fauzan',
|
||||
'password' => 's002'
|
||||
]);
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
'header' => "Content-Type: application/json\r\nAccept: application/json\r\n",
|
||||
'method' => 'POST',
|
||||
'content' => $data,
|
||||
'ignore_errors' => true
|
||||
],
|
||||
];
|
||||
|
||||
$context = stream_context_create($options);
|
||||
$result = @file_get_contents($apiUrl, false, $context);
|
||||
|
||||
if ($result !== false) {
|
||||
$response = json_decode($result, true);
|
||||
if (isset($response['success']) && $response['success']) {
|
||||
echo "✅ <span class='ok'>API Login: SUCCESS</span><br>";
|
||||
echo "<strong>Token:</strong> " . substr($response['token'], 0, 20) . "...<br>";
|
||||
echo "<strong>User:</strong> " . $response['user']['name'] . "<br>";
|
||||
echo "<strong>Role:</strong> " . $response['user']['role'] . "<br>";
|
||||
} else {
|
||||
echo "❌ <span class='error'>API Login: FAILED</span><br>";
|
||||
echo "<pre>" . htmlspecialchars(print_r($response, true)) . "</pre>";
|
||||
}
|
||||
} else {
|
||||
echo "❌ <span class='error'>API: CANNOT CONNECT</span><br>";
|
||||
}
|
||||
echo "</div>";
|
||||
|
||||
// Test 7: Database Check
|
||||
echo "<div class='section'>";
|
||||
echo "<h2>7. Database Wali Accounts</h2>";
|
||||
$dbFile = __DIR__ . '/sim-pkpps/.env';
|
||||
if (file_exists($dbFile)) {
|
||||
echo "✅ <span class='ok'>.env file exists</span><br>";
|
||||
echo "<p>⚠️ Untuk cek database, gunakan phpMyAdmin atau Tinker</p>";
|
||||
echo "<pre>php artisan tinker --execute=\"echo App\\Models\\User::where('role','wali')->count();\"</pre>";
|
||||
} else {
|
||||
echo "❌ <span class='error'>.env file not found</span><br>";
|
||||
}
|
||||
echo "</div>";
|
||||
|
||||
// Summary
|
||||
echo "<hr><div class='section'>";
|
||||
echo "<h2>📋 Summary & Next Steps</h2>";
|
||||
echo "<ol>";
|
||||
echo "<li><strong>Clear Browser Cache:</strong> Ctrl+Shift+R atau Ctrl+F5</li>";
|
||||
echo "<li><strong>Login ke Admin:</strong> <a href='http://localhost/TugasAkhir/sim-pkpps/public/admin/login' target='_blank'>Login Admin</a></li>";
|
||||
echo "<li><strong>Test Wali Accounts:</strong> <a href='http://localhost/TugasAkhir/sim-pkpps/public/admin/users/wali' target='_blank'>Wali Accounts</a></li>";
|
||||
echo "<li><strong>Flutter:</strong> Hot Restart (bukan Hot Reload)</li>";
|
||||
echo "<li><strong>Test Login Mobile:</strong> Username=<code>Aydin Fauzan</code>, Password=<code>s002</code></li>";
|
||||
echo "</ol>";
|
||||
echo "</div>";
|
||||
|
||||
echo "<hr><p><em>Generated at " . date('Y-m-d H:i:s') . "</em></p>";
|
||||
echo "</body></html>";
|
||||
?>
|
||||
202
debug_test.html
202
debug_test.html
|
|
@ -1,202 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Debug - Test Semua Fungsi</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 { color: #333; border-bottom: 3px solid #007bff; padding-bottom: 10px; }
|
||||
h2 { color: #555; margin-top: 30px; }
|
||||
.test-box {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #007bff;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.status.ok { background: #28a745; color: white; }
|
||||
.status.error { background: #dc3545; color: white; }
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin: 5px;
|
||||
}
|
||||
.btn-primary { background: #007bff; color: white; }
|
||||
.btn-danger { background: #dc3545; color: white; }
|
||||
.btn-warning { background: #ffc107; color: black; }
|
||||
.btn-info { background: #17a2b8; color: white; }
|
||||
pre { background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; }
|
||||
.result { margin: 10px 0; padding: 10px; border-radius: 4px; }
|
||||
.result.success { background: #d4edda; color: #155724; }
|
||||
.result.error { background: #f8d7da; color: #721c24; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔍 Debug Test - SIM-PKPPS</h1>
|
||||
<p><strong>Waktu Test:</strong> <?php echo date('Y-m-d H:i:s'); ?></p>
|
||||
|
||||
<!-- Test 1: Laravel Routes -->
|
||||
<div class="test-box">
|
||||
<h2>1️⃣ Test Laravel Routes</h2>
|
||||
<button class="btn-primary" onclick="testRoutes()">Test Routes</button>
|
||||
<div id="routes-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- Test 2: Database Connection -->
|
||||
<div class="test-box">
|
||||
<h2>2️⃣ Test Database</h2>
|
||||
<button class="btn-primary" onclick="testDatabase()">Test Database</button>
|
||||
<div id="db-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- Test 3: API Login -->
|
||||
<div class="test-box">
|
||||
<h2>3️⃣ Test API Login</h2>
|
||||
<p>Username: <strong>Aydin Fauzan</strong>, Password: <strong>s002</strong></p>
|
||||
<button class="btn-info" onclick="testApiLogin()">Test Login</button>
|
||||
<div id="api-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- Test 4: Flutter Config -->
|
||||
<div class="test-box">
|
||||
<h2>4️⃣ Flutter Configuration</h2>
|
||||
<pre>Base URL: http://10.0.2.2/TugasAkhir/sim-pkpps/public/api/v1</pre>
|
||||
<p>✅ URL sudah benar untuk emulator Android</p>
|
||||
<p>⚠️ Pastikan:</p>
|
||||
<ul>
|
||||
<li>Apache/XAMPP sudah running</li>
|
||||
<li>Buka dari emulator (bukan real device)</li>
|
||||
<li>Hot reload Flutter setelah ubah config</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Test 5: Manual Action -->
|
||||
<div class="test-box">
|
||||
<h2>5️⃣ Manual Test Delete & Reset</h2>
|
||||
<p><strong>Akun tersedia:</strong></p>
|
||||
<ul>
|
||||
<li>ID 6: Aydin Fauzan</li>
|
||||
<li>ID 7: HELGA FAISA_1</li>
|
||||
<li>ID 9: Leni Yulia</li>
|
||||
<li>ID 10: Mifta Okta Yanti</li>
|
||||
</ul>
|
||||
<form action="/TugasAkhir/sim-pkpps/public/admin/users/wali/9/reset-password" method="POST" style="display:inline;">
|
||||
<button type="submit" class="btn-warning">🔑 Reset Password ID 9</button>
|
||||
</form>
|
||||
<form action="/TugasAkhir/sim-pkpps/public/admin/users/wali/10/delete" method="POST" style="display:inline;">
|
||||
<button type="submit" class="btn-danger" onclick="return confirm('Hapus ID 10?')">🗑️ Delete ID 10</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div class="test-box">
|
||||
<h2>📋 Troubleshooting Checklist</h2>
|
||||
<ul>
|
||||
<li>✅ Clear cache Laravel (sudah dilakukan)</li>
|
||||
<li>❓ Refresh browser dengan Ctrl+Shift+R (hard refresh)</li>
|
||||
<li>❓ Login dulu ke admin panel</li>
|
||||
<li>❓ Cek console browser (F12) untuk error JavaScript</li>
|
||||
<li>❓ Flutter: Hot restart (bukan hot reload)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function testRoutes() {
|
||||
const result = document.getElementById('routes-result');
|
||||
result.innerHTML = '<p>Testing...</p>';
|
||||
|
||||
fetch('/TugasAkhir/sim-pkpps/public/api/v1/login', {
|
||||
method: 'HEAD'
|
||||
})
|
||||
.then(response => {
|
||||
result.innerHTML = `<div class="result success">✅ Route API accessible (Status: ${response.status})</div>`;
|
||||
})
|
||||
.catch(error => {
|
||||
result.innerHTML = `<div class="result error">❌ Error: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function testDatabase() {
|
||||
const result = document.getElementById('db-result');
|
||||
result.innerHTML = '<p>Testing database...</p>';
|
||||
|
||||
// Simple check via API
|
||||
fetch('/TugasAkhir/sim-pkpps/public/api/v1/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id_santri: 'test',
|
||||
password: 'test'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
result.innerHTML = '<div class="result success">✅ Database connection OK (API responding)</div>';
|
||||
})
|
||||
.catch(error => {
|
||||
result.innerHTML = `<div class="result error">❌ Database error: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function testApiLogin() {
|
||||
const result = document.getElementById('api-result');
|
||||
result.innerHTML = '<p>Testing login...</p>';
|
||||
|
||||
fetch('/TugasAkhir/sim-pkpps/public/api/v1/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id_santri: 'Aydin Fauzan',
|
||||
password: 's002'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
result.innerHTML = `
|
||||
<div class="result success">
|
||||
<strong>✅ LOGIN BERHASIL!</strong><br>
|
||||
Token: ${data.token}<br>
|
||||
User: ${data.user.name}<br>
|
||||
Role: ${data.user.role}<br>
|
||||
Santri: ${data.santri.nama_lengkap}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
result.innerHTML = `<div class="result error">❌ Login gagal: ${data.message}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
result.innerHTML = `<div class="result error">❌ Error: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* INSERT SAMPLE BERITA - Quick Setup
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
$host = 'localhost';
|
||||
$db = 'db_sim_pkpps';
|
||||
$user = 'root';
|
||||
$pass = '';
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $pass);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
echo "<h1>📝 INSERT SAMPLE BERITA</h1>";
|
||||
echo "<hr>";
|
||||
|
||||
// Cek apakah sudah ada berita
|
||||
$stmt = $pdo->query("SELECT COUNT(*) as total FROM berita");
|
||||
$count = $stmt->fetch(PDO::FETCH_ASSOC)['total'];
|
||||
|
||||
if ($count > 0) {
|
||||
echo "<p style='color: orange;'>⚠️ Sudah ada {$count} berita di database.</p>";
|
||||
echo "<p>Apakah Anda ingin menambah berita sample lagi?</p>";
|
||||
echo "<form method='post'>";
|
||||
echo "<button type='submit' name='tambah' style='padding: 10px 20px; font-size: 16px;'>Ya, Tambah Sample Berita</button>";
|
||||
echo "</form>";
|
||||
|
||||
if (!isset($_POST['tambah'])) {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
echo "<h2>🚀 Menambahkan sample berita...</h2>";
|
||||
|
||||
// Ambil santri untuk pivot table
|
||||
$stmt = $pdo->query("SELECT id_santri FROM santris WHERE status = 'Aktif' LIMIT 3");
|
||||
$santriList = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (count($santriList) < 1) {
|
||||
die("<p style='color: red;'>❌ Tidak ada santri aktif! Tidak bisa buat sample berita 'santri_tertentu'</p>");
|
||||
}
|
||||
|
||||
// 1. Berita untuk SEMUA
|
||||
$sql = "INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'published', 'semua', NOW(), NOW())";
|
||||
|
||||
$beritaSemua = [
|
||||
['B101', 'Pengumuman Libur Pondok', 'Assalamualaikum. Pondok akan libur tanggal 10-15 Februari 2026. Harap kembali tanggal 16 Februari jam 07:00. Barakallah.', 'Admin Pondok'],
|
||||
['B102', 'Jadwal Ujian Semester', 'Kepada seluruh santri, ujian semester akan dimulai 20 Februari 2026. Silakan persiapkan diri dengan baik. Semoga sukses!', 'Bagian Pendidikan'],
|
||||
];
|
||||
|
||||
foreach ($beritaSemua as $data) {
|
||||
try {
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($data);
|
||||
echo "✅ Berita {$data[0]} (target: SEMUA) berhasil ditambahkan<br>";
|
||||
} catch (PDOException $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
echo "⚠️ Berita {$data[0]} sudah ada (skip)<br>";
|
||||
} else {
|
||||
echo "❌ Error: {$e->getMessage()}<br>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "<br>";
|
||||
|
||||
// 2. Berita untuk KELAS TERTENTU
|
||||
$sql = "INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, target_kelas, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'published', 'kelas_tertentu', ?, NOW(), NOW())";
|
||||
|
||||
$beritaKelas = [
|
||||
['B103', 'Info Kelas PB', 'Kepada santri kelas PB, akan ada kelas tambahan setiap Kamis jam 15:00. Mohon hadir tepat waktu.', 'Ustadz Nahwu', '["PB"]'],
|
||||
['B104', 'Ujian Kelas Lambatan', 'Santri kelas Lambatan akan ujian kenaikan tingkat tanggal 5 Maret 2026. Harap persiapkan diri!', 'Bagian Pendidikan', '["Lambatan"]'],
|
||||
['B105', 'Kegiatan Kelas Cepatan', 'Kelas Cepatan akan muhadhoroh setiap Jumat malam. Jadwal akan dibagikan minggu depan.', 'Ustadz Pembimbing', '["Cepatan"]'],
|
||||
];
|
||||
|
||||
foreach ($beritaKelas as $data) {
|
||||
try {
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($data);
|
||||
echo "✅ Berita {$data[0]} (target: KELAS) berhasil ditambahkan<br>";
|
||||
} catch (PDOException $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
echo "⚠️ Berita {$data[0]} sudah ada (skip)<br>";
|
||||
} else {
|
||||
echo "❌ Error: {$e->getMessage()}<br>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "<br>";
|
||||
|
||||
// 3. Berita untuk SANTRI TERTENTU
|
||||
$sql = "INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'published', 'santri_tertentu', NOW(), NOW())";
|
||||
|
||||
$beritaSantri = [
|
||||
['B106', 'Pesan Khusus - Menemui Admin', 'Assalamualaikum. Saudara diminta menemui bagian administrasi hari Senin jam 10:00. Terima kasih.', 'Admin'],
|
||||
['B107', 'Reminder Uang Saku', 'Saldo uang saku Anda menipis (di bawah Rp 50.000). Harap segera top up. Barakallah.', 'Bagian Keuangan'],
|
||||
];
|
||||
|
||||
foreach ($beritaSantri as $data) {
|
||||
try {
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($data);
|
||||
echo "✅ Berita {$data[0]} (target: SANTRI TERTENTU) berhasil ditambahkan<br>";
|
||||
|
||||
// Insert ke pivot table untuk santri pertama
|
||||
if (count($santriList) > 0) {
|
||||
$sqlPivot = "INSERT INTO berita_santri (id_berita, id_santri, sudah_dibaca, created_at, updated_at)
|
||||
VALUES (?, ?, FALSE, NOW(), NOW())";
|
||||
$stmtPivot = $pdo->prepare($sqlPivot);
|
||||
$stmtPivot->execute([$data[0], $santriList[0]]);
|
||||
echo " └─ Ditambahkan untuk santri {$santriList[0]}<br>";
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
echo "⚠️ Berita {$data[0]} sudah ada (skip)<br>";
|
||||
} else {
|
||||
echo "❌ Error: {$e->getMessage()}<br>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "<hr>";
|
||||
echo "<h2>✅ SELESAI!</h2>";
|
||||
|
||||
// Tampilkan ringkasan
|
||||
$stmt = $pdo->query("SELECT target_berita, COUNT(*) as jumlah FROM berita GROUP BY target_berita");
|
||||
$summary = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "<h3>📊 Ringkasan Berita:</h3>";
|
||||
foreach ($summary as $row) {
|
||||
echo "- <strong>{$row['target_berita']}</strong>: {$row['jumlah']} berita<br>";
|
||||
}
|
||||
|
||||
echo "<br><br>";
|
||||
echo "<a href='test_query_berita.php' style='padding: 10px 20px; background: #7C3AED; color: white; text-decoration: none; border-radius: 5px;'>Test Query Berita</a> ";
|
||||
echo "<a href='test_api_berita.php' style='padding: 10px 20px; background: #059669; color: white; text-decoration: none; border-radius: 5px;'>Debug API Berita</a>";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "<h1 style='color: red;'>❌ ERROR</h1>";
|
||||
echo "<p>{$e->getMessage()}</p>";
|
||||
}
|
||||
?>
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
# Migration Helper Script for Windows PowerShell
|
||||
# Usage: .\migrate_helper.ps1 [action]
|
||||
|
||||
param(
|
||||
[string]$action = "help"
|
||||
)
|
||||
|
||||
$simdDir = "sim-pkpps"
|
||||
|
||||
function Show-Header {
|
||||
Write-Host "╔══════════════════════════════════════════════════════╗" -ForegroundColor Cyan
|
||||
Write-Host "║ SIM Pondok Pesantren - Kelas Migration ║" -ForegroundColor Cyan
|
||||
Write-Host "╚══════════════════════════════════════════════════════╝" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Show-Help {
|
||||
Show-Header
|
||||
Write-Host "Available actions:" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host " install - Run migrations and seeders" -ForegroundColor Green
|
||||
Write-Host " migrate-test - Test data migration (dry-run)" -ForegroundColor Green
|
||||
Write-Host " migrate - Run actual data migration" -ForegroundColor Green
|
||||
Write-Host " scan - Scan codebase for kelas usage" -ForegroundColor Green
|
||||
Write-Host " report - Open refactoring report" -ForegroundColor Green
|
||||
Write-Host " verify - Verify migration status" -ForegroundColor Green
|
||||
Write-Host " help - Show this help message" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Examples:" -ForegroundColor Yellow
|
||||
Write-Host " .\migrate_helper.ps1 install" -ForegroundColor Gray
|
||||
Write-Host " .\migrate_helper.ps1 migrate-test" -ForegroundColor Gray
|
||||
Write-Host " .\migrate_helper.ps1 scan" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Run-Install {
|
||||
Show-Header
|
||||
Write-Host "📦 Installing new kelas system..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Step 1: Running migrations..." -ForegroundColor Cyan
|
||||
Set-Location $simdDir
|
||||
php artisan migrate
|
||||
Write-Host "✓ Migrations completed" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Step 2: Seeding kelompok kelas..." -ForegroundColor Cyan
|
||||
php artisan db:seed --class=KelompokKelasSeeder
|
||||
Write-Host "✓ Kelompok kelas seeded" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Step 3: Seeding kelas..." -ForegroundColor Cyan
|
||||
php artisan db:seed --class=KelasSeeder
|
||||
Write-Host "✓ Kelas seeded" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
Set-Location ..
|
||||
|
||||
Write-Host "✓ Installation completed!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Run: .\migrate_helper.ps1 migrate-test" -ForegroundColor Gray
|
||||
Write-Host " 2. If OK, run: .\migrate_helper.ps1 migrate" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Run-MigrateTest {
|
||||
Show-Header
|
||||
Write-Host "🔍 Testing data migration (dry-run)..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
Set-Location $simdDir
|
||||
php artisan migrate:santri-kelas --dry-run
|
||||
Set-Location ..
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "If everything looks good:" -ForegroundColor Yellow
|
||||
Write-Host " Run: .\migrate_helper.ps1 migrate" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Run-Migrate {
|
||||
Show-Header
|
||||
Write-Host "⚠️ This will migrate santri kelas data to new system" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
$confirm = Read-Host "Are you sure? (yes/no)"
|
||||
|
||||
if ($confirm -eq "yes") {
|
||||
Write-Host ""
|
||||
Write-Host "🚀 Running migration..." -ForegroundColor Cyan
|
||||
Set-Location $simdDir
|
||||
php artisan migrate:santri-kelas
|
||||
Set-Location ..
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "✓ Migration completed!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Run: .\migrate_helper.ps1 verify" -ForegroundColor Gray
|
||||
Write-Host " 2. Run: .\migrate_helper.ps1 scan" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
} else {
|
||||
Write-Host "Migration cancelled" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
function Run-Scan {
|
||||
Show-Header
|
||||
Write-Host "🔍 Scanning codebase for kelas usage..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
php scan_kelas_usage.php
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "✓ Scan completed!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Review the reports:" -ForegroundColor Yellow
|
||||
Write-Host " - KELAS_USAGE_MAP.md (detailed map)" -ForegroundColor Gray
|
||||
Write-Host " - REFACTORING_GUIDE.md (quick reference)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "Open report? (yes/no)" -ForegroundColor Yellow
|
||||
$openReport = Read-Host
|
||||
|
||||
if ($openReport -eq "yes") {
|
||||
code KELAS_USAGE_MAP.md
|
||||
}
|
||||
}
|
||||
|
||||
function Run-Verify {
|
||||
Show-Header
|
||||
Write-Host "📊 Verifying migration status..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
Set-Location $simdDir
|
||||
|
||||
Write-Host "Migration status:" -ForegroundColor Cyan
|
||||
php artisan migrate:status | Select-String "kelompok_kelas|kelas|santri_kelas|kegiatan_kelas"
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Checking data counts:" -ForegroundColor Cyan
|
||||
php artisan tinker --execute="echo 'Kelompok Kelas: ' . App\Models\KelompokKelas::count() . PHP_EOL;"
|
||||
php artisan tinker --execute="echo 'Kelas: ' . App\Models\Kelas::count() . PHP_EOL;"
|
||||
php artisan tinker --execute="echo 'Santri Kelas: ' . App\Models\SantriKelas::count() . PHP_EOL;"
|
||||
php artisan tinker --execute="echo 'Santri with old kelas: ' . App\Models\Santri::whereNotNull('kelas')->count() . PHP_EOL;"
|
||||
Write-Host ""
|
||||
|
||||
Set-Location ..
|
||||
|
||||
Write-Host "✓ Verification completed!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Open-Report {
|
||||
Show-Header
|
||||
Write-Host "📖 Opening refactoring reports..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
if (Test-Path "KELAS_USAGE_MAP.md") {
|
||||
code KELAS_USAGE_MAP.md
|
||||
Write-Host "✓ Opened KELAS_USAGE_MAP.md" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ KELAS_USAGE_MAP.md not found. Run scan first." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if (Test-Path "REFACTORING_GUIDE.md") {
|
||||
code REFACTORING_GUIDE.md
|
||||
Write-Host "✓ Opened REFACTORING_GUIDE.md" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Main script execution
|
||||
switch ($action.ToLower()) {
|
||||
"install" {
|
||||
Run-Install
|
||||
}
|
||||
"migrate-test" {
|
||||
Run-MigrateTest
|
||||
}
|
||||
"migrate" {
|
||||
Run-Migrate
|
||||
}
|
||||
"scan" {
|
||||
Run-Scan
|
||||
}
|
||||
"verify" {
|
||||
Run-Verify
|
||||
}
|
||||
"report" {
|
||||
Open-Report
|
||||
}
|
||||
"help" {
|
||||
Show-Help
|
||||
}
|
||||
default {
|
||||
Write-Host "Unknown action: $action" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Show-Help
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
-- ============================================
|
||||
-- SAMPLE DATA BERITA - 3 KATEGORI
|
||||
-- ============================================
|
||||
|
||||
-- KATEGORI 1: BERITA UNTUK SEMUA SANTRI
|
||||
-- ======================================
|
||||
INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, created_at, updated_at)
|
||||
VALUES
|
||||
('B001', 'Pengumuman Libur Pondok',
|
||||
'Assalamualaikum wr. wb. Diberitahukan kepada seluruh santri bahwa pondok akan libur pada tanggal 10-15 Februari 2026 dalam rangka peringatan Maulid Nabi Muhammad SAW. Mohon untuk kembali ke pondok pada tanggal 16 Februari 2026 pukul 07:00 pagi. Jazakumullah khairan.',
|
||||
'Admin Pondok', 'published', 'semua', NOW(), NOW()),
|
||||
|
||||
('B002', 'Jadwal Ujian Semester Genap',
|
||||
'Kepada seluruh santri, jadwal ujian semester genap akan dimulai tanggal 20 Februari 2026. Harap mempersiapkan diri dengan baik. Jadwal lengkap akan diumumkan kemudian. Semoga Allah memudahkan.',
|
||||
'Bagian Pendidikan', 'published', 'semua', NOW(), NOW()),
|
||||
|
||||
('B003', 'Pengumuman Kegiatan Haul Akbar',
|
||||
'Bismillah. Dalam rangka memperingati Haul Kyai Pendiri Pondok yang ke-50, akan diadakan kegiatan haul akbar pada tanggal 25 Februari 2026. Seluruh santri diwajibkan mengikuti acara. Mohon kehadiran dan partisipasinya.',
|
||||
'Pengurus Pondok', 'published', 'semua', NOW(), NOW());
|
||||
|
||||
|
||||
-- KATEGORI 2: BERITA UNTUK KELAS TERTENTU
|
||||
-- ========================================
|
||||
|
||||
-- Berita untuk Kelas PB saja
|
||||
INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, target_kelas, created_at, updated_at)
|
||||
VALUES
|
||||
('B004', 'Jadwal Tambahan Kelas PB',
|
||||
'Kepada santri kelas PB, mulai minggu depan akan ada kelas tambahan setiap hari Kamis jam 15:00-16:30 untuk pendalaman materi Nahwu. Mohon kehadirannya tepat waktu. Barakallah.',
|
||||
'Ustadz Nahwu', 'published', 'kelas_tertentu', '["PB"]', NOW(), NOW());
|
||||
|
||||
-- Berita untuk Kelas Lambatan saja
|
||||
INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, target_kelas, created_at, updated_at)
|
||||
VALUES
|
||||
('B005', 'Ujian Kenaikan Kelas Lambatan',
|
||||
'Santri kelas Lambatan akan mengikuti ujian kenaikan tingkat pada tanggal 5 Maret 2026. Materi ujian meliputi Nahwu, Shorof, Fiqih, dan Tajwid. Harap mempersiapkan diri dengan sungguh-sungguh. Semoga sukses!',
|
||||
'Bagian Pendidikan', 'published', 'kelas_tertentu', '["Lambatan"]', NOW(), NOW());
|
||||
|
||||
-- Berita untuk Kelas Cepatan saja
|
||||
INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, target_kelas, created_at, updated_at)
|
||||
VALUES
|
||||
('B006', 'Kegiatan Muhadhoroh Kelas Cepatan',
|
||||
'Kepada santri kelas Cepatan, akan diadakan kegiatan muhadhoroh (latihan pidato) setiap hari Jumat malam. Setiap santri akan mendapat giliran. Jadwal akan dibagikan minggu depan. Siapkan materi pidato dengan baik.',
|
||||
'Ustadz Pembimbing', 'published', 'kelas_tertentu', '["Cepatan"]', NOW(), NOW());
|
||||
|
||||
-- Berita untuk PB dan Lambatan (2 kelas sekaligus)
|
||||
INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, target_kelas, created_at, updated_at)
|
||||
VALUES
|
||||
('B007', 'Info Kegiatan Ekstrakurikuler',
|
||||
'Kepada santri kelas PB dan Lambatan, dibuka pendaftaran ekstrakurikuler Tahfidz dan Kaligrafi. Pendaftaran dilakukan di kantor pondok mulai Senin-Rabu jam 16:00-17:00. Kuota terbatas, siapa cepat dia dapat!',
|
||||
'Bagian Kegiatan', 'published', 'kelas_tertentu', '["PB", "Lambatan"]', NOW(), NOW());
|
||||
|
||||
|
||||
-- KATEGORI 3: BERITA UNTUK SANTRI TERTENTU
|
||||
-- =========================================
|
||||
|
||||
-- Berita pribadi untuk santri tertentu
|
||||
INSERT INTO berita (id_berita, judul, konten, penulis, status, target_berita, created_at, updated_at)
|
||||
VALUES
|
||||
('B008', 'Pesan Khusus - Harap Menemui Admin',
|
||||
'Assalamualaikum. Saudara diminta untuk menemui bagian administrasi pondok pada hari Senin jam 10:00 untuk pengecekan data dan pemberkasan. Mohon datang tepat waktu. Terima kasih.',
|
||||
'Admin', 'published', 'santri_tertentu', NOW(), NOW()),
|
||||
|
||||
('B009', 'Reminder Pembayaran Uang Saku',
|
||||
'Assalamualaikum. Kami informasikan bahwa saldo uang saku Anda sudah menipis (di bawah Rp 50.000). Harap segera melakukan top up agar tidak terkendala dalam pembelian kebutuhan sehari-hari. Barakallah.',
|
||||
'Bagian Keuangan', 'published', 'santri_tertentu', NOW(), NOW()),
|
||||
|
||||
('B010', 'Undangan Pertemuan Wali',
|
||||
'Kepada Bapak/Ibu Wali Santri, kami mengundang untuk hadir dalam pertemuan membahas perkembangan santri pada hari Sabtu, 22 Februari 2026 pukul 09:00 di aula pondok. Kehadiran sangat kami harapkan. Jazakumullah khairan.',
|
||||
'Pengurus Pondok', 'published', 'santri_tertentu', NOW(), NOW());
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- PIVOT TABLE untuk SANTRI TERTENTU (B008, B009, B010)
|
||||
-- ============================================
|
||||
--
|
||||
-- CATATAN: Sesuaikan id_santri dengan data di database Anda!
|
||||
--
|
||||
-- Contoh: Jika di database ada santri dengan id_santri 'S001', 'S002', 'S003'
|
||||
-- maka insert seperti ini:
|
||||
|
||||
-- Berita B008 untuk S001 dan S002
|
||||
INSERT INTO berita_santri (id_berita, id_santri, sudah_dibaca, created_at, updated_at)
|
||||
VALUES
|
||||
('B008', 'S001', FALSE, NOW(), NOW()),
|
||||
('B008', 'S002', FALSE, NOW(), NOW());
|
||||
|
||||
-- Berita B009 untuk S001 saja
|
||||
INSERT INTO berita_santri (id_berita, id_santri, sudah_dibaca, created_at, updated_at)
|
||||
VALUES
|
||||
('B009', 'S001', FALSE, NOW(), NOW());
|
||||
|
||||
-- Berita B010 untuk S001, S002, dan S003
|
||||
INSERT INTO berita_santri (id_berita, id_santri, sudah_dibaca, created_at, updated_at)
|
||||
VALUES
|
||||
('B010', 'S001', FALSE, NOW(), NOW()),
|
||||
('B010', 'S002', FALSE, NOW(), NOW()),
|
||||
('B010', 'S003', FALSE, NOW(), NOW());
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- CARA CEK ID SANTRI YANG ADA
|
||||
-- ============================================
|
||||
-- Jalankan query ini dulu untuk lihat id_santri yang tersedia:
|
||||
--
|
||||
-- SELECT id_santri, nama_lengkap, kelas, status
|
||||
-- FROM santris
|
||||
-- WHERE status = 'Aktif'
|
||||
-- ORDER BY nama_lengkap;
|
||||
--
|
||||
-- Kemudian sesuaikan INSERT berita_santri di atas dengan id_santri yang sesuai
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- HASIL YANG DIHARAPKAN
|
||||
-- ============================================
|
||||
--
|
||||
-- 1. Berita B001-B003 (target: semua)
|
||||
-- → Muncul untuk SEMUA santri yang login
|
||||
--
|
||||
-- 2. Berita B004 (target: kelas PB)
|
||||
-- → Hanya muncul untuk santri kelas PB
|
||||
--
|
||||
-- 3. Berita B005 (target: kelas Lambatan)
|
||||
-- → Hanya muncul untuk santri kelas Lambatan
|
||||
--
|
||||
-- 4. Berita B006 (target: kelas Cepatan)
|
||||
-- → Hanya muncul untuk santri kelas Cepatan
|
||||
--
|
||||
-- 5. Berita B007 (target: kelas PB dan Lambatan)
|
||||
-- → Muncul untuk santri kelas PB DAN Lambatan
|
||||
--
|
||||
-- 6. Berita B008-B010 (target: santri tertentu)
|
||||
-- → Hanya muncul untuk santri yang ada di pivot table berita_santri
|
||||
-- → Akan ada badge "BARU" sampai mereka buka beritanya
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Scan Kelas Usage Script
|
||||
*
|
||||
* Script untuk scan semua penggunaan $santri->kelas di codebase
|
||||
* dan generate laporan markdown untuk refactoring guidance
|
||||
*
|
||||
* Usage:
|
||||
* php scan_kelas_usage.php
|
||||
*
|
||||
* Output:
|
||||
* KELAS_USAGE_MAP.md
|
||||
*/
|
||||
|
||||
// Configuration
|
||||
$baseDir = __DIR__ . '/sim-pkpps';
|
||||
$outputFile = __DIR__ . '/KELAS_USAGE_MAP.md';
|
||||
|
||||
// Check if base directory exists
|
||||
if (!is_dir($baseDir)) {
|
||||
echo "❌ Error: Base directory not found: {$baseDir}\n";
|
||||
echo "Current directory: " . __DIR__ . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Directories to scan
|
||||
$scanDirs = [
|
||||
'app/Http/Controllers',
|
||||
'app/Models',
|
||||
'resources/views',
|
||||
'database/migrations',
|
||||
'database/seeders',
|
||||
'routes',
|
||||
];
|
||||
|
||||
// Patterns to search (regex)
|
||||
$patterns = [
|
||||
'property_access' => '/\$santri\s*->\s*kelas(?!\w)/',
|
||||
'array_access' => '/\$santri\[[\'"]kelas[\'"]\]/',
|
||||
'blade_kelas' => '/\{\{\s*\$santri\s*->\s*kelas\s*\}\}/',
|
||||
'where_kelas' => '/->where\([\'"]kelas[\'"]\s*,/',
|
||||
'wherein_kelas' => '/->whereIn\([\'"]kelas[\'"]\s*,/',
|
||||
'select_kelas' => '/SELECT.*santris\.kelas/i',
|
||||
'enum_values' => '/(\'PB\'|\'Lambatan\'|\'Cepatan\')\s*(,|\]|\))/i',
|
||||
'kelas_column' => '/[\'"]kelas[\'"]\s*=>/i',
|
||||
];
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════╗\n";
|
||||
echo "║ Scanning Santri.kelas Usage in Codebase ║\n";
|
||||
echo "╚══════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
// Initialize results
|
||||
$results = [];
|
||||
$totalFiles = 0;
|
||||
$totalMatches = 0;
|
||||
|
||||
// Scan each directory
|
||||
foreach ($scanDirs as $dir) {
|
||||
$fullPath = $baseDir . '/' . $dir;
|
||||
|
||||
if (!is_dir($fullPath)) {
|
||||
echo "⚠️ Directory not found: {$dir}\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "📁 Scanning: {$dir}\n";
|
||||
|
||||
$files = scanDirectory($fullPath, $dir);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$matches = scanFile($file['full_path'], $patterns);
|
||||
|
||||
if (!empty($matches)) {
|
||||
$totalFiles++;
|
||||
$totalMatches += count($matches);
|
||||
|
||||
$results[$dir][] = [
|
||||
'file' => $file['relative_path'],
|
||||
'full_path' => $file['full_path'],
|
||||
'matches' => $matches,
|
||||
];
|
||||
|
||||
echo " ✓ Found " . count($matches) . " match(es) in: " . basename($file['relative_path']) . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
echo "Summary:\n";
|
||||
echo " 📊 Files scanned: " . countAllFiles($scanDirs, $baseDir) . "\n";
|
||||
echo " ✓ Files with matches: {$totalFiles}\n";
|
||||
echo " 🔍 Total matches: {$totalMatches}\n";
|
||||
echo "\n";
|
||||
|
||||
// Generate markdown report
|
||||
echo "📝 Generating report: KELAS_USAGE_MAP.md\n";
|
||||
generateMarkdownReport($results, $outputFile);
|
||||
|
||||
echo "✓ Report generated successfully!\n";
|
||||
echo "\nNext steps:\n";
|
||||
echo " 1. Review KELAS_USAGE_MAP.md\n";
|
||||
echo " 2. Prioritize refactoring (HIGH -> MEDIUM -> LOW)\n";
|
||||
echo " 3. Test each change thoroughly\n";
|
||||
echo " 4. Use \$santri->kelas_name for backward compatibility\n\n";
|
||||
|
||||
// ============================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Recursively scan directory for PHP and Blade files
|
||||
*/
|
||||
function scanDirectory($dir, $relativePath)
|
||||
{
|
||||
$files = [];
|
||||
$items = scandir($dir);
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullPath = $dir . '/' . $item;
|
||||
$relPath = $relativePath . '/' . $item;
|
||||
|
||||
if (is_dir($fullPath)) {
|
||||
$files = array_merge($files, scanDirectory($fullPath, $relPath));
|
||||
} elseif (preg_match('/\.(php|blade\.php)$/', $item)) {
|
||||
$files[] = [
|
||||
'full_path' => $fullPath,
|
||||
'relative_path' => $relPath,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan file for patterns
|
||||
*/
|
||||
function scanFile($filePath, $patterns)
|
||||
{
|
||||
$content = file_get_contents($filePath);
|
||||
$lines = explode("\n", $content);
|
||||
$matches = [];
|
||||
|
||||
foreach ($lines as $lineNum => $line) {
|
||||
foreach ($patterns as $type => $pattern) {
|
||||
if (preg_match($pattern, $line)) {
|
||||
$matches[] = [
|
||||
'line' => $lineNum + 1,
|
||||
'type' => $type,
|
||||
'content' => trim($line),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all files in directories
|
||||
*/
|
||||
function countAllFiles($dirs, $baseDir)
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($dirs as $dir) {
|
||||
$fullPath = $baseDir . '/' . $dir;
|
||||
if (is_dir($fullPath)) {
|
||||
$count += count(scanDirectory($fullPath, $dir));
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate markdown report
|
||||
*/
|
||||
function generateMarkdownReport($results, $outputFile)
|
||||
{
|
||||
$md = "# Santri.kelas Usage Mapping\n\n";
|
||||
$md .= "_Generated: " . date('Y-m-d H:i:s') . "_\n\n";
|
||||
$md .= "This document maps all usage of `\$santri->kelas` and related patterns in the codebase ";
|
||||
$md .= "to guide refactoring to the new kelas system.\n\n";
|
||||
$md .= "---\n\n";
|
||||
|
||||
$md .= "## 📊 Summary\n\n";
|
||||
$totalFiles = 0;
|
||||
$totalMatches = 0;
|
||||
foreach ($results as $dir => $files) {
|
||||
$totalFiles += count($files);
|
||||
foreach ($files as $file) {
|
||||
$totalMatches += count($file['matches']);
|
||||
}
|
||||
}
|
||||
$md .= "- **Total files with kelas usage:** {$totalFiles}\n";
|
||||
$md .= "- **Total matches found:** {$totalMatches}\n\n";
|
||||
$md .= "---\n\n";
|
||||
|
||||
// Priority mapping
|
||||
$priorities = categorizePriority($results);
|
||||
|
||||
$md .= "## 🎯 Priority Levels\n\n";
|
||||
$md .= "### 🔴 HIGH Priority (Break functionality)\n\n";
|
||||
if (!empty($priorities['high'])) {
|
||||
foreach ($priorities['high'] as $item) {
|
||||
$md .= "- **{$item['file']}**\n";
|
||||
$md .= " - Issue: {$item['reason']}\n";
|
||||
$md .= " - Action Required: {$item['action']}\n\n";
|
||||
}
|
||||
} else {
|
||||
$md .= "_No high priority items found_\n\n";
|
||||
}
|
||||
|
||||
$md .= "### 🟡 MEDIUM Priority (UI/Display)\n\n";
|
||||
if (!empty($priorities['medium'])) {
|
||||
foreach ($priorities['medium'] as $item) {
|
||||
$md .= "- **{$item['file']}**\n";
|
||||
$md .= " - Issue: {$item['reason']}\n";
|
||||
$md .= " - Action Required: {$item['action']}\n\n";
|
||||
}
|
||||
} else {
|
||||
$md .= "_No medium priority items found_\n\n";
|
||||
}
|
||||
|
||||
$md .= "### 🟢 LOW Priority (Backward compatible)\n\n";
|
||||
if (!empty($priorities['low'])) {
|
||||
foreach ($priorities['low'] as $item) {
|
||||
$md .= "- **{$item['file']}**\n";
|
||||
$md .= " - Note: {$item['reason']}\n\n";
|
||||
}
|
||||
} else {
|
||||
$md .= "_No low priority items found_\n\n";
|
||||
}
|
||||
|
||||
$md .= "---\n\n";
|
||||
|
||||
// Detailed listing by directory
|
||||
$md .= "## 📂 Detailed Listing by Directory\n\n";
|
||||
|
||||
foreach ($results as $dir => $files) {
|
||||
$md .= "### " . ucfirst(str_replace('/', ' / ', $dir)) . "\n\n";
|
||||
|
||||
foreach ($files as $file) {
|
||||
$md .= "#### 📄 `{$file['file']}`\n\n";
|
||||
|
||||
// Group matches by type
|
||||
$byType = [];
|
||||
foreach ($file['matches'] as $match) {
|
||||
$byType[$match['type']][] = $match;
|
||||
}
|
||||
|
||||
foreach ($byType as $type => $matches) {
|
||||
$md .= "**Pattern: `{$type}`**\n\n";
|
||||
foreach ($matches as $match) {
|
||||
$md .= "- **Line {$match['line']}:** `{$match['content']}`\n";
|
||||
}
|
||||
$md .= "\n";
|
||||
}
|
||||
|
||||
// Suggested action
|
||||
$action = getRefactoringAction($file['file'], $byType);
|
||||
$md .= "**💡 Suggested Action:**\n";
|
||||
$md .= $action . "\n\n";
|
||||
$md .= "---\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Migration guide
|
||||
$md .= "## 📖 Refactoring Guide\n\n";
|
||||
$md .= "### General Patterns\n\n";
|
||||
$md .= "#### 1. Display in Views (Blade)\n";
|
||||
$md .= "```php\n";
|
||||
$md .= "// OLD:\n";
|
||||
$md .= "{{ \$santri->kelas }}\n\n";
|
||||
$md .= "// NEW (backward compatible):\n";
|
||||
$md .= "{{ \$santri->kelas_name }}\n";
|
||||
$md .= "```\n\n";
|
||||
|
||||
$md .= "#### 2. Filter in Controllers\n";
|
||||
$md .= "```php\n";
|
||||
$md .= "// OLD:\n";
|
||||
$md .= "\$santris = Santri::where('kelas', 'PB')->get();\n\n";
|
||||
$md .= "// NEW:\n";
|
||||
$md .= "\$santris = Santri::whereHas('kelasSantri', function(\$q) {\n";
|
||||
$md .= " \$q->where('id_kelas', 1); // PB = 1\n";
|
||||
$md .= "})->get();\n";
|
||||
$md .= "```\n\n";
|
||||
|
||||
$md .= "#### 3. Kegiatan-Kelas Relation\n";
|
||||
$md .= "```php\n";
|
||||
$md .= "// OLD: Filter santri by kelas for kegiatan\n";
|
||||
$md .= "\$santris = Santri::whereIn('kelas', ['PB', 'Lambatan'])->get();\n\n";
|
||||
$md .= "// NEW: Use kegiatan relation\n";
|
||||
$md .= "\$santris = \$kegiatan->getEligibleSantris();\n";
|
||||
$md .= "```\n\n";
|
||||
|
||||
$md .= "### Testing Checklist\n\n";
|
||||
$md .= "- [ ] Santri detail page displays correct kelas\n";
|
||||
$md .= "- [ ] Santri list filter by kelas works\n";
|
||||
$md .= "- [ ] Dashboard statistics by kelas accurate\n";
|
||||
$md .= "- [ ] Kegiatan filtering by kelas works\n";
|
||||
$md .= "- [ ] Absensi shows correct santri per kegiatan\n";
|
||||
$md .= "- [ ] Reports include correct kelas information\n";
|
||||
$md .= "- [ ] Mobile API returns kelas data correctly\n\n";
|
||||
|
||||
// Write to file
|
||||
file_put_contents($outputFile, $md);
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize by priority
|
||||
*/
|
||||
function categorizePriority($results)
|
||||
{
|
||||
$priorities = [
|
||||
'high' => [],
|
||||
'medium' => [],
|
||||
'low' => [],
|
||||
];
|
||||
|
||||
foreach ($results as $dir => $files) {
|
||||
foreach ($files as $file) {
|
||||
$fileName = basename($file['file']);
|
||||
$priority = determinePriority($file['file'], $file['matches']);
|
||||
|
||||
$priorities[$priority['level']][] = [
|
||||
'file' => $file['file'],
|
||||
'reason' => $priority['reason'],
|
||||
'action' => $priority['action'] ?? 'Review and update',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $priorities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine priority level
|
||||
*/
|
||||
function determinePriority($filePath, $matches)
|
||||
{
|
||||
$fileName = basename($filePath);
|
||||
|
||||
// HIGH: Controllers with where/whereIn
|
||||
if (strpos($filePath, 'Controller') !== false) {
|
||||
foreach ($matches as $match) {
|
||||
if (in_array($match['type'], ['where_kelas', 'wherein_kelas'])) {
|
||||
return [
|
||||
'level' => 'high',
|
||||
'reason' => 'Query filtering by kelas column',
|
||||
'action' => 'Update to use kelasSantri relationship',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HIGH: Migration files
|
||||
if (strpos($filePath, 'migration') !== false) {
|
||||
return [
|
||||
'level' => 'high',
|
||||
'reason' => 'Database schema definition',
|
||||
'action' => 'Review but DO NOT modify old migrations',
|
||||
];
|
||||
}
|
||||
|
||||
// MEDIUM: Views
|
||||
if (strpos($filePath, 'views') !== false || strpos($filePath, '.blade.php') !== false) {
|
||||
return [
|
||||
'level' => 'medium',
|
||||
'reason' => 'Display kelas in UI',
|
||||
'action' => 'Change to use $santri->kelas_name accessor',
|
||||
];
|
||||
}
|
||||
|
||||
// MEDIUM: Models
|
||||
if (strpos($filePath, 'Models') !== false) {
|
||||
return [
|
||||
'level' => 'medium',
|
||||
'reason' => 'Model attribute or accessor',
|
||||
'action' => 'Review accessor implementation',
|
||||
];
|
||||
}
|
||||
|
||||
// LOW: Everything else
|
||||
return [
|
||||
'level' => 'low',
|
||||
'reason' => 'Other usage',
|
||||
'action' => 'Review as needed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get refactoring action suggestion
|
||||
*/
|
||||
function getRefactoringAction($filePath, $matchesByType)
|
||||
{
|
||||
$action = "";
|
||||
|
||||
if (strpos($filePath, 'Controller') !== false) {
|
||||
if (isset($matchesByType['where_kelas']) || isset($matchesByType['wherein_kelas'])) {
|
||||
$action .= "1. Replace `where('kelas')` with `whereHas('kelasSantri')`\n";
|
||||
$action .= "2. Update query to use kelas ID instead of name\n";
|
||||
$action .= "3. Test filter functionality thoroughly\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($filePath, '.blade.php') !== false) {
|
||||
if (isset($matchesByType['blade_kelas']) || isset($matchesByType['property_access'])) {
|
||||
$action .= "1. Replace `{{ \$santri->kelas }}` with `{{ \$santri->kelas_name }}`\n";
|
||||
$action .= "2. Test display in browser\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($filePath, 'Model') !== false) {
|
||||
$action .= "1. Review model methods and accessors\n";
|
||||
$action .= "2. Ensure backward compatibility\n";
|
||||
$action .= "3. Add tests for new relations\n";
|
||||
}
|
||||
|
||||
if (empty($action)) {
|
||||
$action = "Review usage and update as needed based on context.";
|
||||
}
|
||||
|
||||
return $action;
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
# PANDUAN MIGRASI SISTEM KELAS BARU
|
||||
|
||||
## Ringkasan
|
||||
|
||||
Migrasi dari kolom `kelas` hardcoded (PB, Lambatan, Cepatan) di tabel `santris` ke sistem relasional baru menggunakan tabel `santri_kelas`, `kelas`, dan `kelompok_kelas`.
|
||||
|
||||
## Prasyarat
|
||||
|
||||
Pastikan tabel berikut sudah ada dan terisi data:
|
||||
- `kelompok_kelas` — minimal 3 kelompok (PB, Lambatan, Cepatan)
|
||||
- `kelas` — minimal 1 kelas aktif per kelompok
|
||||
|
||||
Cek via tinker:
|
||||
```bash
|
||||
cd sim-pkpps
|
||||
php artisan tinker
|
||||
>>> App\Models\KelompokKelas::active()->count() # harus >= 3
|
||||
>>> App\Models\Kelas::active()->count() # harus >= 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Urutan Eksekusi (Step-by-Step)
|
||||
|
||||
### TAHAP 1: Migrasi Data (Kolom Lama → Tabel Baru)
|
||||
|
||||
```bash
|
||||
cd sim-pkpps
|
||||
|
||||
# 1. Preview dulu (dry-run) — TIDAK mengubah database
|
||||
php artisan migrate:santri-kelas-full --dry-run
|
||||
|
||||
# 2. Periksa output, pastikan mapping benar:
|
||||
# PB → KLS00x (...)
|
||||
# Lambatan → KLS00x (...)
|
||||
# Cepatan → KLS00x (...)
|
||||
|
||||
# 3. Execute migrasi data (real)
|
||||
php artisan migrate:santri-kelas-full
|
||||
|
||||
# 4. Validasi: Periksa tabel santri_kelas sudah terisi
|
||||
php artisan tinker
|
||||
>>> App\Models\SantriKelas::where('is_primary', true)->count()
|
||||
```
|
||||
|
||||
### TAHAP 2: Test Aplikasi
|
||||
|
||||
Setelah TAHAP 1, kode sudah diupdate untuk pakai relasi baru.
|
||||
|
||||
Buka browser dan test:
|
||||
- [ ] **Index**: Buka halaman Data Santri → Filter kelompok kelas berfungsi
|
||||
- [ ] **Create**: Tambah santri baru → Pilih kelompok → Pilih kelas → Simpan
|
||||
- [ ] **Edit**: Edit santri existing → Kelas otomatis terseleksi → Update
|
||||
- [ ] **Show**: Detail santri → Kelompok & Kelas tampil benar
|
||||
- [ ] **Delete**: Hapus santri → Tidak error
|
||||
- [ ] **Foto**: Upload foto masih berfungsi normal
|
||||
|
||||
### TAHAP 3: Drop Kolom Lama
|
||||
|
||||
**SETELAH semua test di TAHAP 2 pass:**
|
||||
|
||||
```bash
|
||||
# Backup database dulu!
|
||||
mysqldump -u root sim_pkpps > backup_before_drop_kelas.sql
|
||||
|
||||
# Jalankan migration drop kolom
|
||||
php artisan migrate
|
||||
|
||||
# Test lagi semua fitur
|
||||
```
|
||||
|
||||
Jika perlu rollback:
|
||||
```bash
|
||||
php artisan migrate:rollback --step=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File yang Diubah
|
||||
|
||||
| File | Perubahan |
|
||||
|------|-----------|
|
||||
| `app/Console/Commands/MigrateSantriToNewKelas.php` | **BARU** — Command migrasi data |
|
||||
| `app/Models/Santri.php` | Hapus `kelas` dari fillable, simplify accessor, tambah scope |
|
||||
| `app/Http/Controllers/Admin/SantriController.php` | Semua method: pakai relasi baru + eager loading |
|
||||
| `resources/views/admin/santri/form.blade.php` | Dropdown bertingkat Kelompok → Kelas (vanilla JS) |
|
||||
| `resources/views/admin/santri/index.blade.php` | Filter kelompok + kelas dari relasi di tabel |
|
||||
| `resources/views/admin/santri/show.blade.php` | Tampil kelompok + kelas dari relasi |
|
||||
| `database/migrations/2026_02_14_..._drop_kelas.php` | **BARU** — Drop kolom `kelas` dari `santris` |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Kelas wajib dipilih" saat create/edit
|
||||
- Pastikan tabel `kelompok_kelas` dan `kelas` sudah ada data
|
||||
- Pastikan `is_active = true` pada kelompok & kelas
|
||||
|
||||
### Dropdown kelas tidak muncul saat edit
|
||||
- Pastikan relasi `kelasPrimary` sudah ter-load: controller harus `$santri->load('kelasPrimary.kelas.kelompok')`
|
||||
|
||||
### Kolom kelas masih ada di database
|
||||
- Jalankan `php artisan migrate` untuk menjalankan migration drop kolom
|
||||
- Atau jalankan manual: `ALTER TABLE santris DROP COLUMN kelas;`
|
||||
|
||||
### Rollback penuh
|
||||
```bash
|
||||
# 1. Rollback drop kolom
|
||||
php artisan migrate:rollback --step=1
|
||||
|
||||
# 2. Restore kode lama dari git
|
||||
git checkout -- app/Models/Santri.php
|
||||
git checkout -- app/Http/Controllers/Admin/SantriController.php
|
||||
git checkout -- resources/views/admin/santri/
|
||||
|
||||
# 3. Bersihkan santri_kelas jika perlu
|
||||
php artisan tinker
|
||||
>>> App\Models\SantriKelas::truncate()
|
||||
```
|
||||
|
|
@ -33,34 +33,38 @@ public function inputAbsensi($kegiatan_id)
|
|||
|
||||
$tanggal = request('tanggal', now()->format('Y-m-d'));
|
||||
|
||||
// Build santri grouped by kegiatan kelas
|
||||
// Build santri grouped by kelas
|
||||
$santriGrouped = collect();
|
||||
|
||||
$allSantris = Santri::where('status', 'Aktif')
|
||||
->with(['kelasSantri.kelas', 'kelasPrimary.kelas'])
|
||||
->orderBy('nama_lengkap')
|
||||
->get();
|
||||
|
||||
if ($kegiatan->isForAllClasses()) {
|
||||
// Kegiatan umum: ambil SEMUA santri aktif, group by primary kelas
|
||||
$allSantris = Santri::where('status', 'Aktif')
|
||||
->with(['kelasSantri.kelas', 'kelasPrimary.kelas'])
|
||||
->orderBy('nama_lengkap')
|
||||
->get();
|
||||
|
||||
// Kegiatan umum: group by primary kelas
|
||||
$santriGrouped = $allSantris->groupBy(function($s) {
|
||||
$primary = $s->kelasPrimary;
|
||||
return $primary && $primary->kelas ? $primary->kelas->nama_kelas : 'Tanpa Kelas';
|
||||
})->sortKeys();
|
||||
} else {
|
||||
// Kegiatan khusus: group by kegiatan kelas
|
||||
// Kegiatan khusus: group by kelas yang di-assign ke kegiatan
|
||||
$placedIds = [];
|
||||
foreach ($kegiatan->kelasKegiatan as $kelas) {
|
||||
$santriInKelas = Santri::where('status', 'Aktif')
|
||||
->whereHas('kelasSantri', function($q) use ($kelas) {
|
||||
$q->where('id_kelas', $kelas->id);
|
||||
})
|
||||
->with(['kelasSantri.kelas', 'kelasPrimary.kelas'])
|
||||
->orderBy('nama_lengkap')
|
||||
->get();
|
||||
|
||||
if ($santriInKelas->count() > 0) {
|
||||
$santriGrouped[$kelas->nama_kelas] = $santriInKelas;
|
||||
$santriForKelas = $allSantris->filter(function($s) use ($kelas, &$placedIds) {
|
||||
if (in_array($s->id_santri, $placedIds)) return false;
|
||||
return $s->kelasSantri->contains('id_kelas', $kelas->id);
|
||||
});
|
||||
foreach ($santriForKelas as $s) {
|
||||
$placedIds[] = $s->id_santri;
|
||||
}
|
||||
if ($santriForKelas->count() > 0) {
|
||||
$santriGrouped[$kelas->nama_kelas] = $santriForKelas;
|
||||
}
|
||||
}
|
||||
// Santri yang tidak termasuk kelas kegiatan manapun
|
||||
$santriLainnya = $allSantris->whereNotIn('id_santri', $placedIds);
|
||||
if ($santriLainnya->count() > 0) {
|
||||
$santriGrouped['Kelas Lain'] = $santriLainnya;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,29 +226,13 @@ public function rekapAbsensi(Request $request, $kegiatan_id)
|
|||
->orderBy('waktu_absen', 'desc')
|
||||
->get();
|
||||
|
||||
// Build kelas list for filter dropdown
|
||||
if ($kegiatan->isForAllClasses()) {
|
||||
$kelasFilterList = Kelas::active()->ordered()->get();
|
||||
} else {
|
||||
$kelasFilterList = $kegiatan->kelasKegiatan;
|
||||
}
|
||||
// Build kelas list for filter dropdown — selalu tampilkan semua kelas aktif
|
||||
$kelasFilterList = Kelas::active()->ordered()->get();
|
||||
|
||||
// Grup per kelas berdasarkan kegiatan kelas
|
||||
if ($kegiatan->isForAllClasses()) {
|
||||
$absensiPerKelas = $absensis->groupBy(function ($item) {
|
||||
return $item->santri->kelas_name ?? 'Belum Ada Kelas';
|
||||
})->sortKeys();
|
||||
} else {
|
||||
$absensiPerKelas = collect();
|
||||
foreach ($kegiatan->kelasKegiatan as $kelas) {
|
||||
$kelasAbsensis = $absensis->filter(function ($item) use ($kelas) {
|
||||
return $item->santri->kelasSantri->contains('id_kelas', $kelas->id);
|
||||
});
|
||||
if ($kelasAbsensis->count() > 0) {
|
||||
$absensiPerKelas[$kelas->nama_kelas] = $kelasAbsensis;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Grup per kelas — selalu group by kelas_name santri
|
||||
$absensiPerKelas = $absensis->groupBy(function ($item) {
|
||||
return $item->santri->kelas_name ?? 'Belum Ada Kelas';
|
||||
})->sortKeys();
|
||||
|
||||
// Statistik
|
||||
$statsQuery = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id);
|
||||
|
|
@ -265,7 +253,73 @@ public function rekapAbsensi(Request $request, $kegiatan_id)
|
|||
->pluck('total', 'status')
|
||||
->toArray();
|
||||
|
||||
return view('admin.kegiatan.absensi.rekap', compact('kegiatan', 'absensis', 'absensiPerKelas', 'stats', 'kelasFilterList'));
|
||||
// ── Hitung total SEMUA santri aktif ──
|
||||
$allSantriQuery = Santri::where('status', 'Aktif');
|
||||
if ($request->filled('kelas_id')) {
|
||||
$allSantriQuery->whereHas('kelasSantri', function($q) use ($request) {
|
||||
$q->where('id_kelas', $request->kelas_id);
|
||||
});
|
||||
}
|
||||
$totalSantriEligible = $allSantriQuery->count();
|
||||
|
||||
// Hitung santri unik yang sudah tercatat absensi (sesuai filter)
|
||||
$recordedQuery = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id);
|
||||
if ($request->filled('tanggal')) {
|
||||
$recordedQuery->whereDate('tanggal', $request->tanggal);
|
||||
}
|
||||
if ($request->filled('bulan')) {
|
||||
$recordedQuery->whereMonth('tanggal', date('m', strtotime($request->bulan)))
|
||||
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
|
||||
}
|
||||
if ($request->filled('kelas_id')) {
|
||||
$recordedQuery->whereHas('santri.kelasSantri', function($q) use ($request) {
|
||||
$q->where('id_kelas', $request->kelas_id);
|
||||
});
|
||||
}
|
||||
$santriSudahAbsen = $recordedQuery->distinct('id_santri')->count('id_santri');
|
||||
$belumAbsen = max(0, $totalSantriEligible - $santriSudahAbsen);
|
||||
|
||||
// Persentase kehadiran berdasarkan total semua santri aktif
|
||||
$totalRecorded = array_sum($stats);
|
||||
$hadirCount = ($stats['Hadir'] ?? 0) + ($stats['Terlambat'] ?? 0);
|
||||
$persenHadir = $totalSantriEligible > 0 ? round($hadirCount / $totalSantriEligible * 100, 1) : 0;
|
||||
|
||||
// Daftar santri yang belum absen (selalu ditampilkan)
|
||||
$santriBelumAbsen = collect();
|
||||
|
||||
// Bangun query ID santri yang sudah absen (sesuai filter aktif)
|
||||
$sudahAbsenQuery = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id);
|
||||
if ($request->filled('tanggal')) {
|
||||
$sudahAbsenQuery->whereDate('tanggal', $request->tanggal);
|
||||
}
|
||||
if ($request->filled('bulan')) {
|
||||
$sudahAbsenQuery->whereMonth('tanggal', date('m', strtotime($request->bulan)))
|
||||
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
|
||||
}
|
||||
if ($request->filled('kelas_id')) {
|
||||
$sudahAbsenQuery->whereHas('santri.kelasSantri', function($q) use ($request) {
|
||||
$q->where('id_kelas', $request->kelas_id);
|
||||
});
|
||||
}
|
||||
$idSantriSudahAbsen = $sudahAbsenQuery->pluck('id_santri')->unique()->toArray();
|
||||
|
||||
$belumQuery = Santri::where('status', 'Aktif');
|
||||
if ($request->filled('kelas_id')) {
|
||||
$belumQuery->whereHas('kelasSantri', function($q) use ($request) {
|
||||
$q->where('id_kelas', $request->kelas_id);
|
||||
});
|
||||
}
|
||||
$santriBelumAbsen = $belumQuery
|
||||
->whereNotIn('id_santri', $idSantriSudahAbsen)
|
||||
->with(['kelasPrimary.kelas'])
|
||||
->orderBy('nama_lengkap')
|
||||
->get();
|
||||
|
||||
return view('admin.kegiatan.absensi.rekap', compact(
|
||||
'kegiatan', 'absensis', 'absensiPerKelas', 'stats', 'kelasFilterList',
|
||||
'totalSantriEligible', 'santriSudahAbsen', 'belumAbsen', 'persenHadir',
|
||||
'totalRecorded', 'hadirCount', 'santriBelumAbsen'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,446 @@
|
|||
<?php
|
||||
// app/Http/Controllers/Admin/ImportMesinController.php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AbsensiKegiatan;
|
||||
use App\Models\ImportMesinLog;
|
||||
use App\Models\Kegiatan;
|
||||
use App\Models\Kepulangan;
|
||||
use App\Models\MesinSantriMapping;
|
||||
use App\Services\EpposGLogParser;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ImportMesinController extends Controller
|
||||
{
|
||||
public function __construct(private EpposGLogParser $parser) {}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// INDEX
|
||||
// ──────────────────────────────────────────────────────────
|
||||
public function index()
|
||||
{
|
||||
// Hitung yang benar-benar belum punya santri (id_santri null atau kosong)
|
||||
$belumMapping = MesinSantriMapping::where('is_active', true)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('id_santri')->orWhere('id_santri', '');
|
||||
})->count();
|
||||
|
||||
$riwayat = ImportMesinLog::with('user')->latest()->take(10)->get();
|
||||
|
||||
return view('admin.mesin.import.index', compact('belumMapping', 'riwayat'));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// PREVIEW — hanya POST
|
||||
// Setelah proses selesai, redirect ke showPreview (GET)
|
||||
// Ini mencegah error "MethodNotAllowed" saat user refresh halaman preview
|
||||
// ──────────────────────────────────────────────────────────
|
||||
public function preview(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file_glog' => 'required|file|max:20480',
|
||||
'tol_sebelum' => 'nullable|integer|min:0|max:60',
|
||||
'tol_sesudah' => 'nullable|integer|min:0|max:60',
|
||||
'isi_alpa' => 'nullable',
|
||||
'conflict_strategy' => 'nullable|in:mesin,exist,manual',
|
||||
]);
|
||||
|
||||
$tolSebelum = (int)($request->tol_sebelum ?? 15);
|
||||
$tolSesudah = (int)($request->tol_sesudah ?? 10);
|
||||
$isiAlpa = $request->has('isi_alpa');
|
||||
$conflictStrategy = $request->input('conflict_strategy', 'mesin');
|
||||
|
||||
// ── Parse GLog ────────────────────────────────────────
|
||||
try {
|
||||
$glogRecords = $this->parser->parseGLog(
|
||||
$request->file('file_glog')->getPathname()
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return back()->with('error', 'Gagal membaca file GLog: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
if (empty($glogRecords)) {
|
||||
return back()->with('error',
|
||||
'File GLog tidak mengandung data scan yang valid. ' .
|
||||
'Pastikan file yang diupload benar (format GLog dari Eppos).'
|
||||
);
|
||||
}
|
||||
|
||||
// ── Ambil infoData dari mapping yang sudah ada ────────
|
||||
// Kita bangun infoData dari tabel mesin_santri_mappings
|
||||
// sehingga tidak perlu upload INFO.XLS lagi
|
||||
$mappingAll = MesinSantriMapping::where('is_active', true)->get();
|
||||
|
||||
// Bangun struktur infoData['jadwal'] dari mapping yang ada
|
||||
// shifts dikosongkan karena matching pakai jam langsung
|
||||
$infoData = [
|
||||
'shifts' => [],
|
||||
'jadwal' => [],
|
||||
];
|
||||
foreach ($mappingAll as $m) {
|
||||
$infoData['jadwal'][$m->id_mesin] = [
|
||||
'nama' => $m->nama_mesin ?? '',
|
||||
'dept' => $m->dept_mesin ?? '',
|
||||
'shift' => 1, // default, tidak dipakai untuk matching jam
|
||||
];
|
||||
}
|
||||
|
||||
// ── Kegiatan dari DB ──────────────────────────────────
|
||||
// Ambil semua kegiatan — waktu_selesai boleh null, pakai waktu_mulai sebagai fallback
|
||||
// getRawOriginal() bypass Eloquent cast (datetime:H:i → Carbon)
|
||||
// sehingga kita dapat string murni "04:00:00" dari DB, lalu substr → "04:00"
|
||||
$kegiatans = Kegiatan::orderBy('hari')->orderBy('waktu_mulai')
|
||||
->get()
|
||||
->map(function ($k) {
|
||||
$rawMulai = $k->getRawOriginal('waktu_mulai');
|
||||
$rawSelesai = $k->getRawOriginal('waktu_selesai');
|
||||
$mulai = $rawMulai ? substr($rawMulai, 0, 5) : '00:00';
|
||||
$selesai = $rawSelesai ? substr($rawSelesai, 0, 5) : $mulai;
|
||||
return [
|
||||
'kegiatan_id' => $k->kegiatan_id,
|
||||
'nama' => $k->nama_kegiatan,
|
||||
'hari' => $k->hari,
|
||||
'waktu_mulai' => $mulai,
|
||||
'waktu_selesai' => $selesai,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
if (empty($kegiatans)) {
|
||||
return back()->with('error',
|
||||
'Tidak ada kegiatan tersimpan di database. ' .
|
||||
'Tambahkan kegiatan terlebih dahulu di menu Kegiatan.'
|
||||
);
|
||||
}
|
||||
|
||||
// ── Match ─────────────────────────────────────────────
|
||||
$glogGrouped = $this->parser->groupGLogByDay($glogRecords);
|
||||
$rawHasil = $this->parser->matchToKegiatan(
|
||||
$glogGrouped, $infoData, $kegiatans, $tolSebelum, $tolSesudah
|
||||
);
|
||||
|
||||
// ── Enrich (santri web + kepulangan + konflik) ────────
|
||||
$kepulanganCache = [];
|
||||
$hasilEnriched = [];
|
||||
|
||||
foreach ($rawHasil as $dayData) {
|
||||
$tanggal = $dayData['tanggal'];
|
||||
$idMesin = $dayData['id_mesin'];
|
||||
|
||||
$mapping = MesinSantriMapping::where('id_mesin', $idMesin)
|
||||
->where('is_active', true)
|
||||
->with('santri')
|
||||
->first();
|
||||
|
||||
$idSantri = $mapping?->santri?->id_santri;
|
||||
$namaWeb = $mapping?->santri?->nama_lengkap;
|
||||
$kelas = $mapping?->santri?->kelasPrimary?->kelas?->nama_kelas ?? '-';
|
||||
|
||||
// Cache kepulangan per tanggal agar tidak query berulang
|
||||
if (!isset($kepulanganCache[$tanggal])) {
|
||||
$kepulanganCache[$tanggal] = Kepulangan::where('status', 'Disetujui')
|
||||
->where('tanggal_pulang', '<=', $tanggal)
|
||||
->where('tanggal_kembali', '>=', $tanggal)
|
||||
->pluck('id_santri')->toArray();
|
||||
}
|
||||
$isPulang = $idSantri && in_array($idSantri, $kepulanganCache[$tanggal]);
|
||||
|
||||
$rows = array_map(
|
||||
function ($row) use ($idSantri, $tanggal, $isPulang, $isiAlpa) {
|
||||
// Override jika santri sedang kepulangan
|
||||
$statusFinal = $isPulang ? 'Pulang' : $row['status'];
|
||||
|
||||
// Jangan isi Alpa jika opsi tidak aktif
|
||||
if (!$isiAlpa && $statusFinal === 'Alpa' && !$row['matched']) {
|
||||
$statusFinal = null;
|
||||
}
|
||||
|
||||
$existing = null;
|
||||
$isConflict = false;
|
||||
|
||||
if ($idSantri) {
|
||||
$rec = AbsensiKegiatan::where('kegiatan_id', $row['kegiatan_id'])
|
||||
->where('id_santri', $idSantri)
|
||||
->whereDate('tanggal', $tanggal)
|
||||
->first();
|
||||
|
||||
if ($rec) {
|
||||
// getRawOriginal bypass Eloquent datetime cast
|
||||
$rawWaktu = $rec->getRawOriginal('waktu_absen');
|
||||
$existing = [
|
||||
'status' => $rec->status,
|
||||
'waktu' => $rawWaktu
|
||||
? substr($rawWaktu, 0, 5) : null,
|
||||
'metode' => $rec->metode_absen ?? 'Manual',
|
||||
];
|
||||
|
||||
// PENTING: Jika mesin TIDAK punya scan untuk kegiatan
|
||||
// ini (matched=false, status=Alpa), jangan override
|
||||
// data manual yang sudah ada. "Tidak ada scan" ≠ "Alpa".
|
||||
// Pertahankan data lama secara otomatis.
|
||||
if (!$row['matched'] && $statusFinal === 'Alpa') {
|
||||
// Tidak override — pakai data existing
|
||||
$statusFinal = $rec->status;
|
||||
$isConflict = false;
|
||||
} else {
|
||||
// Konflik hanya jika mesin MEMANG punya scan
|
||||
// (matched=true) tapi statusnya beda dari manual
|
||||
$isConflict = ($rec->metode_absen !== 'Import_Mesin')
|
||||
&& ($rec->status !== $statusFinal)
|
||||
&& $statusFinal !== null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($row, [
|
||||
'status_final' => $statusFinal,
|
||||
'existing' => $existing,
|
||||
'is_conflict' => $isConflict,
|
||||
]);
|
||||
},
|
||||
$dayData['rows']
|
||||
);
|
||||
|
||||
$hasilEnriched[] = array_merge($dayData, [
|
||||
'id_santri' => $idSantri,
|
||||
'nama_web' => $namaWeb,
|
||||
'kelas' => $kelas,
|
||||
'match_status' => $mapping
|
||||
? ($idSantri ? 'OK' : 'NO_SANTRI')
|
||||
: 'NOT_MAPPED',
|
||||
'is_pulang' => $isPulang,
|
||||
'rows' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
// Urutkan: tanggal → nama
|
||||
usort($hasilEnriched, fn($a, $b) =>
|
||||
[$a['tanggal'], $a['nama_web'] ?? $a['nama_mesin']]
|
||||
<=> [$b['tanggal'], $b['nama_web'] ?? $b['nama_mesin']]
|
||||
);
|
||||
|
||||
// ── Simpan ke session lalu REDIRECT ke showPreview ────
|
||||
// Ini adalah PRG Pattern (Post-Redirect-Get):
|
||||
// POST /import/preview → proses → session → redirect
|
||||
// GET /import/preview → ambil dari session → tampilkan view
|
||||
// Sehingga refresh halaman tidak error MethodNotAllowed
|
||||
session([
|
||||
'eppos_hasil' => $hasilEnriched,
|
||||
'tol_sebelum' => $tolSebelum,
|
||||
'tol_sesudah' => $tolSesudah,
|
||||
'isi_alpa' => $isiAlpa,
|
||||
'conflict_strategy' => $conflictStrategy,
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.mesin.import.show-preview');
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// SHOW PREVIEW — GET (aman di-refresh)
|
||||
// ──────────────────────────────────────────────────────────
|
||||
public function showPreview()
|
||||
{
|
||||
$hasilEnriched = session('eppos_hasil');
|
||||
|
||||
// Jika session kosong (user buka langsung tanpa upload)
|
||||
if (empty($hasilEnriched)) {
|
||||
return redirect()->route('admin.mesin.import.index')
|
||||
->with('error', 'Tidak ada data preview. Silakan upload file GLog terlebih dahulu.');
|
||||
}
|
||||
|
||||
$tolSebelum = session('tol_sebelum', 15);
|
||||
$tolSesudah = session('tol_sesudah', 10);
|
||||
$isiAlpa = session('isi_alpa', true);
|
||||
$conflictStrategy = session('conflict_strategy', 'mesin');
|
||||
|
||||
$tanggalList = array_unique(array_column($hasilEnriched, 'tanggal'));
|
||||
sort($tanggalList);
|
||||
|
||||
// Debug: kumpulkan info scan yang tidak cocok untuk ditampilkan
|
||||
$debugScans = [];
|
||||
foreach ($hasilEnriched as $h) {
|
||||
if (!empty($h['unmatched_scans'])) {
|
||||
$debugScans[] = [
|
||||
'nama' => $h['nama_web'] ?? $h['nama_mesin'],
|
||||
'tanggal' => $h['tanggal'],
|
||||
'id_mesin' => $h['id_mesin'],
|
||||
'scans' => $h['all_scans'],
|
||||
'unmatched'=> $h['unmatched_scans'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$allRows = collect($hasilEnriched)->flatMap(fn($h) => $h['rows']);
|
||||
|
||||
$stats = [
|
||||
'total_santri' => count($hasilEnriched),
|
||||
'ok' => collect($hasilEnriched)->where('match_status', 'OK')->count(),
|
||||
'not_mapped' => collect($hasilEnriched)->where('match_status', 'NOT_MAPPED')->count(),
|
||||
'hadir' => $allRows->where('status_final', 'Hadir')->count(),
|
||||
'terlambat' => $allRows->where('status_final', 'Terlambat')->count(),
|
||||
'alpa' => $allRows->where('status_final', 'Alpa')->count(),
|
||||
'konflik' => $allRows->where('is_conflict', true)->count(),
|
||||
];
|
||||
|
||||
return view('admin.mesin.import.preview', compact(
|
||||
'hasilEnriched', 'tanggalList', 'stats',
|
||||
'tolSebelum', 'tolSesudah', 'isiAlpa',
|
||||
'debugScans', 'conflictStrategy'
|
||||
));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// STORE — simpan ke database
|
||||
// ──────────────────────────────────────────────────────────
|
||||
public function store(Request $request)
|
||||
{
|
||||
$hasilEnriched = session('eppos_hasil', []);
|
||||
|
||||
if (empty($hasilEnriched)) {
|
||||
return redirect()->route('admin.mesin.import.index')
|
||||
->with('error', 'Sesi expired. Silakan upload ulang file GLog.');
|
||||
}
|
||||
|
||||
$bulkStrategy = $request->input('conflict_strategy', 'manual');
|
||||
$choices = $request->input('conflict_choices', []);
|
||||
$counters = [
|
||||
'created' => 0,
|
||||
'updated' => 0,
|
||||
'kept' => 0,
|
||||
'skipped' => 0,
|
||||
'no_santri' => 0,
|
||||
'null_skip' => 0,
|
||||
];
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($hasilEnriched as $dayData) {
|
||||
if (!$dayData['id_santri']) {
|
||||
$counters['no_santri']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($dayData['rows'] as $row) {
|
||||
// Status null = tidak perlu disimpan
|
||||
if ($row['status_final'] === null) {
|
||||
$counters['null_skip']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Alpa tanpa scan (matched=false) + sudah ada data existing
|
||||
// → pertahankan data lama, jangan simpan Alpa
|
||||
if (!$row['matched'] && $row['status_final'] === 'Alpa' && !empty($row['existing'])) {
|
||||
$counters['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Jika mesin tidak punya scan dan statusFinal = status existing
|
||||
// (artinya sudah diset ke status existing di preview), skip
|
||||
if (!$row['matched'] && !empty($row['existing'])
|
||||
&& $row['status_final'] === $row['existing']['status']) {
|
||||
$counters['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = "{$row['kegiatan_id']}_{$dayData['id_santri']}_{$dayData['tanggal']}";
|
||||
$hasExisting = !empty($row['existing']);
|
||||
$isConflict = $row['is_conflict'] ?? false;
|
||||
|
||||
if (!$hasExisting) {
|
||||
// Belum ada data → langsung buat
|
||||
AbsensiKegiatan::create([
|
||||
'kegiatan_id' => $row['kegiatan_id'],
|
||||
'id_santri' => $dayData['id_santri'],
|
||||
'tanggal' => $dayData['tanggal'],
|
||||
'status' => $row['status_final'],
|
||||
'metode_absen' => 'Import_Mesin',
|
||||
'waktu_absen' => $row['jam_scan']
|
||||
? Carbon::parse(
|
||||
$dayData['tanggal'] . ' ' . $row['jam_scan']
|
||||
)->format('H:i:s')
|
||||
: Carbon::parse($dayData['tanggal'])->format('H:i:s'),
|
||||
]);
|
||||
$counters['created']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ada data existing tapi tidak konflik (status sama)
|
||||
// → skip, tidak perlu diubah
|
||||
if (!$isConflict) {
|
||||
$counters['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ada konflik → lihat strategi bulk dulu, baru per-cell
|
||||
$choice = ($bulkStrategy !== 'manual')
|
||||
? $bulkStrategy
|
||||
: ($choices[$key] ?? null);
|
||||
|
||||
if ($choice === 'mesin') {
|
||||
// Admin pilih: pakai data mesin
|
||||
AbsensiKegiatan::where('kegiatan_id', $row['kegiatan_id'])
|
||||
->where('id_santri', $dayData['id_santri'])
|
||||
->whereDate('tanggal', $dayData['tanggal'])
|
||||
->update([
|
||||
'status' => $row['status_final'],
|
||||
'metode_absen' => 'Import_Mesin',
|
||||
'waktu_absen' => $row['jam_scan']
|
||||
? Carbon::parse(
|
||||
$dayData['tanggal'] . ' ' . $row['jam_scan']
|
||||
)->format('H:i:s')
|
||||
: null,
|
||||
'konflik_catatan' => 'Ditimpa import mesin '
|
||||
. now()->format('d/m/Y H:i')
|
||||
. ' (sebelumnya: '
|
||||
. $row['existing']['status']
|
||||
. ' via '
|
||||
. ($row['existing']['metode'] ?? 'Manual')
|
||||
. ')',
|
||||
]);
|
||||
$counters['updated']++;
|
||||
} else {
|
||||
// Admin pilih: pertahankan data lama
|
||||
// Tidak melakukan apa-apa
|
||||
$counters['kept']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Catat ke log
|
||||
ImportMesinLog::create([
|
||||
'user_id' => auth()->id(),
|
||||
'jumlah_scan' => collect($hasilEnriched)
|
||||
->flatMap(fn($h) => $h['all_scans'])->count(),
|
||||
'berhasil' => $counters['created'],
|
||||
'konflik_selesai' => $counters['updated'],
|
||||
'dilewati' => $counters['skipped'],
|
||||
'no_santri' => $counters['no_santri'],
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return back()->with('error', 'Import gagal: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Hapus session setelah berhasil
|
||||
session()->forget(['eppos_hasil', 'tol_sebelum', 'tol_sesudah', 'isi_alpa', 'conflict_strategy']);
|
||||
|
||||
$msg = "Import selesai! "
|
||||
. "{$counters['created']} data baru tersimpan, "
|
||||
. "{$counters['updated']} konflik (pilih mesin), "
|
||||
. "{$counters['kept']} konflik (pertahankan data lama), "
|
||||
. "{$counters['skipped']} duplikat dilewati.";
|
||||
|
||||
if ($counters['no_santri'] > 0) {
|
||||
$msg .= " | {$counters['no_santri']} santri belum ada mapping (tidak tersimpan).";
|
||||
}
|
||||
|
||||
return redirect()->route('admin.riwayat-kegiatan.index')
|
||||
->with('success', $msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +79,10 @@ public function cetakKartu($id_santri)
|
|||
|
||||
// ── Siapkan data untuk view ──────────────────────────────────────
|
||||
$namaSantri = strtoupper($santri->nama_lengkap ?? 'NAMA SANTRI');
|
||||
// Potong nama max 28 karakter agar muat di kartu
|
||||
if (mb_strlen($namaSantri) > 28) {
|
||||
$namaSantri = mb_substr($namaSantri, 0, 27) . '…';
|
||||
}
|
||||
$initial = strtoupper(substr($santri->nama_lengkap ?? 'S', 0, 1));
|
||||
$nis = !empty($santri->nis) ? $santri->nis : '-';
|
||||
$uid = !empty($santri->rfid_uid) ? $santri->rfid_uid : '-';
|
||||
|
|
@ -108,7 +112,7 @@ public function cetakKartu($id_santri)
|
|||
}
|
||||
}
|
||||
|
||||
// Foto santri — embed base64 (tidak butuh GD)
|
||||
// Foto santri — resize ke ukuran kartu lalu embed base64
|
||||
$fotoBase64 = '';
|
||||
$fotoMime = 'image/jpeg';
|
||||
if (!empty($santri->foto)) {
|
||||
|
|
@ -118,9 +122,40 @@ public function cetakKartu($id_santri)
|
|||
public_path($santri->foto),
|
||||
] as $fp) {
|
||||
if (file_exists($fp)) {
|
||||
$ext = strtolower(pathinfo($fp, PATHINFO_EXTENSION));
|
||||
$fotoMime = in_array($ext, ['png', 'gif', 'webp']) ? 'image/' . $ext : 'image/jpeg';
|
||||
$fotoBase64 = base64_encode(file_get_contents($fp));
|
||||
$ext = strtolower(pathinfo($fp, PATHINFO_EXTENSION));
|
||||
$fotoMime = in_array($ext, ['png', 'gif', 'webp']) ? 'image/' . $ext : 'image/jpeg';
|
||||
|
||||
// Resize agar base64 tidak terlalu besar (max 400×400)
|
||||
if (extension_loaded('gd')) {
|
||||
$imgData = file_get_contents($fp);
|
||||
$src = @imagecreatefromstring($imgData);
|
||||
if ($src) {
|
||||
$origW = imagesx($src);
|
||||
$origH = imagesy($src);
|
||||
$max = 400;
|
||||
if ($origW > $max || $origH > $max) {
|
||||
$ratio = min($max / $origW, $max / $origH);
|
||||
$newW = (int) round($origW * $ratio);
|
||||
$newH = (int) round($origH * $ratio);
|
||||
$dst = imagecreatetruecolor($newW, $newH);
|
||||
imagecopyresampled($dst, $src, 0, 0, 0, 0, $newW, $newH, $origW, $origH);
|
||||
imagedestroy($src);
|
||||
ob_start();
|
||||
imagejpeg($dst, null, 80);
|
||||
$resized = ob_get_clean();
|
||||
imagedestroy($dst);
|
||||
$fotoBase64 = base64_encode($resized);
|
||||
$fotoMime = 'image/jpeg';
|
||||
} else {
|
||||
imagedestroy($src);
|
||||
$fotoBase64 = base64_encode($imgData);
|
||||
}
|
||||
} else {
|
||||
$fotoBase64 = base64_encode($imgData);
|
||||
}
|
||||
} else {
|
||||
$fotoBase64 = base64_encode(file_get_contents($fp));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -154,11 +189,18 @@ public function cetakKartu($id_santri)
|
|||
'enableImports' => true,
|
||||
]);
|
||||
|
||||
// Naikkan limit regex agar mPDF tidak error pada HTML besar
|
||||
$prevLimit = ini_get('pcre.backtrack_limit');
|
||||
ini_set('pcre.backtrack_limit', '5000000');
|
||||
|
||||
// Matikan page break otomatis
|
||||
$mpdf->SetAutoPageBreak(false);
|
||||
$mpdf->SetDisplayMode('fullpage');
|
||||
$mpdf->WriteHTML($html);
|
||||
|
||||
// Kembalikan limit semula
|
||||
ini_set('pcre.backtrack_limit', $prevLimit);
|
||||
|
||||
return response($mpdf->Output('Kartu_RFID_' . $santri->id_santri . '.pdf', 'S'))
|
||||
->header('Content-Type', 'application/pdf')
|
||||
->header('Content-Disposition', 'inline; filename="Kartu_RFID_' . $santri->id_santri . '.pdf"');
|
||||
|
|
|
|||
|
|
@ -237,31 +237,67 @@ public function getDetailModal($kegiatan_id, Request $request)
|
|||
->whereDate('tanggal', $tanggal)
|
||||
->orderBy('waktu_absen', 'desc')->get();
|
||||
|
||||
if ($kegiatan->isForAllClasses()) {
|
||||
$isUmum = $kegiatan->isForAllClasses();
|
||||
|
||||
// Grup absensi per kelas kegiatan (khusus) atau kelas_name (umum)
|
||||
if ($isUmum) {
|
||||
$absensiPerKelas = $absensis->groupBy(fn($item) => $item->santri->kelas_name ?? 'Belum Ada Kelas')->sortKeys();
|
||||
} else {
|
||||
$absensiPerKelas = collect();
|
||||
foreach ($kegiatan->kelasKegiatan as $kelas) {
|
||||
$kelasAbsensis = $absensis->filter(fn($item) => $item->santri->kelasSantri->contains('id_kelas', $kelas->id));
|
||||
if ($kelasAbsensis->count() > 0) $absensiPerKelas[$kelas->nama_kelas] = $kelasAbsensis;
|
||||
$filtered = $absensis->filter(fn($item) => $item->santri->kelasSantri->contains('id_kelas', $kelas->id));
|
||||
if ($filtered->count() > 0) $absensiPerKelas[$kelas->nama_kelas] = $filtered;
|
||||
}
|
||||
// Sisanya yang tidak cocok kelas manapun
|
||||
$placedIds = $absensiPerKelas->flatten()->pluck('id')->toArray();
|
||||
$lainnya = $absensis->filter(fn($item) => !$absensiPerKelas->flatten()->contains('id', $item->id));
|
||||
if ($lainnya->count() > 0) $absensiPerKelas['Kelas Lain'] = $lainnya;
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'hadir' => $absensis->where('status', 'Hadir')->count(),
|
||||
'izin' => $absensis->where('status', 'Izin')->count(),
|
||||
'sakit' => $absensis->where('status', 'Sakit')->count(),
|
||||
'alpa' => $absensis->where('status', 'Alpa')->count(),
|
||||
'hadir' => $absensis->where('status', 'Hadir')->count(),
|
||||
'terlambat' => $absensis->where('status', 'Terlambat')->count(),
|
||||
'izin' => $absensis->where('status', 'Izin')->count(),
|
||||
'sakit' => $absensis->where('status', 'Sakit')->count(),
|
||||
'alpa' => $absensis->where('status', 'Alpa')->count(),
|
||||
];
|
||||
$totalSantri = $kegiatan->isForAllClasses()
|
||||
? Santri::where('status', 'Aktif')->count()
|
||||
: $kegiatan->getEligibleSantris()->count();
|
||||
$totalSantri = Santri::where('status', 'Aktif')->count();
|
||||
|
||||
$stats['belum_absen'] = $totalSantri - $absensis->count();
|
||||
$stats['belum_absen'] = max(0, $totalSantri - $absensis->count());
|
||||
$stats['sudah_absen'] = $absensis->count();
|
||||
$stats['total'] = $totalSantri;
|
||||
$stats['persen_hadir'] = $totalSantri > 0 ? round(($stats['hadir'] / $totalSantri) * 100, 1) : 0;
|
||||
|
||||
return view('admin.kegiatan.data.partials.detail-modal', compact('kegiatan', 'absensis', 'absensiPerKelas', 'stats', 'tanggal'));
|
||||
// Daftar santri belum absen, di-group per kelas kegiatan (khusus) atau kelasPrimary (umum)
|
||||
$idSantriSudahAbsen = $absensis->pluck('id_santri')->toArray();
|
||||
$allBelumAbsen = Santri::where('status', 'Aktif')
|
||||
->whereNotIn('id_santri', $idSantriSudahAbsen)
|
||||
->with(['kelasSantri.kelas', 'kelasPrimary.kelas'])
|
||||
->orderBy('nama_lengkap')
|
||||
->get();
|
||||
|
||||
if ($isUmum) {
|
||||
$santriBelumAbsenPerKelas = $allBelumAbsen->groupBy(function($s) {
|
||||
return optional(optional($s->kelasPrimary)->kelas)->nama_kelas ?? 'Tanpa Kelas';
|
||||
})->sortKeys();
|
||||
} else {
|
||||
$santriBelumAbsenPerKelas = collect();
|
||||
$placedBelumIds = [];
|
||||
foreach ($kegiatan->kelasKegiatan as $kelas) {
|
||||
$inKelas = $allBelumAbsen->filter(function($s) use ($kelas, &$placedBelumIds) {
|
||||
if (in_array($s->id_santri, $placedBelumIds)) return false;
|
||||
return $s->kelasSantri->contains('id_kelas', $kelas->id);
|
||||
});
|
||||
foreach ($inKelas as $s) $placedBelumIds[] = $s->id_santri;
|
||||
if ($inKelas->count() > 0) $santriBelumAbsenPerKelas[$kelas->nama_kelas] = $inKelas;
|
||||
}
|
||||
$lainnyaBelum = $allBelumAbsen->whereNotIn('id_santri', $placedBelumIds);
|
||||
if ($lainnyaBelum->count() > 0) $santriBelumAbsenPerKelas['Kelas Lain'] = $lainnyaBelum;
|
||||
}
|
||||
|
||||
$santriBelumAbsen = $allBelumAbsen; // kept for count reference
|
||||
|
||||
return view('admin.kegiatan.data.partials.detail-modal', compact('kegiatan', 'absensis', 'absensiPerKelas', 'stats', 'tanggal', 'santriBelumAbsen', 'santriBelumAbsenPerKelas'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
// app/Http/Controllers/Admin/MesinMappingController.php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\MesinSantriMapping;
|
||||
use App\Models\Santri;
|
||||
use App\Services\EpposGLogParser;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MesinMappingController extends Controller
|
||||
{
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// INDEX
|
||||
// ──────────────────────────────────────────────────────────
|
||||
public function index()
|
||||
{
|
||||
$mappings = MesinSantriMapping::with('santri')
|
||||
->orderByRaw('CAST(id_mesin AS UNSIGNED)')
|
||||
->get();
|
||||
|
||||
$santris = Santri::where('status', 'Aktif')
|
||||
->orderBy('nama_lengkap')
|
||||
->get(['id_santri', 'nama_lengkap']);
|
||||
|
||||
return view('admin.mesin.mapping-santri.index', compact('mappings', 'santris'));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// STORE (tambah manual)
|
||||
// ──────────────────────────────────────────────────────────
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id_mesin' => 'required|string|unique:mesin_santri_mappings,id_mesin',
|
||||
'id_santri' => 'nullable|exists:santris,id_santri',
|
||||
'nama_mesin' => 'nullable|string|max:100',
|
||||
'catatan' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
MesinSantriMapping::create($request->only(
|
||||
'id_mesin', 'id_santri', 'nama_mesin', 'catatan'
|
||||
));
|
||||
|
||||
return back()->with('success', "Mapping ID Mesin {$request->id_mesin} berhasil ditambahkan.");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// UPDATE (ganti santri lewat dropdown)
|
||||
// ──────────────────────────────────────────────────────────
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$mapping = MesinSantriMapping::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'id_santri' => 'nullable|exists:santris,id_santri',
|
||||
]);
|
||||
|
||||
$mapping->update(['id_santri' => $request->id_santri ?: null]);
|
||||
|
||||
return back()->with('success', 'Mapping berhasil diperbarui.');
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// DESTROY
|
||||
// ──────────────────────────────────────────────────────────
|
||||
public function destroy($id)
|
||||
{
|
||||
$mapping = MesinSantriMapping::findOrFail($id);
|
||||
$idMesin = $mapping->id_mesin;
|
||||
$mapping->delete();
|
||||
|
||||
return back()->with('success', "Mapping ID Mesin {$idMesin} berhasil dihapus.");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// IMPORT FROM INFO.XLS
|
||||
// ──────────────────────────────────────────────────────────
|
||||
public function importFromInfo(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file_info' => 'required|file|mimes:xls,xlsx|max:10240',
|
||||
]);
|
||||
|
||||
$parser = app(EpposGLogParser::class);
|
||||
$infoData = $parser->parseInfoFile($request->file('file_info')->getPathname());
|
||||
$jadwal = $infoData['jadwal'];
|
||||
|
||||
$added = 0;
|
||||
$skipped = 0;
|
||||
$matched = 0;
|
||||
|
||||
// Ambil semua santri aktif sekali saja (efisien, tidak query per-santri)
|
||||
$semuaSantri = Santri::where('status', 'Aktif')
|
||||
->get(['id_santri', 'nama_lengkap']);
|
||||
|
||||
foreach ($jadwal as $idMesin => $data) {
|
||||
// Skip jika mapping sudah ada
|
||||
if (MesinSantriMapping::where('id_mesin', $idMesin)->exists()) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Coba cocokkan nama dengan berbagai strategi
|
||||
$santri = $this->cariSantriByNama($data['nama'], $semuaSantri);
|
||||
|
||||
MesinSantriMapping::create([
|
||||
'id_mesin' => $idMesin,
|
||||
'id_santri' => $santri?->id_santri,
|
||||
'nama_mesin' => $data['nama'],
|
||||
'dept_mesin' => $data['dept'] ?? null,
|
||||
]);
|
||||
|
||||
if ($santri) $matched++;
|
||||
$added++;
|
||||
}
|
||||
|
||||
$msg = "{$added} mapping ditambahkan ({$matched} otomatis cocok nama), {$skipped} sudah ada.";
|
||||
if ($added > $matched) {
|
||||
$belum = $added - $matched;
|
||||
$msg .= " {$belum} perlu dipetakan manual (nama tidak cocok).";
|
||||
}
|
||||
|
||||
return back()->with('success', $msg);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// HELPER: Cari Santri Berdasarkan Nama (Fuzzy Matching)
|
||||
//
|
||||
// Strategi (urutan prioritas):
|
||||
// 1. Exact match (nama lengkap sama persis, case-insensitive)
|
||||
// 2. Nama mesin ada di dalam nama lengkap santri
|
||||
// → "helga faisa" ditemukan di "helga faisa fahar"
|
||||
// 3. Nama lengkap santri ada di dalam nama mesin
|
||||
// → "helga" ditemukan di "helga faisa fahar"
|
||||
// 4. Semua kata dari nama mesin ada di nama santri
|
||||
// → nama mesin "helga faisa" → cari santri yang punya "helga" DAN "faisa"
|
||||
// 5. Minimal 1 kata dari nama mesin cocok, pilih santri
|
||||
// dengan skor kata paling banyak cocok
|
||||
// ──────────────────────────────────────────────────────────
|
||||
private function cariSantriByNama(string $namaMesin, $semuaSantri): ?Santri
|
||||
{
|
||||
$namaMesinBersih = strtolower(trim($namaMesin));
|
||||
|
||||
if (empty($namaMesinBersih)) return null;
|
||||
|
||||
// ── Strategi 1: Exact match ───────────────────────────
|
||||
foreach ($semuaSantri as $santri) {
|
||||
$namaDb = strtolower(trim($santri->nama_lengkap));
|
||||
if ($namaDb === $namaMesinBersih) {
|
||||
return $santri;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Strategi 2: nama mesin ada di nama santri ─────────
|
||||
// Contoh: nama mesin "helga faisa" → santri "helga faisa fahar" ✓
|
||||
foreach ($semuaSantri as $santri) {
|
||||
$namaDb = strtolower(trim($santri->nama_lengkap));
|
||||
if (str_contains($namaDb, $namaMesinBersih)) {
|
||||
return $santri;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Strategi 3: nama santri ada di nama mesin ─────────
|
||||
// Contoh: nama mesin "helga faisa fahar" → santri "helga faisa" ✓
|
||||
foreach ($semuaSantri as $santri) {
|
||||
$namaDb = strtolower(trim($santri->nama_lengkap));
|
||||
if (str_contains($namaMesinBersih, $namaDb)) {
|
||||
return $santri;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Strategi 4 & 5: Skor berdasarkan kata ────────────
|
||||
// Pecah nama mesin jadi kata-kata
|
||||
// Contoh: "helga faisa" → ['helga', 'faisa']
|
||||
$kataMesin = array_filter(explode(' ', $namaMesinBersih));
|
||||
|
||||
if (empty($kataMesin)) return null;
|
||||
|
||||
$kandidatTerbaik = null;
|
||||
$skorTerbaik = 0;
|
||||
|
||||
foreach ($semuaSantri as $santri) {
|
||||
$namaDb = strtolower(trim($santri->nama_lengkap));
|
||||
$kataDb = array_filter(explode(' ', $namaDb));
|
||||
|
||||
$skorCocok = 0;
|
||||
|
||||
foreach ($kataMesin as $kata) {
|
||||
// Minimal 3 karakter agar tidak false positive (mis. "al", "bin")
|
||||
if (strlen($kata) < 3) continue;
|
||||
|
||||
foreach ($kataDb as $kataDbItem) {
|
||||
if (
|
||||
$kataDbItem === $kata || // kata persis sama
|
||||
str_contains($kataDbItem, $kata) || // kata mesin ada di kata db
|
||||
str_contains($kata, $kataDbItem) // kata db ada di kata mesin
|
||||
) {
|
||||
$skorCocok++;
|
||||
break; // sudah cocok, lanjut kata berikutnya
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hitung persentase kata yang cocok
|
||||
$persenCocok = $skorCocok / count($kataMesin);
|
||||
|
||||
// Update kandidat jika skor lebih tinggi
|
||||
if ($persenCocok > $skorTerbaik) {
|
||||
$skorTerbaik = $persenCocok;
|
||||
$kandidatTerbaik = $santri;
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil kandidat hanya jika minimal 50% kata cocok
|
||||
// Contoh: nama mesin "helga faisa" (2 kata) → butuh minimal 1 kata cocok
|
||||
// Contoh: nama mesin "helga faisa fahar" (3 kata) → butuh minimal 2 kata cocok
|
||||
if ($skorTerbaik >= 0.5) {
|
||||
return $kandidatTerbaik;
|
||||
}
|
||||
|
||||
// Tidak ada yang cocok
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -201,19 +201,43 @@ public function show($id, Request $request)
|
|||
|
||||
$kelasList = Kelas::active()->ordered()->with('kelompok')->get();
|
||||
|
||||
// Statistik untuk kegiatan ini
|
||||
$stats = AbsensiKegiatan::where('kegiatan_id', $kegiatan->kegiatan_id)
|
||||
->select('status', DB::raw('count(*) as total'))
|
||||
// Statistik untuk kegiatan ini (sesuai filter)
|
||||
$statsQuery = AbsensiKegiatan::where('kegiatan_id', $kegiatan->kegiatan_id);
|
||||
if ($request->filled('tanggal_dari')) {
|
||||
$statsQuery->whereDate('tanggal', '>=', $request->tanggal_dari);
|
||||
}
|
||||
if ($request->filled('tanggal_sampai')) {
|
||||
$statsQuery->whereDate('tanggal', '<=', $request->tanggal_sampai);
|
||||
}
|
||||
if ($request->filled('bulan')) {
|
||||
$statsQuery->whereMonth('tanggal', date('m', strtotime($request->bulan)))
|
||||
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
|
||||
}
|
||||
if ($request->filled('id_kelas')) {
|
||||
$statsQuery->whereHas('santri.kelasSantri', function($q) use ($request) {
|
||||
$q->where('id_kelas', $request->id_kelas);
|
||||
});
|
||||
}
|
||||
$stats = $statsQuery->select('status', DB::raw('count(*) as total'))
|
||||
->groupBy('status')
|
||||
->pluck('total', 'status')
|
||||
->toArray();
|
||||
|
||||
// Hitung total SEMUA santri aktif
|
||||
$totalSantriEligible = Santri::where('status', 'Aktif')->count();
|
||||
$totalRecorded = array_sum($stats);
|
||||
$hadirCount = ($stats['Hadir'] ?? 0) + ($stats['Terlambat'] ?? 0);
|
||||
$persenHadir = $totalSantriEligible > 0 ? round($hadirCount / $totalSantriEligible * 100, 1) : 0;
|
||||
|
||||
return view('admin.kegiatan.riwayat.show', compact(
|
||||
'kegiatan',
|
||||
'riwayats',
|
||||
'santris',
|
||||
'kelasList',
|
||||
'stats'
|
||||
'stats',
|
||||
'totalSantriEligible',
|
||||
'totalRecorded',
|
||||
'persenHadir'
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,40 +19,51 @@ public function statusBulanIni(Request $request)
|
|||
$idSantri = $request->user()->id_santri;
|
||||
$bulanIni = date('n');
|
||||
$tahunIni = date('Y');
|
||||
|
||||
// Cari SPP bulan ini
|
||||
|
||||
$spp = PembayaranSpp::where('id_santri', $idSantri)
|
||||
->where('bulan', $bulanIni)
|
||||
->where('tahun', $tahunIni)
|
||||
->first();
|
||||
|
||||
|
||||
if (!$spp) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'ada_tagihan' => false,
|
||||
'status' => 'Belum Ada Tagihan',
|
||||
'periode' => $this->getNamaBulan($bulanIni) . ' ' . $tahunIni,
|
||||
'status' => 'Belum Ada Tagihan',
|
||||
'periode' => $this->getNamaBulan($bulanIni) . ' ' . $tahunIni,
|
||||
]
|
||||
], 200);
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// ── TAMBAHAN: data cicilan ──────────────────────────────
|
||||
$isCicilan = $spp->isCicilan();
|
||||
$nominalTerbayar = (int) $spp->nominal_terbayar; // accessor dari Model
|
||||
$nominalSisa = (int) $spp->nominal_sisa; // accessor dari Model
|
||||
$porsentase = $spp->porsentase_cicilan; // accessor dari Model
|
||||
// ───────────────────────────────────────────────────────
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'ada_tagihan' => true,
|
||||
'id_pembayaran' => $spp->id_pembayaran,
|
||||
'periode' => $this->getNamaBulan($spp->bulan) . ' ' . $spp->tahun,
|
||||
'nominal' => (int) $spp->nominal,
|
||||
'status' => $spp->status,
|
||||
'tanggal_bayar' => $spp->tanggal_bayar ? $spp->tanggal_bayar->format('Y-m-d') : null,
|
||||
'tanggal_bayar_formatted' => $spp->tanggal_bayar ? $spp->tanggal_bayar->format('d M Y') : null,
|
||||
'batas_bayar' => $spp->batas_bayar->format('Y-m-d'),
|
||||
'batas_bayar_formatted' => $spp->batas_bayar->format('d M Y'),
|
||||
'is_telat' => $spp->isTelat(),
|
||||
'ada_tagihan' => true,
|
||||
'id_pembayaran' => $spp->id_pembayaran,
|
||||
'periode' => $this->getNamaBulan($spp->bulan) . ' ' . $spp->tahun,
|
||||
'nominal' => (int) $spp->nominal,
|
||||
'status' => $spp->status,
|
||||
'tanggal_bayar' => $spp->tanggal_bayar?->format('Y-m-d'),
|
||||
'tanggal_bayar_formatted' => $spp->tanggal_bayar?->format('d M Y'),
|
||||
'batas_bayar' => $spp->batas_bayar->format('Y-m-d'),
|
||||
'batas_bayar_formatted' => $spp->batas_bayar->format('d M Y'),
|
||||
'is_telat' => $spp->isTelat(),
|
||||
// ── field baru ──
|
||||
'is_cicilan' => $isCicilan,
|
||||
'nominal_terbayar' => $nominalTerbayar,
|
||||
'nominal_sisa' => $nominalSisa,
|
||||
'porsentase_cicilan' => $porsentase,
|
||||
]
|
||||
], 200);
|
||||
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
|
|
@ -60,7 +71,7 @@ public function statusBulanIni(Request $request)
|
|||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get info tunggakan
|
||||
*/
|
||||
|
|
@ -68,28 +79,27 @@ public function tunggakan(Request $request)
|
|||
{
|
||||
try {
|
||||
$idSantri = $request->user()->id_santri;
|
||||
|
||||
// Hitung tunggakan
|
||||
|
||||
$tunggakanList = PembayaranSpp::where('id_santri', $idSantri)
|
||||
->where('status', 'Belum Lunas')
|
||||
->orderBy('tahun', 'asc')
|
||||
->orderBy('bulan', 'asc')
|
||||
->get();
|
||||
|
||||
|
||||
$totalTunggakan = $tunggakanList->sum('nominal');
|
||||
$jumlahBulan = $tunggakanList->count();
|
||||
$adaTelat = $tunggakanList->filter(fn($spp) => $spp->isTelat())->count() > 0;
|
||||
|
||||
$jumlahBulan = $tunggakanList->count();
|
||||
$adaTelat = $tunggakanList->filter(fn($spp) => $spp->isTelat())->count() > 0;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'ada_tunggakan' => $jumlahBulan > 0,
|
||||
'ada_tunggakan' => $jumlahBulan > 0,
|
||||
'total_tunggakan' => (int) $totalTunggakan,
|
||||
'jumlah_bulan' => $jumlahBulan,
|
||||
'ada_telat' => $adaTelat,
|
||||
'jumlah_bulan' => $jumlahBulan,
|
||||
'ada_telat' => $adaTelat,
|
||||
]
|
||||
], 200);
|
||||
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
|
|
@ -97,69 +107,86 @@ public function tunggakan(Request $request)
|
|||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get riwayat pembayaran SPP
|
||||
*
|
||||
* Query param ?status= bisa berisi:
|
||||
* semua | Lunas | Belum Lunas | Cicilan
|
||||
*/
|
||||
public function riwayat(Request $request)
|
||||
{
|
||||
try {
|
||||
$idSantri = $request->user()->id_santri;
|
||||
|
||||
// Query riwayat
|
||||
|
||||
$query = PembayaranSpp::where('id_santri', $idSantri)
|
||||
->select([
|
||||
'id',
|
||||
'id_pembayaran',
|
||||
'bulan',
|
||||
'tahun',
|
||||
'nominal',
|
||||
'status',
|
||||
'tanggal_bayar',
|
||||
'batas_bayar',
|
||||
'keterangan'
|
||||
'id', 'id_pembayaran', 'bulan', 'tahun',
|
||||
'nominal', 'status', 'tanggal_bayar',
|
||||
'batas_bayar', 'keterangan',
|
||||
])
|
||||
->orderBy('tahun', 'desc')
|
||||
->orderBy('bulan', 'desc');
|
||||
|
||||
// Filter status (optional)
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->status);
|
||||
|
||||
// ── REVISI: filter status termasuk "Cicilan" ───────────
|
||||
if ($request->filled('status') && $request->status !== 'semua') {
|
||||
if ($request->status === 'Cicilan') {
|
||||
// Cicilan = Belum Lunas + keterangan JSON punya field "terbayar" > 0
|
||||
$query->where('status', 'Belum Lunas')
|
||||
->where(function ($q) {
|
||||
// JSON valid & mengandung "terbayar"
|
||||
$q->whereRaw("JSON_VALID(keterangan) = 1")
|
||||
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(keterangan, '$.terbayar')) > 0");
|
||||
});
|
||||
} else {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
// ───────────────────────────────────────────────────────
|
||||
|
||||
$riwayat = $query->paginate(20);
|
||||
|
||||
// Format data
|
||||
$data = $riwayat->map(function($item) {
|
||||
|
||||
$data = $riwayat->map(function ($item) {
|
||||
// ── TAMBAHAN: data cicilan per item ─────────────────
|
||||
$isCicilan = $item->isCicilan();
|
||||
$nominalTerbayar = (int) $item->nominal_terbayar;
|
||||
$nominalSisa = (int) $item->nominal_sisa;
|
||||
$porsentase = $item->porsentase_cicilan;
|
||||
// ────────────────────────────────────────────────────
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'id_pembayaran' => $item->id_pembayaran,
|
||||
'periode' => $this->getNamaBulan($item->bulan) . ' ' . $item->tahun,
|
||||
'bulan' => $item->bulan,
|
||||
'tahun' => $item->tahun,
|
||||
'bulan_nama' => $this->getNamaBulan($item->bulan),
|
||||
'nominal' => (int) $item->nominal,
|
||||
'status' => $item->status,
|
||||
'tanggal_bayar' => $item->tanggal_bayar ? $item->tanggal_bayar->format('Y-m-d') : null,
|
||||
'tanggal_bayar_formatted' => $item->tanggal_bayar ? $item->tanggal_bayar->format('d M Y') : null,
|
||||
'batas_bayar' => $item->batas_bayar->format('Y-m-d'),
|
||||
'batas_bayar_formatted' => $item->batas_bayar->format('d M Y'),
|
||||
'is_telat' => $item->isTelat(),
|
||||
'keterangan' => $item->keterangan,
|
||||
'id' => $item->id,
|
||||
'id_pembayaran' => $item->id_pembayaran,
|
||||
'periode' => $this->getNamaBulan($item->bulan) . ' ' . $item->tahun,
|
||||
'bulan' => $item->bulan,
|
||||
'tahun' => $item->tahun,
|
||||
'bulan_nama' => $this->getNamaBulan($item->bulan),
|
||||
'nominal' => (int) $item->nominal,
|
||||
'status' => $item->status,
|
||||
'tanggal_bayar' => $item->tanggal_bayar?->format('Y-m-d'),
|
||||
'tanggal_bayar_formatted' => $item->tanggal_bayar?->format('d M Y'),
|
||||
'batas_bayar' => $item->batas_bayar->format('Y-m-d'),
|
||||
'batas_bayar_formatted' => $item->batas_bayar->format('d M Y'),
|
||||
'is_telat' => $item->isTelat(),
|
||||
'keterangan' => $item->keterangan,
|
||||
// ── field baru ──
|
||||
'is_cicilan' => $isCicilan,
|
||||
'nominal_terbayar' => $nominalTerbayar,
|
||||
'nominal_sisa' => $nominalSisa,
|
||||
'porsentase_cicilan' => $porsentase,
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'pagination' => [
|
||||
'current_page' => $riwayat->currentPage(),
|
||||
'last_page' => $riwayat->lastPage(),
|
||||
'total' => $riwayat->total(),
|
||||
]
|
||||
], 200);
|
||||
|
||||
'last_page' => $riwayat->lastPage(),
|
||||
'total' => $riwayat->total(),
|
||||
],
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
|
|
@ -167,7 +194,7 @@ public function riwayat(Request $request)
|
|||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get statistik pembayaran SPP
|
||||
*/
|
||||
|
|
@ -175,33 +202,39 @@ public function statistik(Request $request)
|
|||
{
|
||||
try {
|
||||
$idSantri = $request->user()->id_santri;
|
||||
|
||||
|
||||
$semuaBelumLunas = PembayaranSpp::where('id_santri', $idSantri)
|
||||
->where('status', 'Belum Lunas')
|
||||
->get();
|
||||
|
||||
$totalLunas = PembayaranSpp::where('id_santri', $idSantri)
|
||||
->where('status', 'Lunas')
|
||||
->count();
|
||||
|
||||
$totalBelumLunas = PembayaranSpp::where('id_santri', $idSantri)
|
||||
->where('status', 'Belum Lunas')
|
||||
->count();
|
||||
|
||||
|
||||
// ── TAMBAHAN: pisahkan cicilan dari belum lunas ─────────
|
||||
$totalCicilan = $semuaBelumLunas->filter(fn($s) => $s->isCicilan())->count();
|
||||
$totalBelumLunas = $semuaBelumLunas->filter(fn($s) => !$s->isCicilan())->count();
|
||||
// ────────────────────────────────────────────────────────
|
||||
|
||||
$totalNominalLunas = PembayaranSpp::where('id_santri', $idSantri)
|
||||
->where('status', 'Lunas')
|
||||
->sum('nominal');
|
||||
|
||||
|
||||
$totalNominalBelumLunas = PembayaranSpp::where('id_santri', $idSantri)
|
||||
->where('status', 'Belum Lunas')
|
||||
->sum('nominal');
|
||||
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'total_lunas' => $totalLunas,
|
||||
'total_belum_lunas' => $totalBelumLunas,
|
||||
'total_nominal_lunas' => (int) $totalNominalLunas,
|
||||
'total_lunas' => $totalLunas,
|
||||
'total_cicilan' => $totalCicilan, // ← baru
|
||||
'total_belum_lunas' => $totalBelumLunas, // ← sekarang exclude cicilan
|
||||
'total_nominal_lunas' => (int) $totalNominalLunas,
|
||||
'total_nominal_belum_lunas' => (int) $totalNominalBelumLunas,
|
||||
]
|
||||
], 200);
|
||||
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
|
|
@ -209,19 +242,19 @@ public function statistik(Request $request)
|
|||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper: Get nama bulan
|
||||
*/
|
||||
private function getNamaBulan($bulan)
|
||||
{
|
||||
$namaBulan = [
|
||||
1 => 'Januari', 2 => 'Februari', 3 => 'Maret',
|
||||
4 => 'April', 5 => 'Mei', 6 => 'Juni',
|
||||
7 => 'Juli', 8 => 'Agustus', 9 => 'September',
|
||||
10 => 'Oktober', 11 => 'November', 12 => 'Desember'
|
||||
1 => 'Januari', 2 => 'Februari', 3 => 'Maret',
|
||||
4 => 'April', 5 => 'Mei', 6 => 'Juni',
|
||||
7 => 'Juli', 8 => 'Agustus', 9 => 'September',
|
||||
10 => 'Oktober', 11 => 'November', 12 => 'Desember',
|
||||
];
|
||||
|
||||
|
||||
return $namaBulan[$bulan] ?? '';
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
use App\Models\Kepulangan;
|
||||
use App\Models\PengajuanKepulangan;
|
||||
use App\Models\PembayaranSpp;
|
||||
use App\Models\Keuangan; // ← TAMBAHAN: untuk data kas pondok
|
||||
use App\Models\Keuangan;
|
||||
use App\Models\UangSaku;
|
||||
use App\Models\Capaian;
|
||||
use App\Models\Semester;
|
||||
|
|
@ -26,36 +26,40 @@
|
|||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mapping hari Carbon (English) → DB enum (Indonesia)
|
||||
* Mapping hari Carbon (English) -> DB enum (Indonesia)
|
||||
*/
|
||||
private function hariIndonesia(): array
|
||||
{
|
||||
return [
|
||||
'Monday' => 'Senin', 'Tuesday' => 'Selasa', 'Wednesday' => 'Rabu',
|
||||
'Thursday' => 'Kamis', 'Friday' => 'Jumat', 'Saturday' => 'Sabtu',
|
||||
'Sunday' => 'Ahad',
|
||||
'Monday' => 'Senin',
|
||||
'Tuesday' => 'Selasa',
|
||||
'Wednesday' => 'Rabu',
|
||||
'Thursday' => 'Kamis',
|
||||
'Friday' => 'Jumat',
|
||||
'Saturday' => 'Sabtu',
|
||||
'Sunday' => 'Ahad',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard Admin
|
||||
*/
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// DASHBOARD ADMIN — tidak ada perubahan
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
public function admin()
|
||||
{
|
||||
try {
|
||||
$today = Carbon::today();
|
||||
$now = Carbon::now();
|
||||
$hariIni = $this->hariIndonesia()[$today->format('l')];
|
||||
$bulanIni = (int) $today->format('m');
|
||||
$tahunIni = (int) $today->format('Y');
|
||||
$today = Carbon::today();
|
||||
$now = Carbon::now();
|
||||
$hariIni = $this->hariIndonesia()[$today->format('l')];
|
||||
$bulanIni = (int) $today->format('m');
|
||||
$tahunIni = (int) $today->format('Y');
|
||||
|
||||
// ────────────────────────── KPI CARDS ──────────────────────────
|
||||
// KPI CARDS
|
||||
$user = Auth::user();
|
||||
|
||||
$totalSantriAktif = Cache::remember('dash_santri_aktif', 300, function () {
|
||||
return Santri::aktif()->count();
|
||||
});
|
||||
|
||||
// Kegiatan hari ini + status absensi
|
||||
$kegiatanHariIni = Kegiatan::with(['kategori', 'absensis' => function ($q) use ($today) {
|
||||
$q->whereDate('tanggal', $today);
|
||||
}])
|
||||
|
|
@ -63,24 +67,16 @@ public function admin()
|
|||
->orderBy('waktu_mulai')
|
||||
->get();
|
||||
|
||||
$totalKegiatan = $kegiatanHariIni->count();
|
||||
$sudahAbsensi = $kegiatanHariIni->filter(function ($k) {
|
||||
return $k->absensis->isNotEmpty();
|
||||
})->count();
|
||||
$belumAbsensi = $totalKegiatan - $sudahAbsensi;
|
||||
$totalKegiatan = $kegiatanHariIni->count();
|
||||
$sudahAbsensi = $kegiatanHariIni->filter(fn($k) => $k->absensis->isNotEmpty())->count();
|
||||
$belumAbsensi = $totalKegiatan - $sudahAbsensi;
|
||||
|
||||
// Santri di UKP (sedang dirawat)
|
||||
$santriSakit = KesehatanSantri::dirawat()->count();
|
||||
|
||||
// Pengajuan kepulangan menunggu approval
|
||||
$santriSakit = KesehatanSantri::dirawat()->count();
|
||||
$kepulanganMenunggu = PengajuanKepulangan::where('status', 'Menunggu')->count();
|
||||
|
||||
// Santri aktif yang belum punya akun wali (super_admin only)
|
||||
$santriTanpaWali = 0;
|
||||
if ($user->role === 'super_admin') {
|
||||
$santriTanpaWali = Santri::aktif()
|
||||
->whereDoesntHave('waliUser')
|
||||
->count();
|
||||
$santriTanpaWali = Santri::aktif()->whereDoesntHave('waliUser')->count();
|
||||
}
|
||||
|
||||
$kpiCards = compact(
|
||||
|
|
@ -88,29 +84,28 @@ public function admin()
|
|||
'belumAbsensi', 'santriSakit', 'kepulanganMenunggu', 'santriTanpaWali'
|
||||
);
|
||||
|
||||
// ──────────────────── JADWAL KEGIATAN HARI INI ────────────────────
|
||||
// JADWAL KEGIATAN HARI INI
|
||||
$kegiatanHariIni->each(function ($kegiatan) use ($now, $today, $totalSantriAktif) {
|
||||
$waktuMulaiStr = is_string($kegiatan->waktu_mulai) ? $kegiatan->waktu_mulai : $kegiatan->waktu_mulai->format('H:i');
|
||||
$waktuMulaiStr = is_string($kegiatan->waktu_mulai) ? $kegiatan->waktu_mulai : $kegiatan->waktu_mulai->format('H:i');
|
||||
$waktuSelesaiStr = is_string($kegiatan->waktu_selesai) ? $kegiatan->waktu_selesai : $kegiatan->waktu_selesai->format('H:i');
|
||||
|
||||
$mulai = Carbon::parse($today->format('Y-m-d') . ' ' . $waktuMulaiStr);
|
||||
$mulai = Carbon::parse($today->format('Y-m-d') . ' ' . $waktuMulaiStr);
|
||||
$selesai = Carbon::parse($today->format('Y-m-d') . ' ' . $waktuSelesaiStr);
|
||||
|
||||
$kegiatan->status_kegiatan = $now->lt($mulai) ? 'belum'
|
||||
: ($now->between($mulai, $selesai) ? 'berlangsung' : 'selesai');
|
||||
|
||||
$totalAbsen = $kegiatan->absensis->count();
|
||||
$hadir = $kegiatan->absensis->where('status', 'Hadir')->count();
|
||||
$hadir = $kegiatan->absensis->where('status', 'Hadir')->count();
|
||||
|
||||
$kegiatan->persen_kehadiran = $totalAbsen > 0 ? round(($hadir / $totalAbsen) * 100) : 0;
|
||||
$kegiatan->total_absensi = $totalAbsen;
|
||||
$kegiatan->belum_input = $kegiatan->status_kegiatan === 'selesai' && $totalAbsen === 0;
|
||||
$kegiatan->total_absensi = $totalAbsen;
|
||||
$kegiatan->belum_input = $kegiatan->status_kegiatan === 'selesai' && $totalAbsen === 0;
|
||||
});
|
||||
|
||||
// ────────────────────────── ALERT PANEL ──────────────────────────
|
||||
// 1) Santri alpa beruntun (semua role bisa lihat)
|
||||
// ALERT PANEL
|
||||
$santriAlpaBeruntun = $this->getSantriAlpaBeruntun();
|
||||
|
||||
// 2) SPP jatuh tempo (super_admin only)
|
||||
$sppJatuhTempo = collect([]);
|
||||
if ($user->role === 'super_admin') {
|
||||
$sppJatuhTempo = PembayaranSpp::telat()
|
||||
|
|
@ -121,7 +116,6 @@ public function admin()
|
|||
->get();
|
||||
}
|
||||
|
||||
// 3) Pengajuan kepulangan menunggu review
|
||||
$kepulanganPending = PengajuanKepulangan::where('status', 'Menunggu')
|
||||
->with('santri:id_santri,nama_lengkap')
|
||||
->select('id_pengajuan', 'id_santri', 'tanggal_pulang', 'tanggal_kembali', 'alasan')
|
||||
|
|
@ -131,31 +125,26 @@ public function admin()
|
|||
|
||||
$alerts = compact('santriAlpaBeruntun', 'sppJatuhTempo', 'kepulanganPending');
|
||||
|
||||
// ──────────────── GRAFIK TREN KEHADIRAN (4 MINGGU) ────────────────
|
||||
// GRAFIK TREN KEHADIRAN (4 MINGGU)
|
||||
$trenKehadiran = $this->getTrenKehadiran($today);
|
||||
|
||||
// ──────────────── RINGKASAN SPP + KEUANGAN BULAN INI ─────────────
|
||||
// Default (untuk non super_admin atau jika query gagal)
|
||||
// RINGKASAN SPP + KEUANGAN BULAN INI
|
||||
$sppBulanIni = [
|
||||
'lunas' => 0,
|
||||
'belum' => 0,
|
||||
'terkumpul' => 0,
|
||||
'totalTagihan' => 0,
|
||||
'pemasukanLain' => 0, // pemasukan kas pondok selain SPP
|
||||
'pengeluaran' => 0, // pengeluaran kas pondok
|
||||
'pemasukanLain' => 0,
|
||||
'pengeluaran' => 0,
|
||||
];
|
||||
|
||||
if ($user->role === 'super_admin') {
|
||||
// Pakai cache key baru "dash_spp_full_" agar tidak tumpang-tindih
|
||||
// dengan cache key lama "dash_spp_" yang belum punya key keuangan
|
||||
$sppBulanIni = Cache::remember("dash_spp_full_{$bulanIni}_{$tahunIni}", 300, function () use ($bulanIni, $tahunIni) {
|
||||
// ── Data SPP ──
|
||||
$lunas = PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->lunas()->count();
|
||||
$belum = PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->belumLunas()->count();
|
||||
$terkumpul = (float) PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->lunas()->sum('nominal');
|
||||
$totalTagihan = (float) PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->sum('nominal');
|
||||
|
||||
// ── Data Keuangan Pondok (non-SPP) ──
|
||||
$pemasukanLain = (float) Keuangan::pemasukan()
|
||||
->whereMonth('tanggal', $bulanIni)
|
||||
->whereYear('tanggal', $tahunIni)
|
||||
|
|
@ -185,11 +174,10 @@ public function admin()
|
|||
}
|
||||
}
|
||||
|
||||
// ══════════════════ HELPER METHODS ══════════════════
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// HELPER METHODS (dipakai oleh admin)
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Santri dengan alpa ≥ 3x beruntun dalam 7 hari terakhir
|
||||
*/
|
||||
private function getSantriAlpaBeruntun(int $threshold = 3): \Illuminate\Support\Collection
|
||||
{
|
||||
$weekAgo = Carbon::today()->subDays(7);
|
||||
|
|
@ -210,16 +198,13 @@ private function getSantriAlpaBeruntun(int $threshold = 3): \Illuminate\Support\
|
|||
->whereIn('id_santri', $alpaData->keys())
|
||||
->select('id_santri', 'nama_lengkap')
|
||||
->get()
|
||||
->map(fn ($s) => (object) [
|
||||
'nama' => $s->nama_lengkap,
|
||||
'id_santri' => $s->id_santri,
|
||||
->map(fn($s) => (object) [
|
||||
'nama' => $s->nama_lengkap,
|
||||
'id_santri' => $s->id_santri,
|
||||
'total_alpa' => $alpaData[$s->id_santri],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tren kehadiran 4 minggu terakhir, dikelompokkan per kategori kegiatan
|
||||
*/
|
||||
private function getTrenKehadiran(Carbon $today): array
|
||||
{
|
||||
$labels = [];
|
||||
|
|
@ -228,8 +213,8 @@ private function getTrenKehadiran(Carbon $today): array
|
|||
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
||||
|
||||
for ($i = 3; $i >= 0; $i--) {
|
||||
$start = $today->copy()->subWeeks($i)->startOfWeek(Carbon::MONDAY);
|
||||
$end = $start->copy()->endOfWeek(Carbon::SUNDAY);
|
||||
$start = $today->copy()->subWeeks($i)->startOfWeek(Carbon::MONDAY);
|
||||
$end = $start->copy()->endOfWeek(Carbon::SUNDAY);
|
||||
$labels[] = 'Mg ' . (4 - $i);
|
||||
|
||||
foreach ($kategoris as $kat) {
|
||||
|
|
@ -239,21 +224,21 @@ private function getTrenKehadiran(Carbon $today): array
|
|||
$totalAbsen = AbsensiKegiatan::whereIn('kegiatan_id', $kegiatanIds)
|
||||
->dateRange($start, $end)
|
||||
->count();
|
||||
|
||||
$hadir = AbsensiKegiatan::whereIn('kegiatan_id', $kegiatanIds)
|
||||
->dateRange($start, $end)
|
||||
->where('status', 'Hadir')
|
||||
->count();
|
||||
|
||||
$series[$kat->nama_kategori][] = $totalAbsen > 0 ? round(($hadir / $totalAbsen) * 100, 1) : 0;
|
||||
$series[$kat->nama_kategori][] = $totalAbsen > 0
|
||||
? round(($hadir / $totalAbsen) * 100, 1)
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
return compact('labels', 'series');
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed aktivitas terbaru
|
||||
*/
|
||||
private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
|
||||
{
|
||||
$items = collect();
|
||||
|
|
@ -263,11 +248,11 @@ private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
|
|||
->orderByDesc('created_at')
|
||||
->limit(5)
|
||||
->get()
|
||||
->each(fn ($a) => $items->push((object) [
|
||||
'icon' => 'fa-clipboard-check',
|
||||
->each(fn($a) => $items->push((object) [
|
||||
'icon' => 'fa-clipboard-check',
|
||||
'color' => 'success',
|
||||
'text' => ($a->santri->nama_lengkap ?? '-') . ' — ' . $a->status . ' di ' . ($a->kegiatan->nama_kegiatan ?? '-'),
|
||||
'time' => $a->created_at,
|
||||
'text' => ($a->santri->nama_lengkap ?? '-') . ' — ' . $a->status . ' di ' . ($a->kegiatan->nama_kegiatan ?? '-'),
|
||||
'time' => $a->created_at,
|
||||
]));
|
||||
|
||||
RiwayatPelanggaran::with(['santri:id_santri,nama_lengkap', 'kategori:id_kategori,nama_pelanggaran'])
|
||||
|
|
@ -275,11 +260,11 @@ private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
|
|||
->terbaru()
|
||||
->limit(5)
|
||||
->get()
|
||||
->each(fn ($p) => $items->push((object) [
|
||||
'icon' => 'fa-exclamation-triangle',
|
||||
->each(fn($p) => $items->push((object) [
|
||||
'icon' => 'fa-exclamation-triangle',
|
||||
'color' => 'danger',
|
||||
'text' => ($p->santri->nama_lengkap ?? '-') . ' — ' . ($p->kategori->nama_pelanggaran ?? '-') . ' (' . $p->poin . ' poin)',
|
||||
'time' => $p->created_at,
|
||||
'text' => ($p->santri->nama_lengkap ?? '-') . ' — ' . ($p->kategori->nama_pelanggaran ?? '-') . ' (' . $p->poin . ' poin)',
|
||||
'time' => $p->created_at,
|
||||
]));
|
||||
|
||||
PembayaranSpp::with('santri:id_santri,nama_lengkap')
|
||||
|
|
@ -289,32 +274,79 @@ private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
|
|||
->orderByDesc('tanggal_bayar')
|
||||
->limit(5)
|
||||
->get()
|
||||
->each(fn ($s) => $items->push((object) [
|
||||
'icon' => 'fa-money-bill-wave',
|
||||
->each(fn($s) => $items->push((object) [
|
||||
'icon' => 'fa-money-bill-wave',
|
||||
'color' => 'info',
|
||||
'text' => ($s->santri->nama_lengkap ?? '-') . ' — SPP ' . $s->bulan_nama . '/' . $s->tahun . ' (Rp ' . number_format($s->nominal, 0, ',', '.') . ')',
|
||||
'time' => $s->created_at,
|
||||
'text' => ($s->santri->nama_lengkap ?? '-') . ' — SPP ' . $s->bulan_nama . '/' . $s->tahun . ' (Rp ' . number_format($s->nominal, 0, ',', '.') . ')',
|
||||
'time' => $s->created_at,
|
||||
]));
|
||||
|
||||
return $items->sortByDesc('time')->take(10)->values();
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// HELPER: Absensi per kategori (dipakai santri())
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Dashboard Santri
|
||||
* Ambil statistik absensi per kategori kegiatan untuk 1 santri
|
||||
* dalam rentang tanggal tertentu.
|
||||
*
|
||||
* @param int $idSantri
|
||||
* @param string $dateStart format Y-m-d
|
||||
* @param string $dateEnd format Y-m-d
|
||||
* @return array ['labels'=>[], 'hadir'=>[], 'alpa'=>[], 'izin'=>[], 'sakit'=>[]]
|
||||
*/
|
||||
private function getAbsensiPerKategori(string|int $idSantri, string $dateStart, string $dateEnd): array
|
||||
{
|
||||
$result = ['labels' => [], 'hadir' => [], 'alpa' => [], 'izin' => [], 'sakit' => []];
|
||||
|
||||
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')
|
||||
->orderBy('nama_kategori')
|
||||
->get();
|
||||
|
||||
foreach ($kategoris as $kat) {
|
||||
$kegIds = Kegiatan::where('kategori_id', $kat->kategori_id)
|
||||
->pluck('kegiatan_id');
|
||||
|
||||
if ($kegIds->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$abs = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->whereIn('kegiatan_id', $kegIds)
|
||||
->whereBetween('tanggal', [$dateStart, $dateEnd])
|
||||
->get();
|
||||
|
||||
// Skip kategori yang tidak punya record sama sekali di periode ini
|
||||
if ($abs->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result['labels'][] = $kat->nama_kategori;
|
||||
$result['hadir'][] = $abs->whereIn('status', ['Hadir', 'Terlambat'])->count();
|
||||
$result['alpa'][] = $abs->where('status', 'Alpa')->count();
|
||||
$result['izin'][] = $abs->where('status', 'Izin')->count();
|
||||
$result['sakit'][] = $abs->where('status', 'Sakit')->count();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// DASHBOARD SANTRI
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
public function santri()
|
||||
{
|
||||
try {
|
||||
$account = auth('santri')->user();
|
||||
|
||||
Log::info('=== DASHBOARD SANTRI START ===');
|
||||
Log::info('Account ID: ' . $account->id);
|
||||
Log::info('Role: ' . $account->role);
|
||||
Log::info('ID Santri: ' . $account->id_santri);
|
||||
Log::info('Account ID: ' . $account->id);
|
||||
Log::info('Role: ' . $account->role);
|
||||
Log::info('ID Santri: ' . $account->id_santri);
|
||||
|
||||
$santri = Santri::with([
|
||||
'kelasPrimary.kelas.kelompok',
|
||||
])
|
||||
$santri = Santri::with(['kelasPrimary.kelas.kelompok'])
|
||||
->where('id_santri', $account->id_santri)
|
||||
->select('id_santri', 'nama_lengkap')
|
||||
->first();
|
||||
|
|
@ -327,11 +359,10 @@ public function santri()
|
|||
Log::info('Santri ditemukan: ' . $santri->nama_lengkap);
|
||||
|
||||
$namaKelas = $santri->kelas;
|
||||
$idSantri = $santri->id_santri;
|
||||
$today = Carbon::today();
|
||||
$weekAgo = Carbon::now()->subDays(7);
|
||||
$idSantri = $santri->id_santri;
|
||||
$today = Carbon::today();
|
||||
|
||||
// Ambil semester aktif dengan FALLBACK
|
||||
// ─── Semester aktif ───────────────────────────────────────
|
||||
$semesterAktif = null;
|
||||
try {
|
||||
$semesterAktif = Semester::aktif()
|
||||
|
|
@ -348,118 +379,81 @@ public function santri()
|
|||
Log::info('Semester aktif: ' . ($semesterAktif ? $semesterAktif->nama_semester : 'Tidak ada'));
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error mengambil semester: ' . $e->getMessage());
|
||||
$semesterAktif = null;
|
||||
}
|
||||
|
||||
// Progres Al-Qur'an
|
||||
// ─── Progres Al-Qur'an ────────────────────────────────────
|
||||
$progresAlquran = 0;
|
||||
try {
|
||||
$query = Capaian::where('id_santri', $idSantri);
|
||||
if ($semesterAktif) {
|
||||
$query->where('id_semester', $semesterAktif->id_semester);
|
||||
}
|
||||
$progresAlquran = $query->whereHas('materi', function ($q) {
|
||||
$q->where('kategori', 'Al-Qur\'an');
|
||||
})->avg('persentase') ?? 0;
|
||||
|
||||
Log::info('Progres Al-Quran: ' . $progresAlquran);
|
||||
$progresAlquran = $query->whereHas('materi', fn($q) => $q->where('kategori', "Al-Qur'an"))
|
||||
->avg('persentase') ?? 0;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error progres Al-Quran: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Progres Hadist
|
||||
// ─── Progres Hadist ───────────────────────────────────────
|
||||
$progresHadist = 0;
|
||||
try {
|
||||
$query = Capaian::where('id_santri', $idSantri);
|
||||
if ($semesterAktif) {
|
||||
$query->where('id_semester', $semesterAktif->id_semester);
|
||||
}
|
||||
$progresHadist = $query->whereHas('materi', function ($q) {
|
||||
$q->where('kategori', 'Hadist');
|
||||
})->avg('persentase') ?? 0;
|
||||
|
||||
Log::info('Progres Hadist: ' . $progresHadist);
|
||||
$progresHadist = $query->whereHas('materi', fn($q) => $q->where('kategori', 'Hadist'))
|
||||
->avg('persentase') ?? 0;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error progres Hadist: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Progres Materi Tambahan
|
||||
// ─── Progres Materi Tambahan ──────────────────────────────
|
||||
$progresMateriTambahan = 0;
|
||||
try {
|
||||
$query = Capaian::where('id_santri', $idSantri);
|
||||
if ($semesterAktif) {
|
||||
$query->where('id_semester', $semesterAktif->id_semester);
|
||||
}
|
||||
$progresMateriTambahan = $query->whereHas('materi', function ($q) {
|
||||
$q->where('kategori', 'Materi Tambahan');
|
||||
})->avg('persentase') ?? 0;
|
||||
|
||||
Log::info('Progres Materi Tambahan: ' . $progresMateriTambahan);
|
||||
$progresMateriTambahan = $query->whereHas('materi', fn($q) => $q->where('kategori', 'Materi Tambahan'))
|
||||
->avg('persentase') ?? 0;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error progres Materi Tambahan: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Data untuk grafik: Progress per Materi
|
||||
// ─── Capaian per Materi ───────────────────────────────────
|
||||
$capaianPerMateri = collect([]);
|
||||
try {
|
||||
$query = Capaian::with(['materi' => function ($q) {
|
||||
$q->select('id_materi', 'nama_kitab', 'kategori', 'total_halaman');
|
||||
}])
|
||||
$query = Capaian::with(['materi' => fn($q) => $q->select('id_materi', 'nama_kitab', 'kategori', 'total_halaman')])
|
||||
->where('id_santri', $idSantri);
|
||||
|
||||
if ($semesterAktif) {
|
||||
$query->where('id_semester', $semesterAktif->id_semester);
|
||||
}
|
||||
|
||||
$capaianPerMateri = $query->select('id', 'id_materi', 'persentase', 'halaman_selesai')
|
||||
->orderBy('persentase', 'desc')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
Log::info('Capaian per materi: ' . $capaianPerMateri->count() . ' items');
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error capaian per materi: ' . $e->getMessage());
|
||||
$capaianPerMateri = collect([]);
|
||||
}
|
||||
|
||||
// Data untuk grafik: Distribusi Status
|
||||
$distribusiStatus = [
|
||||
'selesai' => 0,
|
||||
'hampir_selesai' => 0,
|
||||
'sedang_berjalan' => 0,
|
||||
'baru_dimulai' => 0,
|
||||
];
|
||||
// ─── Distribusi Status ────────────────────────────────────
|
||||
$distribusiStatus = ['selesai' => 0, 'hampir_selesai' => 0, 'sedang_berjalan' => 0, 'baru_dimulai' => 0];
|
||||
try {
|
||||
$baseQuery = Capaian::where('id_santri', $idSantri);
|
||||
if ($semesterAktif) {
|
||||
$baseQuery->where('id_semester', $semesterAktif->id_semester);
|
||||
}
|
||||
|
||||
$distribusiStatus = [
|
||||
'selesai' => (clone $baseQuery)->where('persentase', '>=', 100)->count(),
|
||||
'hampir_selesai' => (clone $baseQuery)->whereBetween('persentase', [75, 99.99])->count(),
|
||||
'selesai' => (clone $baseQuery)->where('persentase', '>=', 100)->count(),
|
||||
'hampir_selesai' => (clone $baseQuery)->whereBetween('persentase', [75, 99.99])->count(),
|
||||
'sedang_berjalan' => (clone $baseQuery)->whereBetween('persentase', [25, 74.99])->count(),
|
||||
'baru_dimulai' => (clone $baseQuery)->whereBetween('persentase', [0, 24.99])->count(),
|
||||
'baru_dimulai' => (clone $baseQuery)->whereBetween('persentase', [0, 24.99])->count(),
|
||||
];
|
||||
|
||||
Log::info('Distribusi status: ' . json_encode($distribusiStatus));
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error distribusi status: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$data = [
|
||||
'nama_santri' => $santri->nama_lengkap,
|
||||
'kelas' => $namaKelas,
|
||||
'progres_quran' => round($progresAlquran, 1),
|
||||
'progres_hadist' => round($progresHadist, 1),
|
||||
'progres_materi_tambahan' => round($progresMateriTambahan, 1),
|
||||
'saldo_uang_saku' => $santri->saldo_uang_saku ?? 0,
|
||||
'poin_pelanggaran' => RiwayatPelanggaran::where('id_santri', $idSantri)->sum('poin') ?? 0,
|
||||
];
|
||||
|
||||
Log::info('Data array: ' . json_encode($data));
|
||||
|
||||
// Status kesehatan
|
||||
// ─── Status Kesehatan ─────────────────────────────────────
|
||||
$statusKesehatan = null;
|
||||
try {
|
||||
$statusKesehatan = KesehatanSantri::where('id_santri', $idSantri)
|
||||
|
|
@ -471,7 +465,7 @@ public function santri()
|
|||
Log::warning('Error status kesehatan: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Kepulangan aktif
|
||||
// ─── Kepulangan Aktif ─────────────────────────────────────
|
||||
$kepulanganAktif = null;
|
||||
try {
|
||||
$kepulanganAktif = Kepulangan::where('id_santri', $idSantri)
|
||||
|
|
@ -484,12 +478,12 @@ public function santri()
|
|||
Log::warning('Error kepulangan aktif: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Berita terbaru
|
||||
// ─── Berita Terbaru ───────────────────────────────────────
|
||||
// Tanpa filter tanggal agar semua berita relevan muncul, limit 5
|
||||
$beritaTerbaru = collect([]);
|
||||
try {
|
||||
$beritaTerbaru = Berita::select('id_berita', 'judul', 'created_at')
|
||||
->where('status', 'published')
|
||||
->where('created_at', '>=', $weekAgo)
|
||||
->where(function ($query) use ($namaKelas) {
|
||||
$query->where('target_berita', 'semua')
|
||||
->orWhere(function ($q) use ($namaKelas) {
|
||||
|
|
@ -500,13 +494,157 @@ public function santri()
|
|||
->orderBy('created_at', 'desc')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
Log::info('Berita terbaru: ' . $beritaTerbaru->count() . ' items');
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error berita terbaru: ' . $e->getMessage());
|
||||
$beritaTerbaru = collect([]);
|
||||
}
|
||||
|
||||
// ─── Statistik Kepulangan Tahun Ini ──────────────────────
|
||||
$statistikKepulangan = [
|
||||
'total_hari' => 0,
|
||||
'sisa_kuota' => 12,
|
||||
'persen_kuota' => 0,
|
||||
'disetujui' => 0,
|
||||
'menunggu' => 0,
|
||||
'over_limit' => false,
|
||||
];
|
||||
try {
|
||||
$kepulanganTahunIni = Kepulangan::where('id_santri', $idSantri)
|
||||
->whereYear('tanggal_pulang', $today->year)
|
||||
->get();
|
||||
|
||||
$totalHariKepulangan = $kepulanganTahunIni
|
||||
->whereIn('status', ['Disetujui', 'Selesai'])
|
||||
->sum('durasi_izin');
|
||||
|
||||
$statistikKepulangan = [
|
||||
'total_hari' => $totalHariKepulangan,
|
||||
'sisa_kuota' => max(0, 12 - $totalHariKepulangan),
|
||||
'persen_kuota' => min(100, round(($totalHariKepulangan / 12) * 100)),
|
||||
'disetujui' => $kepulanganTahunIni->whereIn('status', ['Disetujui', 'Selesai'])->count(),
|
||||
'menunggu' => $kepulanganTahunIni->where('status', 'Menunggu')->count(),
|
||||
'over_limit' => $totalHariKepulangan > 12,
|
||||
];
|
||||
|
||||
Log::info('Statistik kepulangan: ' . json_encode($statistikKepulangan));
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error statistik kepulangan: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// ─── Statistik Kesehatan Bulan Ini ───────────────────────
|
||||
$statistikKesehatan = [
|
||||
'total_kunjungan' => 0,
|
||||
'sembuh' => 0,
|
||||
'dirawat' => 0,
|
||||
'izin' => 0,
|
||||
];
|
||||
try {
|
||||
$kesehatanBulanIni = KesehatanSantri::where('id_santri', $idSantri)
|
||||
->whereMonth('tanggal_masuk', $today->month)
|
||||
->whereYear('tanggal_masuk', $today->year)
|
||||
->get();
|
||||
|
||||
$statistikKesehatan = [
|
||||
'total_kunjungan' => $kesehatanBulanIni->count(),
|
||||
'sembuh' => $kesehatanBulanIni->where('status', 'sembuh')->count(),
|
||||
'dirawat' => $kesehatanBulanIni->where('status', 'dirawat')->count(),
|
||||
'izin' => $kesehatanBulanIni->where('status', 'izin')->count(),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error statistik kesehatan: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// ─── 5 Pelanggaran Terbaru ────────────────────────────────
|
||||
$pelanggaranTerbaru = collect([]);
|
||||
try {
|
||||
$pelanggaranTerbaru = RiwayatPelanggaran::with('kategori:id,id_kategori,nama_pelanggaran')
|
||||
->where('id_santri', $idSantri)
|
||||
->select('id', 'id_riwayat', 'id_kategori', 'tanggal', 'poin', 'keterangan')
|
||||
->orderBy('tanggal', 'desc')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
Log::info('Pelanggaran terbaru: ' . $pelanggaranTerbaru->count() . ' items');
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error pelanggaran terbaru: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// ─── [BARU] Absensi per Kategori — Bulan Ini ─────────────
|
||||
$absensiPerKategori = ['labels' => [], 'hadir' => [], 'alpa' => [], 'izin' => [], 'sakit' => []];
|
||||
try {
|
||||
$startBulan = $today->copy()->startOfMonth()->format('Y-m-d');
|
||||
$endBulan = $today->format('Y-m-d');
|
||||
|
||||
$absensiPerKategori = $this->getAbsensiPerKategori($idSantri, $startBulan, $endBulan);
|
||||
|
||||
Log::info('Absensi per kategori bulan ini: ' . count($absensiPerKategori['labels']) . ' kategori');
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error absensi per kategori bulan: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// ─── [BARU] Absensi per Kategori — Minggu Ini ────────────
|
||||
$absensiPerKategoriMinggu = ['labels' => [], 'hadir' => [], 'alpa' => [], 'izin' => [], 'sakit' => []];
|
||||
try {
|
||||
$startMinggu = $today->copy()->startOfWeek(Carbon::MONDAY)->format('Y-m-d');
|
||||
$endMinggu = $today->format('Y-m-d');
|
||||
|
||||
$absensiPerKategoriMinggu = $this->getAbsensiPerKategori($idSantri, $startMinggu, $endMinggu);
|
||||
|
||||
Log::info('Absensi per kategori minggu ini: ' . count($absensiPerKategoriMinggu['labels']) . ' kategori');
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error absensi per kategori minggu: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// ─── [BARU] Status Input Capaian ──────────────────────────
|
||||
$statusInputCapaian = [
|
||||
'is_open' => false,
|
||||
'deadline' => null,
|
||||
'sudah_input' => 0,
|
||||
'total_materi' => 0,
|
||||
];
|
||||
try {
|
||||
if ($semesterAktif) {
|
||||
// Sesuaikan nama kolom jika berbeda di tabel semesters
|
||||
$bukaSemester = $semesterAktif->tanggal_buka_input ?? null;
|
||||
$tutupSemester = $semesterAktif->tanggal_tutup_input ?? null;
|
||||
|
||||
$isOpen = false;
|
||||
if ($bukaSemester && $tutupSemester) {
|
||||
$now = Carbon::now();
|
||||
$isOpen = $now->gte(Carbon::parse($bukaSemester))
|
||||
&& $now->lte(Carbon::parse($tutupSemester));
|
||||
}
|
||||
|
||||
$sudahInput = Capaian::where('id_santri', $idSantri)
|
||||
->where('id_semester', $semesterAktif->id_semester)
|
||||
->where('persentase', '>', 0)
|
||||
->count();
|
||||
|
||||
$totalMateri = Capaian::where('id_santri', $idSantri)
|
||||
->where('id_semester', $semesterAktif->id_semester)
|
||||
->count();
|
||||
|
||||
$statusInputCapaian = [
|
||||
'is_open' => $isOpen,
|
||||
'deadline' => $tutupSemester,
|
||||
'sudah_input' => $sudahInput,
|
||||
'total_materi' => $totalMateri,
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Error status input capaian: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// ─── Data array untuk view ────────────────────────────────
|
||||
$data = [
|
||||
'nama_santri' => $santri->nama_lengkap,
|
||||
'kelas' => $namaKelas,
|
||||
'progres_quran' => round($progresAlquran, 1),
|
||||
'progres_hadist' => round($progresHadist, 1),
|
||||
'progres_materi_tambahan' => round($progresMateriTambahan, 1),
|
||||
'saldo_uang_saku' => $santri->saldo_uang_saku ?? 0,
|
||||
'poin_pelanggaran' => RiwayatPelanggaran::where('id_santri', $idSantri)->sum('poin') ?? 0,
|
||||
];
|
||||
|
||||
Log::info('=== DASHBOARD SANTRI SUCCESS ===');
|
||||
|
||||
return view('santri.dashboardSantri', compact(
|
||||
|
|
@ -518,15 +656,22 @@ public function santri()
|
|||
'kepulanganAktif',
|
||||
'capaianPerMateri',
|
||||
'distribusiStatus',
|
||||
'semesterAktif'
|
||||
'semesterAktif',
|
||||
'statistikKepulangan',
|
||||
'statistikKesehatan',
|
||||
'pelanggaranTerbaru',
|
||||
// ─── variabel baru ───
|
||||
'absensiPerKategori',
|
||||
'absensiPerKategoriMinggu',
|
||||
'statusInputCapaian'
|
||||
));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('=== FATAL ERROR DI DASHBOARD SANTRI ===');
|
||||
Log::error('Message: ' . $e->getMessage());
|
||||
Log::error('File: ' . $e->getFile());
|
||||
Log::error('Line: ' . $e->getLine());
|
||||
Log::error('Trace: ' . $e->getTraceAsString());
|
||||
Log::error('File: ' . $e->getFile());
|
||||
Log::error('Line: ' . $e->getLine());
|
||||
Log::error('Trace: ' . $e->getTraceAsString());
|
||||
|
||||
if (config('app.debug')) {
|
||||
abort(500, 'Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
||||
|
|
|
|||
|
|
@ -17,11 +17,6 @@ private function getSantriId()
|
|||
return auth('santri')->user()->id_santri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve date range.
|
||||
* Jadwal & Riwayat default: today
|
||||
* Statistik default: this_week
|
||||
*/
|
||||
private function resolveDateRange(Request $request, string $defaultPreset = 'today'): array
|
||||
{
|
||||
$preset = $request->input('preset', $defaultPreset);
|
||||
|
|
@ -40,7 +35,6 @@ private function resolveDateRange(Request $request, string $defaultPreset = 'tod
|
|||
$lm = $now->copy()->subMonth();
|
||||
return [$lm->copy()->startOfMonth(), $lm->copy()->endOfMonth(), 'last_month'];
|
||||
default:
|
||||
// custom
|
||||
$from = $request->filled('date_from')
|
||||
? Carbon::parse($request->date_from)->startOfDay()
|
||||
: $now->copy()->startOfDay();
|
||||
|
|
@ -52,34 +46,29 @@ private function resolveDateRange(Request $request, string $defaultPreset = 'tod
|
|||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// INDEX
|
||||
// ================================================================
|
||||
public function index(Request $request)
|
||||
{
|
||||
$idSantri = $this->getSantriId();
|
||||
|
||||
// ✅ FIX: No 'kelas' column, use relasi
|
||||
$santri = Santri::where('id_santri', $idSantri)
|
||||
->with(['kelasPrimary.kelas'])
|
||||
->select('id_santri', 'nama_lengkap', 'nis', 'status')
|
||||
->firstOrFail();
|
||||
|
||||
$namaKelas = optional(optional($santri->kelasPrimary)->kelas)->nama_kelas ?? '-';
|
||||
$kelasSantriId = optional($santri->kelasPrimary)->id_kelas;
|
||||
|
||||
// -- Aktif tab (dari request, default: statistik) --
|
||||
$namaKelas = optional(optional($santri->kelasPrimary)->kelas)->nama_kelas ?? '-';
|
||||
$activeTab = $request->input('tab', 'statistik');
|
||||
|
||||
// -- Tiap tab punya preset/range masing-masing --
|
||||
// Statistik: default this_week
|
||||
// Jadwal & Riwayat: default today
|
||||
// Request bisa bawa preset_stat, preset_jadwal, preset_riwayat
|
||||
// atau preset global (backward compat)
|
||||
|
||||
// Statistik range
|
||||
// ── Statistik range ───────────────────────────────────
|
||||
$statPresetReq = $request->input('preset_stat', $request->input('preset', 'this_week'));
|
||||
[$statFrom, $statTo, $statPreset] = $this->resolveDateRange(
|
||||
$request->merge(['preset' => $statPresetReq,
|
||||
'date_from' => $request->input('stat_date_from'),
|
||||
'date_to' => $request->input('stat_date_to')]),
|
||||
$request->merge([
|
||||
'preset' => $statPresetReq,
|
||||
'date_from' => $request->input('stat_date_from'),
|
||||
'date_to' => $request->input('stat_date_to'),
|
||||
]),
|
||||
'this_week'
|
||||
);
|
||||
if ($statPreset === 'custom') {
|
||||
|
|
@ -87,34 +76,31 @@ public function index(Request $request)
|
|||
$statTo = $request->filled('stat_date_to') ? Carbon::parse($request->stat_date_to)->endOfDay() : $statTo;
|
||||
}
|
||||
|
||||
// Jadwal range
|
||||
// ── Jadwal range ──────────────────────────────────────
|
||||
$jadPresetReq = $request->input('preset_jad', $request->input('preset', 'today'));
|
||||
[$jadFrom, $jadTo, $jadPreset] = $this->resolveDateRange(
|
||||
$request->merge(['preset' => $jadPresetReq,
|
||||
'date_from' => $request->input('jad_date_from'),
|
||||
'date_to' => $request->input('jad_date_to')]),
|
||||
$request->merge([
|
||||
'preset' => $jadPresetReq,
|
||||
'date_from' => $request->input('jad_date_from'),
|
||||
'date_to' => $request->input('jad_date_to'),
|
||||
]),
|
||||
'today'
|
||||
);
|
||||
|
||||
// Riwayat range
|
||||
$riwPresetReq = $request->input('preset_riw', $request->input('preset', 'today'));
|
||||
[$riwFrom, $riwTo, $riwPreset] = $this->resolveDateRange(
|
||||
$request->merge(['preset' => $riwPresetReq,
|
||||
'date_from' => $request->input('riw_date_from'),
|
||||
'date_to' => $request->input('riw_date_to')]),
|
||||
'today'
|
||||
);
|
||||
|
||||
// -- Mapping hari --
|
||||
// ── Mapping hari Carbon → nama hari di DB ─────────────
|
||||
$hariMapDb = [
|
||||
'Senin' => 'Senin', 'Selasa' => 'Selasa', 'Rabu' => 'Rabu',
|
||||
'Kamis' => 'Kamis', 'Jumat' => 'Jumat', 'Sabtu' => 'Sabtu',
|
||||
'Senin' => 'Senin',
|
||||
'Selasa' => 'Selasa',
|
||||
'Rabu' => 'Rabu',
|
||||
'Kamis' => 'Kamis',
|
||||
'Jumat' => 'Jumat',
|
||||
'Sabtu' => 'Sabtu',
|
||||
'Minggu' => 'Ahad',
|
||||
];
|
||||
$hariCarbon = Carbon::now()->locale('id')->dayName;
|
||||
$hariIni = $hariMapDb[$hariCarbon] ?? $hariCarbon;
|
||||
|
||||
// ── KPI stats (pakai stat range) ──────────────────────────────────
|
||||
// ── KPI stats (stat range) ────────────────────────────
|
||||
$statFromStr = $statFrom->format('Y-m-d');
|
||||
$statToStr = $statTo->format('Y-m-d');
|
||||
|
||||
|
|
@ -125,14 +111,28 @@ public function index(Request $request)
|
|||
->pluck('total', 'status')
|
||||
->toArray();
|
||||
|
||||
$totalRange = array_sum($statsRange);
|
||||
$hadirRange = $statsRange['Hadir'] ?? 0;
|
||||
$izinRange = $statsRange['Izin'] ?? 0;
|
||||
$sakitRange = $statsRange['Sakit'] ?? 0;
|
||||
$alpaRange = $statsRange['Alpa'] ?? 0;
|
||||
$persentaseKehadiran = $totalRange > 0 ? round($hadirRange / $totalRange * 100, 1) : 0;
|
||||
$totalRange = array_sum($statsRange);
|
||||
$hadirRange = $statsRange['Hadir'] ?? 0;
|
||||
$terlambatRange = $statsRange['Terlambat'] ?? 0;
|
||||
$izinRange = $statsRange['Izin'] ?? 0;
|
||||
$sakitRange = $statsRange['Sakit'] ?? 0;
|
||||
$alpaRange = $statsRange['Alpa'] ?? 0;
|
||||
$pulangRange = $statsRange['Pulang'] ?? 0;
|
||||
|
||||
// ── JADWAL ────────────────────────────────────────────────────────
|
||||
// ── Expected total: semua kegiatan di hari itu, tanpa filter kelas ──
|
||||
$expectedTotal = 0;
|
||||
$curStat = $statFrom->copy();
|
||||
while ($curStat->lte($statTo)) {
|
||||
$hariDb = $hariMapDb[$curStat->locale('id')->dayName] ?? $curStat->locale('id')->dayName;
|
||||
$expectedTotal += Kegiatan::where('hari', $hariDb)->count();
|
||||
$curStat->addDay();
|
||||
}
|
||||
|
||||
$belumAbsenRange = max(0, $expectedTotal - $totalRange);
|
||||
$hadirEfektif = $hadirRange + $terlambatRange;
|
||||
$persentaseKehadiran = $expectedTotal > 0 ? round($hadirEfektif / $expectedTotal * 100, 1) : 0;
|
||||
|
||||
// ── Jadwal dalam range: semua kegiatan, tanpa filter kelas ───
|
||||
$hariDalamRange = [];
|
||||
$cursor = $jadFrom->copy();
|
||||
while ($cursor->lte($jadTo)) {
|
||||
|
|
@ -144,62 +144,35 @@ public function index(Request $request)
|
|||
|
||||
$jadwalDalamRange = Kegiatan::with('kategori')
|
||||
->whereIn('hari', $hariDalamRange)
|
||||
->where(function ($q) use ($kelasSantriId) {
|
||||
$q->doesntHave('kelasKegiatan')
|
||||
->orWhereHas('kelasKegiatan', function ($q2) use ($kelasSantriId) {
|
||||
if ($kelasSantriId) {
|
||||
$q2->where('kelas.id', $kelasSantriId);
|
||||
}
|
||||
});
|
||||
})
|
||||
->select('kegiatan_id', 'kategori_id', 'nama_kegiatan', 'waktu_mulai', 'waktu_selesai', 'hari', 'materi')
|
||||
->orderByRaw("FIELD(hari, 'Senin','Selasa','Rabu','Kamis','Jumat','Sabtu','Ahad')")
|
||||
->orderBy('waktu_mulai')
|
||||
->get();
|
||||
|
||||
// Status absensi per kegiatan dalam range jadwal
|
||||
// ── Status absensi santri dalam range jadwal ──────────
|
||||
$absensiDalamRange = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->whereBetween('tanggal', [$jadFrom->format('Y-m-d'), $jadTo->format('Y-m-d')])
|
||||
->pluck('status', 'kegiatan_id')
|
||||
->toArray();
|
||||
|
||||
// Status khusus hari ini (untuk badge)
|
||||
$absensiHariIni = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->whereDate('tanggal', Carbon::today())
|
||||
->pluck('status', 'kegiatan_id')
|
||||
->toArray();
|
||||
|
||||
// ── RIWAYAT ───────────────────────────────────────────────────────
|
||||
$riwFromStr = $riwFrom->format('Y-m-d');
|
||||
$riwToStr = $riwTo->format('Y-m-d');
|
||||
|
||||
$queryRiwayat = AbsensiKegiatan::with('kegiatan.kategori')
|
||||
->where('id_santri', $idSantri)
|
||||
->whereBetween('tanggal', [$riwFromStr, $riwToStr]);
|
||||
|
||||
if ($request->filled('filter_status')) {
|
||||
$queryRiwayat->where('status', $request->filter_status);
|
||||
}
|
||||
if ($request->filled('filter_kategori')) {
|
||||
$queryRiwayat->whereHas('kegiatan', fn($q) => $q->where('kategori_id', $request->filter_kategori));
|
||||
}
|
||||
|
||||
$riwayats = $queryRiwayat->orderBy('tanggal', 'desc')
|
||||
->orderBy('waktu_absen', 'desc')
|
||||
->paginate(15)
|
||||
->appends(request()->query());
|
||||
|
||||
// ── STREAK ────────────────────────────────────────────────────────
|
||||
// ── Streak ───────────────────────────────────────────
|
||||
$streak = 0;
|
||||
AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->orderByDesc('tanggal')->orderByDesc('waktu_absen')
|
||||
->select('status')->limit(60)
|
||||
->each(function($a) use (&$streak) {
|
||||
if ($a->status === 'Hadir') $streak++;
|
||||
->orderByDesc('tanggal')
|
||||
->orderByDesc('waktu_absen')
|
||||
->select('status')
|
||||
->limit(60)
|
||||
->each(function ($a) use (&$streak) {
|
||||
if (in_array($a->status, ['Hadir', 'Terlambat'])) $streak++;
|
||||
else return false;
|
||||
});
|
||||
|
||||
// ── GRAFIK TREN (stat range) ──────────────────────────────────────
|
||||
// ── Grafik tren ───────────────────────────────────────
|
||||
$diffDays = $statFrom->diffInDays($statTo);
|
||||
$dataGrafik = [];
|
||||
|
||||
|
|
@ -207,8 +180,13 @@ public function index(Request $request)
|
|||
$cur = $statFrom->copy();
|
||||
while ($cur->lte($statTo)) {
|
||||
$d = $cur->format('Y-m-d');
|
||||
$hadir = AbsensiKegiatan::where('id_santri', $idSantri)->whereDate('tanggal', $d)->where('status', 'Hadir')->count();
|
||||
$total = AbsensiKegiatan::where('id_santri', $idSantri)->whereDate('tanggal', $d)->count();
|
||||
$hadir = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->whereDate('tanggal', $d)
|
||||
->whereIn('status', ['Hadir', 'Terlambat'])
|
||||
->count();
|
||||
$total = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->whereDate('tanggal', $d)
|
||||
->count();
|
||||
$dataGrafik[] = ['label' => $cur->format('d/m'), 'hadir' => $hadir, 'total' => $total];
|
||||
$cur->addDay();
|
||||
}
|
||||
|
|
@ -217,107 +195,88 @@ public function index(Request $request)
|
|||
while ($cur->lte($statTo)) {
|
||||
$wStart = $cur->copy()->max($statFrom);
|
||||
$wEnd = $cur->copy()->endOfWeek()->min($statTo);
|
||||
$hadir = AbsensiKegiatan::where('id_santri', $idSantri)->whereBetween('tanggal', [$wStart->format('Y-m-d'), $wEnd->format('Y-m-d')])->where('status', 'Hadir')->count();
|
||||
$total = AbsensiKegiatan::where('id_santri', $idSantri)->whereBetween('tanggal', [$wStart->format('Y-m-d'), $wEnd->format('Y-m-d')])->count();
|
||||
$dataGrafik[] = ['label' => $wStart->format('d/m') . '–' . $wEnd->format('d/m'), 'hadir' => $hadir, 'total' => $total];
|
||||
$hadir = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->whereBetween('tanggal', [$wStart->format('Y-m-d'), $wEnd->format('Y-m-d')])
|
||||
->whereIn('status', ['Hadir', 'Terlambat'])
|
||||
->count();
|
||||
$total = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->whereBetween('tanggal', [$wStart->format('Y-m-d'), $wEnd->format('Y-m-d')])
|
||||
->count();
|
||||
$dataGrafik[] = [
|
||||
'label' => $wStart->format('d/m') . '–' . $wEnd->format('d/m'),
|
||||
'hadir' => $hadir,
|
||||
'total' => $total,
|
||||
];
|
||||
$cur->addWeek();
|
||||
}
|
||||
}
|
||||
|
||||
// ── CONSISTENCY SCORE per KEGIATAN (stat range) ───────────────────
|
||||
// Score = % hadir, dengan label badge berdasarkan level
|
||||
$consistencyScores = AbsensiKegiatan::where('absensi_kegiatans.id_santri', $idSantri)
|
||||
->whereBetween('absensi_kegiatans.tanggal', [$statFromStr, $statToStr])
|
||||
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
|
||||
->join('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id')
|
||||
->select(
|
||||
'kegiatans.kegiatan_id',
|
||||
'kegiatans.nama_kegiatan',
|
||||
'kategori_kegiatans.nama_kategori',
|
||||
DB::raw('COUNT(*) as total'),
|
||||
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
|
||||
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Alpa" THEN 1 ELSE 0 END) as alpa'),
|
||||
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Izin","Sakit") THEN 1 ELSE 0 END) as dispensasi')
|
||||
)
|
||||
->groupBy('kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan', 'kategori_kegiatans.nama_kategori')
|
||||
->get()
|
||||
->map(function ($row) {
|
||||
$score = $row->total > 0 ? round($row->hadir / $row->total * 100) : 0;
|
||||
// Badge tier
|
||||
if ($score >= 90) { $badge = 'Konsisten'; $tier = 'top'; }
|
||||
elseif ($score >= 75) { $badge = 'Baik'; $tier = 'good'; }
|
||||
elseif ($score >= 60) { $badge = 'Cukup'; $tier = 'fair'; }
|
||||
elseif ($score >= 40) { $badge = 'Perlu Perhatian'; $tier = 'warn'; }
|
||||
else { $badge = 'Kritis'; $tier = 'crit'; }
|
||||
$row->score = $score;
|
||||
$row->badge = $badge;
|
||||
$row->tier = $tier;
|
||||
return $row;
|
||||
})
|
||||
->sortByDesc('score')
|
||||
->values();
|
||||
// ── Recent Absensi (8 terbaru dalam stat range) ───────
|
||||
$recentAbsensi = AbsensiKegiatan::with('kegiatan.kategori')
|
||||
->where('id_santri', $idSantri)
|
||||
->whereBetween('tanggal', [$statFromStr, $statToStr])
|
||||
->orderBy('tanggal', 'desc')
|
||||
->orderBy('waktu_absen', 'desc')
|
||||
->limit(8)
|
||||
->get();
|
||||
|
||||
// ── HEATMAP: kalender bulan aktif (stat range, max tampil 1 bulan) ─
|
||||
// Kita buat kalender bulan-bulan dalam stat range, dengan angka tanggal
|
||||
// ── Heatmap kalender ──────────────────────────────────
|
||||
$heatmapMonths = [];
|
||||
$cur = $statFrom->copy()->startOfMonth();
|
||||
while ($cur->lte($statTo)) {
|
||||
$monthKey = $cur->format('Y-m');
|
||||
$daysInMonth = $cur->daysInMonth;
|
||||
$firstDayOfWeek = $cur->copy()->startOfMonth()->dayOfWeekIso; // 1=Mon..7=Sun
|
||||
|
||||
$daysInMonth = $cur->daysInMonth;
|
||||
$firstDayOfWeek = $cur->copy()->startOfMonth()->dayOfWeekIso;
|
||||
$days = [];
|
||||
for ($d = 1; $d <= $daysInMonth; $d++) {
|
||||
$date = $cur->format('Y-m') . '-' . str_pad($d, 2, '0', STR_PAD_LEFT);
|
||||
$rows = AbsensiKegiatan::where('id_santri', $idSantri)->whereDate('tanggal', $date)->get();
|
||||
$level = 0;
|
||||
if ($rows->count() > 0) {
|
||||
$pct = round($rows->where('status', 'Hadir')->count() / $rows->count() * 100);
|
||||
$level = $pct >= 90 ? 4 : ($pct >= 70 ? 3 : ($pct >= 50 ? 2 : 1));
|
||||
$hadirCount = $rows->whereIn('status', ['Hadir', 'Terlambat'])->count();
|
||||
$pct = round($hadirCount / $rows->count() * 100);
|
||||
$level = $pct >= 90 ? 4 : ($pct >= 70 ? 3 : ($pct >= 50 ? 2 : 1));
|
||||
}
|
||||
$days[] = [
|
||||
'day' => $d,
|
||||
'date' => $date,
|
||||
'level' => $level,
|
||||
'count' => $rows->where('status', 'Hadir')->count(),
|
||||
'count' => $rows->whereIn('status', ['Hadir', 'Terlambat'])->count(),
|
||||
'total' => $rows->count(),
|
||||
'is_today' => $date === Carbon::today()->format('Y-m-d'),
|
||||
'in_range' => $date >= $statFromStr && $date <= $statToStr,
|
||||
];
|
||||
}
|
||||
|
||||
$heatmapMonths[] = [
|
||||
'label' => $cur->locale('id')->isoFormat('MMMM YYYY'),
|
||||
'firstDayOfWeek'=> $firstDayOfWeek,
|
||||
'days' => $days,
|
||||
'label' => $cur->locale('id')->isoFormat('MMMM YYYY'),
|
||||
'firstDayOfWeek' => $firstDayOfWeek,
|
||||
'days' => $days,
|
||||
];
|
||||
|
||||
$cur->addMonth();
|
||||
}
|
||||
|
||||
$kategoriList = \App\Models\KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
||||
|
||||
return view('santri.kegiatan.index', compact(
|
||||
'santri', 'namaKelas',
|
||||
'jadwalDalamRange', 'absensiDalamRange', 'absensiHariIni', 'hariIni',
|
||||
'jadPreset', 'jadFrom', 'jadTo',
|
||||
'riwayats', 'riwPreset', 'riwFrom', 'riwTo',
|
||||
'statsRange', 'totalRange', 'hadirRange', 'izinRange', 'sakitRange', 'alpaRange',
|
||||
'persentaseKehadiran', 'streak',
|
||||
'statsRange', 'totalRange',
|
||||
'hadirRange', 'terlambatRange', 'izinRange', 'sakitRange', 'alpaRange', 'pulangRange',
|
||||
'hadirEfektif',
|
||||
'persentaseKehadiran', 'streak', 'expectedTotal', 'belumAbsenRange',
|
||||
'dataGrafik', 'statPreset', 'statFrom', 'statTo', 'statFromStr', 'statToStr', 'diffDays',
|
||||
'consistencyScores',
|
||||
'recentAbsensi',
|
||||
'heatmapMonths',
|
||||
'kategoriList',
|
||||
'activeTab', 'hariIni'
|
||||
));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SHOW — support filter tanggal, semua data ikut filter
|
||||
// ================================================================
|
||||
public function show($kegiatan_id, Request $request)
|
||||
{
|
||||
$idSantri = $this->getSantriId();
|
||||
|
||||
$santri = Santri::where('id_santri', $idSantri)
|
||||
->with(['kelasPrimary.kelas'])
|
||||
->select('id_santri', 'nama_lengkap', 'nis', 'status')
|
||||
->firstOrFail();
|
||||
|
||||
|
|
@ -325,47 +284,135 @@ public function show($kegiatan_id, Request $request)
|
|||
->where('kegiatan_id', $kegiatan_id)
|
||||
->firstOrFail();
|
||||
|
||||
$riwayats = AbsensiKegiatan::where('id_santri', $santri->id_santri)
|
||||
->where('kegiatan_id', $kegiatan_id)
|
||||
->orderBy('tanggal', 'desc')
|
||||
->paginate(20);
|
||||
// ── Resolve date range ────────────────────────────────
|
||||
$preset = $request->input('preset', 'this_week');
|
||||
$now = Carbon::now();
|
||||
|
||||
$stats = AbsensiKegiatan::where('id_santri', $santri->id_santri)
|
||||
switch ($preset) {
|
||||
case 'this_week':
|
||||
$dateFrom = $now->copy()->startOfWeek();
|
||||
$dateTo = $now->copy()->endOfWeek();
|
||||
break;
|
||||
case 'this_month':
|
||||
$dateFrom = $now->copy()->startOfMonth();
|
||||
$dateTo = $now->copy()->endOfMonth();
|
||||
break;
|
||||
case 'last_month':
|
||||
$dateFrom = $now->copy()->subMonth()->startOfMonth();
|
||||
$dateTo = $now->copy()->subMonth()->endOfMonth();
|
||||
break;
|
||||
case 'last_3m':
|
||||
$dateFrom = $now->copy()->subMonths(3)->startOfDay();
|
||||
$dateTo = $now->copy()->endOfDay();
|
||||
break;
|
||||
case 'all':
|
||||
$oldest = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->where('kegiatan_id', $kegiatan_id)
|
||||
->min('tanggal');
|
||||
$dateFrom = $oldest
|
||||
? Carbon::parse($oldest)->startOfDay()
|
||||
: $now->copy()->startOfWeek();
|
||||
$dateTo = $now->copy()->endOfDay();
|
||||
break;
|
||||
default:
|
||||
$dateFrom = $request->filled('date_from')
|
||||
? Carbon::parse($request->date_from)->startOfDay()
|
||||
: $now->copy()->startOfWeek();
|
||||
$dateTo = $request->filled('date_to')
|
||||
? Carbon::parse($request->date_to)->endOfDay()
|
||||
: $now->copy()->endOfWeek();
|
||||
if ($dateFrom->gt($dateTo)) [$dateFrom, $dateTo] = [$dateTo, $dateFrom];
|
||||
$preset = 'custom';
|
||||
}
|
||||
|
||||
$fromStr = $dateFrom->format('Y-m-d');
|
||||
$toStr = $dateTo->format('Y-m-d');
|
||||
|
||||
// ── Stats dalam range ─────────────────────────────────
|
||||
$stats = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->where('kegiatan_id', $kegiatan_id)
|
||||
->whereBetween('tanggal', [$fromStr, $toStr])
|
||||
->select('status', DB::raw('count(*) as total'))
|
||||
->groupBy('status')
|
||||
->pluck('total', 'status')
|
||||
->toArray();
|
||||
|
||||
$totalAbsensi = array_sum($stats);
|
||||
$hadirEfektif = ($stats['Hadir'] ?? 0) + ($stats['Terlambat'] ?? 0);
|
||||
$persentaseHadir = $totalAbsensi > 0
|
||||
? round(($stats['Hadir'] ?? 0) / $totalAbsensi * 100, 1) : 0;
|
||||
? round($hadirEfektif / $totalAbsensi * 100, 1) : 0;
|
||||
|
||||
$trendBulanan = [];
|
||||
for ($i = 5; $i >= 0; $i--) {
|
||||
$bulan = Carbon::now()->subMonths($i);
|
||||
$data = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->where('kegiatan_id', $kegiatan_id)
|
||||
->whereMonth('tanggal', $bulan->month)
|
||||
->whereYear('tanggal', $bulan->year)
|
||||
->select('status', DB::raw('count(*) as total'))
|
||||
->groupBy('status')
|
||||
->pluck('total', 'status')
|
||||
->toArray();
|
||||
$trendBulanan[] = [
|
||||
'bulan' => $bulan->locale('id')->isoFormat('MMM YY'),
|
||||
'hadir' => $data['Hadir'] ?? 0,
|
||||
'total' => array_sum($data),
|
||||
];
|
||||
// ── Riwayat tabel (paginated, ikut range) ─────────────
|
||||
$riwayats = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->where('kegiatan_id', $kegiatan_id)
|
||||
->whereBetween('tanggal', [$fromStr, $toStr])
|
||||
->orderBy('tanggal', 'desc')
|
||||
->paginate(20)
|
||||
->appends($request->query());
|
||||
|
||||
// ── Lookup tanggal => status untuk kalender visual ────
|
||||
// Query terpisah agar tidak terbatas oleh pagination $riwayats
|
||||
$absensiByDate = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->where('kegiatan_id', $kegiatan_id)
|
||||
->whereBetween('tanggal', [$fromStr, $toStr])
|
||||
->select('tanggal', 'status')
|
||||
->get()
|
||||
->mapWithKeys(fn($a) => [Carbon::parse($a->tanggal)->format('Y-m-d') => $a->status])
|
||||
->toArray();
|
||||
|
||||
// ── Tren data ─────────────────────────────────────────
|
||||
$diffDays = $dateFrom->diffInDays($dateTo);
|
||||
$trendData = [];
|
||||
|
||||
if ($diffDays <= 31) {
|
||||
$cur = $dateFrom->copy();
|
||||
while ($cur->lte($dateTo)) {
|
||||
$d = $cur->format('Y-m-d');
|
||||
$data = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->where('kegiatan_id', $kegiatan_id)
|
||||
->whereDate('tanggal', $d)
|
||||
->select('status', DB::raw('count(*) as total'))
|
||||
->groupBy('status')
|
||||
->pluck('total', 'status')
|
||||
->toArray();
|
||||
$trendData[] = [
|
||||
'label' => $cur->format('d/m'),
|
||||
'hadir' => ($data['Hadir'] ?? 0) + ($data['Terlambat'] ?? 0),
|
||||
'total' => array_sum($data),
|
||||
];
|
||||
$cur->addDay();
|
||||
}
|
||||
$trendLabel = 'Harian';
|
||||
} else {
|
||||
$cur = $dateFrom->copy()->startOfWeek();
|
||||
while ($cur->lte($dateTo)) {
|
||||
$wStart = $cur->copy()->max($dateFrom);
|
||||
$wEnd = $cur->copy()->endOfWeek()->min($dateTo);
|
||||
$data = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||
->where('kegiatan_id', $kegiatan_id)
|
||||
->whereBetween('tanggal', [$wStart->format('Y-m-d'), $wEnd->format('Y-m-d')])
|
||||
->select('status', DB::raw('count(*) as total'))
|
||||
->groupBy('status')
|
||||
->pluck('total', 'status')
|
||||
->toArray();
|
||||
$trendData[] = [
|
||||
'label' => $wStart->format('d/m') . '–' . $wEnd->format('d/m'),
|
||||
'hadir' => ($data['Hadir'] ?? 0) + ($data['Terlambat'] ?? 0),
|
||||
'total' => array_sum($data),
|
||||
];
|
||||
$cur->addWeek();
|
||||
}
|
||||
$trendLabel = 'Mingguan';
|
||||
}
|
||||
|
||||
// Referrer tab untuk tombol kembali
|
||||
$fromTab = $request->input('from_tab', 'riwayat');
|
||||
$fromTab = $request->input('from_tab', 'jadwal');
|
||||
|
||||
return view('santri.kegiatan.show', compact(
|
||||
'santri', 'kegiatan', 'riwayats',
|
||||
'stats', 'totalAbsensi', 'persentaseHadir',
|
||||
'trendBulanan', 'fromTab'
|
||||
'stats', 'totalAbsensi', 'hadirEfektif', 'persentaseHadir',
|
||||
'trendData', 'trendLabel',
|
||||
'dateFrom', 'dateTo', 'fromStr', 'toStr', 'preset', 'fromTab',
|
||||
'absensiByDate'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -16,12 +16,13 @@ class AbsensiKegiatan extends Model
|
|||
'id_santri',
|
||||
'tanggal',
|
||||
'status',
|
||||
'metode_absen',
|
||||
'metode_absen', // ← BARU: 'Manual' | 'RFID' | 'Import_Mesin'
|
||||
'konflik_catatan', // ← BARU: catatan resolusi konflik
|
||||
'waktu_absen',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tanggal' => 'date',
|
||||
'tanggal' => 'date',
|
||||
'waktu_absen' => 'datetime:H:i',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
|
|
@ -37,12 +38,20 @@ protected static function boot()
|
|||
static::creating(function ($model) {
|
||||
if (empty($model->absensi_id)) {
|
||||
$last = self::orderBy('id', 'desc')->first();
|
||||
$num = $last ? intval(substr($last->absensi_id, 1)) + 1 : 1;
|
||||
$num = $last ? intval(substr($last->absensi_id, 1)) + 1 : 1;
|
||||
$model->absensi_id = 'A' . str_pad($num, 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
// Default metode_absen jika tidak diset
|
||||
if (empty($model->metode_absen)) {
|
||||
$model->metode_absen = 'Manual';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// RELASI
|
||||
// ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Relasi ke Santri
|
||||
*/
|
||||
|
|
@ -59,6 +68,10 @@ public function kegiatan()
|
|||
return $this->belongsTo(Kegiatan::class, 'kegiatan_id', 'kegiatan_id');
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// SCOPES
|
||||
// ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Scope: Filter berdasarkan tanggal
|
||||
*/
|
||||
|
|
@ -75,59 +88,6 @@ public function scopeKegiatan($query, $kegiatan_id)
|
|||
return $query->where('kegiatan_id', $kegiatan_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor: Status Badge (HTML - untuk admin)
|
||||
*/
|
||||
public function getStatusBadgeAttribute()
|
||||
{
|
||||
$badges = [
|
||||
'Hadir' => '<span class="badge badge-success"><i class="fas fa-check"></i> Hadir</span>',
|
||||
'Izin' => '<span class="badge badge-warning"><i class="fas fa-info-circle"></i> Izin</span>',
|
||||
'Sakit' => '<span class="badge badge-info"><i class="fas fa-heartbeat"></i> Sakit</span>',
|
||||
'Alpa' => '<span class="badge badge-danger"><i class="fas fa-times"></i> Alpa</span>',
|
||||
'Terlambat' => '<span class="badge" style="background: #FF9800; color: white;"><i class="fas fa-clock"></i> Terlambat</span>',
|
||||
'Pulang' => '<span class="badge" style="background: #FFF3E0; color: #E65100;"><i class="fas fa-home"></i> Pulang</span>',
|
||||
];
|
||||
|
||||
return $badges[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ✅ TAMBAHKAN METHOD-METHOD BARU DI BAWAH INI
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Accessor: Tanggal Formatted (untuk view santri)
|
||||
*/
|
||||
public function getTanggalFormattedAttribute()
|
||||
{
|
||||
return Carbon::parse($this->tanggal)->locale('id')->isoFormat('dddd, D MMMM YYYY');
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor: Waktu Absen Formatted (untuk view santri)
|
||||
*/
|
||||
public function getWaktuAbsenFormattedAttribute()
|
||||
{
|
||||
return $this->waktu_absen ? Carbon::parse($this->waktu_absen)->format('H:i') : '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor: Status Badge Class (CSS class only - untuk view santri)
|
||||
*/
|
||||
public function getStatusBadgeClassAttribute()
|
||||
{
|
||||
return match($this->status) {
|
||||
'Hadir' => 'badge-success',
|
||||
'Izin' => 'badge-info',
|
||||
'Sakit' => 'badge-warning',
|
||||
'Alpa' => 'badge-danger',
|
||||
'Terlambat' => 'badge-warning',
|
||||
'Pulang' => 'badge-secondary',
|
||||
default => 'badge-secondary',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Filter by date range
|
||||
*/
|
||||
|
|
@ -144,4 +104,78 @@ public function scopeByMonth($query, $month, $year)
|
|||
return $query->whereMonth('tanggal', $month)
|
||||
->whereYear('tanggal', $year);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Filter by metode absen
|
||||
*/
|
||||
public function scopeByMetode($query, $metode)
|
||||
{
|
||||
return $query->where('metode_absen', $metode);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// ACCESSORS
|
||||
// ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Accessor: Status Badge HTML (untuk admin)
|
||||
*/
|
||||
public function getStatusBadgeAttribute()
|
||||
{
|
||||
$badges = [
|
||||
'Hadir' => '<span class="badge badge-success"><i class="fas fa-check"></i> Hadir</span>',
|
||||
'Izin' => '<span class="badge badge-warning"><i class="fas fa-info-circle"></i> Izin</span>',
|
||||
'Sakit' => '<span class="badge badge-info"><i class="fas fa-heartbeat"></i> Sakit</span>',
|
||||
'Alpa' => '<span class="badge badge-danger"><i class="fas fa-times"></i> Alpa</span>',
|
||||
'Terlambat' => '<span class="badge" style="background:#FF9800;color:white;"><i class="fas fa-clock"></i> Terlambat</span>',
|
||||
'Pulang' => '<span class="badge" style="background:#FFF3E0;color:#E65100;"><i class="fas fa-home"></i> Pulang</span>',
|
||||
];
|
||||
return $badges[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor: Metode Badge HTML (untuk tampilan tabel absensi)
|
||||
* Manual=biru, RFID=hijau, Import_Mesin=oranye
|
||||
*/
|
||||
public function getMetodeBadgeAttribute()
|
||||
{
|
||||
$badges = [
|
||||
'Manual' => '<span style="background:#DBEAFE;color:#1D4ED8;border-radius:8px;padding:2px 8px;font-size:11px;font-weight:600">✋ Manual</span>',
|
||||
'RFID' => '<span style="background:#DCFCE7;color:#166534;border-radius:8px;padding:2px 8px;font-size:11px;font-weight:600">💳 RFID</span>',
|
||||
'Import_Mesin' => '<span style="background:#FFF7ED;color:#C05621;border-radius:8px;padding:2px 8px;font-size:11px;font-weight:600">👆 Mesin</span>',
|
||||
];
|
||||
return $badges[$this->metode_absen] ?? $this->metode_absen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor: Tanggal Formatted (untuk view santri)
|
||||
*/
|
||||
public function getTanggalFormattedAttribute()
|
||||
{
|
||||
return Carbon::parse($this->tanggal)->locale('id')->isoFormat('dddd, D MMMM YYYY');
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor: Waktu Absen Formatted
|
||||
*/
|
||||
public function getWaktuAbsenFormattedAttribute()
|
||||
{
|
||||
return $this->waktu_absen ? Carbon::parse($this->waktu_absen)->format('H:i') : '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor: Status Badge Class (CSS class only - untuk view santri)
|
||||
*/
|
||||
public function getStatusBadgeClassAttribute()
|
||||
{
|
||||
return match($this->status) {
|
||||
'Hadir' => 'badge-success',
|
||||
'Izin' => 'badge-info',
|
||||
'Sakit' => 'badge-warning',
|
||||
'Alpa' => 'badge-danger',
|
||||
'Terlambat' => 'badge-warning',
|
||||
'Pulang' => 'badge-secondary',
|
||||
default => 'badge-secondary',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ImportMesinLog extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id', 'jumlah_scan', 'berhasil',
|
||||
'konflik_selesai', 'dilewati', 'no_santri',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class MesinSantriMapping extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'id_mesin', 'id_santri', 'nama_mesin', 'dept_mesin', 'is_active', 'catatan',
|
||||
];
|
||||
|
||||
protected $casts = ['is_active' => 'boolean'];
|
||||
|
||||
public function santri(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,553 @@
|
|||
<?php
|
||||
/**
|
||||
* EpposGLogParser.php — versi 2
|
||||
* app/Services/EpposGLogParser.php
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* PERUBAHAN UTAMA dari versi 1:
|
||||
* IOMd TIDAK lagi diabaikan. Setiap slot di shift mesin
|
||||
* (JK1 Masuk, JK1 Pulang, JK2 Masuk, JK2 Pulang, Lb Masuk, Lb Pulang)
|
||||
* bisa dipetakan ke kegiatan web yang BERBEDA.
|
||||
*
|
||||
* CONTOH MESIN SHOLAT:
|
||||
* JK1 Masuk (IOMd=2) jam 04:00 → Shubuh
|
||||
* JK1 Pulang (IOMd=4) jam 05:00 → Dhuhur
|
||||
* JK2 Masuk (IOMd=2) jam 11:45 → Ashar
|
||||
* JK2 Pulang (IOMd=4) jam 12:20 → Maghrib
|
||||
* Lb Masuk (IOMd=2) jam 15:05 → Isya
|
||||
*
|
||||
* CONTOH MESIN NGAJI:
|
||||
* JK1 Masuk (IOMd=2) jam 05:00 → Ngaji Shubuh
|
||||
* JK1 Pulang (IOMd=4) jam 06:00 → sekolah
|
||||
* JK2 Masuk (IOMd=2) jam 13:00 → Ngaji Siang
|
||||
* JK2 Pulang (IOMd=4) jam 15:00 → Ngaji Maghrib
|
||||
* Lb Masuk (IOMd=2) jam 18:00 → Ngaji Malam
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* FORMAT GLOG.TXT (Tab-Separated):
|
||||
* No | Mchn | EnNo | Name | Mode | IOMd | DateTime
|
||||
* 000001 | 1 | 000000001 | helga faisa | 1 | 2 | 2026/02/28 04:05:00
|
||||
*
|
||||
* IOMd=2 → scan MASUK (Check In)
|
||||
* IOMd=4 → scan PULANG (Check Out)
|
||||
*
|
||||
* FORMAT INFO.XLS:
|
||||
* Sheet "Shift" → No.Shift | JK1 Msuk | JK1 Kluar | JK2 Msuk | JK2 Kluar | Lb Msuk | Lb Kluar
|
||||
* Sheet "Jadwal" → No | Nama | Departemen | Shift
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class EpposGLogParser
|
||||
{
|
||||
// IOMd values dari mesin Eppos
|
||||
const IOMD_MASUK = 2;
|
||||
const IOMD_PULANG = 4;
|
||||
|
||||
// 6 slot per shift (nama slot → key di array shift)
|
||||
// Urutan ini penting untuk matching prioritas
|
||||
const SLOT_KEYS = [
|
||||
'jk1_msuk', // JK1 Masuk (IOMd=2)
|
||||
'jk1_kluar', // JK1 Pulang (IOMd=4)
|
||||
'jk2_msuk', // JK2 Masuk (IOMd=2)
|
||||
'jk2_kluar', // JK2 Pulang (IOMd=4)
|
||||
'lb_msuk', // Lembur Masuk (IOMd=2)
|
||||
'lb_kluar', // Lembur Pulang (IOMd=4)
|
||||
];
|
||||
|
||||
// Masing-masing slot → IOMd yang diharapkan
|
||||
const SLOT_IOMD = [
|
||||
'jk1_msuk' => self::IOMD_MASUK,
|
||||
'jk1_kluar' => self::IOMD_PULANG,
|
||||
'jk2_msuk' => self::IOMD_MASUK,
|
||||
'jk2_kluar' => self::IOMD_PULANG,
|
||||
'lb_msuk' => self::IOMD_MASUK,
|
||||
'lb_kluar' => self::IOMD_PULANG,
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// PARSE INFO.XLS
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse INFO.XLS → konfigurasi shift dan daftar santri di mesin
|
||||
*
|
||||
* @return array [
|
||||
* 'shifts' => [
|
||||
* 1 => [
|
||||
* 'jk1_msuk' => '04:00',
|
||||
* 'jk1_kluar' => '05:00',
|
||||
* 'jk2_msuk' => '11:45',
|
||||
* 'jk2_kluar' => '12:20',
|
||||
* 'lb_msuk' => '15:05',
|
||||
* 'lb_kluar' => null, // null = slot tidak dipakai
|
||||
* ],
|
||||
* ],
|
||||
* 'jadwal' => [
|
||||
* '1' => ['nama'=>'helga faisa', 'dept'=>'Office', 'shift'=>1],
|
||||
* ]
|
||||
* ]
|
||||
*/
|
||||
public function parseInfoFile(string $path): array
|
||||
{
|
||||
$spreadsheet = IOFactory::load($path);
|
||||
|
||||
return [
|
||||
'shifts' => $this->parseShifts($spreadsheet->getSheetByName('Shift')),
|
||||
'jadwal' => $this->parseJadwal($spreadsheet->getSheetByName('Jadwal')),
|
||||
];
|
||||
}
|
||||
|
||||
private function parseShifts($sheet): array
|
||||
{
|
||||
$shifts = [];
|
||||
// Kolom: A=No, B=JK1 Msuk, C=JK1 Kluar, D=JK2 Msuk, E=JK2 Kluar, F=Lb Msuk, G=Lb Kluar
|
||||
for ($row = 6; $row <= $sheet->getHighestRow(); $row++) {
|
||||
$no = $sheet->getCell("A{$row}")->getValue();
|
||||
if (!is_numeric($no)) continue;
|
||||
|
||||
$s = [
|
||||
'jk1_msuk' => $this->readTime($sheet->getCell("B{$row}")->getValue()),
|
||||
'jk1_kluar' => $this->readTime($sheet->getCell("C{$row}")->getValue()),
|
||||
'jk2_msuk' => $this->readTime($sheet->getCell("D{$row}")->getValue()),
|
||||
'jk2_kluar' => $this->readTime($sheet->getCell("E{$row}")->getValue()),
|
||||
'lb_msuk' => $this->readTime($sheet->getCell("F{$row}")->getValue()),
|
||||
'lb_kluar' => $this->readTime($sheet->getCell("G{$row}")->getValue()),
|
||||
];
|
||||
|
||||
// Skip shift yang semua slot-nya kosong
|
||||
$adaIsi = array_filter($s);
|
||||
if (empty($adaIsi)) continue;
|
||||
|
||||
$shifts[(int)$no] = $s;
|
||||
}
|
||||
return $shifts;
|
||||
}
|
||||
|
||||
private function parseJadwal($sheet): array
|
||||
{
|
||||
$jadwal = [];
|
||||
for ($row = 3; $row <= $sheet->getHighestRow(); $row++) {
|
||||
$no = $sheet->getCell("A{$row}")->getValue();
|
||||
$nama = $sheet->getCell("B{$row}")->getValue();
|
||||
if (!is_numeric($no) || empty($nama)) continue;
|
||||
|
||||
$jadwal[(string)(int)$no] = [
|
||||
'nama' => trim((string)$nama),
|
||||
'dept' => trim((string)($sheet->getCell("C{$row}")->getValue() ?? '')),
|
||||
'shift' => (int)($sheet->getCell("D{$row}")->getValue() ?? 1),
|
||||
];
|
||||
}
|
||||
return $jadwal;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// PARSE GLOG.TXT ← PERUBAHAN UTAMA: simpan IOMd per scan
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse GLog.txt → semua record scan, TERMASUK IOMd
|
||||
*
|
||||
* @return array [
|
||||
* [
|
||||
* 'id_mesin' => '1',
|
||||
* 'nama_mesin' => 'helga faisa',
|
||||
* 'tanggal' => '2026-02-28',
|
||||
* 'jam' => '04:05',
|
||||
* 'iomd' => 2, // ← BARU: 2=Masuk, 4=Pulang
|
||||
* 'dt_raw' => '2026/02/28 04:05:00',
|
||||
* ],
|
||||
* ]
|
||||
*/
|
||||
public function parseGLog(string $path): array
|
||||
{
|
||||
$content = file_get_contents($path);
|
||||
$content = str_replace(["\r\n", "\r"], "\n", $content);
|
||||
$lines = explode("\n", trim($content));
|
||||
$records = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) continue;
|
||||
|
||||
$cols = explode("\t", $line);
|
||||
$cols = array_values(array_filter(array_map('trim', $cols), fn($v) => $v !== ''));
|
||||
|
||||
// Minimal 7 kolom: No | Mchn | EnNo | Name | Mode | IOMd | DateTime
|
||||
if (count($cols) < 7) continue;
|
||||
if ($cols[0] === 'No') continue; // header
|
||||
|
||||
$enno = $cols[2] ?? '';
|
||||
$namaMesin = $cols[3] ?? '';
|
||||
$iomdRaw = $cols[5] ?? ''; // kolom ke-6 (index 5)
|
||||
$dtRaw = $cols[6] ?? '';
|
||||
|
||||
if (!is_numeric(ltrim($enno, '0') ?: '0')) continue;
|
||||
if (empty($dtRaw)) continue;
|
||||
|
||||
// IOMd: harus 2 atau 4
|
||||
$iomd = (int)$iomdRaw;
|
||||
if (!in_array($iomd, [self::IOMD_MASUK, self::IOMD_PULANG])) continue;
|
||||
|
||||
// Parse DateTime
|
||||
$dtRaw = preg_replace('/\s+/', ' ', trim($dtRaw));
|
||||
$parts = explode(' ', $dtRaw);
|
||||
if (count($parts) < 2) continue;
|
||||
|
||||
$tglStr = $parts[0]; // "2026/02/28"
|
||||
$jamStr = substr($parts[1], 0, 5); // "04:05"
|
||||
|
||||
if (!preg_match('/^\d{4}\/\d{2}\/\d{2}$/', $tglStr)) continue;
|
||||
if (!preg_match('/^\d{2}:\d{2}$/', $jamStr)) continue;
|
||||
|
||||
$tanggal = str_replace('/', '-', $tglStr);
|
||||
$idMesin = (string)(int)ltrim($enno, '0') ?: '0';
|
||||
|
||||
$records[] = [
|
||||
'id_mesin' => $idMesin,
|
||||
'nama_mesin' => trim($namaMesin),
|
||||
'tanggal' => $tanggal,
|
||||
'jam' => $jamStr,
|
||||
'iomd' => $iomd, // ← BARU
|
||||
'dt_raw' => $dtRaw,
|
||||
];
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GROUP BY DAY ← PERUBAHAN: scans sekarang simpan iomd
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Kelompokkan per (id_mesin + tanggal)
|
||||
* scans sekarang array of ['jam'=>'04:05','iomd'=>2]
|
||||
*
|
||||
* @return array [
|
||||
* '1_2026-02-28' => [
|
||||
* 'id_mesin' => '1',
|
||||
* 'nama_mesin' => 'helga faisa',
|
||||
* 'tanggal' => '2026-02-28',
|
||||
* 'scans' => [
|
||||
* ['jam'=>'04:05','iomd'=>2],
|
||||
* ['jam'=>'05:10','iomd'=>4],
|
||||
* ],
|
||||
* ],
|
||||
* ]
|
||||
*/
|
||||
public function groupGLogByDay(array $records): array
|
||||
{
|
||||
$grouped = [];
|
||||
|
||||
foreach ($records as $r) {
|
||||
$key = "{$r['id_mesin']}_{$r['tanggal']}";
|
||||
|
||||
if (!isset($grouped[$key])) {
|
||||
$grouped[$key] = [
|
||||
'id_mesin' => $r['id_mesin'],
|
||||
'nama_mesin' => $r['nama_mesin'],
|
||||
'tanggal' => $r['tanggal'],
|
||||
'scans' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// Hindari duplikat jam+iomd yang persis sama
|
||||
$duplikat = array_filter(
|
||||
$grouped[$key]['scans'],
|
||||
fn($s) => $s['jam'] === $r['jam'] && $s['iomd'] === $r['iomd']
|
||||
);
|
||||
if (!empty($duplikat)) continue;
|
||||
|
||||
$grouped[$key]['scans'][] = [
|
||||
'jam' => $r['jam'],
|
||||
'iomd' => $r['iomd'],
|
||||
];
|
||||
}
|
||||
|
||||
// Sort scan berurutan berdasarkan jam
|
||||
foreach ($grouped as &$g) {
|
||||
usort($g['scans'], fn($a, $b) => strcmp($a['jam'], $b['jam']));
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// MATCH TO KEGIATAN ← PERUBAHAN UTAMA
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Cocokkan setiap scan ke kegiatan web.
|
||||
*
|
||||
* LOGIKA BARU (pakai IOMd):
|
||||
* ──────────────────────────────────────────────────────────
|
||||
* 1. Ambil shift santri dari infoData['jadwal']
|
||||
* 2. Buat "slot windows" dari shift tersebut:
|
||||
* Setiap slot (jk1_msuk, jk1_kluar, dst) punya jam + IOMd
|
||||
* 3. Untuk setiap scan (jam + iomd):
|
||||
* a. Cari slot yang IOMd-nya cocok DAN jam scan masuk window ±toleransi
|
||||
* b. Dari slot yang cocok, cari kegiatan web hari ini yang waktunya paling dekat
|
||||
* 4. Hasilkan baris per kegiatan: Hadir / Terlambat / Alpa
|
||||
*
|
||||
* FALLBACK (tanpa IOMd, jika infoData kosong):
|
||||
* Jika santri tidak ada di infoData (baru daftar, seperti firda),
|
||||
* cocokkan hanya berdasarkan jam (abaikan IOMd) dengan toleransi lebih sempit.
|
||||
* ──────────────────────────────────────────────────────────
|
||||
*
|
||||
* @param array $glogGrouped Output groupGLogByDay()
|
||||
* @param array $infoData Output parseInfoFile() — ['shifts'=>[...],'jadwal'=>[...]]
|
||||
* @param array $kegiatans Dari DB: [['kegiatan_id','nama','hari','waktu_mulai','waktu_selesai'],...]
|
||||
* @param int $tolSebelum Menit toleransi SEBELUM waktu_mulai kegiatan
|
||||
* @param int $tolSesudah Menit toleransi SESUDAH waktu_selesai kegiatan
|
||||
*/
|
||||
public function matchToKegiatan(
|
||||
array $glogGrouped,
|
||||
array $infoData,
|
||||
array $kegiatans,
|
||||
int $tolSebelum = 15,
|
||||
int $tolSesudah = 10
|
||||
): array {
|
||||
$hasil = [];
|
||||
|
||||
foreach ($glogGrouped as $dayData) {
|
||||
$tanggal = $dayData['tanggal'];
|
||||
$idMesin = $dayData['id_mesin'];
|
||||
$scans = $dayData['scans']; // [['jam'=>'04:05','iomd'=>2], ...]
|
||||
$hari = $this->tanggalToHari($tanggal);
|
||||
|
||||
// Kegiatan hari ini dari web
|
||||
$kegHariIni = array_values(
|
||||
array_filter($kegiatans, fn($k) => $k['hari'] === $hari)
|
||||
);
|
||||
|
||||
// Info shift santri ini dari INFO.XLS
|
||||
$jadwalInfo = $infoData['jadwal'][$idMesin] ?? null;
|
||||
$nomorShift = $jadwalInfo ? ($jadwalInfo['shift'] ?? 1) : null;
|
||||
$shiftData = ($nomorShift && isset($infoData['shifts'][$nomorShift]))
|
||||
? $infoData['shifts'][$nomorShift]
|
||||
: null;
|
||||
|
||||
// Build slot windows dari shift santri
|
||||
// slotWindows: [ ['slot'=>'jk1_msuk','jam'=>'04:00','iomd'=>2], ... ]
|
||||
$slotWindows = $shiftData
|
||||
? $this->buildSlotWindows($shiftData)
|
||||
: [];
|
||||
|
||||
// ── Matching ────────────────────────────────────────────
|
||||
$matchedKg = []; // kegiatan_id → true (sudah dapat scan)
|
||||
$usedScans = []; // index scan yang sudah dipakai
|
||||
$rowMap = []; // kegiatan_id → result row
|
||||
|
||||
foreach ($scans as $idx => $scan) {
|
||||
$scanJam = $scan['jam'];
|
||||
$scanIomd = $scan['iomd'];
|
||||
$scanMnt = $this->toMinutes($scanJam);
|
||||
|
||||
$bestKg = null;
|
||||
$bestSelisih = PHP_INT_MAX;
|
||||
$bestSlot = null;
|
||||
|
||||
if (!empty($slotWindows)) {
|
||||
// ── MODE UTAMA: pakai IOMd dari shift ──────────────
|
||||
// Langkah 1: cari slot yang IOMd-nya cocok DAN jam dalam window
|
||||
foreach ($slotWindows as $sw) {
|
||||
if ($sw['iomd'] !== $scanIomd) continue; // IOMd harus cocok
|
||||
if ($sw['jam'] === null) continue; // slot tidak diset
|
||||
|
||||
$slotMnt = $this->toMinutes($sw['jam']);
|
||||
$windowMulai = $slotMnt - $tolSebelum;
|
||||
$windowAkhir = $slotMnt + $tolSesudah;
|
||||
|
||||
if ($scanMnt < $windowMulai || $scanMnt > $windowAkhir) continue;
|
||||
|
||||
// Slot cocok — sekarang cari kegiatan web yang paling dekat
|
||||
foreach ($kegHariIni as $kg) {
|
||||
if (isset($matchedKg[$kg['kegiatan_id']])) continue;
|
||||
|
||||
$kgMulaiMnt = $this->toMinutes($kg['waktu_mulai']);
|
||||
$kgSelesaiMnt = $this->toMinutes($kg['waktu_selesai'] ?: $kg['waktu_mulai']);
|
||||
$kgWindowMul = $kgMulaiMnt - $tolSebelum;
|
||||
$kgWindowAkh = $kgSelesaiMnt + $tolSesudah;
|
||||
|
||||
// Jam slot harus masuk window kegiatan
|
||||
if ($slotMnt < $kgWindowMul || $slotMnt > $kgWindowAkh) continue;
|
||||
|
||||
$selisih = abs($slotMnt - $kgMulaiMnt);
|
||||
if ($selisih < $bestSelisih) {
|
||||
$bestSelisih = $selisih;
|
||||
$bestKg = $kg;
|
||||
$bestSlot = $sw;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ── FALLBACK: shifts kosong, matching hanya berdasarkan jam ──
|
||||
// Pakai toleransi penuh (bukan dikurangi)
|
||||
// Cari kegiatan yang paling dekat jamnya dengan scan
|
||||
foreach ($kegHariIni as $kg) {
|
||||
if (isset($matchedKg[$kg['kegiatan_id']])) continue;
|
||||
|
||||
$kgMulaiMnt = $this->toMinutes($kg['waktu_mulai']);
|
||||
$kgSelesaiMnt = $this->toMinutes($kg['waktu_selesai'] ?: $kg['waktu_mulai']);
|
||||
|
||||
// Window: tolSebelum menit sebelum mulai s/d tolSesudah menit setelah selesai
|
||||
$kgWindowMul = $kgMulaiMnt - $tolSebelum;
|
||||
$kgWindowAkh = $kgSelesaiMnt + $tolSesudah;
|
||||
|
||||
if ($scanMnt < $kgWindowMul || $scanMnt > $kgWindowAkh) continue;
|
||||
|
||||
$selisih = abs($scanMnt - $kgMulaiMnt);
|
||||
if ($selisih < $bestSelisih) {
|
||||
$bestSelisih = $selisih;
|
||||
$bestKg = $kg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Simpan hasil match ────────────────────────────
|
||||
if ($bestKg) {
|
||||
$kgMulaiMnt = $this->toMinutes($bestKg['waktu_mulai']);
|
||||
|
||||
// Grace period: scan sampai 5 menit setelah mulai → masih Hadir
|
||||
// Lebih dari 5 menit → Terlambat
|
||||
$graceMnt = 5;
|
||||
$selisih = $scanMnt - $kgMulaiMnt;
|
||||
$status = $selisih <= $graceMnt ? 'Hadir' : 'Terlambat';
|
||||
|
||||
$matchedKg[$bestKg['kegiatan_id']] = true;
|
||||
$usedScans[] = $idx;
|
||||
|
||||
$rowMap[$bestKg['kegiatan_id']] = [
|
||||
'kegiatan_id' => $bestKg['kegiatan_id'],
|
||||
'nama_kegiatan' => $bestKg['nama'],
|
||||
'waktu_mulai' => $bestKg['waktu_mulai'],
|
||||
'jam_scan' => $scanJam,
|
||||
'iomd_scan' => $scanIomd,
|
||||
'label_iomd' => $scanIomd === self::IOMD_MASUK ? 'Masuk' : 'Pulang',
|
||||
'status' => $status,
|
||||
'selisih_menit' => max(0, $selisih - $graceMnt), // hanya menit yg melebihi grace
|
||||
'matched' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Isi Alpa untuk kegiatan tanpa scan ───────────────
|
||||
foreach ($kegHariIni as $kg) {
|
||||
if (!isset($rowMap[$kg['kegiatan_id']])) {
|
||||
$rowMap[$kg['kegiatan_id']] = [
|
||||
'kegiatan_id' => $kg['kegiatan_id'],
|
||||
'nama_kegiatan' => $kg['nama'],
|
||||
'waktu_mulai' => $kg['waktu_mulai'],
|
||||
'jam_scan' => null,
|
||||
'iomd_scan' => null,
|
||||
'label_iomd' => null,
|
||||
'status' => 'Alpa',
|
||||
'selisih_menit' => null,
|
||||
'matched' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Scan yang tidak cocok ke kegiatan apapun
|
||||
$unmatchedScans = [];
|
||||
foreach ($scans as $idx => $scan) {
|
||||
if (!in_array($idx, $usedScans)) {
|
||||
$unmatchedScans[] = $scan['jam'] . ' (' . ($scan['iomd'] === 2 ? 'Masuk' : 'Pulang') . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$rows = collect($rowMap)->sortBy('waktu_mulai')->values()->toArray();
|
||||
|
||||
$hasil[] = [
|
||||
'id_mesin' => $idMesin,
|
||||
'nama_mesin' => $dayData['nama_mesin'],
|
||||
'tanggal' => $tanggal,
|
||||
'hari' => $hari,
|
||||
'all_scans' => $scans,
|
||||
'unmatched_scans' => $unmatchedScans,
|
||||
'shift_dipakai' => $nomorShift, // ← BARU: untuk debug di preview
|
||||
'rows' => $rows,
|
||||
];
|
||||
}
|
||||
|
||||
return $hasil;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// BUILD SLOT WINDOWS dari data shift
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Dari satu shift, buat array slot windows yang bisa dicocokkan dengan scan.
|
||||
*
|
||||
* @param array $shiftData ['jk1_msuk'=>'04:00','jk1_kluar'=>'05:00', ...]
|
||||
* @return array [
|
||||
* ['slot'=>'jk1_msuk', 'jam'=>'04:00', 'iomd'=>2],
|
||||
* ['slot'=>'jk1_kluar', 'jam'=>'05:00', 'iomd'=>4],
|
||||
* ['slot'=>'jk2_msuk', 'jam'=>'11:45', 'iomd'=>2],
|
||||
* ['slot'=>'jk2_kluar', 'jam'=>'12:20', 'iomd'=>4],
|
||||
* ['slot'=>'lb_msuk', 'jam'=>'15:05', 'iomd'=>2],
|
||||
* ['slot'=>'lb_kluar', 'jam'=>null, 'iomd'=>4], // null = tidak dipakai
|
||||
* ]
|
||||
*/
|
||||
private function buildSlotWindows(array $shiftData): array
|
||||
{
|
||||
$windows = [];
|
||||
foreach (self::SLOT_KEYS as $slotKey) {
|
||||
$windows[] = [
|
||||
'slot' => $slotKey,
|
||||
'jam' => $shiftData[$slotKey] ?? null,
|
||||
'iomd' => self::SLOT_IOMD[$slotKey],
|
||||
];
|
||||
}
|
||||
return $windows;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// HELPERS
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
private function toMinutes(string $hhmm): int
|
||||
{
|
||||
if (!str_contains($hhmm, ':')) return 0;
|
||||
[$h, $m] = explode(':', $hhmm);
|
||||
return (int)$h * 60 + (int)$m;
|
||||
}
|
||||
|
||||
public function tanggalToHari(string $tanggal): string
|
||||
{
|
||||
return [
|
||||
'Monday' => 'Senin',
|
||||
'Tuesday' => 'Selasa',
|
||||
'Wednesday' => 'Rabu',
|
||||
'Thursday' => 'Kamis',
|
||||
'Friday' => 'Jumat',
|
||||
'Saturday' => 'Sabtu',
|
||||
'Sunday' => 'Ahad',
|
||||
][Carbon::parse($tanggal)->format('l')] ?? 'Senin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Baca nilai jam dari Excel — bisa berupa string "05:00" atau float (serial Excel)
|
||||
*/
|
||||
private function readTime($val): ?string
|
||||
{
|
||||
if ($val === null || $val === '') return null;
|
||||
|
||||
if (is_float($val) || (is_string($val) && is_numeric($val) && str_contains($val, '.'))) {
|
||||
$totalMin = round((float)$val * 24 * 60);
|
||||
return sprintf('%02d:%02d', intdiv($totalMin, 60), $totalMin % 60);
|
||||
}
|
||||
|
||||
$str = preg_replace('/\s+/', '', trim((string)$val));
|
||||
if (preg_match('/^(\d{1,2}):(\d{2})$/', $str, $m)) {
|
||||
return sprintf('%02d:%02d', (int)$m[1], (int)$m[2]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,8 @@
|
|||
"laravel/framework": "^10.10",
|
||||
"laravel/sanctum": "^3.3",
|
||||
"laravel/tinker": "^2.8",
|
||||
"mpdf/mpdf": "^8.2"
|
||||
"mpdf/mpdf": "^8.2",
|
||||
"phpoffice/phpspreadsheet": "^5.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "a470717879bee7fca3c22f312f8d5be7",
|
||||
"content-hash": "3a9939a01eba9d3a8fd51aa2d2dfb692",
|
||||
"packages": [
|
||||
{
|
||||
"name": "barryvdh/laravel-dompdf",
|
||||
|
|
@ -212,6 +212,85 @@
|
|||
],
|
||||
"time": "2023-12-11T17:09:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/pcre",
|
||||
"version": "3.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/pcre.git",
|
||||
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan": "<1.11.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.12 || ^2",
|
||||
"phpstan/phpstan-strict-rules": "^1 || ^2",
|
||||
"phpunit/phpunit": "^8 || ^9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"extension.neon"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Composer\\Pcre\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jordi Boggiano",
|
||||
"email": "j.boggiano@seld.be",
|
||||
"homepage": "http://seld.be"
|
||||
}
|
||||
],
|
||||
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
|
||||
"keywords": [
|
||||
"PCRE",
|
||||
"preg",
|
||||
"regex",
|
||||
"regular expression"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/composer/pcre/issues",
|
||||
"source": "https://github.com/composer/pcre/tree/3.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://packagist.com",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/composer",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-12T16:29:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dflydev/dot-access-data",
|
||||
"version": "v3.0.3",
|
||||
|
|
@ -2374,6 +2453,191 @@
|
|||
],
|
||||
"time": "2024-09-21T08:32:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maennchen/zipstream-php",
|
||||
"version": "3.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-zlib": "*",
|
||||
"php-64bit": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.7",
|
||||
"ext-zip": "*",
|
||||
"friendsofphp/php-cs-fixer": "^3.16",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"vimeo/psalm": "^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
"guzzlehttp/psr7": "^2.4",
|
||||
"psr/http-message": "^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ZipStream\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paul Duncan",
|
||||
"email": "pabs@pablotron.org"
|
||||
},
|
||||
{
|
||||
"name": "Jonatan Männchen",
|
||||
"email": "jonatan@maennchen.ch"
|
||||
},
|
||||
{
|
||||
"name": "Jesse Donat",
|
||||
"email": "donatj@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "András Kolesár",
|
||||
"email": "kolesar@kolesar.hu"
|
||||
}
|
||||
],
|
||||
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
|
||||
"keywords": [
|
||||
"stream",
|
||||
"zip"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/maennchen",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-27T12:07:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MarkBaker/PHPComplex.git",
|
||||
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Complex\\": "classes/src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Baker",
|
||||
"email": "mark@lange.demon.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "PHP Class for working with complex numbers",
|
||||
"homepage": "https://github.com/MarkBaker/PHPComplex",
|
||||
"keywords": [
|
||||
"complex",
|
||||
"mathematics"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
|
||||
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
|
||||
},
|
||||
"time": "2022-12-06T16:21:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/matrix",
|
||||
"version": "3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MarkBaker/PHPMatrix.git",
|
||||
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
|
||||
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpdocumentor/phpdocumentor": "2.*",
|
||||
"phploc/phploc": "^4.0",
|
||||
"phpmd/phpmd": "2.*",
|
||||
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||
"sebastian/phpcpd": "^4.0",
|
||||
"squizlabs/php_codesniffer": "^3.7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Matrix\\": "classes/src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Baker",
|
||||
"email": "mark@demon-angel.eu"
|
||||
}
|
||||
],
|
||||
"description": "PHP Class for working with matrices",
|
||||
"homepage": "https://github.com/MarkBaker/PHPMatrix",
|
||||
"keywords": [
|
||||
"mathematics",
|
||||
"matrix",
|
||||
"vector"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
|
||||
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
|
||||
},
|
||||
"time": "2022-12-02T22:17:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.0",
|
||||
|
|
@ -3228,6 +3492,115 @@
|
|||
},
|
||||
"time": "2020-10-15T08:29:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "5.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eecd31b885a1c8192f12738130f85bbc6e8906ba",
|
||||
"reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer/pcre": "^1||^2||^3",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-filter": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-xml": "*",
|
||||
"ext-xmlreader": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-zlib": "*",
|
||||
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": "^8.1",
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||
"dompdf/dompdf": "^2.0 || ^3.0",
|
||||
"ext-intl": "*",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"mitoteam/jpgraph": "^10.5",
|
||||
"mpdf/mpdf": "^8.1.1",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpstan/phpstan": "^1.1 || ^2.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"tecnickcom/tcpdf": "^6.5"
|
||||
},
|
||||
"suggest": {
|
||||
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
|
||||
"ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()",
|
||||
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
|
||||
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
|
||||
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Maarten Balliauw",
|
||||
"homepage": "https://blog.maartenballiauw.be"
|
||||
},
|
||||
{
|
||||
"name": "Mark Baker",
|
||||
"homepage": "https://markbakeruk.net"
|
||||
},
|
||||
{
|
||||
"name": "Franck Lefevre",
|
||||
"homepage": "https://rootslabs.net"
|
||||
},
|
||||
{
|
||||
"name": "Erik Tilt"
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
},
|
||||
{
|
||||
"name": "Owen Leibman"
|
||||
}
|
||||
],
|
||||
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
|
||||
"keywords": [
|
||||
"OpenXML",
|
||||
"excel",
|
||||
"gnumeric",
|
||||
"ods",
|
||||
"php",
|
||||
"spreadsheet",
|
||||
"xls",
|
||||
"xlsx"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.5.0"
|
||||
},
|
||||
"time": "2026-03-01T00:58:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.4",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
|
|
||||
*/
|
||||
'paths' => ['api/*', 'storage/*'],
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public function up(): void
|
|||
$table->string('id_santri', 10);
|
||||
$table->date('tanggal');
|
||||
$table->enum('status', ['Hadir', 'Izin', 'Sakit', 'Alpa']);
|
||||
$table->enum('metode_absen', ['Manual', 'RFID'])->default('Manual');
|
||||
$table->string('metode_absen', 50)->default('Manual');
|
||||
$table->time('waktu_absen')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?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('absensi_kegiatans', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('absensi_kegiatans', 'metode_absen')) {
|
||||
$table->string('metode_absen')->default('Manual')->after('status');
|
||||
// Nilai: 'Manual' | 'RFID' | 'Import_Mesin'
|
||||
}
|
||||
if (!Schema::hasColumn('absensi_kegiatans', 'konflik_catatan')) {
|
||||
$table->string('konflik_catatan')->nullable()->after('metode_absen');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('absensi_kegiatans', function (Blueprint $table) {
|
||||
$table->dropColumnIfExists('metode_absen');
|
||||
$table->dropColumnIfExists('konflik_catatan');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?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('mesin_santri_mappings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('id_mesin')->unique(); // EnNo dari GLog: '1','2','8'
|
||||
$table->string('id_santri')->nullable();
|
||||
$table->string('nama_mesin')->nullable();
|
||||
$table->string('dept_mesin')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->text('catatan')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index('id_santri');
|
||||
$table->index('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mesin_santri_mappings');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?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('import_mesin_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->integer('jumlah_scan')->default(0);
|
||||
$table->integer('berhasil')->default(0);
|
||||
$table->integer('konflik_selesai')->default(0);
|
||||
$table->integer('dilewati')->default(0);
|
||||
$table->integer('no_santri')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('import_mesin_logs');
|
||||
}
|
||||
};
|
||||
|
|
@ -142,6 +142,7 @@ @keyframes spin {
|
|||
.app-wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
|
|
@ -154,10 +155,18 @@ .sidebar {
|
|||
transition: var(--transition-base);
|
||||
flex-shrink: 0;
|
||||
box-shadow: 4px 0 15px rgba(111, 186, 157, 0.15);
|
||||
position: relative;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Hide sidebar scrollbar but keep it functional */
|
||||
.sidebar::-webkit-scrollbar { width: 0; }
|
||||
.sidebar { scrollbar-width: none; }
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px 12px;
|
||||
text-align: center;
|
||||
|
|
@ -314,6 +323,136 @@ .sidebar-toggle-btn-mobile {
|
|||
z-index: 1001;
|
||||
}
|
||||
|
||||
/* ===== SIDEBAR BRAND ===== */
|
||||
.sidebar-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 14px 10px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Icon wrapper dengan ring animasi */
|
||||
.sidebar-brand-icon-wrapper {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sidebar-brand-icon-wrapper::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -6px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.3);
|
||||
animation: brand-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sidebar-brand-icon-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.1), rgba(255,255,255,0.03));
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
@keyframes brand-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.4; }
|
||||
50% { transform: scale(1.12); opacity: 1; }
|
||||
}
|
||||
|
||||
.sidebar-brand-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 1.5rem;
|
||||
color: #FFFFFF;
|
||||
filter: drop-shadow(0 1px 4px rgba(0,0,0,0.15));
|
||||
animation: brand-float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes brand-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
/* Teks nama utama */
|
||||
.sidebar-brand-name {
|
||||
display: block;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.05em;
|
||||
line-height: 1.3;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* Sub-label PKPPS */
|
||||
.sidebar-brand-sub {
|
||||
display: block;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.55rem;
|
||||
letter-spacing: 0.35em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Ornamen garis + titik */
|
||||
.sidebar-brand-ornament {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.ornament-line {
|
||||
width: 22px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.45));
|
||||
}
|
||||
|
||||
.ornament-line:last-child {
|
||||
background: linear-gradient(90deg, rgba(255,255,255,0.45), transparent);
|
||||
}
|
||||
|
||||
.ornament-dot {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 0 4px rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
/* ===== COLLAPSED (desktop) — teks hilang, icon saja ===== */
|
||||
.sidebar.collapsed .sidebar-brand-text,
|
||||
.sidebar.collapsed .sidebar-brand-ornament {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-brand {
|
||||
padding: 12px 6px;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-brand-icon-wrapper {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-brand-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
6. MAIN CONTENT
|
||||
=================================== */
|
||||
|
|
@ -358,6 +497,8 @@ .sidebar-toggle-btn:hover {
|
|||
.main-content {
|
||||
padding: clamp(10px, 1.2vw, 16px);
|
||||
flex-grow: 1;
|
||||
transition: opacity 0.3s ease-in;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
|
|
@ -493,6 +634,43 @@ .content-box {
|
|||
border: 1px solid var(--primary-light);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
RESPONSIVE GRID UTILITY CLASSES
|
||||
Replaces inline grid-template-columns styles
|
||||
=================================== */
|
||||
.grid-2col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.grid-main-sidebar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 280px;
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.grid-main-sidebar-lg {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 340px;
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.grid-auto-fill {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.grid-content-auto {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
10. ALERTS
|
||||
=================================== */
|
||||
|
|
@ -873,6 +1051,17 @@ .table-container {
|
|||
border: 1px solid var(--primary-light);
|
||||
}
|
||||
|
||||
/* Table Wrapper - Horizontal scroll on mobile */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-wrapper .data-table {
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
14. DETAIL SECTIONS
|
||||
=================================== */
|
||||
|
|
@ -1461,20 +1650,74 @@ @media (max-width: 768px) {
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Sidebar Brand — mobile: hide text, show icon only */
|
||||
.sidebar-brand-text,
|
||||
.sidebar-brand-ornament {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
padding: 10px 6px;
|
||||
}
|
||||
|
||||
.sidebar-brand-icon-wrapper {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sidebar-brand-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Tapi kalau sidebar mobile-active (terbuka penuh), tampilkan teks */
|
||||
.sidebar.mobile-active .sidebar-brand-text,
|
||||
.sidebar.mobile-active .sidebar-brand-ornament {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar.mobile-active .sidebar-brand-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar.mobile-active .sidebar-brand {
|
||||
padding: 14px 10px 12px;
|
||||
}
|
||||
|
||||
.sidebar.mobile-active .sidebar-brand-icon-wrapper {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sidebar.mobile-active .sidebar-brand-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: clamp(0.9rem, 1.5vw, 1.1rem);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
/* Cards - 2 columns on tablet */
|
||||
.row-cards {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
/* Responsive grid utilities */
|
||||
.grid-main-sidebar,
|
||||
.grid-main-sidebar-lg {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-content-auto {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.data-table {
|
||||
font-size: 0.78rem;
|
||||
|
|
@ -1680,12 +1923,6 @@ @media (max-width: 480px) {
|
|||
.data-table td {
|
||||
padding: 5px 4px;
|
||||
}
|
||||
|
||||
/* Hide less important columns */
|
||||
.data-table th:nth-child(2),
|
||||
.data-table td:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Detail Header */
|
||||
.detail-header {
|
||||
|
|
@ -1725,6 +1962,30 @@ @media (max-width: 480px) {
|
|||
.kelas-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Row Cards - 2 columns on small mobile */
|
||||
.row-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row-cards-5 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.kpi-grid-kegiatan {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Responsive grid utilities - stack on small mobile */
|
||||
.grid-2col {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-auto-fill {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
|
|
@ -1796,9 +2057,9 @@ @media (max-width: 768px) {
|
|||
max-height: 200px !important;
|
||||
}
|
||||
|
||||
/* Cards Statistik */
|
||||
/* Cards Statistik - 2 columns */
|
||||
.row-cards {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
|
@ -1825,14 +2086,14 @@ @media (max-width: 768px) {
|
|||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Quick Links Grid - Full Width */
|
||||
/* Quick Links Grid - 2 columns */
|
||||
.content-box > div[style*="grid"] {
|
||||
grid-template-columns: 1fr !important;
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
}
|
||||
|
||||
/* Card Values */
|
||||
.card-value {
|
||||
font-size: 1.3rem !important;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
|
|
@ -1871,11 +2132,11 @@ @media (max-width: 768px) {
|
|||
|
||||
@media (max-width: 480px) {
|
||||
.row-cards {
|
||||
grid-template-columns: 1fr !important;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 1.3rem !important;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2073,7 +2334,7 @@ @media (max-width: 768px) {
|
|||
|
||||
@media (max-width: 480px) {
|
||||
.row-cards-5 {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.spp-summary {
|
||||
grid-template-columns: 1fr;
|
||||
|
|
@ -2536,7 +2797,7 @@ @media (max-width: 768px) {
|
|||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.kpi-grid-kegiatan { grid-template-columns: 1fr; }
|
||||
.kpi-grid-kegiatan { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -216,6 +216,91 @@
|
|||
border-color:#6FBA9D; background:#fff; box-shadow:0 0 0 3px rgba(111,186,157,.1);
|
||||
}
|
||||
|
||||
/* ── Demo Account Button ── */
|
||||
.lg-demo-btn {
|
||||
display:flex; align-items:center; justify-content:center; gap:8px;
|
||||
margin-top:10px; padding:9px;
|
||||
background:transparent; border:1.5px dashed #A8D8C6; border-radius:10px;
|
||||
font-size:.76rem; font-weight:600; color:#8AADA0; cursor:pointer;
|
||||
font-family:inherit; width:100%;
|
||||
transition:all .2s;
|
||||
}
|
||||
.lg-demo-btn:hover { border-color:#6FBA9D; color:#3D8A6E; background:#EBF7F2; }
|
||||
|
||||
/* ── Modal Overlay ── */
|
||||
.lg-modal-overlay {
|
||||
position:fixed; inset:0; z-index:9999;
|
||||
background:rgba(15,33,24,.45); backdrop-filter:blur(4px);
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
opacity:0; pointer-events:none; transition:opacity .25s ease;
|
||||
}
|
||||
.lg-modal-overlay.open { opacity:1; pointer-events:all; }
|
||||
|
||||
/* ── Modal Box ── */
|
||||
.lg-modal {
|
||||
background:#fff; border-radius:20px; padding:32px 28px;
|
||||
width:100%; max-width:380px; position:relative;
|
||||
box-shadow:0 20px 60px rgba(15,33,24,.18);
|
||||
transform:translateY(16px) scale(.97); transition:transform .25s ease;
|
||||
}
|
||||
.lg-modal-overlay.open .lg-modal { transform:translateY(0) scale(1); }
|
||||
.lg-modal::before {
|
||||
content:''; position:absolute; top:0; left:0; right:0; height:3px;
|
||||
background:linear-gradient(90deg,#6FBA9D,#A8D8C6,#6FBA9D);
|
||||
border-radius:20px 20px 0 0;
|
||||
}
|
||||
.lg-modal-close {
|
||||
position:absolute; top:14px; right:16px;
|
||||
background:none; border:none; font-size:1.1rem;
|
||||
color:#B8D4C8; cursor:pointer; line-height:1;
|
||||
transition:color .2s;
|
||||
}
|
||||
.lg-modal-close:hover { color:#3D8A6E; }
|
||||
.lg-modal-ico {
|
||||
width:44px; height:44px; border-radius:12px; background:#EBF7F2;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
color:#3D8A6E; font-size:1rem; margin-bottom:14px;
|
||||
}
|
||||
.lg-modal-title {
|
||||
font-family:'DM Serif Display',serif;
|
||||
font-size:1.35rem; color:#0F2118; margin-bottom:4px;
|
||||
}
|
||||
.lg-modal-sub { font-size:.75rem; color:#8AADA0; margin-bottom:20px; line-height:1.5; }
|
||||
.lg-demo-table { width:100%; border-collapse:collapse; }
|
||||
.lg-demo-table thead tr th {
|
||||
font-size:.64rem; font-weight:700; letter-spacing:1.2px;
|
||||
text-transform:uppercase; color:#8AADA0;
|
||||
padding:0 10px 8px; text-align:left;
|
||||
border-bottom:1px solid #EBF7F2;
|
||||
}
|
||||
.lg-demo-table tbody tr { transition:background .15s; }
|
||||
.lg-demo-table tbody tr:hover { background:#F4FCF8; }
|
||||
.lg-demo-table tbody tr td {
|
||||
padding:10px 10px; font-size:.78rem; color:#2A4235;
|
||||
border-bottom:1px solid #F0FAF5; vertical-align:middle;
|
||||
}
|
||||
.lg-demo-table tbody tr:last-child td { border-bottom:none; }
|
||||
.lg-role-badge {
|
||||
display:inline-block; padding:2px 8px;
|
||||
border-radius:20px; font-size:.66rem; font-weight:700;
|
||||
letter-spacing:.5px;
|
||||
}
|
||||
.badge-super { background:#FFF3E0; color:#E65100; }
|
||||
.badge-akademik { background:#E3F2FD; color:#1565C0; }
|
||||
.badge-pamong { background:#F3E5F5; color:#6A1B9A; }
|
||||
.badge-santri { background:#E8F5E9; color:#2E7D32; }
|
||||
.lg-copy-btn {
|
||||
background:none; border:none; color:#A8D8C6; cursor:pointer;
|
||||
font-size:.72rem; padding:2px 4px; border-radius:4px;
|
||||
transition:color .2s;
|
||||
}
|
||||
.lg-copy-btn:hover { color:#3D8A6E; }
|
||||
.lg-modal-note {
|
||||
margin-top:16px; padding:10px 12px;
|
||||
background:#FFFBF0; border-left:3px solid #FFD54F;
|
||||
border-radius:8px; font-size:.72rem; color:#795548; line-height:1.6;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 900px) {
|
||||
.lg-layout { gap:48px; padding:32px 36px; }
|
||||
|
|
@ -358,6 +443,12 @@
|
|||
<a href="{{ route('santri.login') }}" class="lg-santri-link">
|
||||
<i class="fas fa-user-graduate"></i> Login sebagai Santri / Wali
|
||||
</a>
|
||||
|
||||
{{-- ── Tombol Akun Demo (tambahan) ── --}}
|
||||
<button type="button" class="lg-demo-btn" id="lgDemoBtn">
|
||||
<i class="fas fa-info-circle"></i> Lihat Akun Demo untuk Pengujian
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -365,6 +456,76 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Modal Akun Demo ── --}}
|
||||
<div class="lg-modal-overlay" id="lgDemoOverlay">
|
||||
<div class="lg-modal">
|
||||
<button class="lg-modal-close" id="lgModalClose" aria-label="Tutup">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="lg-modal-ico"><i class="fas fa-users"></i></div>
|
||||
<div class="lg-modal-title">Akun Demo</div>
|
||||
|
||||
<table class="lg-demo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<th>Email / Username</th>
|
||||
<th>Password</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="lg-role-badge badge-super">Superadmin</span></td>
|
||||
<td style="font-size:.72rem;">helga.faisa06@gmail.com</td>
|
||||
<td style="font-size:.72rem;">Admin123_</td>
|
||||
<td>
|
||||
<button class="lg-copy-btn" onclick="lgCopy('helga.faisa06@gmail.com','Admin123_')" title="Isi otomatis">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="lg-role-badge badge-akademik">Akademik</span></td>
|
||||
<td style="font-size:.72rem;">akademik@test.com</td>
|
||||
<td style="font-size:.72rem;">password123</td>
|
||||
<td>
|
||||
<button class="lg-copy-btn" onclick="lgCopy('akademik@test.com','password123')" title="Isi otomatis">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="lg-role-badge badge-pamong">Pamong</span></td>
|
||||
<td style="font-size:.72rem;">pamong@test.com</td>
|
||||
<td style="font-size:.72rem;">password123</td>
|
||||
<td>
|
||||
<button class="lg-copy-btn" onclick="lgCopy('pamong@test.com','password123')" title="Isi otomatis">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="lg-role-badge badge-santri">Santri</span></td>
|
||||
<td style="font-size:.72rem;">Helga Faisa</td>
|
||||
<td style="font-size:.72rem;">s001</td>
|
||||
<td>
|
||||
<button class="lg-copy-btn" onclick="lgCopy('Helga Faisa','s001')" title="Isi otomatis">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="lg-modal-note">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Klik <i class="fas fa-arrow-right"></i> untuk mengisi form login otomatis.
|
||||
Akun santri dapat digunakan di halaman login santri.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Toggle password
|
||||
|
|
@ -420,6 +581,33 @@
|
|||
if (e.key === 'Enter') { e.preventDefault(); p.focus(); }
|
||||
});
|
||||
}
|
||||
|
||||
// ── Demo Modal ──
|
||||
const demoBtn = document.getElementById('lgDemoBtn');
|
||||
const overlay = document.getElementById('lgDemoOverlay');
|
||||
const modalClose = document.getElementById('lgModalClose');
|
||||
|
||||
function openModal() { overlay.classList.add('open'); }
|
||||
function closeModal() { overlay.classList.remove('open'); }
|
||||
|
||||
if (demoBtn) demoBtn.addEventListener('click', openModal);
|
||||
if (modalClose) modalClose.addEventListener('click', closeModal);
|
||||
if (overlay) overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) closeModal();
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-fill form dari modal
|
||||
function lgCopy(email, pass) {
|
||||
const u = document.getElementById('username');
|
||||
const p = document.getElementById('password');
|
||||
if (u) u.value = email;
|
||||
if (p) p.value = pass;
|
||||
document.getElementById('lgDemoOverlay').classList.remove('open');
|
||||
if (u) u.focus();
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Daftar Berita')
|
||||
|
||||
|
|
@ -68,6 +68,7 @@ class="form-control"
|
|||
<!-- Tabel Berita -->
|
||||
<div class="content-box">
|
||||
@if($berita->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -142,6 +143,7 @@ class="btn btn-warning btn-sm"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div style="margin-top: 14px; display: flex; justify-content: center;">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
<option value="">Semua Semester</option>
|
||||
@foreach($semesters as $semester)
|
||||
<option value="{{ $semester->id_semester }}" {{ $selectedSemester == $semester->id_semester ? 'selected' : '' }}>
|
||||
{{ $semester->nama_semester }} @if($semester->is_active) ★ @endif
|
||||
{{ $semester->nama_semester }} @if($semester->is_active) ★ @endif
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
|
@ -88,6 +88,7 @@
|
|||
</h4>
|
||||
|
||||
@if($capaians->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -151,6 +152,7 @@ class="btn btn-sm btn-warning" title="Edit">
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
|
|
@ -92,6 +92,7 @@
|
|||
@endif
|
||||
|
||||
@if($santriData->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -150,6 +151,7 @@ class="btn btn-sm btn-primary" title="Lihat Detail Capaian">
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox"></i>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
|
|
@ -106,6 +106,8 @@
|
|||
Kategori: {{ $kategori }}
|
||||
</h4>
|
||||
|
||||
<div class="table-wrapper">
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -157,6 +159,8 @@ class="btn btn-sm btn-warning" title="Edit">
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{{-- resources/views/admin/dashboard/_jadwal-kegiatan.blade.php --}}
|
||||
{{-- resources/views/admin/dashboard/_jadwal-kegiatan.blade.php --}}
|
||||
<div class="content-box" style="margin-bottom:16px;">
|
||||
<h4 style="margin:0 0 12px;font-size:.88rem;font-weight:700;color:var(--text-color);display:flex;align-items:center;gap:8px;">
|
||||
<span style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;background:linear-gradient(135deg,var(--primary-color),var(--primary-dark));border-radius:6px;flex-shrink:0;">
|
||||
<i class="fas fa-calendar-day" style="font-size:.7rem;color:#fff;"></i>
|
||||
</span>
|
||||
Jadwal Kegiatan — {{ $hari }}
|
||||
Jadwal Kegiatan {{ $hari }}
|
||||
</h4>
|
||||
|
||||
@if($kegiatan->isEmpty())
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
</div>
|
||||
@else
|
||||
<div class="table-responsive" style="overflow-x:auto;">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" style="margin-top:0;">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -40,7 +41,7 @@
|
|||
</td>
|
||||
<td style="font-size:.78rem;font-weight:600;white-space:nowrap;color:var(--text-color);">
|
||||
{{ is_string($k->waktu_mulai) ? $k->waktu_mulai : $k->waktu_mulai->format('H:i') }}
|
||||
<span style="color:var(--text-light);margin:0 2px;">–</span>
|
||||
<span style="color:var(--text-light);margin:0 2px;"> - </span>
|
||||
{{ is_string($k->waktu_selesai) ? $k->waktu_selesai : $k->waktu_selesai->format('H:i') }}
|
||||
</td>
|
||||
<td>
|
||||
|
|
@ -68,13 +69,14 @@
|
|||
<span style="color:#bbb;">({{ $k->total_absensi }} data)</span>
|
||||
</small>
|
||||
@else
|
||||
<small class="text-muted">—</small>
|
||||
<small class="text-muted"></small>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Master Pelanggaran')
|
||||
|
||||
|
|
@ -80,6 +80,7 @@
|
|||
</div>
|
||||
|
||||
@if($data->isNotEmpty())
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -160,6 +161,7 @@ class="btn btn-sm btn-danger"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detail Pelanggaran')
|
||||
|
||||
|
|
@ -75,6 +75,7 @@
|
|||
<h3 style="color: var(--primary-color); margin-bottom: 15px;">
|
||||
<i class="fas fa-history"></i> Riwayat Penggunaan (5 Terbaru)
|
||||
</h3>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -112,6 +113,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="btn-group" style="margin-top: 22px;">
|
||||
|
|
|
|||
|
|
@ -33,9 +33,10 @@
|
|||
@if($kegiatanInfo['is_umum'])
|
||||
<strong>Kegiatan Umum</strong> - Diikuti oleh semua santri aktif ({{ $santris->count() }} santri)
|
||||
@else
|
||||
<strong>Kegiatan Khusus</strong> - Diikuti oleh kelas:
|
||||
<strong>Kegiatan Khusus</strong> - Untuk kelas:
|
||||
<strong style="color: var(--primary-color);">{{ $kegiatanInfo['kelas_list'] }}</strong>
|
||||
({{ $kegiatanInfo['jumlah_kelas'] }} kelas, {{ $santris->count() }} santri)
|
||||
({{ $kegiatanInfo['jumlah_kelas'] }} kelas)
|
||||
| Total semua santri aktif: <strong>{{ $santris->count() }} santri</strong>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
@ -46,7 +47,9 @@
|
|||
@if($sudahAdaData)
|
||||
<div class="alert alert-info" style="margin-bottom: 14px;">
|
||||
<i class="fas fa-edit"></i>
|
||||
<strong>Mode Edit</strong> - Data absensi untuk tanggal ini sudah ada ({{ count($absensiData) }} santri).
|
||||
<strong>Mode Edit</strong> - Data absensi untuk tanggal ini sudah ada
|
||||
(<strong>{{ count($absensiData) }}</strong> dari <strong>{{ $santris->count() }}</strong> santri sudah diinput,
|
||||
<strong style="color: {{ ($santris->count() - count($absensiData)) > 0 ? '#dc2626' : '#059669' }};">{{ $santris->count() - count($absensiData) }} belum absen</strong>).
|
||||
Anda dapat mengubah status absensi lalu klik Simpan.
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -124,6 +127,7 @@
|
|||
<span class="badge badge-primary">{{ $totalKelas }} santri</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" style="margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -202,6 +206,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- views/admin/kegiatan/absensi/rekap.blade.php --}}
|
||||
{{-- views/admin/kegiatan/absensi/rekap.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
|
@ -6,6 +6,77 @@
|
|||
<h2><i class="fas fa-chart-bar"></i> Rekap Absensi: {{ $kegiatan->nama_kegiatan }}</h2>
|
||||
</div>
|
||||
|
||||
{{-- Ringkasan Total Santri & Progress --}}
|
||||
<div style="background: #fff; border-radius: 12px; padding: 18px 22px; margin-bottom: 16px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); border-left: 4px solid #2563eb;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px;">
|
||||
<div>
|
||||
<h3 style="margin: 0; font-size: 1rem; color: #1a2332;">
|
||||
<i class="fas fa-users" style="color: #2563eb;"></i> Total Semua Santri: <strong>{{ $totalSantriEligible }}</strong>
|
||||
</h3>
|
||||
<p style="margin: 4px 0 0; font-size: 0.84rem; color: #6b7280;">
|
||||
Sudah absen: <strong style="color: #059669;">{{ $santriSudahAbsen }}</strong>
|
||||
·
|
||||
Belum absen: <strong style="color: {{ $belumAbsen > 0 ? '#dc2626' : '#059669' }};">{{ $belumAbsen }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="font-size: 1.6rem; font-weight: 800; color: {{ $persenHadir >= 85 ? '#059669' : ($persenHadir >= 70 ? '#d97706' : '#dc2626') }};">
|
||||
{{ $persenHadir }}%
|
||||
</div>
|
||||
<div style="font-size: 0.78rem; color: #6b7280;">Kehadiran</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- Progress bar --}}
|
||||
<div style="height: 28px; background: #f3f4f6; border-radius: 14px; overflow: hidden; display: flex;">
|
||||
@php
|
||||
$pctHadir = $totalSantriEligible > 0 ? round(($stats['Hadir'] ?? 0) / $totalSantriEligible * 100, 1) : 0;
|
||||
$pctTerlambat = $totalSantriEligible > 0 ? round(($stats['Terlambat'] ?? 0) / $totalSantriEligible * 100, 1) : 0;
|
||||
$pctIzin = $totalSantriEligible > 0 ? round(($stats['Izin'] ?? 0) / $totalSantriEligible * 100, 1) : 0;
|
||||
$pctSakit = $totalSantriEligible > 0 ? round(($stats['Sakit'] ?? 0) / $totalSantriEligible * 100, 1) : 0;
|
||||
$pctAlpa = $totalSantriEligible > 0 ? round(($stats['Alpa'] ?? 0) / $totalSantriEligible * 100, 1) : 0;
|
||||
$pctBelum = $totalSantriEligible > 0 ? round($belumAbsen / $totalSantriEligible * 100, 1) : 0;
|
||||
@endphp
|
||||
@if($pctHadir > 0)
|
||||
<div style="width: {{ $pctHadir }}%; background: #22c55e; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.73rem; font-weight: 700;" title="Hadir: {{ $stats['Hadir'] ?? 0 }}">
|
||||
{{ ($stats['Hadir'] ?? 0) > 0 ? ($stats['Hadir'] ?? 0) : '' }}
|
||||
</div>
|
||||
@endif
|
||||
@if($pctTerlambat > 0)
|
||||
<div style="width: {{ $pctTerlambat }}%; background: #FF9800; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.73rem; font-weight: 700;" title="Terlambat: {{ $stats['Terlambat'] ?? 0 }}">
|
||||
{{ ($stats['Terlambat'] ?? 0) > 0 ? ($stats['Terlambat'] ?? 0) : '' }}
|
||||
</div>
|
||||
@endif
|
||||
@if($pctIzin > 0)
|
||||
<div style="width: {{ $pctIzin }}%; background: #f59e0b; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.73rem; font-weight: 700;" title="Izin: {{ $stats['Izin'] ?? 0 }}">
|
||||
{{ ($stats['Izin'] ?? 0) > 0 ? ($stats['Izin'] ?? 0) : '' }}
|
||||
</div>
|
||||
@endif
|
||||
@if($pctSakit > 0)
|
||||
<div style="width: {{ $pctSakit }}%; background: #3b82f6; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.73rem; font-weight: 700;" title="Sakit: {{ $stats['Sakit'] ?? 0 }}">
|
||||
{{ ($stats['Sakit'] ?? 0) > 0 ? ($stats['Sakit'] ?? 0) : '' }}
|
||||
</div>
|
||||
@endif
|
||||
@if($pctAlpa > 0)
|
||||
<div style="width: {{ $pctAlpa }}%; background: #ef4444; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.73rem; font-weight: 700;" title="Alpa: {{ $stats['Alpa'] ?? 0 }}">
|
||||
{{ ($stats['Alpa'] ?? 0) > 0 ? ($stats['Alpa'] ?? 0) : '' }}
|
||||
</div>
|
||||
@endif
|
||||
@if($pctBelum > 0)
|
||||
<div style="width: {{ $pctBelum }}%; background: #d1d5db; display: flex; align-items: center; justify-content: center; color: #6b7280; font-size: 0.73rem; font-weight: 700;" title="Belum Absen: {{ $belumAbsen }}">
|
||||
{{ $belumAbsen > 0 ? $belumAbsen : '' }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div style="display: flex; gap: 14px; flex-wrap: wrap; margin-top: 8px; font-size: 0.75rem; color: #6b7280;">
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#22c55e;margin-right:3px;"></span> Hadir</span>
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#FF9800;margin-right:3px;"></span> Terlambat</span>
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#f59e0b;margin-right:3px;"></span> Izin</span>
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#3b82f6;margin-right:3px;"></span> Sakit</span>
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#ef4444;margin-right:3px;"></span> Alpa</span>
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#d1d5db;margin-right:3px;"></span> Belum Absen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-cards">
|
||||
<div class="card card-success">
|
||||
<h3>Hadir</h3>
|
||||
|
|
@ -32,6 +103,11 @@
|
|||
<div class="card-value">{{ $stats['Alpa'] ?? 0 }}</div>
|
||||
<i class="fas fa-times-circle card-icon"></i>
|
||||
</div>
|
||||
<div class="card" style="border-top: 3px solid #9ca3af;">
|
||||
<h3>Belum Absen</h3>
|
||||
<div class="card-value" style="color: {{ $belumAbsen > 0 ? '#dc2626' : '#6b7280' }};">{{ $belumAbsen }}</div>
|
||||
<i class="fas fa-hourglass-half card-icon" style="color: #9ca3af;"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-box">
|
||||
|
|
@ -75,6 +151,8 @@
|
|||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="table-wrapper">
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -99,6 +177,8 @@
|
|||
<td>
|
||||
@if($absensi->metode_absen == 'RFID')
|
||||
<span class="badge badge-primary"><i class="fas fa-id-card"></i> RFID</span>
|
||||
@elseif($absensi->metode_absen == 'Import_Mesin')
|
||||
<span class="badge" style="background: #7c3aed; color: white;"><i class="fas fa-desktop"></i> Mesin</span>
|
||||
@else
|
||||
<span class="badge badge-secondary"><i class="fas fa-hand-pointer"></i> Manual</span>
|
||||
@endif
|
||||
|
|
@ -122,6 +202,8 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
|
|
@ -134,5 +216,50 @@
|
|||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Daftar santri yang belum absen --}}
|
||||
@if($santriBelumAbsen->count() > 0)
|
||||
<div class="content-box" style="margin-top: 18px; border-left: 4px solid #f59e0b;">
|
||||
<h4 style="margin: 0 0 12px; color: #d97706;">
|
||||
<i class="fas fa-exclamation-triangle"></i> Santri Belum Absen ({{ $santriBelumAbsen->count() }} orang)
|
||||
@if(request('tanggal'))
|
||||
<span style="font-size: 0.8rem; font-weight: 400; color: #6b7280; margin-left: 6px;">
|
||||
Tanggal: {{ \Carbon\Carbon::parse(request('tanggal'))->format('d/m/Y') }}
|
||||
</span>
|
||||
@endif
|
||||
</h4>
|
||||
|
||||
<div class="table-wrapper">
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50px;">No</th>
|
||||
<th style="width: 100px;">ID Santri</th>
|
||||
<th>Nama Santri</th>
|
||||
<th style="width: 150px;">Kelas</th>
|
||||
<th style="width: 120px; text-align: center;">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($santriBelumAbsen as $index => $santri)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
<td><strong>{{ $santri->id_santri }}</strong></td>
|
||||
<td>{{ $santri->nama_lengkap }}</td>
|
||||
<td>{{ optional(optional($santri->kelasPrimary)->kelas)->nama_kelas ?? '-' }}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge" style="background: #fef3c7; color: #92400e; padding: 4px 10px; border-radius: 12px; font-size: 0.8rem;">
|
||||
<i class="fas fa-hourglass-half"></i> Belum Absen
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -29,8 +29,20 @@
|
|||
{{-- ============================================================ --}}
|
||||
{{-- PAGE HEADER --}}
|
||||
{{-- ============================================================ --}}
|
||||
<div class="page-header">
|
||||
<div class="page-header" style="display:flex; align-items:center; justify-content:space-between;">
|
||||
<h2><i class="fas fa-tachometer-alt"></i> Dashboard Absensi</h2>
|
||||
|
||||
<div style="display:flex; gap:8px;">
|
||||
<a href="{{ route('admin.mesin.mapping-santri.index') }}"
|
||||
class="btn btn-sm btn-secondary">
|
||||
<i class="fas fa-link"></i> Mapping Fingerprint
|
||||
</a>
|
||||
<a href="{{ route('admin.mesin.import.index') }}"
|
||||
class="btn btn-sm btn-success"
|
||||
style="background:#0F7B6C; border-color:#0F7B6C;">
|
||||
<i class="fas fa-file-import"></i> Import
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="color: var(--text-light); margin-top: 5px; margin-bottom: 14px;">
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@
|
|||
<span class="badge badge-primary">{{ $kegiatanHari->count() }} kegiatan</span>
|
||||
</h4>
|
||||
|
||||
<div class="table-wrapper">
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -165,6 +167,8 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -145,6 +145,10 @@
|
|||
<span style="background: #6FBAA5; color: white; padding: 3px 8px; border-radius: 8px; font-size: 0.75rem;">
|
||||
<i class="fas fa-id-card"></i> RFID
|
||||
</span>
|
||||
@elseif($absensi->metode_absen == 'Import_Mesin')
|
||||
<span style="background: #7c3aed; color: white; padding: 3px 8px; border-radius: 8px; font-size: 0.75rem;">
|
||||
<i class="fas fa-desktop"></i> Mesin
|
||||
</span>
|
||||
@else
|
||||
<span style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 8px; font-size: 0.75rem;">
|
||||
<i class="fas fa-hand-pointer"></i> Manual
|
||||
|
|
@ -166,6 +170,56 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Daftar Santri Belum Absen --}}
|
||||
@if(isset($santriBelumAbsen) && $santriBelumAbsen->count() > 0)
|
||||
<div style="margin-top: 18px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<h5 style="color: #d97706; margin: 0;">
|
||||
<i class="fas fa-exclamation-triangle"></i> Santri Belum Absen ({{ $santriBelumAbsen->count() }} orang)
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
@foreach($santriBelumAbsenPerKelas as $namaKelas => $kelasSantris)
|
||||
<div style="margin-bottom: 12px;">
|
||||
<div style="background: linear-gradient(135deg, #fef3c7, #fef9c3); padding: 8px 14px; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h6 style="margin: 0; color: #92400e; font-size: 0.9rem;">
|
||||
<i class="fas fa-school"></i> {{ $namaKelas }}
|
||||
</h6>
|
||||
<span style="background: #f59e0b; color: white; padding: 2px 10px; border-radius: 10px; font-size: 0.75rem; font-weight: 600;">
|
||||
{{ $kelasSantris->count() }} santri
|
||||
</span>
|
||||
</div>
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #fde68a; border-top: 0; border-radius: 0 0 8px 8px;">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead style="position: sticky; top: 0; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<tr style="border-bottom: 2px solid #fde68a;">
|
||||
<th style="padding: 8px; text-align: left; font-size: 0.8rem; color: #6c757d;">No</th>
|
||||
<th style="padding: 8px; text-align: left; font-size: 0.8rem; color: #6c757d;">ID</th>
|
||||
<th style="padding: 8px; text-align: left; font-size: 0.8rem; color: #6c757d;">Nama Santri</th>
|
||||
<th style="padding: 8px; text-align: center; font-size: 0.8rem; color: #6c757d;">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($kelasSantris as $idx => $santri)
|
||||
<tr style="border-bottom: 1px solid #fef3c7;">
|
||||
<td style="padding: 6px 8px; font-size: 0.85rem;">{{ $idx + 1 }}</td>
|
||||
<td style="padding: 6px 8px; font-size: 0.85rem; font-weight: 600;">{{ $santri->id_santri }}</td>
|
||||
<td style="padding: 6px 8px; font-size: 0.85rem;">{{ $santri->nama_lengkap }}</td>
|
||||
<td style="padding: 6px 8px; text-align: center;">
|
||||
<span style="background: #fef3c7; color: #92400e; padding: 3px 8px; border-radius: 10px; font-size: 0.75rem; font-weight: 600;">
|
||||
<i class="fas fa-hourglass-half"></i> Belum
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Action Buttons --}}
|
||||
<div style="margin-top: 25px; padding-top: 20px; border-top: 2px solid #e9ecef; display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<a href="{{ route('admin.absensi-kegiatan.input', $kegiatan->kegiatan_id) }}?tanggal={{ $tanggal }}"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding: 1.5mm 2mm;
|
||||
overflow: hidden;
|
||||
}
|
||||
.h-sub {
|
||||
display:block; font-size:3.5pt; color:#a89060;
|
||||
|
|
@ -62,6 +63,7 @@
|
|||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── DIVIDER 1.6mm ── */
|
||||
|
|
@ -85,22 +87,27 @@
|
|||
vertical-align: middle;
|
||||
text-align: center;
|
||||
padding: 0.5mm 3mm;
|
||||
overflow: hidden;
|
||||
}
|
||||
.nama-box {
|
||||
background: #0d1f3c;
|
||||
border: 0.5mm solid #c9a227;
|
||||
border-radius: 1mm;
|
||||
text-align: center;
|
||||
padding: 1.8mm 2mm;
|
||||
padding: 1.2mm 1.5mm;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
max-height: 7.5mm;
|
||||
}
|
||||
.nama-text {
|
||||
font-size:8pt; font-weight:bold; color:#fff;
|
||||
letter-spacing:0.5pt; font-family:Georgia,serif;
|
||||
font-size:6pt; font-weight:bold; color:#fff;
|
||||
letter-spacing:0.3pt; font-family:Georgia,serif;
|
||||
text-align: center;
|
||||
display: block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── INFO 9mm ── */
|
||||
|
|
@ -109,6 +116,7 @@
|
|||
background: #0b1a2e;
|
||||
vertical-align: middle;
|
||||
padding: 1mm 3mm 0 3mm;
|
||||
overflow: hidden;
|
||||
}
|
||||
.info-t {
|
||||
width:48mm; height:8mm;
|
||||
|
|
@ -118,14 +126,15 @@
|
|||
}
|
||||
.info-t td {
|
||||
height:8mm; padding:0 1.5mm; vertical-align:middle;
|
||||
overflow:hidden;
|
||||
}
|
||||
.ic-nis { width:30%; }
|
||||
.ic-kelas { width:37%; border-left:0.4mm solid rgba(201,162,39,0.5); border-right:0.4mm solid rgba(201,162,39,0.5); }
|
||||
.ic-rfid { width:33%; }
|
||||
.lbl { display:block; font-size:3pt; color:#c9a227; font-weight:bold; letter-spacing:0.3pt; text-transform:uppercase; margin-bottom:0.5mm; }
|
||||
.val { display:block; font-size:5.5pt; color:#fff; font-weight:bold; }
|
||||
.val-sm { display:block; font-size:4.5pt; color:#fff; font-weight:bold; }
|
||||
.val-rfid { display:block; font-size:3.5pt; color:#90c4f0; font-weight:bold; font-family:monospace; word-break:break-all; }
|
||||
.val { display:block; font-size:4.5pt; color:#fff; font-weight:bold; white-space:nowrap; overflow:hidden; }
|
||||
.val-sm { display:block; font-size:4pt; color:#fff; font-weight:bold; overflow:hidden; }
|
||||
.val-rfid { display:block; font-size:3pt; color:#90c4f0; font-weight:bold; font-family:monospace; word-break:break-all; overflow:hidden; }
|
||||
|
||||
/* ── BOTTOM 9mm ── */
|
||||
.td-bottom {
|
||||
|
|
@ -153,39 +162,47 @@
|
|||
<span class="h-loc">PKPPS Riyadlul Jannah</span>
|
||||
</td></tr>
|
||||
|
||||
{{-- FOTO: SVG dengan clipPath lingkaran --}}
|
||||
{{-- FOTO: teknik "mask ring" — 100% kompatibel mPDF --}}
|
||||
<tr><td class="td-foto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="34mm" height="34mm"
|
||||
viewBox="0 0 100 100"
|
||||
overflow="hidden"
|
||||
style="display:block;margin:0 auto;">
|
||||
<defs>
|
||||
<clipPath id="cp">
|
||||
<circle cx="50" cy="50" r="44"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
{{-- Background --}}
|
||||
<circle cx="50" cy="50" r="44" fill="#1a2f4a"/>
|
||||
{{-- 1. Background bulat (terlihat jika tidak ada foto) --}}
|
||||
<circle cx="50" cy="50" r="37" fill="#1a2f4a"/>
|
||||
|
||||
@if($fotoBase64 !== '')
|
||||
{{-- Foto: x=6,y=6 supaya pas dalam circle r=44 → lebar=88 --}}
|
||||
<image x="6" y="6" width="88" height="88"
|
||||
clip-path="url(#cp)"
|
||||
{{-- 2. Foto persegi biasa — sengaja lebih besar dari lingkaran --}}
|
||||
<image x="10" y="10" width="80" height="80"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
xlink:href="data:{{ $fotoMime }};base64,{{ $fotoBase64 }}"
|
||||
href="data:{{ $fotoMime }};base64,{{ $fotoBase64 }}"/>
|
||||
@else
|
||||
<text x="50" y="50" text-anchor="middle" dominant-baseline="central"
|
||||
font-size="36" font-weight="bold" fill="#c9a227"
|
||||
font-size="34" font-weight="bold" fill="#c9a227"
|
||||
font-family="Georgia">{{ $initial }}</text>
|
||||
@endif
|
||||
|
||||
{{-- Ring emas luar --}}
|
||||
<circle cx="50" cy="50" r="48" fill="none" stroke="#c9a227" stroke-width="3.5"/>
|
||||
{{-- Ring tipis dalam --}}
|
||||
<circle cx="50" cy="50" r="44" fill="none" stroke="#8b6914" stroke-width="1"/>
|
||||
{{-- 3. MASK RING — lingkaran tebal warna background menutupi
|
||||
semua bagian foto di luar radius 37.
|
||||
r=54 sw=34 → inner=54-17=37, outer=54+17=71 (sampai sudut) --}}
|
||||
<circle cx="50" cy="50" r="54" fill="none"
|
||||
stroke="#0b1a2e" stroke-width="34"/>
|
||||
|
||||
{{-- 4. Bingkai emas utama --}}
|
||||
<circle cx="50" cy="50" r="38.5" fill="none"
|
||||
stroke="#c9a227" stroke-width="3"/>
|
||||
|
||||
{{-- 5. Aksen tipis luar --}}
|
||||
<circle cx="50" cy="50" r="41" fill="none"
|
||||
stroke="rgba(201,162,39,0.4)" stroke-width="0.6"/>
|
||||
|
||||
{{-- 6. Aksen tipis dalam --}}
|
||||
<circle cx="50" cy="50" r="36" fill="none"
|
||||
stroke="rgba(139,105,20,0.5)" stroke-width="0.5"/>
|
||||
</svg>
|
||||
</td></tr>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
</div>
|
||||
|
||||
@if($santris->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -94,6 +95,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 14px;">
|
||||
{{ $santris->links() }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
</div>
|
||||
|
||||
@if($kategoris->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -75,6 +76,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 14px;">
|
||||
{{ $kategoris->links() }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
|
|
@ -121,6 +121,7 @@
|
|||
@if(!empty($breakdownPerKelas) && count($breakdownPerKelas) > 0)
|
||||
<div class="chart-box">
|
||||
<h4><i class="fas fa-school"></i> Breakdown Per Kelas</h4>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" style="font-size:0.85rem;">
|
||||
<thead><tr><th>Kelas</th><th class="text-center">Total</th><th class="text-center">Hadir</th><th class="text-center" style="min-width:180px;">% Kehadiran</th></tr></thead>
|
||||
<tbody>
|
||||
|
|
@ -139,6 +140,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
@if($streak > 0)
|
||||
<div class="insight-box i-success">
|
||||
<i class="fas fa-fire"></i>
|
||||
Streak kehadiran beruntun: <strong>{{ $streak }} kegiatan</strong> 🔥
|
||||
Streak kehadiran beruntun: <strong>{{ $streak }} kegiatan</strong> ðŸâ€Â¥
|
||||
</div>
|
||||
@endif
|
||||
@if(($stats->total ?? 0) > 0)
|
||||
|
|
@ -131,6 +131,7 @@
|
|||
<div class="chart-box">
|
||||
<h4><i class="fas fa-tasks"></i> Kehadiran Per Kegiatan</h4>
|
||||
@if($perKegiatan->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" style="font-size:0.85rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -159,6 +160,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p style="color:var(--text-light); font-size:0.85rem;">Belum ada data kehadiran per kegiatan.</p>
|
||||
@endif
|
||||
|
|
@ -168,6 +170,7 @@
|
|||
<div class="chart-box">
|
||||
<h4><i class="fas fa-history"></i> Riwayat Absensi Terbaru</h4>
|
||||
@if($riwayatTerbaru->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" style="font-size:0.85rem;">
|
||||
<thead><tr><th>Tanggal</th><th>Kegiatan</th><th>Kategori</th><th class="text-center">Status</th></tr></thead>
|
||||
<tbody>
|
||||
|
|
@ -181,6 +184,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p style="color:var(--text-light);">Belum ada riwayat.</p>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -385,6 +385,7 @@ class="item-name" style="color: inherit; text-decoration: none;">
|
|||
<details>
|
||||
<summary>{{ $kelompok['nama_kelompok'] }} ({{ count($kelompok['kelas']) }} kelas)</summary>
|
||||
<div class="kelas-detail-body">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" style="font-size: 0.85rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -409,6 +410,7 @@ class="item-name" style="color: inherit; text-decoration: none;">
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
|
||||
<div class="content-box">
|
||||
@if($santris->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" style="font-size:0.85rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -83,6 +84,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px;">
|
||||
{{ $santris->links() }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/kegiatan/riwayat/detail-santri.blade.php --}}
|
||||
{{-- resources/views/admin/kegiatan/riwayat/detail-santri.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
|
@ -73,6 +73,7 @@
|
|||
<h3 style="margin: 0 0 20px 0; color: var(--primary-color);">
|
||||
<i class="fas fa-layer-group"></i> Kehadiran Per Kelas
|
||||
</h3>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -109,6 +110,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
@ -119,6 +121,7 @@
|
|||
</h3>
|
||||
|
||||
@if($riwayats->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -143,6 +146,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 14px;">
|
||||
{{ $riwayats->links() }}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,47 @@ class="btn-back">
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Ringkasan Total Santri --}}
|
||||
@if(isset($totalSantriEligible))
|
||||
<div style="background: #fff; border-radius: 12px; padding: 16px 20px; margin-bottom: 14px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); border-left: 4px solid #2563eb;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 10px;">
|
||||
<div>
|
||||
<h4 style="margin: 0; font-size: 1rem; color: #1a2332;">
|
||||
<i class="fas fa-users" style="color: #2563eb;"></i> Total Semua Santri: <strong>{{ $totalSantriEligible }}</strong>
|
||||
</h4>
|
||||
<p style="margin: 4px 0 0; font-size: 0.84rem; color: #6b7280;">
|
||||
Sudah absen: <strong style="color: #059669;">{{ $totalRecorded }}</strong>
|
||||
·
|
||||
Belum absen: <strong style="color: {{ ($totalSantriEligible - $totalRecorded) > 0 ? '#dc2626' : '#059669' }};">{{ max(0, $totalSantriEligible - $totalRecorded) }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="font-size: 1.5rem; font-weight: 800; color: {{ $persenHadir >= 85 ? '#059669' : ($persenHadir >= 70 ? '#d97706' : '#dc2626') }};">
|
||||
{{ $persenHadir }}%
|
||||
</div>
|
||||
<div style="font-size: 0.78rem; color: #6b7280;">Kehadiran</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- Progress bar --}}
|
||||
@php
|
||||
$pctSudah = $totalSantriEligible > 0 ? round($totalRecorded / $totalSantriEligible * 100, 1) : 0;
|
||||
$pctBelumRiwayat = 100 - $pctSudah;
|
||||
@endphp
|
||||
<div style="height: 24px; background: #f3f4f6; border-radius: 12px; overflow: hidden; display: flex;">
|
||||
@if($pctSudah > 0)
|
||||
<div style="width: {{ $pctSudah }}%; background: linear-gradient(90deg, #22c55e, #16a34a); display: flex; align-items: center; justify-content: center; color: white; font-size: 0.73rem; font-weight: 700;" title="Sudah Absen: {{ $totalRecorded }}">
|
||||
{{ $totalRecorded }}
|
||||
</div>
|
||||
@endif
|
||||
@if($pctBelumRiwayat > 0 && ($totalSantriEligible - $totalRecorded) > 0)
|
||||
<div style="width: {{ $pctBelumRiwayat }}%; background: #d1d5db; display: flex; align-items: center; justify-content: center; color: #6b7280; font-size: 0.73rem; font-weight: 700;" title="Belum Absen: {{ $totalSantriEligible - $totalRecorded }}">
|
||||
{{ $totalSantriEligible - $totalRecorded }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 6 KPI Cards --}}
|
||||
<div class="stats-row">
|
||||
<div class="stat-card hadir">
|
||||
|
|
@ -274,6 +315,27 @@ class="btn-reset">
|
|||
$dayAlpa = $records->where('status', 'Alpa')->count();
|
||||
$dayPulang = $records->where('status', 'Pulang')->count();
|
||||
$dayTotal = $records->count();
|
||||
|
||||
// Group per kelas kegiatan (khusus) atau kelas_name santri (umum)
|
||||
$isUmum = $kegiatan->kelasKegiatan->isEmpty();
|
||||
if ($isUmum) {
|
||||
$recordsPerKelas = $records->groupBy(fn($r) =>
|
||||
optional(optional($r->santri->kelasSantri->first())->kelas)->nama_kelas ?? 'Tanpa Kelas'
|
||||
)->sortKeys();
|
||||
} else {
|
||||
$recordsPerKelas = collect();
|
||||
$placedIds = [];
|
||||
foreach ($kegiatan->kelasKegiatan as $kls) {
|
||||
$inKelas = $records->filter(function($r) use ($kls, &$placedIds) {
|
||||
if (in_array($r->id, $placedIds)) return false;
|
||||
return $r->santri->kelasSantri->contains('id_kelas', $kls->id);
|
||||
});
|
||||
foreach ($inKelas as $r) $placedIds[] = $r->id;
|
||||
if ($inKelas->count() > 0) $recordsPerKelas[$kls->nama_kelas] = $inKelas;
|
||||
}
|
||||
$lainnya = $records->filter(fn($r) => !in_array($r->id, $placedIds));
|
||||
if ($lainnya->count() > 0) $recordsPerKelas['Kelas Lain'] = $lainnya;
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="day-group">
|
||||
|
|
@ -306,20 +368,28 @@ class="btn-reset">
|
|||
</div>
|
||||
</div>
|
||||
<div class="day-body">
|
||||
@foreach($recordsPerKelas as $namaKelas => $kelasRecords)
|
||||
<div style="background: linear-gradient(135deg, #f0fdf4, #e8f5e9); padding: 8px 18px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e2e8f0;">
|
||||
<span style="font-size: 0.85rem; font-weight: 600; color: #065f46;">
|
||||
<i class="fas fa-school"></i> {{ $namaKelas }}
|
||||
</span>
|
||||
<span style="background: #6FBAA5; color: white; padding: 2px 10px; border-radius: 10px; font-size: 0.75rem; font-weight: 600;">
|
||||
{{ $kelasRecords->count() }} santri
|
||||
</span>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 45px;">No</th>
|
||||
<th style="width: 90px;">ID Santri</th>
|
||||
<th>Nama Santri</th>
|
||||
<th style="width: 140px;">Kelas</th>
|
||||
<th style="width: 90px; text-align: center;">Status</th>
|
||||
<th style="width: 80px; text-align: center;">Waktu</th>
|
||||
<th style="width: 80px;">Metode</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($records as $index => $riwayat)
|
||||
@foreach($kelasRecords->values() as $index => $riwayat)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
<td><strong>{{ $riwayat->id_santri }}</strong></td>
|
||||
|
|
@ -329,13 +399,6 @@ class="btn-reset">
|
|||
{{ $riwayat->santri->nama_lengkap }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@if($riwayat->santri->kelasSantri->first() && $riwayat->santri->kelasSantri->first()->kelas)
|
||||
{{ $riwayat->santri->kelasSantri->first()->kelas->nama_kelas }}
|
||||
@else
|
||||
<span style="color: #9CA3AF;">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align: center;">{!! $riwayat->status_badge !!}</td>
|
||||
<td style="text-align: center;">
|
||||
{{ $riwayat->waktu_absen ? \Carbon\Carbon::parse($riwayat->waktu_absen)->format('H:i') : '-' }}
|
||||
|
|
@ -345,6 +408,10 @@ class="btn-reset">
|
|||
<span style="background: #DBEAFE; color: #1E40AF; padding: 3px 8px; border-radius: 10px; font-size: 0.75rem; font-weight: 600;">
|
||||
<i class="fas fa-id-card"></i> RFID
|
||||
</span>
|
||||
@elseif($riwayat->metode_absen == 'Import_Mesin')
|
||||
<span style="background: #EDE9FE; color: #6B21A8; padding: 3px 8px; border-radius: 10px; font-size: 0.75rem; font-weight: 600;">
|
||||
<i class="fas fa-desktop"></i> Mesin
|
||||
</span>
|
||||
@else
|
||||
<span style="background: #E5E7EB; color: #374151; padding: 3px 8px; border-radius: 10px; font-size: 0.75rem; font-weight: 600;">
|
||||
<i class="fas fa-hand-pointer"></i> Manual
|
||||
|
|
@ -355,6 +422,7 @@ class="btn-reset">
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{--
|
||||
{{--
|
||||
============================================================================
|
||||
LOKASI FILE: resources/views/admin/kelas/index.blade.php
|
||||
============================================================================
|
||||
|
|
@ -104,6 +104,7 @@ class="form-control"
|
|||
<!-- Kelas List -->
|
||||
<div class="content-box">
|
||||
@if ($kelas->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -169,6 +170,7 @@ class="btn btn-sm btn-danger"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if ($kelas->hasPages())
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{--
|
||||
{{--
|
||||
============================================================================
|
||||
LOKASI FILE: resources/views/admin/kelas/kelompok/index.blade.php
|
||||
============================================================================
|
||||
|
|
@ -89,6 +89,7 @@ class="form-control"
|
|||
<!-- Kelompok List -->
|
||||
<div class="content-box">
|
||||
@if ($kelompokKelas->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -155,6 +156,7 @@ class="btn btn-sm btn-danger"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if ($kelompokKelas->hasPages())
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Kenaikan Kelas Massal')
|
||||
|
||||
|
|
@ -85,6 +85,8 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -132,7 +134,7 @@
|
|||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
<span class="text-muted" style="font-size:0.85rem;">—</span>
|
||||
<span class="text-muted" style="font-size:0.85rem;"></span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
|
|
@ -163,6 +165,8 @@ class="btn btn-sm btn-info"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="content-box">
|
||||
|
|
@ -196,7 +200,7 @@ class="btn btn-sm btn-info"
|
|||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
// ── Enable/disable tombol Naikkan berdasarkan pilihan dropdown ──
|
||||
// ── Enable/disable tombol Naikkan berdasarkan pilihan dropdown ──
|
||||
document.querySelectorAll('.target-kelas-select').forEach(function (select) {
|
||||
var kelasId = select.dataset.kelasId;
|
||||
var button = document.querySelector('.btn-naikkan[data-kelas-id="' + kelasId + '"]');
|
||||
|
|
@ -213,7 +217,7 @@ class="btn btn-sm btn-info"
|
|||
});
|
||||
});
|
||||
|
||||
// ── Handle klik tombol Naikkan ──
|
||||
// ── Handle klik tombol Naikkan ──
|
||||
document.querySelectorAll('.btn-naikkan').forEach(function (button) {
|
||||
button.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Preview Kenaikan Kelas')
|
||||
|
||||
|
|
@ -98,6 +98,7 @@
|
|||
</div>
|
||||
|
||||
@if ($santriList->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -152,6 +153,7 @@ class="santri-avatar">
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detail Kelas')
|
||||
|
||||
|
|
@ -97,6 +97,8 @@
|
|||
<span class="badge badge-info">{{ $santriList->count() }} santri</span>
|
||||
</h3>
|
||||
|
||||
<div class="table-wrapper">
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -131,6 +133,8 @@ class="btn btn-sm btn-info"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="content-box" style="margin-top: 14px;">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/kepulangan/index.blade.php --}}
|
||||
{{-- resources/views/admin/kepulangan/index.blade.php --}}
|
||||
|
||||
@extends('layouts.app')
|
||||
|
||||
|
|
@ -174,6 +174,7 @@ class="form-control"
|
|||
|
||||
{{-- Data Table (SAMA SEPERTI SEBELUMNYA) --}}
|
||||
<div style="overflow-x: auto;">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -325,6 +326,7 @@ class="btn btn-sm btn-danger"
|
|||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
|
|
@ -618,14 +620,14 @@ function calculateDurasiAktual() {
|
|||
|
||||
if (durasiAktual < durasiRencana) {
|
||||
const selisih = durasiRencana - durasiAktual;
|
||||
selisihText = `✅ Santri pulang ${selisih} hari lebih cepat dari rencana. Kuota akan berkurang ${durasiAktual} hari.`;
|
||||
selisihText = `✅ Santri pulang ${selisih} hari lebih cepat dari rencana. Kuota akan berkurang ${durasiAktual} hari.`;
|
||||
selisihColor = '#28a745';
|
||||
} else if (durasiAktual > durasiRencana) {
|
||||
const selisih = durasiAktual - durasiRencana;
|
||||
selisihText = `âš ï¸ Santri pulang ${selisih} hari lebih lambat dari rencana. Kuota akan bertambah ${selisih} hari.`;
|
||||
selisihText = `⚠︠Santri pulang ${selisih} hari lebih lambat dari rencana. Kuota akan bertambah ${selisih} hari.`;
|
||||
selisihColor = '#ffc107';
|
||||
} else {
|
||||
selisihText = `✓ Sesuai rencana (${durasiAktual} hari).`;
|
||||
selisihText = `✓ Sesuai rencana (${durasiAktual} hari).`;
|
||||
selisihColor = '#007bff';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/kepulangan/over-limit.blade.php --}}
|
||||
{{-- resources/views/admin/kepulangan/over-limit.blade.php --}}
|
||||
|
||||
@extends('layouts.app')
|
||||
|
||||
|
|
@ -13,17 +13,17 @@
|
|||
<div style="background: linear-gradient(135deg, #ff5252 0%, #f48fb1 100%); color: white; padding: 14px; border-radius: 12px; margin-bottom: 14px;">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; align-items: center;">
|
||||
<div>
|
||||
<h4 style="margin: 0 0 5px 0; opacity: 0.9;">âš ï¸ Total Santri Over Limit</h4>
|
||||
<h4 style="margin: 0 0 5px 0; opacity: 0.9;">⚠︠Total Santri Over Limit</h4>
|
||||
<p style="margin: 0; font-size: 2rem; font-weight: 700;">{{ $santriList->count() }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style="margin: 0 0 5px 0; opacity: 0.9;">📅 Periode Kuota</h4>
|
||||
<h4 style="margin: 0 0 5px 0; opacity: 0.9;">📅 Periode Kuota</h4>
|
||||
<p style="margin: 0; font-size: 1.1rem; font-weight: 600;">
|
||||
{{ $settings->periode_mulai->format('d M Y') }} - {{ $settings->periode_akhir->format('d M Y') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style="margin: 0 0 5px 0; opacity: 0.9;">📊 Kuota Maksimal</h4>
|
||||
<h4 style="margin: 0 0 5px 0; opacity: 0.9;">📊Kuota Maksimal</h4>
|
||||
<p style="margin: 0; font-size: 1.1rem; font-weight: 600;">{{ $settings->kuota_maksimal }} Hari / Tahun</p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
{{-- Alert Info --}}
|
||||
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; margin-bottom: 14px; border-left: 4px solid #ffc107;">
|
||||
<strong>â„¹ï¸ Informasi:</strong>
|
||||
<strong>ℹ︠Informasi:</strong>
|
||||
<p style="margin: 10px 0 0 0;">
|
||||
Berikut adalah daftar santri yang telah melebihi kuota maksimal <strong>{{ $settings->kuota_maksimal }} hari</strong> dalam periode ini.
|
||||
Santri tetap bisa mengajukan izin, namun akan mendapat peringatan visual.
|
||||
|
|
@ -46,6 +46,7 @@
|
|||
<div class="content-box">
|
||||
@if($santriList->count() > 0)
|
||||
<div style="overflow-x: auto;">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -125,11 +126,12 @@ class="btn btn-sm btn-warning"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Summary Statistics --}}
|
||||
<div style="margin-top: 22px; padding: 14px; background: #f8f9fa; border-radius: 8px;">
|
||||
<h4 style="margin: 0 0 15px 0; color: #2C3E50;">📊 Ringkasan Statistik</h4>
|
||||
<h4 style="margin: 0 0 15px 0; color: #2C3E50;">📊Ringkasan Statistik</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px;">
|
||||
<div style="text-align: center; padding: 15px; background: white; border-radius: 8px; border: 2px solid #dc3545;">
|
||||
<div style="font-size: 0.85rem; color: #7F8C8D; margin-bottom: 5px;">Total Santri Over Limit</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/kepulangan/pengajuan.blade.php --}}
|
||||
{{-- resources/views/admin/kepulangan/pengajuan.blade.php --}}
|
||||
|
||||
@extends('layouts.app')
|
||||
|
||||
|
|
@ -98,6 +98,7 @@ class="form-control"
|
|||
|
||||
{{-- Data Table --}}
|
||||
<div style="overflow-x: auto;">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -197,6 +198,7 @@ class="btn btn-sm btn-danger"
|
|||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/kepulangan/settings.blade.php --}}
|
||||
{{-- resources/views/admin/kepulangan/settings.blade.php --}}
|
||||
|
||||
@extends('layouts.app')
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ class="form-control"
|
|||
</div>
|
||||
|
||||
<div style="background: #E8F7F2; padding: 15px; border-radius: 8px; margin-bottom: 14px; border-left: 4px solid #6FBA9D;">
|
||||
<strong>â„¹ï¸ Informasi:</strong>
|
||||
<strong>ℹ︠Informasi:</strong>
|
||||
<ul style="margin: 10px 0 0 20px; padding: 0;">
|
||||
<li>Periode ini menentukan rentang waktu perhitungan kuota</li>
|
||||
<li>Perubahan periode akan mempengaruhi perhitungan kuota santri</li>
|
||||
|
|
@ -133,7 +133,7 @@ class="form-control"
|
|||
</h3>
|
||||
|
||||
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; margin-bottom: 14px; border-left: 4px solid #ffc107;">
|
||||
<strong>âš ï¸ PERHATIAN:</strong>
|
||||
<strong>⚠︠PERHATIAN:</strong>
|
||||
<p style="margin: 10px 0 0 0;">
|
||||
Reset kuota akan mengubah status semua izin yang "Disetujui" dalam periode saat ini menjadi "Selesai".
|
||||
Ini akan mereset perhitungan kuota untuk memulai periode baru.
|
||||
|
|
@ -175,7 +175,7 @@ class="form-control"
|
|||
|
||||
{{-- Info Tambahan --}}
|
||||
<div style="background: #e3f2fd; padding: 15px; border-radius: 8px; border-left: 4px solid #2196f3;">
|
||||
<strong>💡 Tips:</strong>
|
||||
<strong>💡 Tips:</strong>
|
||||
<ul style="margin: 10px 0 0 20px; padding: 0; font-size: 0.9rem;">
|
||||
<li>Reset individual dapat dilakukan dari halaman detail santri</li>
|
||||
<li>Reset massal sebaiknya dilakukan di akhir periode</li>
|
||||
|
|
@ -194,6 +194,7 @@ class="form-control"
|
|||
|
||||
@if($resetLogs->count() > 0)
|
||||
<div style="overflow-x: auto;">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -214,7 +215,7 @@ class="form-control"
|
|||
<td>
|
||||
<span style="display: inline-block; padding: 4px 10px; border-radius: 4px; font-size: 0.85rem; font-weight: 600;
|
||||
{{ $log->jenis_reset == 'massal' ? 'background: #dc3545; color: white;' : 'background: #ffc107; color: #000;' }}">
|
||||
{{ $log->jenis_reset == 'massal' ? '👥 Massal' : '👤 Individual' }}
|
||||
{{ $log->jenis_reset == 'massal' ? '👥 Massal' : '👤 Individual' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
|
@ -248,6 +249,7 @@ class="form-control"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div style="text-align: center; padding: 22px; color: #7F8C8D;">
|
||||
|
|
@ -267,7 +269,7 @@ class="form-control"
|
|||
</h3>
|
||||
</div>
|
||||
<div style="padding: 14px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; margin-bottom: 15px;">
|
||||
<h4 style="margin: 0 0 10px 0; color: #856404;">âš ï¸ PERINGATAN PENTING!</h4>
|
||||
<h4 style="margin: 0 0 10px 0; color: #856404;">⚠︠PERINGATAN PENTING!</h4>
|
||||
<p style="margin: 0; color: #856404;">
|
||||
Anda akan mereset kuota untuk <strong>{{ $totalSantri }} santri aktif</strong>.
|
||||
Semua izin yang berstatus "Disetujui" akan diubah menjadi "Selesai".
|
||||
|
|
@ -279,7 +281,7 @@ class="form-control"
|
|||
</p>
|
||||
<ul style="margin: 10px 0 0 20px; padding: 0; font-size: 0.9rem;">
|
||||
<li>Semua perhitungan kuota akan direset ke 0</li>
|
||||
<li>Status izin "Disetujui" → "Selesai"</li>
|
||||
<li>Status izin "Disetujui" → "Selesai"</li>
|
||||
<li>Data arsip tetap tersimpan</li>
|
||||
<li>Aktivitas tercatat dalam log</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/kepulangan/show.blade.php --}}
|
||||
{{-- resources/views/admin/kepulangan/show.blade.php --}}
|
||||
|
||||
@extends('layouts.app')
|
||||
|
||||
|
|
@ -67,19 +67,19 @@
|
|||
<td>
|
||||
@if($kepulangan->is_aktif)
|
||||
<span style="display: inline-block; background: #28a745; color: white; padding: 4px 10px; border-radius: 4px; font-size: 0.9rem;">
|
||||
ðŸ Sedang Izin
|
||||
ðŸÂ Sedang Izin
|
||||
</span>
|
||||
@elseif($kepulangan->is_terlambat)
|
||||
<span style="display: inline-block; background: #dc3545; color: white; padding: 4px 10px; border-radius: 4px; font-size: 0.9rem;">
|
||||
â° Terlambat Kembali
|
||||
â° Terlambat Kembali
|
||||
</span>
|
||||
@elseif($kepulangan->status == 'Selesai')
|
||||
<span style="display: inline-block; background: #6c757d; color: white; padding: 4px 10px; border-radius: 4px; font-size: 0.9rem;">
|
||||
✅ Sudah Selesai
|
||||
✅ Sudah Selesai
|
||||
</span>
|
||||
@else
|
||||
<span style="display: inline-block; background: #81C6E8; color: white; padding: 4px 10px; border-radius: 4px; font-size: 0.9rem;">
|
||||
📅 Belum Dimulai
|
||||
📅 Belum Dimulai
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
|
@ -287,6 +287,7 @@ class="btn btn-warning"
|
|||
<i class="fas fa-list"></i> Riwayat Kepulangan Lainnya (5 Terakhir)
|
||||
</h4>
|
||||
<div style="overflow-x: auto;">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -329,6 +330,7 @@ class="btn btn-sm btn-primary">
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Data Kesehatan Santri')
|
||||
|
||||
|
|
@ -91,6 +91,7 @@ class="form-control"
|
|||
|
||||
<!-- Data Table -->
|
||||
@if($kesehatanSantri->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -182,6 +183,7 @@ class="btn btn-danger btn-sm"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div style="margin-top: 14px; display: flex; justify-content: center;">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Riwayat Kesehatan Santri')
|
||||
|
||||
|
|
@ -84,6 +84,7 @@
|
|||
</div>
|
||||
|
||||
@if($riwayatKesehatan->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -164,6 +165,7 @@ class="btn btn-secondary btn-sm"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div style="margin-top: 14px; display: flex; justify-content: center;">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detail Kesehatan Santri')
|
||||
|
||||
|
|
@ -289,7 +289,7 @@ class="btn btn-primary">
|
|||
<form action="{{ route('admin.kesehatan-santri.destroy', $kesehatanSantri) }}"
|
||||
method="POST"
|
||||
style="display: inline;"
|
||||
onsubmit="return confirm('âš ï¸ Yakin ingin menghapus data kesehatan ini?\n\nData yang dihapus tidak dapat dikembalikan!')">
|
||||
onsubmit="return confirm('⚠︠Yakin ingin menghapus data kesehatan ini?\n\nData yang dihapus tidak dapat dikembalikan!')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger">
|
||||
|
|
@ -311,6 +311,8 @@ class="btn btn-primary">
|
|||
</span>
|
||||
</h3>
|
||||
|
||||
<div class="table-wrapper">
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -361,6 +363,8 @@ class="btn btn-primary btn-sm">
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 14px;">
|
||||
<a href="{{ route('admin.kesehatan-santri.riwayat', $kesehatanSantri->id_santri) }}"
|
||||
class="btn btn-primary">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
|
|
@ -57,6 +57,7 @@
|
|||
</form>
|
||||
|
||||
@if($transaksi->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -98,6 +99,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-top: 14px;">{{ $transaksi->links() }}</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
|
|
@ -63,6 +63,7 @@
|
|||
<div class="content-box">
|
||||
<h4 style="margin-bottom:12px;"><i class="fas fa-arrow-up" style="color:var(--danger-color);"></i> Pengeluaran Terbesar</h4>
|
||||
@if($detailPengeluaran->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Tanggal</th><th>Keterangan</th><th>Nominal</th></tr></thead>
|
||||
<tbody>
|
||||
|
|
@ -75,6 +76,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-muted">Tidak ada pengeluaran bulan ini.</p>
|
||||
@endif
|
||||
|
|
@ -84,6 +86,7 @@
|
|||
<div class="content-box">
|
||||
<h4 style="margin-bottom:12px;"><i class="fas fa-arrow-down" style="color:var(--success-color);"></i> Pemasukan Non-SPP</h4>
|
||||
@if($detailPemasukan->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Tanggal</th><th>Keterangan</th><th>Nominal</th></tr></thead>
|
||||
<tbody>
|
||||
|
|
@ -96,6 +99,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-muted">Tidak ada pemasukan non-SPP bulan ini.</p>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Klasifikasi Pelanggaran')
|
||||
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
</div>
|
||||
|
||||
@if($data->isNotEmpty())
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -97,6 +98,7 @@ class="btn btn-sm btn-warning"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detail Klasifikasi')
|
||||
|
||||
|
|
@ -53,6 +53,7 @@
|
|||
<h3 style="color: var(--primary-color); margin-bottom: 15px;">
|
||||
<i class="fas fa-list"></i> Daftar Pelanggaran
|
||||
</h3>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -83,6 +84,7 @@
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox"></i>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
|
|
@ -85,6 +85,7 @@ class="btn btn-secondary"
|
|||
{{-- Table Section --}}
|
||||
<div class="content-box">
|
||||
@if($materis->count() > 0)
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -139,6 +140,7 @@ class="btn btn-sm btn-warning" title="Edit">
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
<div style="margin-top: 14px;">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,259 @@
|
|||
{{-- resources/views/admin/mesin/import/index.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Import Absensi Mesin')
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<h2><i class="fas fa-file-import"></i> Import Absensi Fingerprint</h2>
|
||||
<a href="{{ route('admin.kegiatan.index') }}" class="btn btn-sm btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i> {{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-times-circle"></i> {{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($belumMapping > 0)
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>{{ $belumMapping }}</strong> ID mesin belum dipetakan ke santri.
|
||||
Data santri tersebut tidak akan tersimpan saat import.
|
||||
<a href="{{ route('admin.mesin.mapping-santri.index') }}"
|
||||
class="btn btn-sm btn-warning" style="margin-left:8px;">
|
||||
Lengkapi Mapping
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Info Cara Kerja --}}
|
||||
<div class="content-box" style="margin-bottom:14px">
|
||||
<h4 style="margin:0 0 12px;color:var(--primary-color)">
|
||||
<i class="fas fa-info-circle"></i> Cara Kerja Matching
|
||||
</h4>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
|
||||
<div style="background:#FFF3E8;border:1px solid #FDBA74;border-radius:8px;padding:12px">
|
||||
<div style="font-size:18px;margin-bottom:4px"></div>
|
||||
<div style="font-weight:700;color:#C05621;margin-bottom:4px">Mesin Sholat</div>
|
||||
<div style="font-size:12px;color:#374151;line-height:1.6">
|
||||
JK1 Masuk Shubuh<br>
|
||||
JK1 Pulang Dhuhur<br>
|
||||
JK2 Masuk Ashar<br>
|
||||
JK2 Pulang Maghrib<br>
|
||||
Lb Masuk Isya
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;padding:12px">
|
||||
<div style="font-size:18px;margin-bottom:4px"></div>
|
||||
<div style="font-weight:700;color:#1D4ED8;margin-bottom:4px">Mesin Ngaji</div>
|
||||
<div style="font-size:12px;color:#374151;line-height:1.6">
|
||||
JK1 Masuk Ngaji Shubuh<br>
|
||||
JK1 Pulang sekolah<br>
|
||||
JK2 Masuk Ngaji Siang<br>
|
||||
JK2 Pulang Ngaji Maghrib<br>
|
||||
Lb Masuk Ngaji Malam
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#FEF9C3;border:1px solid #FDE68A;border-radius:8px;padding:12px">
|
||||
<div style="font-size:18px;margin-bottom:4px"></div>
|
||||
<div style="font-weight:700;color:#92400E;margin-bottom:4px">Konflik</div>
|
||||
<div style="font-size:12px;color:#374151;line-height:1.6">
|
||||
Jika santri sudah punya absen<br>
|
||||
Manual/RFID, sistem akan<br>
|
||||
minta pilihan Anda di halaman<br>
|
||||
preview sebelum disimpan.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Form Upload --}}
|
||||
<div class="content-box">
|
||||
<h4 style="margin:0 0 16px">
|
||||
<i class="fas fa-upload" style="color:var(--primary-color)"></i>
|
||||
Upload File GLog.txt
|
||||
</h4>
|
||||
|
||||
<form action="{{ route('admin.mesin.import.preview') }}"
|
||||
method="POST"
|
||||
enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="font-weight:600;font-size:14px">
|
||||
<i class="fas fa-database" style="color:#1A56DB"></i>
|
||||
File GLog.txt <span style="color:red">*</span>
|
||||
</label>
|
||||
<input type="file" name="file_glog" class="form-control"
|
||||
accept=".txt,.csv,.xls,.xlsx" required>
|
||||
<small class="text-muted">
|
||||
Export dari software Eppos: menu
|
||||
<strong>Report Download Log</strong>.
|
||||
Pilih periode tanggal yang diinginkan lalu export.
|
||||
</small>
|
||||
@error('file_glog')
|
||||
<div style="color:#EF4444;font-size:12px;margin-top:4px">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px">
|
||||
<div class="form-group" style="margin:0">
|
||||
<label style="font-weight:600;font-size:14px">
|
||||
<i class="fas fa-clock"></i>
|
||||
Toleransi Sebelum Kegiatan (menit)
|
||||
</label>
|
||||
<input type="number" name="tol_sebelum" class="form-control"
|
||||
value="15" min="0" max="60">
|
||||
<small class="text-muted">
|
||||
Scan diterima berapa menit <strong>sebelum</strong> kegiatan mulai
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group" style="margin:0">
|
||||
<label style="font-weight:600;font-size:14px">
|
||||
<i class="fas fa-clock"></i>
|
||||
Toleransi Sesudah Kegiatan (menit)
|
||||
</label>
|
||||
<input type="number" name="tol_sesudah" class="form-control"
|
||||
value="10" min="0" max="60">
|
||||
<small class="text-muted">
|
||||
Scan diterima berapa menit <strong>setelah</strong> kegiatan selesai
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:20px">
|
||||
<div style="display:flex;align-items:center;gap:10px;
|
||||
background:#F8FAFC;border:1px solid #E2E8F0;
|
||||
border-radius:8px;padding:12px">
|
||||
<input type="checkbox" name="isi_alpa" value="1"
|
||||
id="isiAlpa" checked
|
||||
style="width:18px;height:18px;cursor:pointer">
|
||||
<label for="isiAlpa" style="margin:0;cursor:pointer;font-weight:500">
|
||||
Isi <strong>Alpa</strong> otomatis untuk santri yang tidak scan
|
||||
<span style="color:#6B7280;font-size:12px;display:block;font-weight:400">
|
||||
Jika tidak dicentang, santri yang tidak scan tidak akan diisi apapun
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Strategi penanganan konflik --}}
|
||||
<div class="form-group" style="margin-bottom:20px">
|
||||
<label style="font-weight:600;font-size:14px;margin-bottom:8px;display:block">
|
||||
<i class="fas fa-exchange-alt" style="color:#DC2626"></i>
|
||||
Jika ada konflik dengan data absen yang sudah ada:
|
||||
</label>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px">
|
||||
<label style="background:#DCFCE7;border:2px solid #86EFAC;border-radius:8px;padding:12px;cursor:pointer;display:flex;align-items:flex-start;gap:8px;margin:0"
|
||||
id="lbl_mesin">
|
||||
<input type="radio" name="conflict_strategy" value="mesin" checked
|
||||
style="margin-top:3px;width:16px;height:16px">
|
||||
<div>
|
||||
<div style="font-weight:700;color:#166534">Utamakan Data Mesin</div>
|
||||
<div style="font-size:11px;color:#374151;margin-top:2px">
|
||||
Timpa semua data lama dengan hasil mesin. Paling umum dipakai.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label style="background:#DBEAFE;border:2px solid #93C5FD;border-radius:8px;padding:12px;cursor:pointer;display:flex;align-items:flex-start;gap:8px;margin:0"
|
||||
id="lbl_exist">
|
||||
<input type="radio" name="conflict_strategy" value="exist"
|
||||
style="margin-top:3px;width:16px;height:16px">
|
||||
<div>
|
||||
<div style="font-weight:700;color:#1D4ED8">Pertahankan Data Lama</div>
|
||||
<div style="font-size:11px;color:#374151;margin-top:2px">
|
||||
Data Manual/RFID yang sudah ada tidak diubah.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label style="background:#FEF9C3;border:2px solid #FDE68A;border-radius:8px;padding:12px;cursor:pointer;display:flex;align-items:flex-start;gap:8px;margin:0"
|
||||
id="lbl_manual">
|
||||
<input type="radio" name="conflict_strategy" value="manual"
|
||||
style="margin-top:3px;width:16px;height:16px">
|
||||
<div>
|
||||
<div style="font-weight:700;color:#92400E">Pilih Manual per Sel</div>
|
||||
<div style="font-size:11px;color:#374151;margin-top:2px">
|
||||
Review tiap konflik satu per satu di halaman preview.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:10px;align-items:center">
|
||||
<button type="submit" class="btn btn-primary"
|
||||
style="padding:10px 28px;font-size:15px">
|
||||
<i class="fas fa-search"></i> Preview Data Import
|
||||
</button>
|
||||
<a href="{{ route('admin.mesin.mapping-santri.index') }}"
|
||||
class="btn btn-secondary" style="padding:10px 20px">
|
||||
<i class="fas fa-link"></i> Kelola Mapping Santri
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Riwayat --}}
|
||||
@if($riwayat->count() > 0)
|
||||
<div class="content-box" style="margin-top:14px">
|
||||
<h4 style="margin:0 0 12px">
|
||||
<i class="fas fa-history"></i> Riwayat Import Terakhir
|
||||
</h4>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Waktu</th>
|
||||
<th>Jumlah Scan</th>
|
||||
<th>Berhasil</th>
|
||||
<th>Konflik Selesai</th>
|
||||
<th>Duplikat</th>
|
||||
<th>Tanpa Mapping</th>
|
||||
<th>Oleh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($riwayat as $r)
|
||||
<tr>
|
||||
<td>{{ $r->created_at->format('d/m/Y H:i') }}</td>
|
||||
<td>{{ number_format($r->jumlah_scan) }}</td>
|
||||
<td>
|
||||
<span class="badge badge-success">{{ $r->berhasil }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($r->konflik_selesai > 0)
|
||||
<span class="badge badge-warning">{{ $r->konflik_selesai }}</span>
|
||||
@else <span style="color:#9CA3AF">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($r->dilewati > 0)
|
||||
<span class="badge badge-secondary">{{ $r->dilewati }}</span>
|
||||
@else <span style="color:#9CA3AF">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($r->no_santri > 0)
|
||||
<span class="badge badge-danger">{{ $r->no_santri }}</span>
|
||||
@else <span style="color:#9CA3AF">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="color:#6B7280">{{ $r->user->name ?? '-' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,492 @@
|
|||
{{-- resources/views/admin/mesin/import/preview.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Preview Import Absensi')
|
||||
@section('content')
|
||||
@php
|
||||
use Carbon\Carbon;
|
||||
|
||||
$statusStyle = [
|
||||
'Hadir' => ['bg'=>'#DCFCE7','c'=>'#166534','ic'=>'✅'],
|
||||
'Terlambat' => ['bg'=>'#FEF9C3','c'=>'#92400E','ic'=>'⏰'],
|
||||
'Alpa' => ['bg'=>'#FEE2E2','c'=>'#991B1B','ic'=>'❌'],
|
||||
'Pulang' => ['bg'=>'#FFF7ED','c'=>'#9A3412','ic'=>'🏠'],
|
||||
'Izin' => ['bg'=>'#F3E8FF','c'=>'#6B21A8','ic'=>'📋'],
|
||||
'Sakit' => ['bg'=>'#E0F2FE','c'=>'#0C4A6E','ic'=>'🏥'],
|
||||
];
|
||||
|
||||
// ── 1. Kolom kegiatan: UNIK, diurutkan waktu_mulai ───────────────────────────
|
||||
$kegiatanCols = collect($hasilEnriched)
|
||||
->flatMap(fn($h) => $h['rows'])
|
||||
->unique('kegiatan_id')
|
||||
->sortBy('waktu_mulai')
|
||||
->values()
|
||||
->map(fn($r) => [
|
||||
'kegiatan_id' => $r['kegiatan_id'],
|
||||
'nama' => $r['nama_kegiatan'],
|
||||
'waktu_mulai' => $r['waktu_mulai'],
|
||||
]);
|
||||
|
||||
// ── 2. Susun data: [tanggal][id_santri_or_mesin] = data ──────────────────────
|
||||
$byTanggalSantri = [];
|
||||
$santriList = []; // untuk urutan santri konsisten
|
||||
|
||||
foreach ($hasilEnriched as $h) {
|
||||
$tgl = $h['tanggal'];
|
||||
$key = $h['id_santri'] ?? ('__'.$h['id_mesin']);
|
||||
$byTanggalSantri[$tgl][$key] = $h;
|
||||
|
||||
if (!isset($santriList[$key])) {
|
||||
$santriList[$key] = [
|
||||
'nama' => $h['nama_web'] ?? $h['nama_mesin'],
|
||||
'kelas' => $h['kelas'] ?? '-',
|
||||
'status' => $h['match_status'],
|
||||
];
|
||||
}
|
||||
}
|
||||
ksort($byTanggalSantri);
|
||||
|
||||
// ── 3. Statistik ─────────────────────────────────────────────────────────────
|
||||
$allRows = collect($hasilEnriched)->flatMap(fn($h) => $h['rows']);
|
||||
$totalKonflik = $allRows->where('is_conflict', true)->count();
|
||||
$hadir = $allRows->where('status_final','Hadir')->count();
|
||||
$terlambat = $allRows->where('status_final','Terlambat')->count();
|
||||
$alpa = $allRows->where('status_final','Alpa')->count();
|
||||
$notMapped = collect($hasilEnriched)->where('match_status','NOT_MAPPED')->count();
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
/* Sticky top bar */
|
||||
.top-bar {
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
background: #0F172A; color: #F1F5F9;
|
||||
padding: 8px 16px; display: flex; align-items: center;
|
||||
gap: 10px; flex-wrap: wrap; box-shadow: 0 2px 8px rgba(0,0,0,.3);
|
||||
font-size: 13px;
|
||||
}
|
||||
.chip {
|
||||
border-radius: 8px; padding: 4px 10px;
|
||||
text-align: center; min-width: 56px;
|
||||
font-size: 11px; line-height: 1.3;
|
||||
}
|
||||
.chip .n { font-size: 17px; font-weight: 700; display: block }
|
||||
.btn-act {
|
||||
border: none; border-radius: 6px; padding: 6px 12px;
|
||||
cursor: pointer; font-weight: 600; font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-save {
|
||||
border: none; border-radius: 8px; padding: 8px 20px;
|
||||
cursor: pointer; font-weight: 700; font-size: 13px;
|
||||
color: #fff; white-space: nowrap; transition: background .2s;
|
||||
}
|
||||
|
||||
/* Matrix table */
|
||||
.wrap { overflow-x: auto }
|
||||
.mx { border-collapse: collapse; font-size: 12px; width: 100% }
|
||||
.mx th, .mx td {
|
||||
border: 1px solid #E5E7EB; padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Sticky kolom tanggal + nama */
|
||||
.col-tgl {
|
||||
position: sticky; left: 0; z-index: 4;
|
||||
background: #F8FAFC; min-width: 90px;
|
||||
border-right: 2px solid #CBD5E1;
|
||||
padding: 6px 10px; font-size: 11px;
|
||||
}
|
||||
.col-nama {
|
||||
position: sticky; left: 90px; z-index: 4;
|
||||
background: #F8FAFC; min-width: 130px;
|
||||
border-right: 2px solid #CBD5E1;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
/* Header kegiatan (rotate) */
|
||||
.th-wrap {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
display: flex; align-items: center; justify-content: flex-end;
|
||||
gap: 2px; height: 80px; padding: 5px 6px;
|
||||
}
|
||||
/* Status pill */
|
||||
.pill {
|
||||
display: inline-block; border-radius: 8px;
|
||||
padding: 2px 6px; font-size: 10px; font-weight: 700;
|
||||
}
|
||||
/* Konflik cell */
|
||||
.conf-cell { background: #FFF5F5 !important; border: 2px solid #FCA5A5 !important }
|
||||
.conf-wrap { display: flex; flex-direction: column }
|
||||
.conf-opt {
|
||||
padding: 4px 8px; cursor: pointer;
|
||||
font-size: 11px; display: flex; align-items: center; gap: 4px;
|
||||
border-bottom: 1px solid #F1F5F9; transition: background .12s;
|
||||
}
|
||||
.conf-opt:last-child { border-bottom: none }
|
||||
.conf-opt:hover { background: #F8FAFC }
|
||||
.sel-m { background: #DCFCE7 !important }
|
||||
.sel-e { background: #DBEAFE !important }
|
||||
/* Date separator row */
|
||||
.date-sep td {
|
||||
background: #1E293B; color: #94A3B8; font-weight: 700;
|
||||
font-size: 11px; padding: 5px 12px; border-bottom: 2px solid #334155;
|
||||
}
|
||||
/* Alternating santri rows */
|
||||
.row-alt { background: #FAFAFA }
|
||||
/* Sticky header */
|
||||
.th-sticky {
|
||||
position: sticky; top: 52px; z-index: 6;
|
||||
background: #1E293B;
|
||||
}
|
||||
.th-tgl { position: sticky; left: 0; z-index: 8 }
|
||||
.th-nama { position: sticky; left: 90px; z-index: 8 }
|
||||
</style>
|
||||
|
||||
<form action="{{ route('admin.mesin.import.store') }}" method="POST" id="frm">
|
||||
@csrf
|
||||
<input type="hidden" name="conflict_strategy" value="manual" id="stratInput">
|
||||
|
||||
{{-- Error flash --}}
|
||||
@if(session('error'))
|
||||
<div style="background:#FEE2E2;border:1px solid #FCA5A5;border-left:4px solid #DC2626;
|
||||
padding:12px 16px;font-size:13px;color:#991B1B">
|
||||
<strong>❌ Error:</strong> {{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ── TOP BAR ──────────────────────────────────────────────────────────────── --}}
|
||||
<div class="top-bar">
|
||||
<div style="flex:1;min-width:160px">
|
||||
<div style="color:#64748B;font-size:10px;text-transform:uppercase;letter-spacing:1px">
|
||||
Preview Import
|
||||
</div>
|
||||
<div style="font-weight:700;font-size:14px;margin-top:1px">
|
||||
{{ count($santriList) }} santri
|
||||
@if($totalKonflik > 0)
|
||||
· <span style="color:#FCA5A5" id="lbl">
|
||||
⚡ <span id="cnt">{{ $totalKonflik }}</span> konflik perlu diselesaikan
|
||||
</span>
|
||||
@else
|
||||
· <span style="color:#86EFAC">✅ Siap disimpan</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Stat chips --}}
|
||||
<div class="chip" style="background:#DCFCE7;color:#166534">
|
||||
<span class="n">{{ $hadir }}</span>Hadir
|
||||
</div>
|
||||
<div class="chip" style="background:#FEF9C3;color:#92400E">
|
||||
<span class="n">{{ $terlambat }}</span>Telat
|
||||
</div>
|
||||
<div class="chip" style="background:#FEE2E2;color:#991B1B">
|
||||
<span class="n">{{ $alpa }}</span>Alpa
|
||||
</div>
|
||||
@if($totalKonflik > 0)
|
||||
<div class="chip" style="background:#FEE2E2;color:#DC2626">
|
||||
<span class="n" id="chip">{{ $totalKonflik }}</span>Konflik
|
||||
</div>
|
||||
@endif
|
||||
@if($notMapped > 0)
|
||||
<div class="chip" style="background:#FFF3E8;color:#C05621">
|
||||
<span class="n">{{ $notMapped }}</span>Blm Map
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Conflict actions --}}
|
||||
@if($totalKonflik > 0)
|
||||
<div style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#94A3B8">
|
||||
Konflik:
|
||||
</div>
|
||||
<button type="button" class="btn-act" style="background:#DCFCE7;color:#166534"
|
||||
onclick="resolveAll('m');document.getElementById('stratInput').value='mesin'">👆 Mesin</button>
|
||||
<button type="button" class="btn-act" style="background:#DBEAFE;color:#1D4ED8"
|
||||
onclick="resolveAll('e');document.getElementById('stratInput').value='exist'">🔒 Lama</button>
|
||||
@endif
|
||||
|
||||
{{-- Save --}}
|
||||
<button type="button" class="btn-save" id="saveBtn"
|
||||
style="background:{{ $totalKonflik > 0 ? '#64748B' : 'linear-gradient(135deg,#166534,#22C55E)' }}"
|
||||
{{ $totalKonflik > 0 ? 'disabled' : '' }}
|
||||
onclick="submitForm()">
|
||||
@if($totalKonflik > 0) ⏳ Selesaikan konflik dulu
|
||||
@else 💾 Simpan ke Database @endif
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.mesin.import.index') }}"
|
||||
class="btn-act" style="background:#374151;color:#F1F5F9;text-decoration:none">
|
||||
← Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Legenda --}}
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;padding:8px 16px;
|
||||
background:#F8FAFC;border-bottom:1px solid #E5E7EB;font-size:11px">
|
||||
<span style="color:#6B7280;font-weight:600">Status:</span>
|
||||
@foreach($statusStyle as $st => $s)
|
||||
<span class="pill" style="background:{{$s['bg']}};color:{{$s['c']}}">
|
||||
{{ $s['ic'] }} {{ $st }}
|
||||
</span>
|
||||
@endforeach
|
||||
<span style="color:#9CA3AF;margin-left:4px">| — = tidak ada data</span>
|
||||
<span style="border:2px solid #FCA5A5;border-radius:4px;padding:1px 6px;color:#991B1B">
|
||||
⚡ Konflik
|
||||
</span>
|
||||
<span style="color:#9CA3AF">= ada data berbeda, pilih salah satu</span>
|
||||
@if($notMapped > 0)
|
||||
<span style="margin-left:auto">
|
||||
⚠️ {{ $notMapped }} belum dipetakan →
|
||||
<a href="{{ route('admin.mesin.mapping-santri.index') }}" target="_blank">
|
||||
Lengkapi Mapping
|
||||
</a>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- ── MATRIX TABLE ──────────────────────────────────────────────────────────── --}}
|
||||
<div class="wrap">
|
||||
<table class="mx">
|
||||
|
||||
{{-- Sticky header --}}
|
||||
<thead>
|
||||
<tr>
|
||||
{{-- Tanggal header --}}
|
||||
<th class="th-sticky th-tgl"
|
||||
style="min-width:90px;padding:8px 10px;text-align:left;
|
||||
color:#94A3B8;font-size:10px;border-right:2px solid #334155">
|
||||
Tanggal
|
||||
</th>
|
||||
{{-- Nama header --}}
|
||||
<th class="th-sticky th-nama"
|
||||
style="min-width:130px;padding:8px 10px;text-align:left;
|
||||
color:#94A3B8;font-size:10px;border-right:2px solid #334155">
|
||||
Santri
|
||||
</th>
|
||||
{{-- Kolom kegiatan — UNIK, diurutkan waktu --}}
|
||||
@foreach($kegiatanCols as $kg)
|
||||
<th class="th-sticky" style="min-width:70px;vertical-align:bottom">
|
||||
<div class="th-wrap">
|
||||
<span style="color:#F1F5F9;font-size:10px;font-weight:600">
|
||||
{{ $kg['nama'] }}
|
||||
</span>
|
||||
<span style="color:#64748B;font-size:9px">
|
||||
{{ $kg['waktu_mulai'] }}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{{-- Body: iterasi per tanggal, lalu per santri --}}
|
||||
<tbody>
|
||||
@foreach($byTanggalSantri as $tgl => $santriRows)
|
||||
|
||||
@php
|
||||
$tglCarbon = Carbon::parse($tgl);
|
||||
$tglLabel = $tglCarbon->locale('id')->isoFormat('ddd, D MMM');
|
||||
$isOdd = ($loop->index % 2 === 1);
|
||||
@endphp
|
||||
|
||||
{{-- Setiap tanggal: satu baris per santri --}}
|
||||
@foreach($santriList as $key => $info)
|
||||
@php
|
||||
$data = $santriRows[$key] ?? null;
|
||||
$rowBg = ($loop->index % 2 === 0) ? 'white' : '#FAFAFA';
|
||||
if ($data && $data['match_status'] === 'NOT_MAPPED') $rowBg = '#FFF5F5';
|
||||
@endphp
|
||||
<tr style="background:{{ $rowBg }}">
|
||||
|
||||
{{-- Kolom Tanggal (hanya tampil di baris pertama per tanggal) --}}
|
||||
<td class="col-tgl" style="background:{{ $rowBg }}">
|
||||
@if($loop->first)
|
||||
<strong style="color:#1E293B">{{ $tglLabel }}</strong>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Kolom Nama --}}
|
||||
<td class="col-nama" style="background:{{ $rowBg }}">
|
||||
@if($info['status'] === 'NOT_MAPPED')
|
||||
<div style="font-size:10px;font-weight:700;color:#DC2626">⚠ BELUM MAP</div>
|
||||
<div style="font-size:10px;color:#9CA3AF">{{ $info['nama'] }}</div>
|
||||
@else
|
||||
<div style="font-weight:600;color:#1F2937;font-size:12px">
|
||||
{{ $info['nama'] }}
|
||||
</div>
|
||||
<div style="font-size:10px;color:#9CA3AF">{{ $info['kelas'] }}</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Kolom per kegiatan --}}
|
||||
@foreach($kegiatanCols as $kg)
|
||||
@php
|
||||
$row = $data
|
||||
? collect($data['rows'])->firstWhere('kegiatan_id', $kg['kegiatan_id'])
|
||||
: null;
|
||||
$sf = $row['status_final'] ?? null;
|
||||
$st = $sf ? ($statusStyle[$sf] ?? null) : null;
|
||||
$isConf = $row['is_conflict'] ?? false;
|
||||
$key2 = "{$kg['kegiatan_id']}_{$data['id_santri']}_{$tgl}";
|
||||
@endphp
|
||||
|
||||
<td style="padding:0;text-align:center;vertical-align:middle;min-width:70px"
|
||||
class="{{ $isConf ? 'conf-cell' : '' }}">
|
||||
|
||||
@if(!$data || !$row || $sf === null)
|
||||
{{-- Tidak ada data --}}
|
||||
<span style="color:#D1D5DB">—</span>
|
||||
|
||||
@elseif($isConf)
|
||||
{{-- ── KONFLIK: 2 pilihan ── --}}
|
||||
@php
|
||||
$ex = $row['existing'];
|
||||
$exSt = $statusStyle[$ex['status']] ?? ['bg'=>'#F9FAFB','c'=>'#6B7280','ic'=>'?'];
|
||||
@endphp
|
||||
<div class="conf-wrap" data-key="{{ $key2 }}">
|
||||
{{-- Pilihan mesin --}}
|
||||
<div class="conf-opt" data-ch="m" onclick="pick('{{ $key2 }}','m',this)">
|
||||
<input type="radio" name="conflict_choices[{{ $key2 }}]"
|
||||
value="mesin" id="cm_{{ $key2 }}" style="display:none">
|
||||
<span>👆</span>
|
||||
<div>
|
||||
<span class="pill" style="background:{{$st['bg']}};color:{{$st['c']}}">
|
||||
{{$st['ic']}} {{$sf}}
|
||||
</span>
|
||||
<div style="font-size:9px;color:#6B7280">
|
||||
Mesin·{{ $row['jam_scan'] ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- Pilihan lama --}}
|
||||
<div class="conf-opt" data-ch="e" onclick="pick('{{ $key2 }}','e',this)">
|
||||
<input type="radio" name="conflict_choices[{{ $key2 }}]"
|
||||
value="exist" id="ce_{{ $key2 }}" style="display:none">
|
||||
<span>🔒</span>
|
||||
<div>
|
||||
<span class="pill" style="background:{{$exSt['bg']}};color:{{$exSt['c']}}">
|
||||
{{$exSt['ic']}} {{$ex['status']}}
|
||||
</span>
|
||||
<div style="font-size:9px;color:#6B7280">
|
||||
{{ $ex['metode'] ?? 'Manual' }}
|
||||
@if($ex['waktu']) ·{{ substr($ex['waktu'],0,5) }}@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#FEE2E2;padding:1px 4px;font-size:9px;
|
||||
color:#DC2626;font-weight:700;text-align:center">
|
||||
⚡ pilih
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@else
|
||||
{{-- ── Normal ── --}}
|
||||
<div style="padding:5px 3px">
|
||||
<span class="pill" style="background:{{$st['bg']}};color:{{$st['c']}}">
|
||||
{{$st['ic']}} {{$sf}}
|
||||
</span>
|
||||
@if($row['jam_scan'])
|
||||
<div style="font-size:9px;color:#9CA3AF;margin-top:1px">
|
||||
{{ $row['jam_scan'] }}
|
||||
@if(($row['selisih_menit'] ?? 0) > 0)
|
||||
<span style="color:#F59E0B">+{{ $row['selisih_menit'] }}m</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
@endforeach
|
||||
</tbody>
|
||||
|
||||
{{-- Repeat header di bawah untuk tabel panjang --}}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th style="background:#1E293B;color:#94A3B8;font-size:10px;
|
||||
padding:6px 10px;border-right:2px solid #334155;
|
||||
position:sticky;left:0">Tanggal</th>
|
||||
<th style="background:#1E293B;color:#94A3B8;font-size:10px;
|
||||
padding:6px 10px;border-right:2px solid #334155;
|
||||
position:sticky;left:90px">Santri</th>
|
||||
@foreach($kegiatanCols as $kg)
|
||||
<th style="background:#1E293B;color:#94A3B8;font-size:9px;padding:4px 6px">
|
||||
{{ $kg['nama'] }}
|
||||
</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Footer stats --}}
|
||||
<div style="padding:8px 16px;background:#F8FAFC;border-top:1px solid #E5E7EB;
|
||||
font-size:11px;color:#6B7280;display:flex;gap:12px;flex-wrap:wrap">
|
||||
<span>📊 {{ count($santriList) }} santri · {{ count($byTanggalSantri) }} hari</span>
|
||||
<span>✅ {{ $hadir }} Hadir</span>
|
||||
<span>⏰ {{ $terlambat }} Terlambat</span>
|
||||
<span>❌ {{ $alpa }} Alpa</span>
|
||||
@if($totalKonflik > 0)
|
||||
<span style="color:#DC2626">⚡ {{ $totalKonflik }} Konflik</span>
|
||||
@endif
|
||||
<span style="margin-left:auto">
|
||||
Toleransi: {{ $tolSebelum }}m sebelum / {{ $tolSesudah }}m sesudah
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const totalK = {{ $totalKonflik }};
|
||||
const done = new Set();
|
||||
|
||||
function pick(key, ch, el) {
|
||||
const wrap = el.closest('.conf-wrap');
|
||||
wrap.querySelectorAll('.conf-opt').forEach(o => {
|
||||
o.classList.remove('sel-m','sel-e');
|
||||
});
|
||||
el.classList.add(ch === 'm' ? 'sel-m' : 'sel-e');
|
||||
const radio = document.getElementById((ch==='m'?'cm_':'ce_') + key);
|
||||
if (radio) radio.checked = true;
|
||||
done.add(key);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function resolveAll(ch) {
|
||||
document.querySelectorAll('.conf-wrap').forEach(wrap => {
|
||||
const key = wrap.dataset.key;
|
||||
const opt = wrap.querySelector('[data-ch="'+ch+'"]');
|
||||
if (opt) pick(key, ch, opt);
|
||||
});
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
const btn = document.getElementById('saveBtn');
|
||||
btn.disabled = true;
|
||||
btn.style.background = '#64748B';
|
||||
btn.textContent = '⏳ Menyimpan...';
|
||||
document.getElementById('frm').submit();
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const rem = totalK - done.size;
|
||||
const btn = document.getElementById('saveBtn');
|
||||
const lbl = document.getElementById('lbl');
|
||||
const chip = document.getElementById('chip');
|
||||
if (rem <= 0) {
|
||||
btn.disabled = false;
|
||||
btn.style.background = 'linear-gradient(135deg,#166534,#22C55E)';
|
||||
btn.textContent = '💾 Simpan ke Database';
|
||||
if (lbl) lbl.innerHTML = '<span style="color:#86EFAC">✅ Semua konflik selesai</span>';
|
||||
if (chip) chip.textContent = '0';
|
||||
} else {
|
||||
btn.disabled = true;
|
||||
btn.style.background = '#64748B';
|
||||
btn.textContent = '⏳ Selesaikan ' + rem + ' konflik';
|
||||
if (lbl) lbl.innerHTML = '<span style="color:#FCA5A5">⚡ <span id="cnt">'+rem+'</span> konflik</span>';
|
||||
if (chip) chip.textContent = rem;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
{{-- resources/views/admin/mesin/mapping-santri/index.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Mapping ID Fingerprint')
|
||||
@section('content')
|
||||
|
||||
<div class="page-header" style="display:flex;align-items:center;justify-content:space-between">
|
||||
<h2><i class="fas fa-link"></i> Mapping ID Fingerprint</h2>
|
||||
<a href="{{ route('admin.kegiatan.index') }}" class="btn btn-sm btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> {{ session('success') }}</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger"><i class="fas fa-times-circle"></i> {{ session('error') }}</div>
|
||||
@endif
|
||||
|
||||
{{-- Auto-import dari INFO.XLS --}}
|
||||
<div class="content-box" style="margin-bottom:14px">
|
||||
<h4 style="margin:0 0 6px;font-size:15px">
|
||||
<i class="fas fa-magic" style="color:#166534"></i> Auto-Import dari INFO.XLS
|
||||
</h4>
|
||||
<p style="margin:0 0 12px;color:#6B7280;font-size:13px">
|
||||
Upload INFO.XLS dari mesin Eppos sistem otomatis cocokkan nama dan buat mapping.
|
||||
Nama yang tidak cocok otomatis ke dropdown untuk dipilih manual.
|
||||
</p>
|
||||
<form action="{{ route('admin.mesin.mapping-santri.import-info') }}"
|
||||
method="POST" enctype="multipart/form-data"
|
||||
style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||||
@csrf
|
||||
<input type="file" name="file_info" accept=".xls,.xlsx" required
|
||||
class="form-control" style="max-width:320px">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-magic"></i> Auto-Import
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Statistik --}}
|
||||
@php
|
||||
$total = $mappings->count();
|
||||
$terpetakan = $mappings->filter(fn($m) => !empty($m->id_santri))->count();
|
||||
$belum = $total - $terpetakan;
|
||||
@endphp
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:14px">
|
||||
<div style="background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;padding:14px;text-align:center">
|
||||
<div style="font-size:28px;font-weight:700;color:#1F2937">{{ $total }}</div>
|
||||
<div style="font-size:12px;color:#6B7280">Total ID Mesin</div>
|
||||
</div>
|
||||
<div style="background:#DCFCE7;border:1px solid #BBF7D0;border-radius:10px;padding:14px;text-align:center">
|
||||
<div style="font-size:28px;font-weight:700;color:#166534">{{ $terpetakan }}</div>
|
||||
<div style="font-size:12px;color:#166534">Terpetakan</div>
|
||||
</div>
|
||||
<div style="background:{{ $belum > 0 ? '#FEE2E2' : '#DCFCE7' }};border:1px solid {{ $belum > 0 ? '#FECACA' : '#BBF7D0' }};border-radius:10px;padding:14px;text-align:center">
|
||||
<div style="font-size:28px;font-weight:700;color:{{ $belum > 0 ? '#991B1B' : '#166534' }}">{{ $belum }}</div>
|
||||
<div style="font-size:12px;color:{{ $belum > 0 ? '#991B1B' : '#166534' }}">
|
||||
{{ $belum > 0 ? 'Belum Dipetakan' : 'Semua Terpetakan' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Peringatan jika ada yang belum --}}
|
||||
@if($belum > 0)
|
||||
<div style="background:#FEF9C3;border:1px solid #FDE68A;border-left:4px solid #F59E0B;
|
||||
border-radius:8px;padding:12px 16px;margin-bottom:14px;font-size:13px">
|
||||
<strong>âš {{ $belum }} ID mesin belum dipetakan ke santri.</strong>
|
||||
Data scan santri tersebut tidak akan tersimpan saat import absensi.
|
||||
Pilih santri yang sesuai dari dropdown di bawah.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Tabel Mapping --}}
|
||||
<div class="content-box" style="padding:0;overflow:hidden;margin-bottom:14px">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:70px;text-align:center">ID Mesin</th>
|
||||
<th>Nama di Mesin</th>
|
||||
<th style="width:90px">Dept/Kel</th>
|
||||
<th>Santri Web yang Dipetakan</th>
|
||||
<th style="width:110px;text-align:center">Status</th>
|
||||
<th style="width:70px;text-align:center">Hapus</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($mappings as $m)
|
||||
<tr style="background:{{ empty($m->id_santri) ? '#FFFBEB' : 'white' }}">
|
||||
|
||||
{{-- ID Mesin --}}
|
||||
<td style="text-align:center">
|
||||
<strong style="font-family:monospace;font-size:15px;color:#1D4ED8">
|
||||
{{ $m->id_mesin }}
|
||||
</strong>
|
||||
</td>
|
||||
|
||||
{{-- Nama di Mesin --}}
|
||||
<td>
|
||||
<div style="font-weight:600;color:#1F2937">{{ $m->nama_mesin ?? '-' }}</div>
|
||||
@if($m->catatan)
|
||||
<div style="font-size:11px;color:#9CA3AF">{{ $m->catatan }}</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Dept --}}
|
||||
<td style="color:#6B7280;font-size:12px">{{ $m->dept_mesin ?? '-' }}</td>
|
||||
|
||||
{{-- Dropdown Santri --}}
|
||||
<td>
|
||||
<form action="{{ route('admin.mesin.mapping-santri.update', $m->id) }}"
|
||||
method="POST" style="margin:0">
|
||||
@csrf @method('PUT')
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<select name="id_santri"
|
||||
class="form-control"
|
||||
style="font-size:13px;
|
||||
border-color:{{ empty($m->id_santri) ? '#FCA5A5' : '#D1D5DB' }};
|
||||
background:{{ empty($m->id_santri) ? '#FFF5F5' : 'white' }}"
|
||||
onchange="this.form.submit()">
|
||||
<option value="">-- Pilih Santri --</option>
|
||||
@foreach($santris as $s)
|
||||
<option value="{{ $s->id_santri }}"
|
||||
{{ $m->id_santri == $s->id_santri ? 'selected' : '' }}>
|
||||
{{ $s->nama_lengkap }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(!empty($m->id_santri))
|
||||
<i class="fas fa-check-circle" style="color:#22C55E;font-size:16px" title="Sudah dipetakan"></i>
|
||||
@else
|
||||
<i class="fas fa-exclamation-circle" style="color:#EF4444;font-size:16px" title="Belum dipetakan"></i>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
{{-- Status --}}
|
||||
<td style="text-align:center">
|
||||
@if(!empty($m->id_santri))
|
||||
<span style="background:#DCFCE7;color:#166534;border-radius:12px;
|
||||
padding:3px 10px;font-size:11px;font-weight:700">
|
||||
Terpetakan
|
||||
</span>
|
||||
@else
|
||||
<span style="background:#FEE2E2;color:#991B1B;border-radius:12px;
|
||||
padding:3px 10px;font-size:11px;font-weight:700">
|
||||
âš Belum
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Hapus --}}
|
||||
<td style="text-align:center">
|
||||
<form action="{{ route('admin.mesin.mapping-santri.destroy', $m->id) }}"
|
||||
method="POST"
|
||||
onsubmit="return confirm('Hapus mapping ID Mesin {{ $m->id_mesin }} ({{ $m->nama_mesin }})?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit"
|
||||
class="btn btn-sm btn-danger"
|
||||
style="padding:4px 10px">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center;padding:40px;color:#9CA3AF">
|
||||
<i class="fas fa-inbox" style="font-size:32px;display:block;margin-bottom:8px"></i>
|
||||
Belum ada mapping. Upload INFO.XLS di atas untuk mulai.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tambah Manual --}}
|
||||
<div class="content-box">
|
||||
<h4 style="margin:0 0 6px;font-size:15px">
|
||||
<i class="fas fa-plus-circle" style="color:#1D4ED8"></i> Tambah Mapping Manual
|
||||
</h4>
|
||||
<p style="margin:0 0 12px;color:#6B7280;font-size:13px">
|
||||
Untuk santri yang baru daftar ke mesin setelah INFO.XLS diekspor,
|
||||
atau santri yang nama di mesin sangat berbeda dari nama di sistem.
|
||||
</p>
|
||||
<form action="{{ route('admin.mesin.mapping-santri.store') }}" method="POST">
|
||||
@csrf
|
||||
<div style="display:grid;grid-template-columns:120px 160px 1fr auto;gap:10px;align-items:end">
|
||||
<div class="form-group" style="margin:0">
|
||||
<label style="font-size:12px;font-weight:600">ID Mesin <span style="color:red">*</span></label>
|
||||
<input type="text" name="id_mesin" class="form-control"
|
||||
placeholder="cth: 8" required value="{{ old('id_mesin') }}"
|
||||
style="font-family:monospace;font-size:15px;font-weight:700">
|
||||
@error('id_mesin')
|
||||
<p style="color:#EF4444;font-size:11px;margin:3px 0 0">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="form-group" style="margin:0">
|
||||
<label style="font-size:12px;font-weight:600">Nama di Mesin</label>
|
||||
<input type="text" name="nama_mesin" class="form-control"
|
||||
placeholder="cth: ilham" value="{{ old('nama_mesin') }}">
|
||||
</div>
|
||||
<div class="form-group" style="margin:0">
|
||||
<label style="font-size:12px;font-weight:600">Santri Web</label>
|
||||
<select name="id_santri" class="form-control">
|
||||
<option value="">-- Pilih Santri (bisa diisi nanti) --</option>
|
||||
@foreach($santris as $s)
|
||||
<option value="{{ $s->id_santri }}"
|
||||
{{ old('id_santri') == $s->id_santri ? 'selected' : '' }}>
|
||||
{{ $s->nama_lengkap }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary" style="white-space:nowrap;padding:9px 18px">
|
||||
<i class="fas fa-plus"></i> Tambah
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/pembayaran-spp/index.blade.php --}}
|
||||
{{-- resources/views/admin/pembayaran-spp/index.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Pembayaran SPP')
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
<div class="content-box">
|
||||
|
||||
{{-- ── Filter ── --}}
|
||||
{{-- ── Filter ── --}}
|
||||
<div style="background:#f8f9fa;padding:14px;border-radius:8px;margin-bottom:14px;">
|
||||
<form method="GET" action="{{ route('admin.pembayaran-spp.index') }}" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;">
|
||||
<input type="hidden" name="tab" value="{{ $tab }}">
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
{{-- ── KPI Cards ── --}}
|
||||
{{-- ── KPI Cards ── --}}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-bottom:14px;">
|
||||
<div class="kpi-card" style="background:linear-gradient(135deg,#667eea,#764ba2);">
|
||||
<div><div class="kpi-label">Total Santri Aktif</div><div class="kpi-val">{{ $totalSantriAll }}</div><div class="kpi-sub">Periode ini</div></div>
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Tab Navigation ── --}}
|
||||
{{-- ── Tab Navigation ── --}}
|
||||
<div style="display:flex;gap:6px;margin-bottom:14px;border-bottom:2px solid #e0e0e0;flex-wrap:wrap;">
|
||||
<a href="{{ route('admin.pembayaran-spp.index', array_merge(request()->except('tab'),['tab'=>'belum-bayar'])) }}"
|
||||
class="spp-tab {{ $tab==='belum-bayar'?'spp-tab-danger':'spp-tab-outline-danger' }}">
|
||||
|
|
@ -109,7 +109,7 @@ class="spp-tab {{ $tab==='sudah-bayar'?'spp-tab-success':'spp-tab-outline-succes
|
|||
</a>
|
||||
</div>
|
||||
|
||||
{{-- ── Action Buttons ── --}}
|
||||
{{-- ── Action Buttons ── --}}
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;flex-wrap:wrap;gap:8px;">
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<a href="{{ route('admin.pembayaran-spp.generate') }}" class="btn btn-warning btn-sm"><i class="fas fa-cogs"></i> Generate SPP</a>
|
||||
|
|
@ -118,12 +118,13 @@ class="spp-tab {{ $tab==='sudah-bayar'?'spp-tab-success':'spp-tab-outline-succes
|
|||
</div>
|
||||
<div style="font-size:11px;color:#666;">
|
||||
Periode: <strong>{{ $bulanIndo[$bulan]??'' }} {{ $tahun }}</strong>
|
||||
@if($tab==='sudah-bayar') · <i class="fas fa-sort-amount-down"></i> Terbaru bayar di atas @endif
|
||||
@if($tab==='sudah-bayar') · <i class="fas fa-sort-amount-down"></i> Terbaru bayar di atas @endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Table ── --}}
|
||||
{{-- ── Table ── --}}
|
||||
<div style="overflow-x:auto;">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -194,7 +195,7 @@ class="spp-tab {{ $tab==='sudah-bayar'?'spp-tab-success':'spp-tab-outline-succes
|
|||
</td>
|
||||
@endif
|
||||
|
||||
{{-- ── Aksi (1 baris, tombol ikon) ── --}}
|
||||
{{-- ── Aksi (1 baris, tombol ikon) ── --}}
|
||||
<td class="text-center" style="white-space:nowrap;">
|
||||
@if($item['pembayaran'])
|
||||
{{-- Riwayat selalu ada --}}
|
||||
|
|
@ -249,9 +250,10 @@ class="btn btn-sm btn-primary" title="Buat Tagihan"><i class="fas fa-plus"></i>
|
|||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Pagination ── --}}
|
||||
{{-- ── Pagination ── --}}
|
||||
@if($totalPages>1)
|
||||
<div style="margin-top:14px;display:flex;justify-content:center;align-items:center;gap:10px;">
|
||||
@if($currentPage>1)
|
||||
|
|
@ -265,7 +267,7 @@ class="btn btn-sm btn-primary" title="Buat Tagihan"><i class="fas fa-plus"></i>
|
|||
@endif
|
||||
</div>
|
||||
|
||||
{{-- ── Modal Catat Cicilan ── --}}
|
||||
{{-- ── Modal Catat Cicilan ── --}}
|
||||
<div id="modalCicilan" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;align-items:center;justify-content:center;">
|
||||
<div style="background:#fff;border-radius:12px;padding:24px;width:100%;max-width:400px;box-shadow:0 20px 60px rgba(0,0,0,.3);margin:16px;">
|
||||
<h4 style="margin:0 0 4px;"><i class="fas fa-coins" style="color:#9c27b0;"></i> Catat Cicilan</h4>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/pembayaran-spp/riwayat.blade.php --}}
|
||||
{{-- resources/views/admin/pembayaran-spp/riwayat.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Riwayat Pembayaran SPP')
|
||||
|
|
@ -56,6 +56,7 @@
|
|||
</h4>
|
||||
|
||||
<div style="overflow-x: auto;">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -115,6 +116,7 @@ class="btn btn-sm btn-warning"
|
|||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Pembinaan & Sanksi')
|
||||
|
||||
|
|
@ -37,6 +37,8 @@
|
|||
<strong>Info:</strong> Konten akan ditampilkan sesuai urutan. Drag atau ubah nomor urutan untuk mengatur tampilan.
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -107,6 +109,8 @@ class="btn btn-sm btn-warning"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/riwayat_pelanggaran/index.blade.php --}}
|
||||
{{-- resources/views/admin/riwayat_pelanggaran/index.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Riwayat Pelanggaran')
|
||||
|
|
@ -155,6 +155,7 @@ class="form-control"
|
|||
</div>
|
||||
|
||||
@if($data->isNotEmpty())
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -233,6 +234,7 @@ class="btn btn-sm btn-danger"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div style="margin-top: 14px;">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{-- resources/views/admin/riwayat_pelanggaran/riwayat_santri.blade.php --}}
|
||||
{{-- resources/views/admin/riwayat_pelanggaran/riwayat_santri.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Riwayat Pelanggaran - ' . $santri->nama_lengkap)
|
||||
|
|
@ -68,6 +68,7 @@
|
|||
</div>
|
||||
|
||||
@if($riwayat->isNotEmpty())
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -145,6 +146,7 @@ class="btn btn-sm btn-danger"
|
|||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div style="margin-top: 14px;">
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue