siswa
This commit is contained in:
parent
7586a2beb5
commit
7ce8586b8c
|
|
@ -0,0 +1,249 @@
|
||||||
|
# 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.
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
# 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)
|
||||||
|
|
@ -11,7 +11,7 @@ LOG_LEVEL=debug
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=laravel
|
DB_DATABASE=sim_santri
|
||||||
DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
// app/Console/Commands/CleanSantriAccounts.php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\SantriAccount;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class CleanSantriAccounts extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Nama dan signature command.
|
||||||
|
*/
|
||||||
|
protected $signature = 'santri:clean-accounts
|
||||||
|
{--dry-run : Tampilkan perubahan tanpa menyimpan}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deskripsi command.
|
||||||
|
*/
|
||||||
|
protected $description = 'Reset username & password semua akun santri/wali agar konsisten (santri=nama_lengkap, wali=nama_orang_tua, password=NIS)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jalankan command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->info('=== DRY RUN MODE (tidak ada perubahan yang disimpan) ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
$accounts = SantriAccount::with('santri')->get();
|
||||||
|
$updated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($accounts as $account) {
|
||||||
|
$santri = $account->santri;
|
||||||
|
|
||||||
|
// -- Skip jika santri tidak ditemukan --
|
||||||
|
if (!$santri) {
|
||||||
|
$errors[] = "Account ID {$account->id} (id_santri={$account->id_santri}): Data santri tidak ditemukan.";
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Skip jika NIS kosong --
|
||||||
|
if (empty($santri->nis)) {
|
||||||
|
$errors[] = "Account ID {$account->id} ({$santri->nama_lengkap}): NIS kosong, dilewati.";
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Tentukan username yang benar --
|
||||||
|
if ($account->role === 'wali') {
|
||||||
|
$usernameBenar = $santri->nama_orang_tua ?: $santri->nama_lengkap;
|
||||||
|
} else {
|
||||||
|
$usernameBenar = $santri->nama_lengkap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Cek apakah username sudah benar --
|
||||||
|
$usernameChanged = ($account->username !== $usernameBenar);
|
||||||
|
|
||||||
|
if ($usernameChanged) {
|
||||||
|
// -- Pastikan username unik --
|
||||||
|
$existing = SantriAccount::where('username', $usernameBenar)
|
||||||
|
->where('id', '!=', $account->id)
|
||||||
|
->exists();
|
||||||
|
if ($existing) {
|
||||||
|
$usernameBenar = $usernameBenar . '_' . $santri->nis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($usernameChanged) {
|
||||||
|
$this->line(" [{$account->role}] {$santri->nama_lengkap}: username '{$account->username}' -> '{$usernameBenar}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
$account->username = $usernameBenar;
|
||||||
|
$account->password = Hash::make($santri->nis);
|
||||||
|
$account->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("Selesai! Updated: {$updated}, Skipped: {$skipped}");
|
||||||
|
|
||||||
|
if (count($errors) > 0) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->warn('Masalah ditemukan:');
|
||||||
|
foreach ($errors as $err) {
|
||||||
|
$this->warn(" - {$err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->comment('Jalankan tanpa --dry-run untuk menyimpan perubahan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\AbsensiKegiatan;
|
use App\Models\AbsensiKegiatan;
|
||||||
use App\Models\Kegiatan;
|
use App\Models\Kegiatan;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Kepulangan;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
@ -12,49 +14,11 @@
|
||||||
class AbsensiKegiatanController extends Controller
|
class AbsensiKegiatanController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Daftar kegiatan untuk absensi
|
* Daftar kegiatan untuk absensi — diarahkan ke Dashboard Kegiatan (tidak redundan)
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
// Query dengan eager loading untuk optimasi
|
return redirect()->route('admin.kegiatan.jadwal');
|
||||||
$query = Kegiatan::with(['kategori', 'kelasKegiatan'])
|
|
||||||
->select('id', 'kegiatan_id', 'kategori_id', 'nama_kegiatan', 'hari', 'waktu_mulai', 'waktu_selesai');
|
|
||||||
|
|
||||||
// Filter Hari
|
|
||||||
if ($request->filled('hari')) {
|
|
||||||
$query->where('hari', $request->hari);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter Kategori
|
|
||||||
if ($request->filled('kategori_id')) {
|
|
||||||
$query->where('kategori_id', $request->kategori_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter Kelas
|
|
||||||
if ($request->filled('id_kelas')) {
|
|
||||||
$query->whereHas('kelasKegiatan', function($q) use ($request) {
|
|
||||||
$q->where('kelas.id', $request->id_kelas);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search
|
|
||||||
if ($request->filled('search')) {
|
|
||||||
$search = $request->search;
|
|
||||||
$query->where(function($q) use ($search) {
|
|
||||||
$q->where('nama_kegiatan', 'like', "%{$search}%")
|
|
||||||
->orWhere('kegiatan_id', 'like', "%{$search}%");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pagination dengan 15 item per page
|
|
||||||
$kegiatans = $query->orderBy('hari')->orderBy('waktu_mulai')->paginate(15)->appends(request()->query());
|
|
||||||
|
|
||||||
// Data untuk filter
|
|
||||||
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
|
|
||||||
$kategoris = \App\Models\KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
|
||||||
$kelasList = \App\Models\Kelas::with('kelompok')->orderBy('urutan')->get();
|
|
||||||
|
|
||||||
return view('admin.kegiatan.absensi.index', compact('kegiatans', 'hariList', 'kategoris', 'kelasList'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -63,42 +27,45 @@ public function index(Request $request)
|
||||||
public function inputAbsensi($kegiatan_id)
|
public function inputAbsensi($kegiatan_id)
|
||||||
{
|
{
|
||||||
// Get kegiatan dengan relasi kategori dan kelas
|
// Get kegiatan dengan relasi kategori dan kelas
|
||||||
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan'])
|
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok'])
|
||||||
->where('kegiatan_id', $kegiatan_id)
|
->where('kegiatan_id', $kegiatan_id)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$tanggal = request('tanggal', now()->format('Y-m-d'));
|
$tanggal = request('tanggal', now()->format('Y-m-d'));
|
||||||
|
|
||||||
// Get santri sesuai kelas kegiatan
|
// Build santri grouped by kegiatan kelas
|
||||||
|
$santriGrouped = collect();
|
||||||
|
|
||||||
if ($kegiatan->isForAllClasses()) {
|
if ($kegiatan->isForAllClasses()) {
|
||||||
// Kegiatan umum: ambil SEMUA santri aktif
|
// Kegiatan umum: ambil SEMUA santri aktif, group by primary kelas
|
||||||
$santris = Santri::where('status', 'Aktif')
|
$allSantris = Santri::where('status', 'Aktif')
|
||||||
->with('kelasSantri.kelas')
|
->with(['kelasSantri.kelas', 'kelasPrimary.kelas'])
|
||||||
->orderBy('nama_lengkap')
|
->orderBy('nama_lengkap')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$santriGrouped = $allSantris->groupBy(function($s) {
|
||||||
|
$primary = $s->kelasPrimary;
|
||||||
|
return $primary && $primary->kelas ? $primary->kelas->nama_kelas : 'Tanpa Kelas';
|
||||||
|
})->sortKeys();
|
||||||
} else {
|
} else {
|
||||||
// Kegiatan khusus: ambil santri yang kelasnya match
|
// Kegiatan khusus: group by kegiatan kelas
|
||||||
$kelasIds = $kegiatan->kelasKegiatan->pluck('id')->toArray();
|
foreach ($kegiatan->kelasKegiatan as $kelas) {
|
||||||
|
$santriInKelas = Santri::where('status', 'Aktif')
|
||||||
// Coba ambil santri dari sistem kelas baru
|
->whereHas('kelasSantri', function($q) use ($kelas) {
|
||||||
$santris = Santri::where('status', 'Aktif')
|
$q->where('id_kelas', $kelas->id);
|
||||||
->whereHas('kelasSantri', function($query) use ($kelasIds) {
|
|
||||||
$query->whereIn('id_kelas', $kelasIds);
|
|
||||||
})
|
})
|
||||||
->with('kelasSantri.kelas')
|
->with(['kelasSantri.kelas', 'kelasPrimary.kelas'])
|
||||||
->orderBy('nama_lengkap')
|
->orderBy('nama_lengkap')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Fallback: Jika tidak ada santri (belum migrasi), gunakan old column kelas
|
if ($santriInKelas->count() > 0) {
|
||||||
if ($santris->isEmpty()) {
|
$santriGrouped[$kelas->nama_kelas] = $santriInKelas;
|
||||||
$kelasNames = $kegiatan->kelasKegiatan->pluck('nama_kelas')->toArray();
|
|
||||||
$santris = Santri::where('status', 'Aktif')
|
|
||||||
->whereIn('kelas', $kelasNames)
|
|
||||||
->with('kelasSantri.kelas')
|
|
||||||
->orderBy('nama_lengkap')
|
|
||||||
->get();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten for total count
|
||||||
|
$santris = $santriGrouped->flatten()->unique('id_santri');
|
||||||
|
|
||||||
// Ambil data absensi yang sudah ada
|
// Ambil data absensi yang sudah ada
|
||||||
$absensiData = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
|
$absensiData = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
|
||||||
|
|
@ -106,6 +73,13 @@ public function inputAbsensi($kegiatan_id)
|
||||||
->pluck('status', 'id_santri')
|
->pluck('status', 'id_santri')
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
|
// Cek santri yang sedang pulang
|
||||||
|
$santriSedangPulang = Kepulangan::where('status', 'Disetujui')
|
||||||
|
->where('tanggal_pulang', '<=', $tanggal)
|
||||||
|
->where('tanggal_kembali', '>=', $tanggal)
|
||||||
|
->pluck('id_santri')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
// Info kelas kegiatan untuk view
|
// Info kelas kegiatan untuk view
|
||||||
$kegiatanInfo = [
|
$kegiatanInfo = [
|
||||||
'is_umum' => $kegiatan->isForAllClasses(),
|
'is_umum' => $kegiatan->isForAllClasses(),
|
||||||
|
|
@ -113,24 +87,42 @@ public function inputAbsensi($kegiatan_id)
|
||||||
'jumlah_kelas' => $kegiatan->kelasKegiatan->count(),
|
'jumlah_kelas' => $kegiatan->kelasKegiatan->count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return view('admin.kegiatan.absensi.input', compact('kegiatan', 'santris', 'absensiData', 'tanggal', 'kegiatanInfo'));
|
return view('admin.kegiatan.absensi.input', compact('kegiatan', 'santris', 'santriGrouped', 'absensiData', 'tanggal', 'kegiatanInfo', 'santriSedangPulang'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simpan absensi manual
|
* Simpan absensi manual (hanya santri yang dikirim form)
|
||||||
*/
|
*/
|
||||||
public function simpanAbsensi(Request $request)
|
public function simpanAbsensi(Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'kegiatan_id' => 'required|exists:kegiatans,kegiatan_id',
|
'kegiatan_id' => 'required|exists:kegiatans,kegiatan_id',
|
||||||
'tanggal' => 'required|date',
|
'tanggal' => 'required|date',
|
||||||
'absensi' => 'required|array',
|
'absensi' => 'nullable|array',
|
||||||
'absensi.*' => 'required|in:Hadir,Izin,Sakit,Alpa',
|
'absensi.*' => 'nullable|in:Hadir,Izin,Sakit,Alpa,Terlambat,Pulang',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Cek santri yang sedang pulang
|
||||||
|
$santriSedangPulang = Kepulangan::where('status', 'Disetujui')
|
||||||
|
->where('tanggal_pulang', '<=', $request->tanggal)
|
||||||
|
->where('tanggal_kembali', '>=', $request->tanggal)
|
||||||
|
->pluck('id_santri')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$absensiInput = $request->absensi ?? [];
|
||||||
|
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
try {
|
try {
|
||||||
foreach ($request->absensi as $id_santri => $status) {
|
$saved = 0;
|
||||||
|
foreach ($absensiInput as $id_santri => $status) {
|
||||||
|
// Skip jika kosong (santri dilewati)
|
||||||
|
if (empty($status)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paksa status Pulang untuk santri yang sedang pulang
|
||||||
|
$finalStatus = in_array($id_santri, $santriSedangPulang) ? 'Pulang' : $status;
|
||||||
|
|
||||||
AbsensiKegiatan::updateOrCreate(
|
AbsensiKegiatan::updateOrCreate(
|
||||||
[
|
[
|
||||||
'kegiatan_id' => $request->kegiatan_id,
|
'kegiatan_id' => $request->kegiatan_id,
|
||||||
|
|
@ -138,22 +130,66 @@ public function simpanAbsensi(Request $request)
|
||||||
'tanggal' => $request->tanggal,
|
'tanggal' => $request->tanggal,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'status' => $status,
|
'status' => $finalStatus,
|
||||||
'metode_absen' => 'Manual',
|
'metode_absen' => 'Manual',
|
||||||
'waktu_absen' => now()->format('H:i:s'),
|
'waktu_absen' => now()->format('H:i:s'),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
$saved++;
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
return redirect()->route('admin.absensi-kegiatan.index')
|
return redirect()->route('admin.kegiatan.index')
|
||||||
->with('success', 'Absensi berhasil disimpan.');
|
->with('success', "Absensi berhasil disimpan ({$saved} santri).");
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
return back()->with('error', 'Gagal menyimpan absensi: ' . $e->getMessage());
|
return back()->with('error', 'Gagal menyimpan absensi: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit single absensi record
|
||||||
|
*/
|
||||||
|
public function editAbsensi($id)
|
||||||
|
{
|
||||||
|
$absensi = AbsensiKegiatan::with(['santri', 'kegiatan.kategori'])->findOrFail($id);
|
||||||
|
return view('admin.kegiatan.absensi.edit', compact('absensi'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update single absensi record
|
||||||
|
*/
|
||||||
|
public function updateAbsensi(Request $request, $id)
|
||||||
|
{
|
||||||
|
$absensi = AbsensiKegiatan::findOrFail($id);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'status' => 'required|in:Hadir,Izin,Sakit,Alpa,Terlambat,Pulang',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$absensi->update([
|
||||||
|
'status' => $validated['status'],
|
||||||
|
'waktu_absen' => now()->format('H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.absensi-kegiatan.rekap', $absensi->kegiatan_id)
|
||||||
|
->with('success', 'Status absensi ' . $absensi->santri->nama_lengkap . ' berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hapus single absensi record
|
||||||
|
*/
|
||||||
|
public function hapusAbsensi($id)
|
||||||
|
{
|
||||||
|
$absensi = AbsensiKegiatan::findOrFail($id);
|
||||||
|
$kegiatanId = $absensi->kegiatan_id;
|
||||||
|
$nama = $absensi->santri->nama_lengkap;
|
||||||
|
$absensi->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.absensi-kegiatan.rekap', $kegiatanId)
|
||||||
|
->with('success', 'Data absensi ' . $nama . ' berhasil dihapus.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rekap absensi kegiatan
|
* Rekap absensi kegiatan
|
||||||
*/
|
*/
|
||||||
|
|
@ -161,7 +197,7 @@ public function rekapAbsensi(Request $request, $kegiatan_id)
|
||||||
{
|
{
|
||||||
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan'])->where('kegiatan_id', $kegiatan_id)->firstOrFail();
|
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan'])->where('kegiatan_id', $kegiatan_id)->firstOrFail();
|
||||||
|
|
||||||
$query = AbsensiKegiatan::with('santri')
|
$query = AbsensiKegiatan::with(['santri.kelasSantri.kelas'])
|
||||||
->where('kegiatan_id', $kegiatan_id);
|
->where('kegiatan_id', $kegiatan_id);
|
||||||
|
|
||||||
// Filter tanggal
|
// Filter tanggal
|
||||||
|
|
@ -175,18 +211,61 @@ public function rekapAbsensi(Request $request, $kegiatan_id)
|
||||||
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
|
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter kelas
|
||||||
|
if ($request->filled('kelas_id')) {
|
||||||
|
$query->whereHas('santri.kelasSantri', function($q) use ($request) {
|
||||||
|
$q->where('id_kelas', $request->kelas_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$absensis = $query->orderBy('tanggal', 'desc')
|
$absensis = $query->orderBy('tanggal', 'desc')
|
||||||
->orderBy('waktu_absen', 'desc')
|
->orderBy('waktu_absen', 'desc')
|
||||||
->paginate(20);
|
->get();
|
||||||
|
|
||||||
|
// Build kelas list for filter dropdown
|
||||||
|
if ($kegiatan->isForAllClasses()) {
|
||||||
|
$kelasFilterList = Kelas::active()->ordered()->get();
|
||||||
|
} else {
|
||||||
|
$kelasFilterList = $kegiatan->kelasKegiatan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Statistik
|
// Statistik
|
||||||
$stats = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
|
$statsQuery = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id);
|
||||||
->select('status', DB::raw('count(*) as total'))
|
if ($request->filled('tanggal')) {
|
||||||
|
$statsQuery->whereDate('tanggal', $request->tanggal);
|
||||||
|
}
|
||||||
|
if ($request->filled('bulan')) {
|
||||||
|
$statsQuery->whereMonth('tanggal', date('m', strtotime($request->bulan)))
|
||||||
|
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
|
||||||
|
}
|
||||||
|
if ($request->filled('kelas_id')) {
|
||||||
|
$statsQuery->whereHas('santri.kelasSantri', function($q) use ($request) {
|
||||||
|
$q->where('id_kelas', $request->kelas_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$stats = $statsQuery->select('status', DB::raw('count(*) as total'))
|
||||||
->groupBy('status')
|
->groupBy('status')
|
||||||
->pluck('total', 'status')
|
->pluck('total', 'status')
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
return view('admin.kegiatan.absensi.rekap', compact('kegiatan', 'absensis', 'stats'));
|
return view('admin.kegiatan.absensi.rekap', compact('kegiatan', 'absensis', 'absensiPerKelas', 'stats', 'kelasFilterList'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,18 +5,16 @@
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Barryvdh\DomPDF\Facade\Pdf;
|
use Mpdf\Mpdf;
|
||||||
|
use Mpdf\Config\ConfigVariables;
|
||||||
|
use Mpdf\Config\FontVariables;
|
||||||
|
|
||||||
class KartuRfidController extends Controller
|
class KartuRfidController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Halaman kelola kartu RFID
|
|
||||||
*/
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$query = Santri::where('status', 'Aktif');
|
$query = Santri::where('status', 'Aktif');
|
||||||
|
|
||||||
// Filter: Santri yang sudah/belum punya RFID
|
|
||||||
if ($request->filled('filter')) {
|
if ($request->filled('filter')) {
|
||||||
if ($request->filter == 'ada_rfid') {
|
if ($request->filter == 'ada_rfid') {
|
||||||
$query->whereNotNull('rfid_uid');
|
$query->whereNotNull('rfid_uid');
|
||||||
|
|
@ -25,28 +23,24 @@ public function index(Request $request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$santris = $query->select('id', 'id_santri', 'nama_lengkap', 'kelas', 'rfid_uid')
|
$santris = $query
|
||||||
|
->select('id', 'id_santri', 'nis', 'nama_lengkap', 'rfid_uid', 'foto', 'status')
|
||||||
|
->with(['kelasSantri.kelas'])
|
||||||
->orderBy('nama_lengkap')
|
->orderBy('nama_lengkap')
|
||||||
->paginate(15);
|
->paginate(15);
|
||||||
|
|
||||||
return view('admin.kegiatan.kartu.index', compact('santris'));
|
return view('admin.kegiatan.kartu.index', compact('santris'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Form daftarkan RFID ke santri
|
|
||||||
*/
|
|
||||||
public function daftarRfid($id_santri)
|
public function daftarRfid($id_santri)
|
||||||
{
|
{
|
||||||
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
||||||
return view('admin.kegiatan.kartu.daftar', compact('santri'));
|
return view('admin.kegiatan.kartu.daftar', compact('santri'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simpan RFID UID ke santri
|
|
||||||
*/
|
|
||||||
public function simpanRfid(Request $request, $id_santri)
|
public function simpanRfid(Request $request, $id_santri)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$request->validate([
|
||||||
'rfid_uid' => 'required|string|max:50|unique:santris,rfid_uid',
|
'rfid_uid' => 'required|string|max:50|unique:santris,rfid_uid',
|
||||||
], [
|
], [
|
||||||
'rfid_uid.required' => 'UID RFID wajib diisi.',
|
'rfid_uid.required' => 'UID RFID wajib diisi.',
|
||||||
|
|
@ -60,9 +54,6 @@ public function simpanRfid(Request $request, $id_santri)
|
||||||
->with('success', 'RFID berhasil didaftarkan untuk ' . $santri->nama_lengkap);
|
->with('success', 'RFID berhasil didaftarkan untuk ' . $santri->nama_lengkap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hapus RFID dari santri
|
|
||||||
*/
|
|
||||||
public function hapusRfid($id_santri)
|
public function hapusRfid($id_santri)
|
||||||
{
|
{
|
||||||
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
||||||
|
|
@ -72,20 +63,104 @@ public function hapusRfid($id_santri)
|
||||||
->with('success', 'RFID berhasil dihapus dari ' . $santri->nama_lengkap);
|
->with('success', 'RFID berhasil dihapus dari ' . $santri->nama_lengkap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cetak kartu RFID santri (PDF)
|
|
||||||
*/
|
|
||||||
public function cetakKartu($id_santri)
|
public function cetakKartu($id_santri)
|
||||||
{
|
{
|
||||||
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
$santri = Santri::where('id_santri', $id_santri)
|
||||||
|
->with([
|
||||||
|
'kelasPrimary.kelas',
|
||||||
|
'kelasSantri' => fn($q) => $q->orderByDesc('is_primary')->orderBy('id'),
|
||||||
|
'kelasSantri.kelas',
|
||||||
|
])
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
if (!$santri->rfid_uid) {
|
if (!$santri->rfid_uid) {
|
||||||
return back()->with('error', 'Santri belum memiliki RFID yang terdaftar.');
|
return back()->with('error', 'Santri belum memiliki RFID yang terdaftar.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdf = Pdf::loadView('admin.kegiatan.kartu.cetak', compact('santri'));
|
// ── Siapkan data untuk view ──────────────────────────────────────
|
||||||
$pdf->setPaper([0, 0, 243, 153], 'landscape'); // Ukuran kartu ID (85.6mm x 54mm)
|
$namaSantri = strtoupper($santri->nama_lengkap ?? 'NAMA SANTRI');
|
||||||
|
$initial = strtoupper(substr($santri->nama_lengkap ?? 'S', 0, 1));
|
||||||
|
$nis = !empty($santri->nis) ? $santri->nis : '-';
|
||||||
|
$uid = !empty($santri->rfid_uid) ? $santri->rfid_uid : '-';
|
||||||
|
|
||||||
return $pdf->stream('Kartu_RFID_' . $santri->id_santri . '.pdf');
|
// Kelas: pakai kelasPrimary, fallback ke first kelasSantri
|
||||||
|
$kelasNama = '-';
|
||||||
|
if ($santri->kelasPrimary && $santri->kelasPrimary->kelas) {
|
||||||
|
$kelasNama = strtoupper($santri->kelasPrimary->kelas->nama_kelas);
|
||||||
|
} elseif ($santri->kelasSantri->first() && $santri->kelasSantri->first()->kelas) {
|
||||||
|
$kelasNama = strtoupper($santri->kelasSantri->first()->kelas->nama_kelas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logo — embed base64 (tidak butuh GD)
|
||||||
|
$logoBase64 = '';
|
||||||
|
$logoMime = 'image/png';
|
||||||
|
foreach ([
|
||||||
|
public_path('images/logo.png'),
|
||||||
|
public_path('images/logo.jpg'),
|
||||||
|
public_path('img/logo.png'),
|
||||||
|
public_path('logo.png'),
|
||||||
|
] as $lp) {
|
||||||
|
if (file_exists($lp)) {
|
||||||
|
$ext = strtolower(pathinfo($lp, PATHINFO_EXTENSION));
|
||||||
|
$logoMime = $ext === 'png' ? 'image/png' : 'image/jpeg';
|
||||||
|
$logoBase64 = base64_encode(file_get_contents($lp));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Foto santri — embed base64 (tidak butuh GD)
|
||||||
|
$fotoBase64 = '';
|
||||||
|
$fotoMime = 'image/jpeg';
|
||||||
|
if (!empty($santri->foto)) {
|
||||||
|
foreach ([
|
||||||
|
storage_path('app/public/' . $santri->foto),
|
||||||
|
public_path('storage/' . $santri->foto),
|
||||||
|
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));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Render HTML dari blade ────────────────────────────────────────
|
||||||
|
$html = view('admin.kegiatan.kartu.cetak', compact(
|
||||||
|
'santri',
|
||||||
|
'namaSantri', 'initial', 'nis', 'uid', 'kelasNama',
|
||||||
|
'logoBase64', 'logoMime',
|
||||||
|
'fotoBase64', 'fotoMime'
|
||||||
|
))->render();
|
||||||
|
|
||||||
|
// ── Inisialisasi mPDF ─────────────────────────────────────────────
|
||||||
|
// Format: 54mm × 85.6mm (ukuran kartu ID standar)
|
||||||
|
$mpdf = new Mpdf([
|
||||||
|
'mode' => 'utf-8',
|
||||||
|
'format' => [54, 85.6],
|
||||||
|
'orientation' => 'P',
|
||||||
|
'margin_top' => 0,
|
||||||
|
'margin_bottom' => 0,
|
||||||
|
'margin_left' => 0,
|
||||||
|
'margin_right' => 0,
|
||||||
|
'margin_header' => 0,
|
||||||
|
'margin_footer' => 0,
|
||||||
|
'default_font' => 'dejavusans',
|
||||||
|
'tempDir' => storage_path('app/mpdf_tmp'),
|
||||||
|
'autoScriptToLang' => false,
|
||||||
|
'autoLangToFont' => false,
|
||||||
|
// Aktifkan dukungan SVG (untuk foto bulat)
|
||||||
|
'enableImports' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Matikan page break otomatis
|
||||||
|
$mpdf->SetAutoPageBreak(false);
|
||||||
|
$mpdf->SetDisplayMode('fullpage');
|
||||||
|
$mpdf->WriteHTML($html);
|
||||||
|
|
||||||
|
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"');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +19,6 @@ class KegiatanController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
// Tentukan tanggal yang dipilih (default: hari ini, tapi bisa pilih tanggal lain)
|
|
||||||
$selectedDate = $request->filled('tanggal')
|
$selectedDate = $request->filled('tanggal')
|
||||||
? Carbon::parse($request->tanggal)
|
? Carbon::parse($request->tanggal)
|
||||||
: Carbon::now();
|
: Carbon::now();
|
||||||
|
|
@ -31,145 +30,102 @@ public function index(Request $request)
|
||||||
'Thursday' => 'Kamis',
|
'Thursday' => 'Kamis',
|
||||||
'Friday' => 'Jumat',
|
'Friday' => 'Jumat',
|
||||||
'Saturday' => 'Sabtu',
|
'Saturday' => 'Sabtu',
|
||||||
'Sunday' => 'Ahad'
|
'Sunday' => 'Ahad',
|
||||||
];
|
];
|
||||||
|
|
||||||
$selectedHari = $hariIndonesia[$selectedDate->format('l')];
|
$selectedHari = $hariIndonesia[$selectedDate->format('l')];
|
||||||
|
|
||||||
// Filter kelas (optional)
|
|
||||||
$selectedKelasId = $request->filled('kelas') ? $request->kelas : null;
|
$selectedKelasId = $request->filled('kelas') ? $request->kelas : null;
|
||||||
|
|
||||||
// Query kegiatan hari yang dipilih
|
$query = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok', 'absensis' => function ($q) use ($selectedDate) {
|
||||||
$query = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok', 'absensis' => function($q) use ($selectedDate) {
|
|
||||||
$q->whereDate('tanggal', $selectedDate->format('Y-m-d'));
|
$q->whereDate('tanggal', $selectedDate->format('Y-m-d'));
|
||||||
}])->where('hari', $selectedHari);
|
}])->where('hari', $selectedHari);
|
||||||
|
|
||||||
// Filter by kelas if selected
|
|
||||||
if ($selectedKelasId) {
|
if ($selectedKelasId) {
|
||||||
if ($selectedKelasId === 'umum') {
|
if ($selectedKelasId === 'umum') {
|
||||||
// Kegiatan umum (tidak punya relasi kelas)
|
|
||||||
$query->doesntHave('kelasKegiatan');
|
$query->doesntHave('kelasKegiatan');
|
||||||
} else {
|
} else {
|
||||||
// Kegiatan untuk kelas tertentu
|
$query->whereHas('kelasKegiatan', function ($q) use ($selectedKelasId) {
|
||||||
$query->whereHas('kelasKegiatan', function($q) use ($selectedKelasId) {
|
|
||||||
$q->where('kelas.id', $selectedKelasId);
|
$q->where('kelas.id', $selectedKelasId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$kegiatanHariIni = $query->orderBy('waktu_mulai')->get();
|
if ($request->filled('kategori_id')) {
|
||||||
|
$query->where('kategori_id', $request->kategori_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Total santri aktif (untuk perhitungan %)
|
$kegiatanHariIni = $query->orderBy('waktu_mulai')->get();
|
||||||
$totalSantriAktif = Santri::where('status', 'Aktif')->count();
|
$totalSantriAktif = Santri::where('status', 'Aktif')->count();
|
||||||
|
|
||||||
// Hitung statistik untuk setiap kegiatan
|
|
||||||
$kegiatanHariIni->each(function ($kegiatan) use ($totalSantriAktif, $selectedDate) {
|
$kegiatanHariIni->each(function ($kegiatan) use ($totalSantriAktif, $selectedDate) {
|
||||||
$totalAbsensi = $kegiatan->absensis->count();
|
$totalAbsensi = $kegiatan->absensis->count();
|
||||||
$hadir = $kegiatan->absensis->where('status', 'Hadir')->count();
|
$hadir = $kegiatan->absensis->where('status', 'Hadir')->count();
|
||||||
|
|
||||||
// Persentase kehadiran
|
|
||||||
$persenKehadiran = $totalAbsensi > 0 ? round(($hadir / $totalAbsensi) * 100) : 0;
|
$persenKehadiran = $totalAbsensi > 0 ? round(($hadir / $totalAbsensi) * 100) : 0;
|
||||||
|
|
||||||
// Status kegiatan berdasarkan waktu
|
|
||||||
$now = Carbon::now();
|
$now = Carbon::now();
|
||||||
$waktuMulaiStr = is_string($kegiatan->waktu_mulai)
|
$waktuMulaiStr = is_string($kegiatan->waktu_mulai) ? $kegiatan->waktu_mulai : $kegiatan->waktu_mulai->format('H:i');
|
||||||
? $kegiatan->waktu_mulai
|
$waktuSelesaiStr = is_string($kegiatan->waktu_selesai) ? $kegiatan->waktu_selesai : $kegiatan->waktu_selesai->format('H:i');
|
||||||
: $kegiatan->waktu_mulai->format('H:i');
|
|
||||||
$waktuSelesaiStr = is_string($kegiatan->waktu_selesai)
|
|
||||||
? $kegiatan->waktu_selesai
|
|
||||||
: $kegiatan->waktu_selesai->format('H:i');
|
|
||||||
|
|
||||||
$waktuMulai = Carbon::parse($selectedDate->format('Y-m-d') . ' ' . $waktuMulaiStr);
|
$waktuMulai = Carbon::parse($selectedDate->format('Y-m-d') . ' ' . $waktuMulaiStr);
|
||||||
$waktuSelesai = Carbon::parse($selectedDate->format('Y-m-d') . ' ' . $waktuSelesaiStr);
|
$waktuSelesai = Carbon::parse($selectedDate->format('Y-m-d') . ' ' . $waktuSelesaiStr);
|
||||||
|
|
||||||
if ($selectedDate->isToday()) {
|
if ($selectedDate->isToday()) {
|
||||||
if ($now->lt($waktuMulai)) {
|
if ($now->lt($waktuMulai)) $status = 'belum';
|
||||||
$status = 'belum';
|
elseif ($now->between($waktuMulai, $waktuSelesai)) $status = 'berlangsung';
|
||||||
} elseif ($now->between($waktuMulai, $waktuSelesai)) {
|
else $status = 'selesai';
|
||||||
$status = 'berlangsung';
|
|
||||||
} else {
|
|
||||||
$status = 'selesai';
|
|
||||||
}
|
|
||||||
} elseif ($selectedDate->isFuture()) {
|
} elseif ($selectedDate->isFuture()) {
|
||||||
$status = 'belum';
|
$status = 'belum';
|
||||||
} else {
|
} else {
|
||||||
$status = 'selesai';
|
$status = 'selesai';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tambahkan data ke object
|
|
||||||
$kegiatan->total_hadir = $hadir;
|
$kegiatan->total_hadir = $hadir;
|
||||||
$kegiatan->total_absensi = $totalAbsensi;
|
$kegiatan->total_absensi = $totalAbsensi;
|
||||||
$kegiatan->persen_kehadiran = $persenKehadiran;
|
$kegiatan->persen_kehadiran = $persenKehadiran;
|
||||||
$kegiatan->status_kegiatan = $status;
|
$kegiatan->status_kegiatan = $status;
|
||||||
});
|
});
|
||||||
|
|
||||||
// KPI Cards
|
|
||||||
$totalKegiatanHariIni = $kegiatanHariIni->count();
|
$totalKegiatanHariIni = $kegiatanHariIni->count();
|
||||||
$kegiatanSelesai = $kegiatanHariIni->where('status_kegiatan', 'selesai')->count();
|
$kegiatanSelesai = $kegiatanHariIni->where('status_kegiatan', 'selesai')->count();
|
||||||
$kegiatanBerlangsung = $kegiatanHariIni->where('status_kegiatan', 'berlangsung')->count();
|
$kegiatanBerlangsung = $kegiatanHariIni->where('status_kegiatan', 'berlangsung')->count();
|
||||||
$avgKehadiran = $kegiatanHariIni->count() > 0
|
$avgKehadiran = $kegiatanHariIni->count() > 0 ? round($kegiatanHariIni->avg('persen_kehadiran')) : 0;
|
||||||
? round($kegiatanHariIni->avg('persen_kehadiran'))
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// KPI Comparison vs minggu lalu (same day)
|
|
||||||
$lastWeekDate = $selectedDate->copy()->subWeek();
|
$lastWeekDate = $selectedDate->copy()->subWeek();
|
||||||
$lastWeekHari = $hariIndonesia[$lastWeekDate->format('l')];
|
$lastWeekHari = $hariIndonesia[$lastWeekDate->format('l')];
|
||||||
|
$kegiatanLastWeekCount = Kegiatan::where('hari', $lastWeekHari)->count();
|
||||||
|
$comparisonTotal = $totalKegiatanHariIni - $kegiatanLastWeekCount;
|
||||||
|
|
||||||
$kegiatanLastWeek = Kegiatan::where('hari', $lastWeekHari)->count();
|
$avgKehadiranLastWeek = Cache::remember('avg_kehadiran_' . $lastWeekDate->format('Y-m-d'), 600, function () use ($lastWeekDate, $lastWeekHari) {
|
||||||
$comparisonTotal = $totalKegiatanHariIni - $kegiatanLastWeek;
|
$list = Kegiatan::where('hari', $lastWeekHari)->get();
|
||||||
|
|
||||||
// Avg kehadiran minggu lalu
|
|
||||||
$avgKehadiranLastWeek = Cache::remember('avg_kehadiran_' . $lastWeekDate->format('Y-m-d'), 600, function() use ($lastWeekDate, $lastWeekHari) {
|
|
||||||
$kegiatanLastWeek = Kegiatan::where('hari', $lastWeekHari)->get();
|
|
||||||
$totalPersen = 0;
|
$totalPersen = 0;
|
||||||
$count = 0;
|
$count = 0;
|
||||||
|
foreach ($list as $kg) {
|
||||||
foreach ($kegiatanLastWeek as $kg) {
|
$abs = AbsensiKegiatan::where('kegiatan_id', $kg->kegiatan_id)
|
||||||
$absensi = AbsensiKegiatan::where('kegiatan_id', $kg->kegiatan_id)
|
->whereDate('tanggal', $lastWeekDate->format('Y-m-d'))->get();
|
||||||
->whereDate('tanggal', $lastWeekDate->format('Y-m-d'))
|
if ($abs->count() > 0) {
|
||||||
->get();
|
$totalPersen += ($abs->where('status', 'Hadir')->count() / $abs->count()) * 100;
|
||||||
if ($absensi->count() > 0) {
|
|
||||||
$hadir = $absensi->where('status', 'Hadir')->count();
|
|
||||||
$totalPersen += ($hadir / $absensi->count()) * 100;
|
|
||||||
$count++;
|
$count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $count > 0 ? round($totalPersen / $count) : 0;
|
return $count > 0 ? round($totalPersen / $count) : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
$comparisonAvg = $avgKehadiran - $avgKehadiranLastWeek;
|
$comparisonAvg = $avgKehadiran - $avgKehadiranLastWeek;
|
||||||
|
|
||||||
// Get kelas list for filter tabs
|
|
||||||
$kelasList = Kelas::with('kelompok')->active()->ordered()->get();
|
$kelasList = Kelas::with('kelompok')->active()->ordered()->get();
|
||||||
|
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
||||||
// Generate Quick Insights
|
|
||||||
$insights = $this->generateInsights($kegiatanHariIni, $totalSantriAktif, $selectedDate);
|
$insights = $this->generateInsights($kegiatanHariIni, $totalSantriAktif, $selectedDate);
|
||||||
|
|
||||||
// Heatmap data (30 hari terakhir) - cached
|
$heatmapData = Cache::remember('heatmap_30days_' . now()->format('Y-m-d'), 600, function () {
|
||||||
$heatmapData = Cache::remember('heatmap_30days_' . now()->format('Y-m-d'), 600, function() {
|
|
||||||
return $this->generateHeatmapData();
|
return $this->generateHeatmapData();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Data untuk view
|
|
||||||
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
|
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
|
||||||
|
|
||||||
return view('admin.kegiatan.data.dashboard', compact(
|
return view('admin.kegiatan.data.dashboard', compact(
|
||||||
'kegiatanHariIni',
|
'kegiatanHariIni', 'totalKegiatanHariIni', 'kegiatanSelesai',
|
||||||
'totalKegiatanHariIni',
|
'kegiatanBerlangsung', 'avgKehadiran', 'totalSantriAktif',
|
||||||
'kegiatanSelesai',
|
'selectedDate', 'selectedHari', 'hariList', 'kelasList',
|
||||||
'kegiatanBerlangsung',
|
'selectedKelasId', 'comparisonTotal', 'comparisonAvg',
|
||||||
'avgKehadiran',
|
'insights', 'heatmapData', 'kategoris'
|
||||||
'totalSantriAktif',
|
|
||||||
'selectedDate',
|
|
||||||
'selectedHari',
|
|
||||||
'hariList',
|
|
||||||
'kelasList',
|
|
||||||
'selectedKelasId',
|
|
||||||
'comparisonTotal',
|
|
||||||
'comparisonAvg',
|
|
||||||
'insights',
|
|
||||||
'heatmapData'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,66 +136,53 @@ private function generateInsights($kegiatanHariIni, $totalSantriAktif, $selected
|
||||||
{
|
{
|
||||||
$insights = [];
|
$insights = [];
|
||||||
|
|
||||||
// Rule 1: Kehadiran rendah (<70%)
|
|
||||||
foreach ($kegiatanHariIni as $kegiatan) {
|
foreach ($kegiatanHariIni as $kegiatan) {
|
||||||
if ($kegiatan->total_absensi > 0 && $kegiatan->persen_kehadiran < 70) {
|
if ($kegiatan->total_absensi > 0 && $kegiatan->persen_kehadiran < 70) {
|
||||||
$insights[] = [
|
$insights[] = [
|
||||||
'type' => 'warning',
|
'type' => 'warning', 'icon' => 'exclamation-triangle',
|
||||||
'icon' => 'exclamation-triangle',
|
|
||||||
'message' => "Kegiatan {$kegiatan->nama_kegiatan} kehadiran rendah ({$kegiatan->persen_kehadiran}%)",
|
'message' => "Kegiatan {$kegiatan->nama_kegiatan} kehadiran rendah ({$kegiatan->persen_kehadiran}%)",
|
||||||
'detail' => "{$kegiatan->total_hadir} dari {$kegiatan->total_absensi} santri hadir",
|
'detail' => "{$kegiatan->total_hadir} dari {$kegiatan->total_absensi} santri hadir",
|
||||||
'action_url' => route('admin.absensi-kegiatan.input', $kegiatan->kegiatan_id) . '?tanggal=' . $selectedDate->format('Y-m-d'),
|
'action_url' => route('admin.absensi-kegiatan.input', $kegiatan->kegiatan_id) . '?tanggal=' . $selectedDate->format('Y-m-d'),
|
||||||
'action_text' => 'Input Absensi'
|
'action_text' => 'Input Absensi',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule 2: Kehadiran perfect (100%)
|
|
||||||
foreach ($kegiatanHariIni as $kegiatan) {
|
foreach ($kegiatanHariIni as $kegiatan) {
|
||||||
if ($kegiatan->persen_kehadiran == 100 && $kegiatan->total_absensi > 0) {
|
if ($kegiatan->persen_kehadiran == 100 && $kegiatan->total_absensi > 0) {
|
||||||
$insights[] = [
|
$insights[] = [
|
||||||
'type' => 'success',
|
'type' => 'success', 'icon' => 'check-circle',
|
||||||
'icon' => 'check-circle',
|
|
||||||
'message' => "Perfect! {$kegiatan->nama_kegiatan} kehadiran 100%",
|
'message' => "Perfect! {$kegiatan->nama_kegiatan} kehadiran 100%",
|
||||||
'detail' => 'Semua santri hadir',
|
'detail' => 'Semua santri hadir', 'action_url' => null, 'action_text' => null,
|
||||||
'action_url' => null,
|
|
||||||
'action_text' => null
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule 3: Kegiatan sedang berlangsung
|
|
||||||
$kegiatanLive = $kegiatanHariIni->where('status_kegiatan', 'berlangsung')->first();
|
$kegiatanLive = $kegiatanHariIni->where('status_kegiatan', 'berlangsung')->first();
|
||||||
if ($kegiatanLive) {
|
if ($kegiatanLive) {
|
||||||
$insights[] = [
|
$insights[] = [
|
||||||
'type' => 'info',
|
'type' => 'info', 'icon' => 'clock',
|
||||||
'icon' => 'clock',
|
|
||||||
'message' => "Kegiatan {$kegiatanLive->nama_kegiatan} sedang berlangsung",
|
'message' => "Kegiatan {$kegiatanLive->nama_kegiatan} sedang berlangsung",
|
||||||
'detail' => "Progress absensi: {$kegiatanLive->persen_kehadiran}%",
|
'detail' => "Progress absensi: {$kegiatanLive->persen_kehadiran}%",
|
||||||
'action_url' => route('admin.absensi-kegiatan.input', $kegiatanLive->kegiatan_id) . '?tanggal=' . $selectedDate->format('Y-m-d'),
|
'action_url' => route('admin.absensi-kegiatan.input', $kegiatanLive->kegiatan_id) . '?tanggal=' . $selectedDate->format('Y-m-d'),
|
||||||
'action_text' => 'Input Absensi Sekarang'
|
'action_text' => 'Input Absensi Sekarang',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule 4: Kegiatan selesai tapi belum input absensi
|
|
||||||
foreach ($kegiatanHariIni as $kegiatan) {
|
foreach ($kegiatanHariIni as $kegiatan) {
|
||||||
if ($kegiatan->status_kegiatan == 'selesai' && $kegiatan->total_absensi == 0) {
|
if ($kegiatan->status_kegiatan == 'selesai' && $kegiatan->total_absensi == 0) {
|
||||||
$waktuSelesai = is_string($kegiatan->waktu_selesai)
|
$waktuSelesai = is_string($kegiatan->waktu_selesai) ? $kegiatan->waktu_selesai : $kegiatan->waktu_selesai->format('H:i');
|
||||||
? $kegiatan->waktu_selesai
|
|
||||||
: $kegiatan->waktu_selesai->format('H:i');
|
|
||||||
|
|
||||||
$insights[] = [
|
$insights[] = [
|
||||||
'type' => 'danger',
|
'type' => 'danger', 'icon' => 'exclamation-circle',
|
||||||
'icon' => 'exclamation-circle',
|
|
||||||
'message' => "Kegiatan {$kegiatan->nama_kegiatan} belum input absensi",
|
'message' => "Kegiatan {$kegiatan->nama_kegiatan} belum input absensi",
|
||||||
'detail' => "Sudah selesai pukul {$waktuSelesai}",
|
'detail' => "Sudah selesai pukul {$waktuSelesai}",
|
||||||
'action_url' => route('admin.absensi-kegiatan.input', $kegiatan->kegiatan_id) . '?tanggal=' . $selectedDate->format('Y-m-d'),
|
'action_url' => route('admin.absensi-kegiatan.input', $kegiatan->kegiatan_id) . '?tanggal=' . $selectedDate->format('Y-m-d'),
|
||||||
'action_text' => 'Input Sekarang'
|
'action_text' => 'Input Sekarang',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return collect($insights)->take(5)->toArray(); // Max 5 insights
|
return collect($insights)->take(5)->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -253,39 +196,31 @@ private function generateHeatmapData()
|
||||||
for ($i = 0; $i < 30; $i++) {
|
for ($i = 0; $i < 30; $i++) {
|
||||||
$date = $startDate->copy()->addDays($i);
|
$date = $startDate->copy()->addDays($i);
|
||||||
$dateStr = $date->format('Y-m-d');
|
$dateStr = $date->format('Y-m-d');
|
||||||
|
|
||||||
// Hitung rata-rata kehadiran hari tersebut
|
|
||||||
$absensi = AbsensiKegiatan::whereDate('tanggal', $dateStr)->get();
|
$absensi = AbsensiKegiatan::whereDate('tanggal', $dateStr)->get();
|
||||||
|
|
||||||
if ($absensi->count() > 0) {
|
$percentage = $absensi->count() > 0
|
||||||
$hadir = $absensi->where('status', 'Hadir')->count();
|
? round(($absensi->where('status', 'Hadir')->count() / $absensi->count()) * 100, 1)
|
||||||
$percentage = round(($hadir / $absensi->count()) * 100, 1);
|
: 0;
|
||||||
} else {
|
|
||||||
$percentage = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$heatmapData[] = [
|
$heatmapData[] = [
|
||||||
'date' => $dateStr,
|
'date' => $dateStr,
|
||||||
'day_name' => $date->locale('id')->isoFormat('ddd'),
|
'day_name' => $date->locale('id')->isoFormat('ddd'),
|
||||||
'percentage' => $percentage,
|
'percentage' => $percentage,
|
||||||
'level' => $this->getHeatmapLevel($percentage),
|
'level' => $this->getHeatmapLevel($percentage),
|
||||||
'is_today' => $date->isToday()
|
'is_today' => $date->isToday(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $heatmapData;
|
return $heatmapData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Heatmap Level (0-4)
|
|
||||||
*/
|
|
||||||
private function getHeatmapLevel($percentage)
|
private function getHeatmapLevel($percentage)
|
||||||
{
|
{
|
||||||
if ($percentage >= 90) return 4; // Dark green
|
if ($percentage >= 90) return 4;
|
||||||
if ($percentage >= 80) return 3; // Green
|
if ($percentage >= 80) return 3;
|
||||||
if ($percentage >= 70) return 2; // Yellow
|
if ($percentage >= 70) return 2;
|
||||||
if ($percentage > 0) return 1; // Red
|
if ($percentage > 0) return 1;
|
||||||
return 0; // No data
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -294,80 +229,63 @@ private function getHeatmapLevel($percentage)
|
||||||
public function getDetailModal($kegiatan_id, Request $request)
|
public function getDetailModal($kegiatan_id, Request $request)
|
||||||
{
|
{
|
||||||
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
|
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
|
||||||
|
|
||||||
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok'])
|
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok'])
|
||||||
->where('kegiatan_id', $kegiatan_id)
|
->where('kegiatan_id', $kegiatan_id)->firstOrFail();
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
// Get absensi untuk tanggal tersebut
|
$absensis = AbsensiKegiatan::with(['santri.kelasSantri.kelas'])
|
||||||
$absensis = AbsensiKegiatan::with('santri')
|
|
||||||
->where('kegiatan_id', $kegiatan_id)
|
->where('kegiatan_id', $kegiatan_id)
|
||||||
->whereDate('tanggal', $tanggal)
|
->whereDate('tanggal', $tanggal)
|
||||||
->orderBy('waktu_absen', 'desc')
|
->orderBy('waktu_absen', 'desc')->get();
|
||||||
->get();
|
|
||||||
|
if ($kegiatan->isForAllClasses()) {
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Statistik
|
|
||||||
$stats = [
|
$stats = [
|
||||||
'hadir' => $absensis->where('status', 'Hadir')->count(),
|
'hadir' => $absensis->where('status', 'Hadir')->count(),
|
||||||
'izin' => $absensis->where('status', 'Izin')->count(),
|
'izin' => $absensis->where('status', 'Izin')->count(),
|
||||||
'sakit' => $absensis->where('status', 'Sakit')->count(),
|
'sakit' => $absensis->where('status', 'Sakit')->count(),
|
||||||
'alpa' => $absensis->where('status', 'Alpa')->count(),
|
'alpa' => $absensis->where('status', 'Alpa')->count(),
|
||||||
];
|
];
|
||||||
|
$totalSantri = $kegiatan->isForAllClasses()
|
||||||
// Total santri yang seharusnya
|
? Santri::where('status', 'Aktif')->count()
|
||||||
if ($kegiatan->isForAllClasses()) {
|
: $kegiatan->getEligibleSantris()->count();
|
||||||
$totalSantri = Santri::where('status', 'Aktif')->count();
|
|
||||||
} else {
|
|
||||||
$totalSantri = $kegiatan->getEligibleSantris()->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
$stats['belum_absen'] = $totalSantri - $absensis->count();
|
$stats['belum_absen'] = $totalSantri - $absensis->count();
|
||||||
$stats['total'] = $totalSantri;
|
$stats['total'] = $totalSantri;
|
||||||
$stats['persen_hadir'] = $totalSantri > 0 ? round(($stats['hadir'] / $totalSantri) * 100, 1) : 0;
|
$stats['persen_hadir'] = $totalSantri > 0 ? round(($stats['hadir'] / $totalSantri) * 100, 1) : 0;
|
||||||
|
|
||||||
return view('admin.kegiatan.data.partials.detail-modal', compact('kegiatan', 'absensis', 'stats', 'tanggal'));
|
return view('admin.kegiatan.data.partials.detail-modal', compact('kegiatan', 'absensis', 'absensiPerKelas', 'stats', 'tanggal'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jadwal Kegiatan Lengkap (untuk "Lihat Semua Jadwal")
|
* Jadwal Kegiatan Lengkap
|
||||||
*/
|
*/
|
||||||
public function jadwal(Request $request)
|
public function jadwal(Request $request)
|
||||||
{
|
{
|
||||||
$query = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok']);
|
$query = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok']);
|
||||||
|
|
||||||
// Filter hari
|
if ($request->filled('hari')) $query->where('hari', $request->hari);
|
||||||
if ($request->filled('hari')) {
|
if ($request->filled('kategori_id')) $query->where('kategori_id', $request->kategori_id);
|
||||||
$query->where('hari', $request->hari);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter kategori
|
|
||||||
if ($request->filled('kategori_id')) {
|
|
||||||
$query->where('kategori_id', $request->kategori_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter kelas
|
|
||||||
if ($request->filled('kelas_id')) {
|
if ($request->filled('kelas_id')) {
|
||||||
if ($request->kelas_id === 'umum') {
|
if ($request->kelas_id === 'umum') {
|
||||||
$query->doesntHave('kelasKegiatan');
|
$query->doesntHave('kelasKegiatan');
|
||||||
} else {
|
} else {
|
||||||
$query->whereHas('kelasKegiatan', function($q) use ($request) {
|
$query->whereHas('kelasKegiatan', fn($q) => $q->where('kelas.id', $request->kelas_id));
|
||||||
$q->where('kelas.id', $request->kelas_id);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($request->filled('search')) $query->search($request->search);
|
||||||
// Search
|
|
||||||
if ($request->filled('search')) {
|
|
||||||
$query->search($request->search);
|
|
||||||
}
|
|
||||||
|
|
||||||
$kegiatans = $query->select('id', 'kegiatan_id', 'kategori_id', 'nama_kegiatan', 'hari', 'waktu_mulai', 'waktu_selesai', 'materi')
|
$kegiatans = $query->select('id', 'kegiatan_id', 'kategori_id', 'nama_kegiatan', 'hari', 'waktu_mulai', 'waktu_selesai', 'materi')
|
||||||
->orderBy('hari')
|
->orderBy('hari')->orderBy('waktu_mulai')
|
||||||
->orderBy('waktu_mulai')
|
->paginate(15)->appends(request()->query());
|
||||||
->paginate(15)
|
|
||||||
->appends(request()->query());
|
|
||||||
|
|
||||||
// Data untuk filter
|
|
||||||
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
||||||
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
|
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
|
||||||
$kelasList = Kelas::with('kelompok')->active()->ordered()->get();
|
$kelasList = Kelas::with('kelompok')->active()->ordered()->get();
|
||||||
|
|
@ -388,9 +306,8 @@ public function create()
|
||||||
|
|
||||||
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
||||||
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
|
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
|
||||||
$kelompokKelas = KelompokKelas::with(['kelas' => function($q) {
|
$kelompokKelas = KelompokKelas::with(['kelas' => fn($q) => $q->where('is_active', true)->orderBy('urutan')])
|
||||||
$q->where('is_active', true)->orderBy('urutan');
|
->active()->ordered()->get();
|
||||||
}])->active()->ordered()->get();
|
|
||||||
|
|
||||||
return view('admin.kegiatan.data.create', compact('nextId', 'kategoris', 'hariList', 'kelompokKelas'));
|
return view('admin.kegiatan.data.create', compact('nextId', 'kategoris', 'hariList', 'kelompokKelas'));
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +320,8 @@ public function store(Request $request)
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'kategori_id' => 'required|exists:kategori_kegiatans,kategori_id',
|
'kategori_id' => 'required|exists:kategori_kegiatans,kategori_id',
|
||||||
'nama_kegiatan' => 'required|string|max:150',
|
'nama_kegiatan' => 'required|string|max:150',
|
||||||
'hari' => 'required|in:Senin,Selasa,Rabu,Kamis,Jumat,Sabtu,Ahad',
|
'hari' => 'required|array|min:1',
|
||||||
|
'hari.*' => 'in:Senin,Selasa,Rabu,Kamis,Jumat,Sabtu,Ahad',
|
||||||
'waktu_mulai' => 'required|date_format:H:i',
|
'waktu_mulai' => 'required|date_format:H:i',
|
||||||
'waktu_selesai' => 'required|date_format:H:i|after:waktu_mulai',
|
'waktu_selesai' => 'required|date_format:H:i|after:waktu_mulai',
|
||||||
'materi' => 'nullable|string|max:200',
|
'materi' => 'nullable|string|max:200',
|
||||||
|
|
@ -413,23 +331,32 @@ public function store(Request $request)
|
||||||
], [
|
], [
|
||||||
'kategori_id.required' => 'Kategori wajib dipilih.',
|
'kategori_id.required' => 'Kategori wajib dipilih.',
|
||||||
'nama_kegiatan.required' => 'Nama kegiatan wajib diisi.',
|
'nama_kegiatan.required' => 'Nama kegiatan wajib diisi.',
|
||||||
'hari.required' => 'Hari wajib dipilih.',
|
'hari.required' => 'Minimal pilih satu hari.',
|
||||||
|
'hari.min' => 'Minimal pilih satu hari.',
|
||||||
'waktu_mulai.required' => 'Waktu mulai wajib diisi.',
|
'waktu_mulai.required' => 'Waktu mulai wajib diisi.',
|
||||||
'waktu_selesai.required' => 'Waktu selesai wajib diisi.',
|
'waktu_selesai.required' => 'Waktu selesai wajib diisi.',
|
||||||
'waktu_selesai.after' => 'Waktu selesai harus lebih dari waktu mulai.',
|
'waktu_selesai.after' => 'Waktu selesai harus lebih dari waktu mulai.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$kegiatan = Kegiatan::create($validated);
|
$hariList = $validated['hari'];
|
||||||
|
unset($validated['hari']);
|
||||||
|
$createdCount = 0;
|
||||||
|
|
||||||
// Assign kelas to kegiatan if selected
|
foreach ($hariList as $hari) {
|
||||||
|
$kg = Kegiatan::create(array_merge($validated, ['hari' => $hari]));
|
||||||
if ($request->has('kelas_ids') && !empty($request->kelas_ids)) {
|
if ($request->has('kelas_ids') && !empty($request->kelas_ids)) {
|
||||||
$kegiatan->assignKelas($request->kelas_ids);
|
$kg->assignKelas($request->kelas_ids);
|
||||||
|
}
|
||||||
|
$createdCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache::forget('next_kegiatan_id');
|
Cache::forget('next_kegiatan_id');
|
||||||
|
|
||||||
return redirect()->route('admin.kegiatan.index')
|
$message = $createdCount > 1
|
||||||
->with('success', 'Kegiatan berhasil ditambahkan.');
|
? "Berhasil menambahkan kegiatan untuk {$createdCount} hari."
|
||||||
|
: 'Kegiatan berhasil ditambahkan.';
|
||||||
|
|
||||||
|
return redirect()->route('admin.kegiatan.jadwal')->with('success', $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -448,25 +375,30 @@ public function edit(Kegiatan $kegiatan)
|
||||||
{
|
{
|
||||||
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
||||||
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
|
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
|
||||||
$kelompokKelas = KelompokKelas::with(['kelas' => function($q) {
|
$kelompokKelas = KelompokKelas::with(['kelas' => fn($q) => $q->where('is_active', true)->orderBy('urutan')])
|
||||||
$q->where('is_active', true)->orderBy('urutan');
|
->active()->ordered()->get();
|
||||||
}])->active()->ordered()->get();
|
|
||||||
|
|
||||||
// Load existing kelas relations
|
|
||||||
$kegiatan->load('kelasKegiatan');
|
$kegiatan->load('kelasKegiatan');
|
||||||
|
|
||||||
return view('admin.kegiatan.data.edit', compact('kegiatan', 'kategoris', 'hariList', 'kelompokKelas'));
|
return view('admin.kegiatan.data.edit', compact('kegiatan', 'kategoris', 'hariList', 'kelompokKelas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update kegiatan
|
* Update kegiatan — smart multi-hari
|
||||||
|
*
|
||||||
|
* Logika:
|
||||||
|
* - Cari semua kegiatan "saudara" = nama_kegiatan + kategori_id LAMA yang sama
|
||||||
|
* - Hari yang DIPILIH & sudah ada di saudara → UPDATE kegiatan saudara tsb
|
||||||
|
* - Hari yang DIPILIH tapi belum ada di saudara → BUAT kegiatan baru
|
||||||
|
* - Hari yang TIDAK DIPILIH → tidak disentuh sama sekali
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, Kegiatan $kegiatan)
|
public function update(Request $request, Kegiatan $kegiatan)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'kategori_id' => 'required|exists:kategori_kegiatans,kategori_id',
|
'kategori_id' => 'required|exists:kategori_kegiatans,kategori_id',
|
||||||
'nama_kegiatan' => 'required|string|max:150',
|
'nama_kegiatan' => 'required|string|max:150',
|
||||||
'hari' => 'required|in:Senin,Selasa,Rabu,Kamis,Jumat,Sabtu,Ahad',
|
'hari' => 'required|array|min:1',
|
||||||
|
'hari.*' => 'in:Senin,Selasa,Rabu,Kamis,Jumat,Sabtu,Ahad',
|
||||||
'waktu_mulai' => 'required|date_format:H:i',
|
'waktu_mulai' => 'required|date_format:H:i',
|
||||||
'waktu_selesai' => 'required|date_format:H:i|after:waktu_mulai',
|
'waktu_selesai' => 'required|date_format:H:i|after:waktu_mulai',
|
||||||
'materi' => 'nullable|string|max:200',
|
'materi' => 'nullable|string|max:200',
|
||||||
|
|
@ -476,18 +408,49 @@ public function update(Request $request, Kegiatan $kegiatan)
|
||||||
], [
|
], [
|
||||||
'kategori_id.required' => 'Kategori wajib dipilih.',
|
'kategori_id.required' => 'Kategori wajib dipilih.',
|
||||||
'nama_kegiatan.required' => 'Nama kegiatan wajib diisi.',
|
'nama_kegiatan.required' => 'Nama kegiatan wajib diisi.',
|
||||||
|
'hari.required' => 'Minimal pilih satu hari.',
|
||||||
|
'hari.min' => 'Minimal pilih satu hari.',
|
||||||
'waktu_selesai.after' => 'Waktu selesai harus lebih dari waktu mulai.',
|
'waktu_selesai.after' => 'Waktu selesai harus lebih dari waktu mulai.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$kegiatan->update($validated);
|
$hariDipilih = $validated['hari'];
|
||||||
|
$kelasIds = $request->input('kelas_ids', []);
|
||||||
|
|
||||||
// Update kelas assignments
|
// Data dasar tanpa hari & kelas_ids
|
||||||
if ($request->has('kelas_ids')) {
|
$baseData = collect($validated)->except(['hari', 'kelas_ids'])->toArray();
|
||||||
$kegiatan->assignKelas($request->kelas_ids ?? []);
|
|
||||||
|
// Cari semua saudara berdasarkan nama + kategori LAMA (sebelum diubah)
|
||||||
|
$saudara = Kegiatan::where('nama_kegiatan', $kegiatan->nama_kegiatan)
|
||||||
|
->where('kategori_id', $kegiatan->kategori_id)
|
||||||
|
->get()
|
||||||
|
->keyBy('hari'); // ['Senin' => obj, 'Rabu' => obj, ...]
|
||||||
|
|
||||||
|
$updatedCount = 0;
|
||||||
|
$createdCount = 0;
|
||||||
|
|
||||||
|
foreach ($hariDipilih as $hari) {
|
||||||
|
if ($saudara->has($hari)) {
|
||||||
|
// Kegiatan di hari ini sudah ada → update
|
||||||
|
$target = $saudara->get($hari);
|
||||||
|
$target->update(array_merge($baseData, ['hari' => $hari]));
|
||||||
|
$target->assignKelas($kelasIds);
|
||||||
|
$updatedCount++;
|
||||||
|
} else {
|
||||||
|
// Belum ada kegiatan di hari ini → buat baru
|
||||||
|
$newKg = Kegiatan::create(array_merge($baseData, ['hari' => $hari]));
|
||||||
|
$newKg->assignKelas($kelasIds);
|
||||||
|
$createdCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('admin.kegiatan.index')
|
Cache::forget('next_kegiatan_id');
|
||||||
->with('success', 'Kegiatan berhasil diperbarui.');
|
|
||||||
|
$parts = [];
|
||||||
|
if ($updatedCount > 0) $parts[] = "{$updatedCount} kegiatan diperbarui";
|
||||||
|
if ($createdCount > 0) $parts[] = "{$createdCount} kegiatan baru dibuat";
|
||||||
|
|
||||||
|
return redirect()->route('admin.kegiatan.jadwal')
|
||||||
|
->with('success', 'Berhasil: ' . implode(', ', $parts) . '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -499,7 +462,7 @@ public function destroy(Kegiatan $kegiatan)
|
||||||
$kegiatan->delete();
|
$kegiatan->delete();
|
||||||
Cache::forget('next_kegiatan_id');
|
Cache::forget('next_kegiatan_id');
|
||||||
|
|
||||||
return redirect()->route('admin.kegiatan.index')
|
return redirect()->route('admin.kegiatan.jadwal')
|
||||||
->with('success', "Kegiatan \"$nama\" berhasil dihapus.");
|
->with('success', "Kegiatan \"{$nama}\" berhasil dihapus.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,18 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* ============================================================================
|
|
||||||
* LOKASI FILE: app/Http/Controllers/Admin/KelasController.php
|
|
||||||
* ============================================================================
|
|
||||||
*
|
|
||||||
* INSTRUKSI:
|
|
||||||
* 1. Backup file KelasController.php yang lama
|
|
||||||
* 2. Replace dengan file ini
|
|
||||||
* 3. File ini sudah include semua fitur:
|
|
||||||
* - CRUD Kelas
|
|
||||||
* - CRUD Kelompok Kelas
|
|
||||||
* - Kenaikan Kelas Massal
|
|
||||||
* ============================================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
|
@ -31,66 +17,49 @@ class KelasController extends Controller
|
||||||
// SECTION 1: CRUD KELAS
|
// SECTION 1: CRUD KELAS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a listing of kelas.
|
|
||||||
*/
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$query = Kelas::with('kelompok');
|
$query = Kelas::with('kelompok');
|
||||||
|
|
||||||
// Search by nama kelas atau kode kelas
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$search = $request->search;
|
$search = $request->search;
|
||||||
$query->where(function($q) use ($search) {
|
$query->where(function ($q) use ($search) {
|
||||||
$q->where('nama_kelas', 'like', "%{$search}%")
|
$q->where('nama_kelas', 'like', "%{$search}%")
|
||||||
->orWhere('kode_kelas', 'like', "%{$search}%");
|
->orWhere('kode_kelas', 'like', "%{$search}%");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by kelompok kelas
|
|
||||||
if ($request->filled('kelompok')) {
|
if ($request->filled('kelompok')) {
|
||||||
$query->where('id_kelompok', $request->kelompok);
|
$query->where('id_kelompok', $request->kelompok);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by status
|
|
||||||
if ($request->filled('status')) {
|
if ($request->filled('status')) {
|
||||||
$isActive = $request->status === 'active';
|
$query->where('is_active', $request->status === 'active');
|
||||||
$query->where('is_active', $isActive);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order by kelompok then urutan
|
|
||||||
$kelas = $query->orderBy('id_kelompok', 'asc')
|
$kelas = $query->orderBy('id_kelompok', 'asc')
|
||||||
->orderBy('urutan', 'asc')
|
->orderBy('urutan', 'asc')
|
||||||
->paginate(15)
|
->paginate(15)
|
||||||
->appends(request()->query());
|
->appends(request()->query());
|
||||||
|
|
||||||
// Get kelompok kelas for filter dropdown
|
|
||||||
$kelompokKelas = KelompokKelas::active()->ordered()->get();
|
$kelompokKelas = KelompokKelas::active()->ordered()->get();
|
||||||
|
|
||||||
return view('admin.kelas.index', compact('kelas', 'kelompokKelas'));
|
return view('admin.kelas.index', compact('kelas', 'kelompokKelas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for creating a new kelas.
|
|
||||||
*/
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
// Get next kode_kelas
|
|
||||||
$nextKodeKelas = Cache::remember('next_kelas_kode', 60, function () {
|
$nextKodeKelas = Cache::remember('next_kelas_kode', 60, function () {
|
||||||
$lastKelas = Kelas::orderBy('id', 'desc')->first();
|
$lastKelas = Kelas::orderBy('id', 'desc')->first();
|
||||||
$nextNum = $lastKelas ? intval(substr($lastKelas->kode_kelas, 3)) + 1 : 1;
|
$nextNum = $lastKelas ? intval(substr($lastKelas->kode_kelas, 3)) + 1 : 1;
|
||||||
return 'KLS' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
|
return 'KLS' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get kelompok kelas for dropdown
|
|
||||||
$kelompokKelas = KelompokKelas::active()->ordered()->get();
|
$kelompokKelas = KelompokKelas::active()->ordered()->get();
|
||||||
|
|
||||||
return view('admin.kelas.create', compact('nextKodeKelas', 'kelompokKelas'));
|
return view('admin.kelas.create', compact('nextKodeKelas', 'kelompokKelas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created kelas in storage.
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
|
|
@ -98,63 +67,32 @@ public function store(Request $request)
|
||||||
'id_kelompok' => 'required|string|exists:kelompok_kelas,id_kelompok',
|
'id_kelompok' => 'required|string|exists:kelompok_kelas,id_kelompok',
|
||||||
'urutan' => 'required|integer|min:0',
|
'urutan' => 'required|integer|min:0',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
], [
|
|
||||||
'nama_kelas.required' => 'Nama kelas wajib diisi.',
|
|
||||||
'nama_kelas.unique' => 'Nama kelas sudah digunakan.',
|
|
||||||
'id_kelompok.required' => 'Kelompok kelas wajib dipilih.',
|
|
||||||
'id_kelompok.exists' => 'Kelompok kelas tidak valid.',
|
|
||||||
'urutan.required' => 'Urutan wajib diisi.',
|
|
||||||
'urutan.integer' => 'Urutan harus berupa angka.',
|
|
||||||
'urutan.min' => 'Urutan minimal 0.',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set is_active default to true if not provided
|
$validated['is_active'] = $request->has('is_active');
|
||||||
$validated['is_active'] = $request->has('is_active') ? true : false;
|
|
||||||
|
|
||||||
// Create kelas (kode_kelas will be auto-generated in model)
|
|
||||||
Kelas::create($validated);
|
Kelas::create($validated);
|
||||||
|
|
||||||
// Clear cache
|
|
||||||
Cache::forget('next_kelas_kode');
|
Cache::forget('next_kelas_kode');
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.index')
|
return redirect()->route('admin.kelas.index')
|
||||||
->with('success', 'Kelas berhasil ditambahkan.');
|
->with('success', 'Kelas berhasil ditambahkan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified kelas.
|
|
||||||
*/
|
|
||||||
public function show(Kelas $kela)
|
public function show(Kelas $kela)
|
||||||
{
|
{
|
||||||
// Load relationships
|
|
||||||
$kela->load(['kelompok', 'santriKelas.santri']);
|
$kela->load(['kelompok', 'santriKelas.santri']);
|
||||||
|
|
||||||
// Get santri count in this kelas for current academic year
|
|
||||||
$tahunAjaranAktif = SantriKelas::getCurrentAcademicYear();
|
|
||||||
$santriCount = $kela->santriKelas()
|
$santriCount = $kela->santriKelas()
|
||||||
->where('tahun_ajaran', $tahunAjaranAktif)
|
->whereHas('santri', fn($q) => $q->where('status', 'Aktif'))
|
||||||
->whereHas('santri', function($q) {
|
|
||||||
$q->where('status', 'Aktif');
|
|
||||||
})
|
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
return view('admin.kelas.show', compact('kela', 'santriCount', 'tahunAjaranAktif'));
|
return view('admin.kelas.show', compact('kela', 'santriCount'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for editing the specified kelas.
|
|
||||||
*/
|
|
||||||
public function edit(Kelas $kela)
|
public function edit(Kelas $kela)
|
||||||
{
|
{
|
||||||
// Get kelompok kelas for dropdown
|
|
||||||
$kelompokKelas = KelompokKelas::active()->ordered()->get();
|
$kelompokKelas = KelompokKelas::active()->ordered()->get();
|
||||||
|
|
||||||
return view('admin.kelas.edit', compact('kela', 'kelompokKelas'));
|
return view('admin.kelas.edit', compact('kela', 'kelompokKelas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified kelas in storage.
|
|
||||||
*/
|
|
||||||
public function update(Request $request, Kelas $kela)
|
public function update(Request $request, Kelas $kela)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
|
|
@ -162,32 +100,17 @@ public function update(Request $request, Kelas $kela)
|
||||||
'id_kelompok' => 'required|string|exists:kelompok_kelas,id_kelompok',
|
'id_kelompok' => 'required|string|exists:kelompok_kelas,id_kelompok',
|
||||||
'urutan' => 'required|integer|min:0',
|
'urutan' => 'required|integer|min:0',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
], [
|
|
||||||
'nama_kelas.required' => 'Nama kelas wajib diisi.',
|
|
||||||
'nama_kelas.unique' => 'Nama kelas sudah digunakan.',
|
|
||||||
'id_kelompok.required' => 'Kelompok kelas wajib dipilih.',
|
|
||||||
'id_kelompok.exists' => 'Kelompok kelas tidak valid.',
|
|
||||||
'urutan.required' => 'Urutan wajib diisi.',
|
|
||||||
'urutan.integer' => 'Urutan harus berupa angka.',
|
|
||||||
'urutan.min' => 'Urutan minimal 0.',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set is_active
|
$validated['is_active'] = $request->has('is_active');
|
||||||
$validated['is_active'] = $request->has('is_active') ? true : false;
|
|
||||||
|
|
||||||
// Update kelas
|
|
||||||
$kela->update($validated);
|
$kela->update($validated);
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.index')
|
return redirect()->route('admin.kelas.index')
|
||||||
->with('success', 'Kelas berhasil diperbarui.');
|
->with('success', 'Kelas berhasil diperbarui.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified kelas from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(Kelas $kela)
|
public function destroy(Kelas $kela)
|
||||||
{
|
{
|
||||||
// Check if kelas is still being used
|
|
||||||
$santriCount = $kela->santriKelas()->count();
|
$santriCount = $kela->santriKelas()->count();
|
||||||
$kegiatanCount = $kela->kegiatans()->count();
|
$kegiatanCount = $kela->kegiatans()->count();
|
||||||
|
|
||||||
|
|
@ -201,9 +124,7 @@ public function destroy(Kelas $kela)
|
||||||
->with('error', "Kelas tidak dapat dihapus karena masih memiliki {$kegiatanCount} kegiatan.");
|
->with('error', "Kelas tidak dapat dihapus karena masih memiliki {$kegiatanCount} kegiatan.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete kelas
|
|
||||||
$kela->delete();
|
$kela->delete();
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.index')
|
return redirect()->route('admin.kelas.index')
|
||||||
->with('success', 'Kelas berhasil dihapus.');
|
->with('success', 'Kelas berhasil dihapus.');
|
||||||
}
|
}
|
||||||
|
|
@ -212,26 +133,17 @@ public function destroy(Kelas $kela)
|
||||||
// SECTION 2: CRUD KELOMPOK KELAS
|
// SECTION 2: CRUD KELOMPOK KELAS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a listing of kelompok kelas.
|
|
||||||
*/
|
|
||||||
public function kelompokIndex(Request $request)
|
public function kelompokIndex(Request $request)
|
||||||
{
|
{
|
||||||
$query = KelompokKelas::withCount('kelas');
|
$query = KelompokKelas::withCount('kelas');
|
||||||
|
|
||||||
// Search by nama kelompok
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$search = $request->search;
|
$query->where('nama_kelompok', 'like', '%' . $request->search . '%');
|
||||||
$query->where('nama_kelompok', 'like', "%{$search}%");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by status
|
|
||||||
if ($request->filled('status')) {
|
if ($request->filled('status')) {
|
||||||
$isActive = $request->status === 'active';
|
$query->where('is_active', $request->status === 'active');
|
||||||
$query->where('is_active', $isActive);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order by urutan
|
|
||||||
$kelompokKelas = $query->orderBy('urutan', 'asc')
|
$kelompokKelas = $query->orderBy('urutan', 'asc')
|
||||||
->paginate(15)
|
->paginate(15)
|
||||||
->appends(request()->query());
|
->appends(request()->query());
|
||||||
|
|
@ -239,24 +151,17 @@ public function kelompokIndex(Request $request)
|
||||||
return view('admin.kelas.kelompok.index', compact('kelompokKelas'));
|
return view('admin.kelas.kelompok.index', compact('kelompokKelas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for creating a new kelompok kelas.
|
|
||||||
*/
|
|
||||||
public function kelompokCreate()
|
public function kelompokCreate()
|
||||||
{
|
{
|
||||||
// Get next id_kelompok
|
|
||||||
$nextIdKelompok = Cache::remember('next_kelompok_id', 60, function () {
|
$nextIdKelompok = Cache::remember('next_kelompok_id', 60, function () {
|
||||||
$lastKelompok = KelompokKelas::orderBy('id', 'desc')->first();
|
$last = KelompokKelas::orderBy('id', 'desc')->first();
|
||||||
$nextNum = $lastKelompok ? intval(substr($lastKelompok->id_kelompok, 3)) + 1 : 1;
|
$nextNum = $last ? intval(substr($last->id_kelompok, 3)) + 1 : 1;
|
||||||
return 'KEL' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
|
return 'KEL' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
|
||||||
});
|
});
|
||||||
|
|
||||||
return view('admin.kelas.kelompok.create', compact('nextIdKelompok'));
|
return view('admin.kelas.kelompok.create', compact('nextIdKelompok'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created kelompok kelas in storage.
|
|
||||||
*/
|
|
||||||
public function kelompokStore(Request $request)
|
public function kelompokStore(Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
|
|
@ -264,78 +169,43 @@ public function kelompokStore(Request $request)
|
||||||
'deskripsi' => 'nullable|string|max:500',
|
'deskripsi' => 'nullable|string|max:500',
|
||||||
'urutan' => 'required|integer|min:0',
|
'urutan' => 'required|integer|min:0',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
], [
|
|
||||||
'nama_kelompok.required' => 'Nama kelompok wajib diisi.',
|
|
||||||
'nama_kelompok.unique' => 'Nama kelompok sudah digunakan.',
|
|
||||||
'urutan.required' => 'Urutan wajib diisi.',
|
|
||||||
'urutan.integer' => 'Urutan harus berupa angka.',
|
|
||||||
'urutan.min' => 'Urutan minimal 0.',
|
|
||||||
'deskripsi.max' => 'Deskripsi maksimal 500 karakter.',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set is_active default to true if not provided
|
$validated['is_active'] = $request->has('is_active');
|
||||||
$validated['is_active'] = $request->has('is_active') ? true : false;
|
|
||||||
|
|
||||||
// Create kelompok (id_kelompok will be auto-generated in model)
|
|
||||||
KelompokKelas::create($validated);
|
KelompokKelas::create($validated);
|
||||||
|
|
||||||
// Clear cache
|
|
||||||
Cache::forget('next_kelompok_id');
|
Cache::forget('next_kelompok_id');
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.kelompok.index')
|
return redirect()->route('admin.kelas.kelompok.index')
|
||||||
->with('success', 'Kelompok kelas berhasil ditambahkan.');
|
->with('success', 'Kelompok kelas berhasil ditambahkan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for editing the specified kelompok kelas.
|
|
||||||
*/
|
|
||||||
public function kelompokEdit($id)
|
public function kelompokEdit($id)
|
||||||
{
|
{
|
||||||
$kelompok = KelompokKelas::findOrFail($id);
|
$kelompok = KelompokKelas::findOrFail($id);
|
||||||
$kelompok->loadCount('kelas');
|
$kelompok->loadCount('kelas');
|
||||||
|
|
||||||
return view('admin.kelas.kelompok.edit', compact('kelompok'));
|
return view('admin.kelas.kelompok.edit', compact('kelompok'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified kelompok kelas in storage.
|
|
||||||
*/
|
|
||||||
public function kelompokUpdate(Request $request, $id)
|
public function kelompokUpdate(Request $request, $id)
|
||||||
{
|
{
|
||||||
$kelompok = KelompokKelas::findOrFail($id);
|
$kelompok = KelompokKelas::findOrFail($id);
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'nama_kelompok' => 'required|string|max:100|unique:kelompok_kelas,nama_kelompok,' . $kelompok->id,
|
'nama_kelompok' => 'required|string|max:100|unique:kelompok_kelas,nama_kelompok,' . $kelompok->id,
|
||||||
'deskripsi' => 'nullable|string|max:500',
|
'deskripsi' => 'nullable|string|max:500',
|
||||||
'urutan' => 'required|integer|min:0',
|
'urutan' => 'required|integer|min:0',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
], [
|
|
||||||
'nama_kelompok.required' => 'Nama kelompok wajib diisi.',
|
|
||||||
'nama_kelompok.unique' => 'Nama kelompok sudah digunakan.',
|
|
||||||
'urutan.required' => 'Urutan wajib diisi.',
|
|
||||||
'urutan.integer' => 'Urutan harus berupa angka.',
|
|
||||||
'urutan.min' => 'Urutan minimal 0.',
|
|
||||||
'deskripsi.max' => 'Deskripsi maksimal 500 karakter.',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set is_active
|
$validated['is_active'] = $request->has('is_active');
|
||||||
$validated['is_active'] = $request->has('is_active') ? true : false;
|
|
||||||
|
|
||||||
// Update kelompok
|
|
||||||
$kelompok->update($validated);
|
$kelompok->update($validated);
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.kelompok.index')
|
return redirect()->route('admin.kelas.kelompok.index')
|
||||||
->with('success', 'Kelompok kelas berhasil diperbarui.');
|
->with('success', 'Kelompok kelas berhasil diperbarui.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified kelompok kelas from storage.
|
|
||||||
*/
|
|
||||||
public function kelompokDestroy($id)
|
public function kelompokDestroy($id)
|
||||||
{
|
{
|
||||||
$kelompok = KelompokKelas::findOrFail($id);
|
$kelompok = KelompokKelas::findOrFail($id);
|
||||||
|
|
||||||
// Check if kelompok still has kelas
|
|
||||||
$kelasCount = $kelompok->kelas()->count();
|
$kelasCount = $kelompok->kelas()->count();
|
||||||
|
|
||||||
if ($kelasCount > 0) {
|
if ($kelasCount > 0) {
|
||||||
|
|
@ -343,9 +213,7 @@ public function kelompokDestroy($id)
|
||||||
->with('error', "Kelompok tidak dapat dihapus karena masih memiliki {$kelasCount} kelas.");
|
->with('error', "Kelompok tidak dapat dihapus karena masih memiliki {$kelasCount} kelas.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete kelompok
|
|
||||||
$kelompok->delete();
|
$kelompok->delete();
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.kelompok.index')
|
return redirect()->route('admin.kelas.kelompok.index')
|
||||||
->with('success', 'Kelompok kelas berhasil dihapus.');
|
->with('success', 'Kelompok kelas berhasil dihapus.');
|
||||||
}
|
}
|
||||||
|
|
@ -354,47 +222,30 @@ public function kelompokDestroy($id)
|
||||||
// SECTION 3: KENAIKAN KELAS MASSAL
|
// SECTION 3: KENAIKAN KELAS MASSAL
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Display kenaikan kelas index page
|
|
||||||
*/
|
|
||||||
public function kenaikanIndex(Request $request)
|
public function kenaikanIndex(Request $request)
|
||||||
{
|
{
|
||||||
$tahunAjaranAktif = SantriKelas::getCurrentAcademicYear();
|
$tahunAjaranAktif = $this->getActiveTahunAjaran();
|
||||||
$tahunAjaranBaru = $this->getNextAcademicYear($tahunAjaranAktif);
|
$tahunAjaranBaru = $this->getNextAcademicYear($tahunAjaranAktif);
|
||||||
|
|
||||||
// Get total santri aktif
|
|
||||||
$totalSantriAktif = Santri::where('status', 'Aktif')->count();
|
$totalSantriAktif = Santri::where('status', 'Aktif')->count();
|
||||||
|
|
||||||
// Get all kelompok kelas for dropdown
|
$kelompokKelas = KelompokKelas::with([
|
||||||
$kelompokKelas = KelompokKelas::with(['kelas' => function($q) {
|
'kelas' => fn($q) => $q->where('is_active', true)->orderBy('urutan'),
|
||||||
$q->where('is_active', true)->orderBy('urutan');
|
])->active()->ordered()->get();
|
||||||
}])
|
|
||||||
->active()
|
|
||||||
->ordered()
|
|
||||||
->get();
|
|
||||||
|
|
||||||
// Determine selected kelompok (default: first kelompok)
|
|
||||||
$selectedKelompok = $request->get('kelompok');
|
$selectedKelompok = $request->get('kelompok');
|
||||||
if (!$selectedKelompok && $kelompokKelas->isNotEmpty()) {
|
if (!$selectedKelompok && $kelompokKelas->isNotEmpty()) {
|
||||||
$selectedKelompok = $kelompokKelas->first()->id_kelompok;
|
$selectedKelompok = $kelompokKelas->first()->id_kelompok;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get kelas list for selected kelompok only
|
|
||||||
$kelasList = Kelas::with('kelompok')
|
$kelasList = Kelas::with('kelompok')
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->when($selectedKelompok, function($q) use ($selectedKelompok) {
|
->when($selectedKelompok, fn($q) => $q->where('id_kelompok', $selectedKelompok))
|
||||||
$q->where('id_kelompok', $selectedKelompok);
|
->withCount([
|
||||||
})
|
'santriKelas as santri_aktif_count' => fn($q) => $q->whereHas('santri', fn($s) => $s->where('status', 'Aktif')),
|
||||||
->withCount(['santriKelas as santri_aktif_count' => function($q) use ($tahunAjaranAktif) {
|
])
|
||||||
$q->where('tahun_ajaran', $tahunAjaranAktif)
|
|
||||||
->whereHas('santri', function($q2) {
|
|
||||||
$q2->where('status', 'Aktif');
|
|
||||||
});
|
|
||||||
}])
|
|
||||||
->orderBy('urutan', 'asc')
|
->orderBy('urutan', 'asc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Get all kelas for dropdown selection (bisa naik ke kelas manapun)
|
|
||||||
$allKelasList = Kelas::with('kelompok')
|
$allKelasList = Kelas::with('kelompok')
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->orderBy('id_kelompok', 'asc')
|
->orderBy('id_kelompok', 'asc')
|
||||||
|
|
@ -402,76 +253,50 @@ public function kenaikanIndex(Request $request)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('admin.kelas.kenaikan.index', compact(
|
return view('admin.kelas.kenaikan.index', compact(
|
||||||
'tahunAjaranAktif',
|
'tahunAjaranAktif', 'tahunAjaranBaru', 'totalSantriAktif',
|
||||||
'tahunAjaranBaru',
|
'kelompokKelas', 'kelasList', 'allKelasList', 'selectedKelompok'
|
||||||
'totalSantriAktif',
|
|
||||||
'kelompokKelas',
|
|
||||||
'kelasList',
|
|
||||||
'allKelasList',
|
|
||||||
'selectedKelompok'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Preview santri in a class before kenaikan
|
|
||||||
*/
|
|
||||||
public function kenaikanPreview($id)
|
public function kenaikanPreview($id)
|
||||||
{
|
{
|
||||||
$kelas = Kelas::with('kelompok')->findOrFail($id);
|
$kelas = Kelas::with('kelompok')->findOrFail($id);
|
||||||
$tahunAjaranAktif = SantriKelas::getCurrentAcademicYear();
|
$tahunAjaranAktif = $this->getActiveTahunAjaran();
|
||||||
$tahunAjaranBaru = $this->getNextAcademicYear($tahunAjaranAktif);
|
$tahunAjaranBaru = $this->getNextAcademicYear($tahunAjaranAktif);
|
||||||
|
|
||||||
// Get santri in this class (tahun ajaran aktif, status aktif)
|
$santriList = Santri::whereHas('kelasSantri', fn($q) => $q->where('id_kelas', $id))
|
||||||
$santriList = Santri::whereHas('kelasSantri', function($q) use ($id, $tahunAjaranAktif) {
|
|
||||||
$q->where('id_kelas', $id)
|
|
||||||
->where('tahun_ajaran', $tahunAjaranAktif);
|
|
||||||
})
|
|
||||||
->where('status', 'Aktif')
|
->where('status', 'Aktif')
|
||||||
->orderBy('nama_lengkap')
|
->orderBy('nama_lengkap')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Get all kelompok with kelas for dropdown
|
$kelasOptions = KelompokKelas::with([
|
||||||
$kelasOptions = KelompokKelas::with(['kelas' => function($q) {
|
'kelas' => fn($q) => $q->where('is_active', true)->orderBy('urutan'),
|
||||||
$q->where('is_active', true)->orderBy('urutan');
|
])->active()->ordered()->get();
|
||||||
}])
|
|
||||||
->active()
|
|
||||||
->ordered()
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return view('admin.kelas.kenaikan.preview', compact(
|
return view('admin.kelas.kenaikan.preview', compact(
|
||||||
'kelas',
|
'kelas', 'santriList', 'tahunAjaranAktif', 'tahunAjaranBaru', 'kelasOptions'
|
||||||
'santriList',
|
|
||||||
'tahunAjaranAktif',
|
|
||||||
'tahunAjaranBaru',
|
|
||||||
'kelasOptions'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Process kenaikan kelas for all santri in a class
|
|
||||||
*/
|
|
||||||
public function kenaikanProcess(Request $request)
|
public function kenaikanProcess(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'id_kelas_asal' => 'required|exists:kelas,id',
|
'id_kelas_asal' => 'required|exists:kelas,id',
|
||||||
'id_kelas_tujuan' => 'required|exists:kelas,id',
|
'id_kelas_tujuan' => 'required|exists:kelas,id|different:id_kelas_asal',
|
||||||
|
], [
|
||||||
|
'id_kelas_tujuan.different' => 'Kelas tujuan tidak boleh sama dengan kelas asal.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$kelasAsal = Kelas::findOrFail($request->id_kelas_asal);
|
$kelasAsal = Kelas::findOrFail($request->id_kelas_asal);
|
||||||
$kelasTujuan = Kelas::findOrFail($request->id_kelas_tujuan);
|
$kelasTujuan = Kelas::findOrFail($request->id_kelas_tujuan);
|
||||||
$tahunAjaranAktif = SantriKelas::getCurrentAcademicYear();
|
|
||||||
|
|
||||||
// Get all santri aktif in kelas asal
|
$santriIds = Santri::whereHas('kelasSantri', fn($q) => $q->where('id_kelas', $request->id_kelas_asal))
|
||||||
$santriIds = Santri::whereHas('kelasSantri', function($q) use ($request, $tahunAjaranAktif) {
|
|
||||||
$q->where('id_kelas', $request->id_kelas_asal)
|
|
||||||
->where('tahun_ajaran', $tahunAjaranAktif);
|
|
||||||
})
|
|
||||||
->where('status', 'Aktif')
|
->where('status', 'Aktif')
|
||||||
->pluck('id_santri');
|
->pluck('id_santri');
|
||||||
|
|
||||||
if ($santriIds->isEmpty()) {
|
if ($santriIds->isEmpty()) {
|
||||||
return redirect()->route('admin.kelas.kenaikan.index')
|
return redirect()->route('admin.kelas.kenaikan.index')
|
||||||
->with('error', 'Tidak ada santri aktif di kelas ' . $kelasAsal->nama_kelas);
|
->with('error', 'Tidak ada santri aktif di kelas ' . $kelasAsal->nama_kelas . '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$processed = 0;
|
$processed = 0;
|
||||||
|
|
@ -479,95 +304,109 @@ public function kenaikanProcess(Request $request)
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
try {
|
try {
|
||||||
foreach ($santriIds as $idSantri) {
|
foreach ($santriIds as $idSantri) {
|
||||||
// Cari record santri_kelas yg ada di kelas asal
|
|
||||||
$record = SantriKelas::where('id_santri', $idSantri)
|
$record = SantriKelas::where('id_santri', $idSantri)
|
||||||
->where('id_kelas', $kelasAsal->id)
|
->where('id_kelas', $kelasAsal->id)
|
||||||
->where('tahun_ajaran', $tahunAjaranAktif)
|
->orderBy('tahun_ajaran', 'desc')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($record) {
|
if (!$record) continue;
|
||||||
// Update record: ganti kelas saja, tahun_ajaran & is_primary TETAP
|
|
||||||
$record->update([
|
// Cek duplikasi: jika sudah ada di kelas tujuan + tahun_ajaran sama, hapus record lama
|
||||||
'id_kelas' => $kelasTujuan->id,
|
$sudahAda = SantriKelas::where('id_santri', $idSantri)
|
||||||
]);
|
->where('id_kelas', $kelasTujuan->id)
|
||||||
$processed++;
|
->where('tahun_ajaran', $record->tahun_ajaran)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($sudahAda) {
|
||||||
|
$record->delete();
|
||||||
|
} else {
|
||||||
|
$record->update(['id_kelas' => $kelasTujuan->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$processed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.kenaikan.index')
|
return redirect()->route('admin.kelas.kenaikan.index')
|
||||||
->with('success', "Berhasil menaikkan {$processed} santri dari kelas {$kelasAsal->nama_kelas} ke {$kelasTujuan->nama_kelas}.");
|
->with('success', "Berhasil menaikkan {$processed} santri dari {$kelasAsal->nama_kelas} ke {$kelasTujuan->nama_kelas}.");
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.kenaikan.index')
|
return redirect()->route('admin.kelas.kenaikan.index')
|
||||||
->with('error', 'Terjadi kesalahan saat memproses kenaikan kelas: ' . $e->getMessage());
|
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Process kenaikan kelas for selected santri only
|
|
||||||
*/
|
|
||||||
public function kenaikanProcessSelected(Request $request)
|
public function kenaikanProcessSelected(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'id_kelas_asal' => 'required|exists:kelas,id',
|
'id_kelas_asal' => 'required|exists:kelas,id',
|
||||||
'id_kelas_tujuan' => 'required|exists:kelas,id',
|
'id_kelas_tujuan' => 'required|exists:kelas,id|different:id_kelas_asal',
|
||||||
'santri_ids' => 'required|array|min:1',
|
'santri_ids' => 'required|array|min:1',
|
||||||
'santri_ids.*' => 'exists:santris,id_santri',
|
'santri_ids.*' => 'exists:santris,id_santri',
|
||||||
], [
|
], [
|
||||||
'santri_ids.required' => 'Pilih minimal 1 santri untuk dinaikkan kelasnya.',
|
'santri_ids.required' => 'Pilih minimal 1 santri untuk dinaikkan kelasnya.',
|
||||||
'santri_ids.min' => 'Pilih minimal 1 santri untuk dinaikkan kelasnya.',
|
'santri_ids.min' => 'Pilih minimal 1 santri untuk dinaikkan kelasnya.',
|
||||||
|
'id_kelas_tujuan.different' => 'Kelas tujuan tidak boleh sama dengan kelas asal.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$kelasAsal = Kelas::findOrFail($request->id_kelas_asal);
|
$kelasAsal = Kelas::findOrFail($request->id_kelas_asal);
|
||||||
$kelasTujuan = Kelas::findOrFail($request->id_kelas_tujuan);
|
$kelasTujuan = Kelas::findOrFail($request->id_kelas_tujuan);
|
||||||
$tahunAjaranAktif = SantriKelas::getCurrentAcademicYear();
|
|
||||||
|
|
||||||
$processed = 0;
|
$processed = 0;
|
||||||
|
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
try {
|
try {
|
||||||
foreach ($request->santri_ids as $idSantri) {
|
foreach ($request->santri_ids as $idSantri) {
|
||||||
// Cari record santri_kelas yg ada di kelas asal
|
|
||||||
$record = SantriKelas::where('id_santri', $idSantri)
|
$record = SantriKelas::where('id_santri', $idSantri)
|
||||||
->where('id_kelas', $kelasAsal->id)
|
->where('id_kelas', $kelasAsal->id)
|
||||||
->where('tahun_ajaran', $tahunAjaranAktif)
|
->orderBy('tahun_ajaran', 'desc')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($record) {
|
if (!$record) continue;
|
||||||
// Update record: ganti kelas saja, tahun_ajaran & is_primary TETAP
|
|
||||||
$record->update([
|
// Cek duplikasi: jika sudah ada di kelas tujuan + tahun_ajaran sama, hapus record lama
|
||||||
'id_kelas' => $kelasTujuan->id,
|
$sudahAda = SantriKelas::where('id_santri', $idSantri)
|
||||||
]);
|
->where('id_kelas', $kelasTujuan->id)
|
||||||
$processed++;
|
->where('tahun_ajaran', $record->tahun_ajaran)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($sudahAda) {
|
||||||
|
$record->delete();
|
||||||
|
} else {
|
||||||
|
$record->update(['id_kelas' => $kelasTujuan->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$processed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.kenaikan.index')
|
return redirect()->route('admin.kelas.kenaikan.index')
|
||||||
->with('success', "Berhasil menaikkan {$processed} santri dari kelas {$kelasAsal->nama_kelas} ke {$kelasTujuan->nama_kelas}.");
|
->with('success', "Berhasil menaikkan {$processed} santri dari {$kelasAsal->nama_kelas} ke {$kelasTujuan->nama_kelas}.");
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
|
|
||||||
return redirect()->route('admin.kelas.kenaikan.preview', $request->id_kelas_asal)
|
return redirect()->route('admin.kelas.kenaikan.preview', $request->id_kelas_asal)
|
||||||
->with('error', 'Terjadi kesalahan saat memproses kenaikan kelas: ' . $e->getMessage());
|
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper: Get next academic year
|
* Helper: tahun ajaran aktif berdasarkan data yang ada di santri_kelas.
|
||||||
* Input: 2024/2025
|
* Menggunakan tahun ajaran terbaru yang punya record, fallback ke kalkulasi.
|
||||||
* Output: 2025/2026
|
|
||||||
*/
|
*/
|
||||||
private function getNextAcademicYear($currentYear)
|
private function getActiveTahunAjaran(): string
|
||||||
|
{
|
||||||
|
return SantriKelas::max('tahun_ajaran') ?? SantriKelas::getCurrentAcademicYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: hitung tahun ajaran berikutnya
|
||||||
|
* Contoh: "2024/2025" -> "2025/2026"
|
||||||
|
*/
|
||||||
|
private function getNextAcademicYear(string $currentYear): string
|
||||||
{
|
{
|
||||||
$parts = explode('/', $currentYear);
|
$parts = explode('/', $currentYear);
|
||||||
$startYear = (int) $parts[0] + 1;
|
return ((int) $parts[0] + 1) . '/' . ((int) $parts[1] + 1);
|
||||||
$endYear = (int) $parts[1] + 1;
|
|
||||||
|
|
||||||
return $startYear . '/' . $endYear;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -196,9 +196,9 @@ public function detailSantri($id_santri, Request $request)
|
||||||
DB::raw('SUM(CASE WHEN status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
|
DB::raw('SUM(CASE WHEN status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
|
||||||
DB::raw('SUM(CASE WHEN status = "Alpa" THEN 1 ELSE 0 END) as alpa')
|
DB::raw('SUM(CASE WHEN status = "Alpa" THEN 1 ELSE 0 END) as alpa')
|
||||||
)
|
)
|
||||||
->first();
|
->first() ?? (object) ['total' => 0, 'hadir' => 0, 'izin' => 0, 'sakit' => 0, 'alpa' => 0];
|
||||||
|
|
||||||
$persenKehadiran = $stats->total > 0 ? round(($stats->hadir / $stats->total) * 100, 1) : 0;
|
$persenKehadiran = ($stats->total ?? 0) > 0 ? round(($stats->hadir / $stats->total) * 100, 1) : 0;
|
||||||
|
|
||||||
// Kehadiran per kegiatan
|
// Kehadiran per kegiatan
|
||||||
$perKegiatan = AbsensiKegiatan::where('id_santri', $id_santri)
|
$perKegiatan = AbsensiKegiatan::where('id_santri', $id_santri)
|
||||||
|
|
@ -360,8 +360,8 @@ public function analisKegiatan($kegiatan_id, Request $request)
|
||||||
DB::raw('SUM(CASE WHEN status="Sakit" THEN 1 ELSE 0 END) as sakit'),
|
DB::raw('SUM(CASE WHEN status="Sakit" THEN 1 ELSE 0 END) as sakit'),
|
||||||
DB::raw('SUM(CASE WHEN status="Alpa" THEN 1 ELSE 0 END) as alpa')
|
DB::raw('SUM(CASE WHEN status="Alpa" THEN 1 ELSE 0 END) as alpa')
|
||||||
)
|
)
|
||||||
->first();
|
->first() ?? (object) ['total' => 0, 'hadir' => 0, 'izin' => 0, 'sakit' => 0, 'alpa' => 0];
|
||||||
$stats->persen = $stats->total > 0 ? round(($stats->hadir / $stats->total) * 100, 1) : 0;
|
$stats->persen = ($stats->total ?? 0) > 0 ? round(($stats->hadir / $stats->total) * 100, 1) : 0;
|
||||||
|
|
||||||
// Trend 4 minggu
|
// Trend 4 minggu
|
||||||
$trend = [];
|
$trend = [];
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@
|
||||||
|
|
||||||
class PembayaranSppController extends Controller
|
class PembayaranSppController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
// ══════════════════════════════════════════════════════
|
||||||
* Display a listing of the resource.
|
// INDEX
|
||||||
*/
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
// Default tab
|
// Default tab
|
||||||
|
|
@ -23,12 +24,7 @@ public function index(Request $request)
|
||||||
$bulan = $request->filled('bulan') ? $request->bulan : date('n');
|
$bulan = $request->filled('bulan') ? $request->bulan : date('n');
|
||||||
$tahun = $request->filled('tahun') ? $request->tahun : date('Y');
|
$tahun = $request->filled('tahun') ? $request->tahun : date('Y');
|
||||||
|
|
||||||
// Query untuk mendapatkan data pembayaran berdasarkan filter
|
// Data untuk filter tahun
|
||||||
$query = PembayaranSpp::with('santri')
|
|
||||||
->where('bulan', $bulan)
|
|
||||||
->where('tahun', $tahun);
|
|
||||||
|
|
||||||
// Data untuk filter
|
|
||||||
$tahunList = PembayaranSpp::selectRaw('DISTINCT tahun')
|
$tahunList = PembayaranSpp::selectRaw('DISTINCT tahun')
|
||||||
->orderBy('tahun', 'desc')
|
->orderBy('tahun', 'desc')
|
||||||
->pluck('tahun');
|
->pluck('tahun');
|
||||||
|
|
@ -40,126 +36,108 @@ public function index(Request $request)
|
||||||
|
|
||||||
// Get santri dengan status pembayaran untuk periode yang dipilih
|
// Get santri dengan status pembayaran untuk periode yang dipilih
|
||||||
$santriList = Santri::where('status', 'Aktif')
|
$santriList = Santri::where('status', 'Aktif')
|
||||||
->with(['pembayaranSpp' => function($q) use ($bulan, $tahun) {
|
->with(['pembayaranSpp' => function ($q) use ($bulan, $tahun) {
|
||||||
$q->where('bulan', $bulan)->where('tahun', $tahun);
|
$q->where('bulan', $bulan)->where('tahun', $tahun);
|
||||||
}])
|
}])
|
||||||
->get()
|
->get()
|
||||||
->map(function($santri) use ($bulan, $tahun) {
|
->map(function ($santri) {
|
||||||
$pembayaran = $santri->pembayaranSpp->first();
|
$p = $santri->pembayaranSpp->first();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id_santri' => $santri->id_santri,
|
'id_santri' => $santri->id_santri,
|
||||||
'nama_lengkap' => $santri->nama_lengkap,
|
'nama_lengkap' => $santri->nama_lengkap,
|
||||||
'nis' => $santri->nis,
|
'nis' => $santri->nis,
|
||||||
'kelas' => $santri->kelas,
|
'kelas' => $santri->kelas,
|
||||||
'pembayaran' => $pembayaran,
|
'pembayaran' => $p,
|
||||||
'status' => $pembayaran ? $pembayaran->status : 'Belum Ada Tagihan',
|
// status virtual: Lunas / Cicilan / Belum Lunas / Belum Ada Tagihan
|
||||||
'is_telat' => $pembayaran ? $pembayaran->isTelat() : false,
|
'status' => $p ? ($p->status === 'Lunas' ? 'Lunas' : ($p->isCicilan() ? 'Cicilan' : 'Belum Lunas')) : 'Belum Ada Tagihan',
|
||||||
'nominal' => $pembayaran ? $pembayaran->nominal : 0,
|
'is_telat' => $p ? $p->isTelat() : false,
|
||||||
'tanggal_bayar' => $pembayaran ? $pembayaran->tanggal_bayar : null,
|
'nominal' => $p ? (float) $p->nominal : 0,
|
||||||
'batas_bayar' => $pembayaran ? $pembayaran->batas_bayar : null,
|
'tanggal_bayar'=> $p ? $p->tanggal_bayar : null,
|
||||||
|
'batas_bayar' => $p ? $p->batas_bayar : null,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter berdasarkan tab
|
// ─── KPI (hitung dari data PENUH sebelum filter tab) ─────────
|
||||||
if ($tab === 'sudah-bayar') {
|
$totalSantriAll = $santriList->count();
|
||||||
$santriList = $santriList->filter(function($item) {
|
|
||||||
return $item['pembayaran'] && $item['status'] === 'Lunas';
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Belum bayar (termasuk yang belum ada tagihan dan yang telat)
|
|
||||||
$santriList = $santriList->filter(function($item) {
|
|
||||||
return !$item['pembayaran'] || $item['status'] !== 'Lunas';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter search
|
|
||||||
if ($request->filled('search')) {
|
|
||||||
$search = strtolower($request->search);
|
|
||||||
$santriList = $santriList->filter(function($item) use ($search) {
|
|
||||||
return str_contains(strtolower($item['nama_lengkap']), $search) ||
|
|
||||||
str_contains(strtolower($item['id_santri']), $search) ||
|
|
||||||
str_contains(strtolower($item['nis']), $search);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter status spesifik
|
|
||||||
if ($request->filled('filter_status')) {
|
|
||||||
if ($request->filter_status === 'Telat') {
|
|
||||||
$santriList = $santriList->filter(function($item) {
|
|
||||||
return $item['is_telat'];
|
|
||||||
});
|
|
||||||
} elseif ($request->filter_status === 'Belum Ada Tagihan') {
|
|
||||||
$santriList = $santriList->filter(function($item) {
|
|
||||||
return !$item['pembayaran'];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$santriList = $santriList->filter(function($item) use ($request) {
|
|
||||||
return $item['status'] === $request->filter_status;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hitung statistik
|
|
||||||
$totalSantri = $santriList->count();
|
|
||||||
$totalLunas = $santriList->where('status', 'Lunas')->count();
|
$totalLunas = $santriList->where('status', 'Lunas')->count();
|
||||||
$totalBelumBayar = $santriList->where('status', 'Belum Lunas')->count();
|
$totalCicilan = $santriList->where('status', 'Cicilan')->count();
|
||||||
|
$totalBelumBayar = $santriList->whereIn('status', ['Belum Lunas', 'Belum Ada Tagihan'])->count();
|
||||||
$totalTelat = $santriList->where('is_telat', true)->count();
|
$totalTelat = $santriList->where('is_telat', true)->count();
|
||||||
$totalBelumAdaTagihan = $santriList->where('status', 'Belum Ada Tagihan')->count();
|
$totalBelumAdaTagihan = $santriList->where('status', 'Belum Ada Tagihan')->count();
|
||||||
|
|
||||||
$nominalLunas = $santriList->where('status', 'Lunas')->sum('nominal');
|
$nominalLunas = $santriList->where('status', 'Lunas')->sum('nominal');
|
||||||
$nominalBelumLunas = $santriList->where('status', 'Belum Lunas')->sum('nominal');
|
$nominalBelumLunas = $santriList->whereIn('status', ['Belum Lunas', 'Cicilan'])->sum('nominal');
|
||||||
|
|
||||||
// Sort
|
// ─── Filter tab ───────────────────────────────────────────────
|
||||||
$santriList = $santriList->sortBy('nama_lengkap')->values();
|
if ($tab === 'sudah-bayar') {
|
||||||
|
$santriList = $santriList
|
||||||
|
->filter(fn($i) => $i['pembayaran'] && $i['status'] === 'Lunas')
|
||||||
|
->sortByDesc(fn($i) => $i['tanggal_bayar'] ? $i['tanggal_bayar']->timestamp : 0);
|
||||||
|
|
||||||
// Manual pagination
|
} elseif ($tab === 'cicilan') {
|
||||||
|
$santriList = $santriList
|
||||||
|
->filter(fn($i) => $i['pembayaran'] && $i['status'] === 'Cicilan')
|
||||||
|
->sortBy('nama_lengkap');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// belum-bayar: status Belum Lunas atau Belum Ada Tagihan
|
||||||
|
$santriList = $santriList
|
||||||
|
->filter(fn($i) => in_array($i['status'], ['Belum Lunas', 'Belum Ada Tagihan']))
|
||||||
|
->sortBy('nama_lengkap');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Search ───────────────────────────────────────────────────
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = strtolower($request->search);
|
||||||
|
$santriList = $santriList->filter(fn($i) =>
|
||||||
|
str_contains(strtolower($i['nama_lengkap']), $search) ||
|
||||||
|
str_contains(strtolower($i['id_santri']), $search) ||
|
||||||
|
str_contains(strtolower($i['nis']), $search)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Filter status spesifik (tab belum-bayar) ─────────────────
|
||||||
|
if ($request->filled('filter_status')) {
|
||||||
|
if ($request->filter_status === 'Telat') {
|
||||||
|
$santriList = $santriList->filter(fn($i) => $i['is_telat']);
|
||||||
|
} elseif ($request->filter_status === 'Belum Ada Tagihan') {
|
||||||
|
$santriList = $santriList->filter(fn($i) => !$i['pembayaran']);
|
||||||
|
} else {
|
||||||
|
$santriList = $santriList->filter(fn($i) => $i['status'] === $request->filter_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Pagination manual ────────────────────────────────────────
|
||||||
|
$santriList = $santriList->values();
|
||||||
$perPage = 20;
|
$perPage = 20;
|
||||||
$currentPage = $request->get('page', 1);
|
$currentPage = $request->get('page', 1);
|
||||||
$offset = ($currentPage - 1) * $perPage;
|
$offset = ($currentPage - 1) * $perPage;
|
||||||
|
|
||||||
$santriPaginated = $santriList->slice($offset, $perPage)->values();
|
$santriPaginated = $santriList->slice($offset, $perPage)->values();
|
||||||
$totalPages = ceil($santriList->count() / $perPage);
|
$totalPages = ceil($santriList->count() / $perPage);
|
||||||
|
$totalSantri = $santriList->count();
|
||||||
|
|
||||||
return view('admin.pembayaran-spp.index', compact(
|
return view('admin.pembayaran-spp.index', compact(
|
||||||
'santriPaginated',
|
'santriPaginated', 'tab', 'bulan', 'tahun', 'tahunList',
|
||||||
'tab',
|
'totalSantri', 'totalSantriAll',
|
||||||
'bulan',
|
'totalLunas', 'totalCicilan', 'totalBelumBayar',
|
||||||
'tahun',
|
'totalTelat', 'totalBelumAdaTagihan',
|
||||||
'tahunList',
|
'nominalLunas', 'nominalBelumLunas',
|
||||||
'totalSantri',
|
'currentPage', 'totalPages'
|
||||||
'totalLunas',
|
|
||||||
'totalBelumBayar',
|
|
||||||
'totalTelat',
|
|
||||||
'totalBelumAdaTagihan',
|
|
||||||
'nominalLunas',
|
|
||||||
'nominalBelumLunas',
|
|
||||||
'currentPage',
|
|
||||||
'totalPages'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ══════════════════════════════════════════════════════
|
||||||
* Show the form for creating a new resource.
|
// CREATE / STORE
|
||||||
*/
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
// Ambil santri yang aktif
|
$santris = Santri::where('status', 'Aktif')->orderBy('nama_lengkap', 'asc')->get();
|
||||||
$santris = Santri::where('status', 'Aktif')
|
|
||||||
->orderBy('nama_lengkap', 'asc')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
// Generate preview ID
|
|
||||||
$last = PembayaranSpp::orderBy('id', 'desc')->first();
|
$last = PembayaranSpp::orderBy('id', 'desc')->first();
|
||||||
$nextNum = $last ? intval(substr($last->id_pembayaran, 3)) + 1 : 1;
|
$nextNum = $last ? intval(substr($last->id_pembayaran, 3)) + 1 : 1;
|
||||||
$nextId = 'SPP' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
|
$nextId = 'SPP' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
return view('admin.pembayaran-spp.create', compact('santris', 'nextId'));
|
return view('admin.pembayaran-spp.create', compact('santris', 'nextId'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
|
|
@ -168,7 +146,7 @@ public function store(Request $request)
|
||||||
'tahun' => 'required|integer|min:2020|max:2100',
|
'tahun' => 'required|integer|min:2020|max:2100',
|
||||||
'nominal' => 'required|numeric|min:0',
|
'nominal' => 'required|numeric|min:0',
|
||||||
'status' => 'required|in:Lunas,Belum Lunas',
|
'status' => 'required|in:Lunas,Belum Lunas',
|
||||||
'tanggal_bayar' => 'nullable|date',
|
'tanggal_bayar'=> 'nullable|date',
|
||||||
'batas_bayar' => 'required|date',
|
'batas_bayar' => 'required|date',
|
||||||
'keterangan' => 'nullable|string',
|
'keterangan' => 'nullable|string',
|
||||||
], [
|
], [
|
||||||
|
|
@ -184,14 +162,15 @@ public function store(Request $request)
|
||||||
'batas_bayar.required' => 'Batas bayar wajib diisi.',
|
'batas_bayar.required' => 'Batas bayar wajib diisi.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Cek duplikasi
|
// Cek duplikasi — jika sudah ada, arahkan ke edit
|
||||||
$exists = PembayaranSpp::where('id_santri', $validated['id_santri'])
|
$existing = PembayaranSpp::where('id_santri', $validated['id_santri'])
|
||||||
->where('bulan', $validated['bulan'])
|
->where('bulan', $validated['bulan'])
|
||||||
->where('tahun', $validated['tahun'])
|
->where('tahun', $validated['tahun'])
|
||||||
->exists();
|
->first();
|
||||||
|
|
||||||
if ($exists) {
|
if ($existing) {
|
||||||
return back()->withInput()->with('error', 'Data pembayaran untuk periode ini sudah ada.');
|
return redirect()->route('admin.pembayaran-spp.edit', $existing->id)
|
||||||
|
->with('info', 'Data SPP untuk periode ini sudah ada. Silakan edit data berikut untuk mengubah status pembayaran.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika status lunas dan tanggal_bayar kosong, set ke hari ini
|
// Jika status lunas dan tanggal_bayar kosong, set ke hari ini
|
||||||
|
|
@ -205,27 +184,22 @@ public function store(Request $request)
|
||||||
->with('success', 'Data pembayaran SPP berhasil ditambahkan.');
|
->with('success', 'Data pembayaran SPP berhasil ditambahkan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ══════════════════════════════════════════════════════
|
||||||
* Display the specified resource.
|
// SHOW / EDIT / UPDATE / DESTROY
|
||||||
*/
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
public function show(PembayaranSpp $pembayaranSpp)
|
public function show(PembayaranSpp $pembayaranSpp)
|
||||||
{
|
{
|
||||||
$pembayaranSpp->load('santri');
|
$pembayaranSpp->load('santri');
|
||||||
return view('admin.pembayaran-spp.show', compact('pembayaranSpp'));
|
return view('admin.pembayaran-spp.show', compact('pembayaranSpp'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for editing the specified resource.
|
|
||||||
*/
|
|
||||||
public function edit(PembayaranSpp $pembayaranSpp)
|
public function edit(PembayaranSpp $pembayaranSpp)
|
||||||
{
|
{
|
||||||
$santris = Santri::orderBy('nama_lengkap', 'asc')->get();
|
$santris = Santri::orderBy('nama_lengkap', 'asc')->get();
|
||||||
return view('admin.pembayaran-spp.edit', compact('pembayaranSpp', 'santris'));
|
return view('admin.pembayaran-spp.edit', compact('pembayaranSpp', 'santris'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(Request $request, PembayaranSpp $pembayaranSpp)
|
public function update(Request $request, PembayaranSpp $pembayaranSpp)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
|
|
@ -234,7 +208,7 @@ public function update(Request $request, PembayaranSpp $pembayaranSpp)
|
||||||
'tahun' => 'required|integer|min:2020|max:2100',
|
'tahun' => 'required|integer|min:2020|max:2100',
|
||||||
'nominal' => 'required|numeric|min:0',
|
'nominal' => 'required|numeric|min:0',
|
||||||
'status' => 'required|in:Lunas,Belum Lunas',
|
'status' => 'required|in:Lunas,Belum Lunas',
|
||||||
'tanggal_bayar' => 'nullable|date',
|
'tanggal_bayar'=> 'nullable|date',
|
||||||
'batas_bayar' => 'required|date',
|
'batas_bayar' => 'required|date',
|
||||||
'keterangan' => 'nullable|string',
|
'keterangan' => 'nullable|string',
|
||||||
], [
|
], [
|
||||||
|
|
@ -262,29 +236,103 @@ public function update(Request $request, PembayaranSpp $pembayaranSpp)
|
||||||
$validated['tanggal_bayar'] = Carbon::now()->format('Y-m-d');
|
$validated['tanggal_bayar'] = Carbon::now()->format('Y-m-d');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Jika diubah ke Lunas, hapus data cicilan dari keterangan
|
||||||
|
if ($validated['status'] === 'Lunas' && $pembayaranSpp->isCicilan()) {
|
||||||
|
$validated['keterangan'] = $pembayaranSpp->catatan_teks; // simpan teks catatan saja
|
||||||
|
}
|
||||||
|
|
||||||
$pembayaranSpp->update($validated);
|
$pembayaranSpp->update($validated);
|
||||||
|
|
||||||
return redirect()->route('admin.pembayaran-spp.index')
|
return redirect()->route('admin.pembayaran-spp.index')
|
||||||
->with('success', 'Data pembayaran SPP berhasil diperbarui.');
|
->with('success', 'Data pembayaran SPP berhasil diperbarui.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(PembayaranSpp $pembayaranSpp)
|
public function destroy(PembayaranSpp $pembayaranSpp)
|
||||||
{
|
{
|
||||||
$periode = $pembayaranSpp->periode_lengkap;
|
$periode = $pembayaranSpp->periode_lengkap;
|
||||||
$santri = $pembayaranSpp->santri->nama_lengkap;
|
$santri = $pembayaranSpp->santri->nama_lengkap;
|
||||||
|
|
||||||
$pembayaranSpp->delete();
|
$pembayaranSpp->delete();
|
||||||
|
|
||||||
return redirect()->route('admin.pembayaran-spp.index')
|
return redirect()->route('admin.pembayaran-spp.index')
|
||||||
->with('success', "Data pembayaran SPP {$periode} untuk {$santri} berhasil dihapus.");
|
->with('success', "Data pembayaran SPP {$periode} untuk {$santri} berhasil dihapus.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// QUICK ACTIONS
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan riwayat pembayaran per santri
|
* Tandai Lunas langsung (quick pay)
|
||||||
*/
|
*/
|
||||||
|
public function bayar(Request $request, PembayaranSpp $pembayaranSpp)
|
||||||
|
{
|
||||||
|
if ($pembayaranSpp->status === 'Lunas') {
|
||||||
|
return redirect()->back()->with('info', 'Pembayaran ini sudah berstatus Lunas.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pembayaranSpp->update([
|
||||||
|
'status' => 'Lunas',
|
||||||
|
'tanggal_bayar' => $request->filled('tanggal_bayar')
|
||||||
|
? $request->tanggal_bayar
|
||||||
|
: Carbon::now()->format('Y-m-d'),
|
||||||
|
// Bersihkan data cicilan dari keterangan, simpan catatan teks jika ada
|
||||||
|
'keterangan' => $pembayaranSpp->catatan_teks,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$nama = $pembayaranSpp->santri->nama_lengkap;
|
||||||
|
$periode = $pembayaranSpp->periode_lengkap;
|
||||||
|
|
||||||
|
return redirect()->route('admin.pembayaran-spp.index', [
|
||||||
|
'tab' => 'sudah-bayar',
|
||||||
|
'bulan' => $pembayaranSpp->bulan,
|
||||||
|
'tahun' => $pembayaranSpp->tahun,
|
||||||
|
])->with('success', "Pembayaran SPP {$periode} untuk {$nama} berhasil ditandai Lunas.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catat cicilan (tambah nominal terbayar)
|
||||||
|
* Status DB tetap "Belum Lunas" — cicilan disimpan di keterangan (JSON).
|
||||||
|
*/
|
||||||
|
public function catatCicilan(Request $request, PembayaranSpp $pembayaranSpp)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'nominal_cicilan' => 'required|numeric|min:1',
|
||||||
|
'catatan' => 'nullable|string|max:200',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sudahTerbayar = $pembayaranSpp->nominal_terbayar;
|
||||||
|
$totalTagihan = (float) $pembayaranSpp->nominal;
|
||||||
|
$baru = $sudahTerbayar + (float) $request->nominal_cicilan;
|
||||||
|
|
||||||
|
// Jika total cicilan >= tagihan → otomatis Lunas
|
||||||
|
if ($baru >= $totalTagihan) {
|
||||||
|
$pembayaranSpp->update([
|
||||||
|
'status' => 'Lunas',
|
||||||
|
'tanggal_bayar' => Carbon::now()->format('Y-m-d'),
|
||||||
|
'keterangan' => $request->catatan ?? $pembayaranSpp->catatan_teks,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.pembayaran-spp.index', [
|
||||||
|
'tab' => 'sudah-bayar',
|
||||||
|
'bulan' => $pembayaranSpp->bulan,
|
||||||
|
'tahun' => $pembayaranSpp->tahun,
|
||||||
|
])->with('success', "Cicilan terakhir diterima. SPP {$pembayaranSpp->periode_lengkap} untuk {$pembayaranSpp->santri->nama_lengkap} sekarang Lunas.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Masih cicilan — update keterangan saja, status tetap "Belum Lunas"
|
||||||
|
$pembayaranSpp->setCicilan($baru, $request->catatan ?? $pembayaranSpp->catatan_teks);
|
||||||
|
$pembayaranSpp->save();
|
||||||
|
|
||||||
|
$sisaFormat = 'Rp ' . number_format($totalTagihan - $baru, 0, ',', '.');
|
||||||
|
$cicilanFormat = 'Rp ' . number_format((float) $request->nominal_cicilan, 0, ',', '.');
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->with('success', "Cicilan {$cicilanFormat} berhasil dicatat. Sisa: {$sisaFormat}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// RIWAYAT PER SANTRI
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
public function riwayat($id_santri)
|
public function riwayat($id_santri)
|
||||||
{
|
{
|
||||||
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
||||||
|
|
@ -309,17 +357,14 @@ public function riwayat($id_santri)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
return view('admin.pembayaran-spp.riwayat', compact(
|
return view('admin.pembayaran-spp.riwayat', compact(
|
||||||
'santri',
|
'santri', 'pembayaranSpp', 'totalBayar', 'totalTunggakan', 'jumlahTelat'
|
||||||
'pembayaranSpp',
|
|
||||||
'totalBayar',
|
|
||||||
'totalTunggakan',
|
|
||||||
'jumlahTelat'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ══════════════════════════════════════════════════════
|
||||||
* Generate SPP untuk semua santri aktif dalam periode tertentu
|
// GENERATE SPP MASSAL
|
||||||
*/
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
public function generate(Request $request)
|
public function generate(Request $request)
|
||||||
{
|
{
|
||||||
if ($request->isMethod('post')) {
|
if ($request->isMethod('post')) {
|
||||||
|
|
@ -335,7 +380,6 @@ public function generate(Request $request)
|
||||||
$skipped = 0;
|
$skipped = 0;
|
||||||
|
|
||||||
foreach ($santris as $santri) {
|
foreach ($santris as $santri) {
|
||||||
// Cek apakah sudah ada
|
|
||||||
$exists = PembayaranSpp::where('id_santri', $santri->id_santri)
|
$exists = PembayaranSpp::where('id_santri', $santri->id_santri)
|
||||||
->where('bulan', $validated['bulan'])
|
->where('bulan', $validated['bulan'])
|
||||||
->where('tahun', $validated['tahun'])
|
->where('tahun', $validated['tahun'])
|
||||||
|
|
@ -363,28 +407,21 @@ public function generate(Request $request)
|
||||||
return view('admin.pembayaran-spp.generate');
|
return view('admin.pembayaran-spp.generate');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ══════════════════════════════════════════════════════
|
||||||
* Halaman pilihan laporan
|
// LAPORAN & CETAK
|
||||||
*/
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
public function laporan()
|
public function laporan()
|
||||||
{
|
{
|
||||||
return view('admin.pembayaran-spp.laporan');
|
return view('admin.pembayaran-spp.laporan');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cetak laporan SPP (semua data atau filter)
|
|
||||||
*/
|
|
||||||
public function cetakLaporan(Request $request)
|
public function cetakLaporan(Request $request)
|
||||||
{
|
{
|
||||||
$query = PembayaranSpp::with('santri');
|
$query = PembayaranSpp::with('santri');
|
||||||
|
|
||||||
// Filter
|
if ($request->filled('bulan')) $query->where('bulan', $request->bulan);
|
||||||
if ($request->filled('bulan')) {
|
if ($request->filled('tahun')) $query->where('tahun', $request->tahun);
|
||||||
$query->where('bulan', $request->bulan);
|
|
||||||
}
|
|
||||||
if ($request->filled('tahun')) {
|
|
||||||
$query->where('tahun', $request->tahun);
|
|
||||||
}
|
|
||||||
if ($request->filled('status')) {
|
if ($request->filled('status')) {
|
||||||
if ($request->status === 'Telat') {
|
if ($request->status === 'Telat') {
|
||||||
$query->telat();
|
$query->telat();
|
||||||
|
|
@ -393,56 +430,33 @@ public function cetakLaporan(Request $request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$pembayaranSpp = $query->orderBy('tahun', 'desc')
|
$pembayaranSpp = $query->orderBy('tahun', 'desc')->orderBy('bulan', 'desc')->get();
|
||||||
->orderBy('bulan', 'desc')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
// Statistik
|
|
||||||
$totalLunas = $pembayaranSpp->where('status', 'Lunas')->sum('nominal');
|
$totalLunas = $pembayaranSpp->where('status', 'Lunas')->sum('nominal');
|
||||||
$totalTunggakan = $pembayaranSpp->where('status', 'Belum Lunas')->sum('nominal');
|
$totalTunggakan = $pembayaranSpp->where('status', 'Belum Lunas')->sum('nominal');
|
||||||
$jumlahTelat = $pembayaranSpp->filter(function($spp) {
|
$jumlahTelat = $pembayaranSpp->filter(fn($s) => $s->isTelat())->count();
|
||||||
return $spp->isTelat();
|
|
||||||
})->count();
|
|
||||||
|
|
||||||
return view('admin.pembayaran-spp.cetak-laporan', compact(
|
return view('admin.pembayaran-spp.cetak-laporan', compact(
|
||||||
'pembayaranSpp',
|
'pembayaranSpp', 'totalLunas', 'totalTunggakan', 'jumlahTelat'
|
||||||
'totalLunas',
|
|
||||||
'totalTunggakan',
|
|
||||||
'jumlahTelat'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cetak laporan SPP per santri
|
|
||||||
*/
|
|
||||||
public function cetakLaporanSantri($id_santri)
|
public function cetakLaporanSantri($id_santri)
|
||||||
{
|
{
|
||||||
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
||||||
|
|
||||||
$pembayaranSpp = PembayaranSpp::where('id_santri', $id_santri)
|
$pembayaranSpp = PembayaranSpp::where('id_santri', $id_santri)
|
||||||
->orderBy('tahun', 'desc')
|
->orderBy('tahun', 'desc')
|
||||||
->orderBy('bulan', 'desc')
|
->orderBy('bulan', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Statistik
|
|
||||||
$totalLunas = $pembayaranSpp->where('status', 'Lunas')->sum('nominal');
|
$totalLunas = $pembayaranSpp->where('status', 'Lunas')->sum('nominal');
|
||||||
$totalTunggakan = $pembayaranSpp->where('status', 'Belum Lunas')->sum('nominal');
|
$totalTunggakan = $pembayaranSpp->where('status', 'Belum Lunas')->sum('nominal');
|
||||||
$jumlahTelat = $pembayaranSpp->filter(function($spp) {
|
$jumlahTelat = $pembayaranSpp->filter(fn($s) => $s->isTelat())->count();
|
||||||
return $spp->isTelat();
|
|
||||||
})->count();
|
|
||||||
|
|
||||||
return view('admin.pembayaran-spp.cetak-laporan-santri', compact(
|
return view('admin.pembayaran-spp.cetak-laporan-santri', compact(
|
||||||
'santri',
|
'santri', 'pembayaranSpp', 'totalLunas', 'totalTunggakan', 'jumlahTelat'
|
||||||
'pembayaranSpp',
|
|
||||||
'totalLunas',
|
|
||||||
'totalTunggakan',
|
|
||||||
'jumlahTelat'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cetak bukti pembayaran
|
|
||||||
*/
|
|
||||||
public function cetakBukti(PembayaranSpp $pembayaranSpp)
|
public function cetakBukti(PembayaranSpp $pembayaranSpp)
|
||||||
{
|
{
|
||||||
$pembayaranSpp->load('santri');
|
$pembayaranSpp->load('santri');
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,46 @@ class UangSakuController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Tampilkan daftar uang saku — Grouped per Santri
|
* Tampilkan daftar uang saku — Grouped per Santri
|
||||||
|
* Default: bulan ini
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$search = $request->get('search');
|
$search = $request->get('search');
|
||||||
|
|
||||||
// Query santri aktif yang punya transaksi (atau semua jika tidak ada filter)
|
// ── Default: bulan ini ──────────────────────────────────────
|
||||||
|
$dari = $request->get('dari', now()->startOfMonth()->format('Y-m-d'));
|
||||||
|
$sampai = $request->get('sampai', now()->endOfMonth()->format('Y-m-d'));
|
||||||
|
$sort = $request->get('sort', 'nama'); // nama | saldo_asc | saldo_desc | transaksi_desc | terakhir
|
||||||
|
|
||||||
|
// ── KPI ringkasan periode (dipengaruhi filter tanggal) ──────
|
||||||
|
$kpiQuery = UangSaku::whereBetween('tanggal_transaksi', [$dari, $sampai]);
|
||||||
|
$kpi = [
|
||||||
|
'total_transaksi' => (clone $kpiQuery)->count(),
|
||||||
|
'total_pemasukan' => (float)(clone $kpiQuery)->where('jenis_transaksi', 'pemasukan')->sum('nominal'),
|
||||||
|
'total_pengeluaran' => (float)(clone $kpiQuery)->where('jenis_transaksi', 'pengeluaran')->sum('nominal'),
|
||||||
|
'total_santri' => (clone $kpiQuery)->distinct('id_santri')->count('id_santri'),
|
||||||
|
];
|
||||||
|
// Selisih periode: apakah dalam rentang ini uang yang masuk lebih besar dari yang keluar
|
||||||
|
$kpi['selisih'] = $kpi['total_pemasukan'] - $kpi['total_pengeluaran'];
|
||||||
|
|
||||||
|
// ── KPI Real-time: Total saldo aktual seluruh santri (tidak dipengaruhi filter) ──
|
||||||
|
// Ambil saldo_sesudah dari transaksi TERAKHIR masing-masing santri
|
||||||
|
$totalSaldoSemua = DB::table('uang_saku as u1')
|
||||||
|
->join(DB::raw('(
|
||||||
|
SELECT id_santri, MAX(id) as max_id
|
||||||
|
FROM uang_saku
|
||||||
|
GROUP BY id_santri
|
||||||
|
) as latest'), function ($join) {
|
||||||
|
$join->on('u1.id_santri', '=', 'latest.id_santri')
|
||||||
|
->on('u1.id', '=', 'latest.max_id');
|
||||||
|
})
|
||||||
|
->sum('u1.saldo_sesudah');
|
||||||
|
|
||||||
|
$kpi['total_saldo_realtime'] = (float) $totalSaldoSemua;
|
||||||
|
|
||||||
|
// ── Query santri ────────────────────────────────────────────
|
||||||
$santriQuery = Santri::aktif()
|
$santriQuery = Santri::aktif()
|
||||||
->select('id_santri', 'nama_lengkap')
|
->select('id_santri', 'nama_lengkap')
|
||||||
->withCount(['uangSaku as transaksi_bulan_ini' => function ($q) {
|
|
||||||
$q->whereMonth('tanggal_transaksi', now()->month)
|
|
||||||
->whereYear('tanggal_transaksi', now()->year);
|
|
||||||
}])
|
|
||||||
->has('uangSaku');
|
->has('uangSaku');
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
|
|
@ -35,37 +63,72 @@ public function index(Request $request)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$santriList = $santriQuery->orderBy('nama_lengkap')
|
$santriQuery->orderBy('nama_lengkap');
|
||||||
->paginate(20)
|
|
||||||
->appends(request()->query());
|
$santriList = $santriQuery->paginate(20)->appends(request()->query());
|
||||||
|
|
||||||
// Ambil saldo terakhir & transaksi terbaru per santri (batch)
|
|
||||||
$ids = $santriList->pluck('id_santri');
|
$ids = $santriList->pluck('id_santri');
|
||||||
|
|
||||||
// Saldo terakhir per santri (dari transaksi terbaru)
|
// ── Saldo terakhir per santri (efisien: subquery per-id) ────
|
||||||
$saldoMap = UangSaku::whereIn('id_santri', $ids)
|
// Ambil id transaksi terakhir per santri lalu join, hindari get()->unique() yang boros
|
||||||
->select('id_santri', 'saldo_sesudah')
|
$latestIds = DB::table('uang_saku')
|
||||||
->orderByDesc('tanggal_transaksi')
|
->whereIn('id_santri', $ids)
|
||||||
->orderByDesc('created_at')
|
->select('id_santri', DB::raw('MAX(id) as max_id'))
|
||||||
|
->groupBy('id_santri')
|
||||||
|
->pluck('max_id', 'id_santri');
|
||||||
|
|
||||||
|
$saldoMap = UangSaku::whereIn('id', $latestIds->values())
|
||||||
->get()
|
->get()
|
||||||
->unique('id_santri')
|
|
||||||
->keyBy('id_santri');
|
->keyBy('id_santri');
|
||||||
|
|
||||||
// Transaksi terbaru per santri (max 5)
|
// ── Pemasukan & pengeluaran bulan ini per santri ────────────
|
||||||
|
$bulanIniStats = UangSaku::whereIn('id_santri', $ids)
|
||||||
|
->whereMonth('tanggal_transaksi', now()->month)
|
||||||
|
->whereYear('tanggal_transaksi', now()->year)
|
||||||
|
->select(
|
||||||
|
'id_santri',
|
||||||
|
DB::raw('SUM(CASE WHEN jenis_transaksi="pemasukan" THEN nominal ELSE 0 END) as pemasukan_bulan'),
|
||||||
|
DB::raw('SUM(CASE WHEN jenis_transaksi="pengeluaran" THEN nominal ELSE 0 END) as pengeluaran_bulan'),
|
||||||
|
DB::raw('COUNT(*) as total_bulan')
|
||||||
|
)
|
||||||
|
->groupBy('id_santri')
|
||||||
|
->get()
|
||||||
|
->keyBy('id_santri');
|
||||||
|
|
||||||
|
// ── Transaksi terbaru per santri (max 5, untuk collapsed detail) ──
|
||||||
$transaksiMap = UangSaku::whereIn('id_santri', $ids)
|
$transaksiMap = UangSaku::whereIn('id_santri', $ids)
|
||||||
->orderByDesc('tanggal_transaksi')
|
->orderByDesc('tanggal_transaksi')
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->get()
|
->get()
|
||||||
->groupBy('id_santri')
|
->groupBy('id_santri')
|
||||||
->map(fn ($group) => $group->take(5));
|
->map(fn($g) => $g->take(5));
|
||||||
|
|
||||||
// Attach ke santri objects
|
// ── Attach semua data ke santri objects ─────────────────────
|
||||||
$santriList->getCollection()->each(function ($santri) use ($saldoMap, $transaksiMap) {
|
$collection = $santriList->getCollection()->map(function ($santri) use ($saldoMap, $bulanIniStats, $transaksiMap) {
|
||||||
$santri->saldo_terakhir = $saldoMap[$santri->id_santri]->saldo_sesudah ?? 0;
|
$saldoRow = $saldoMap[$santri->id_santri] ?? null;
|
||||||
|
$bulan = $bulanIniStats[$santri->id_santri] ?? null;
|
||||||
|
|
||||||
|
$santri->saldo_terakhir = $saldoRow ? (float)$saldoRow->saldo_sesudah : 0;
|
||||||
|
$santri->transaksi_terakhir_tgl = $saldoRow ? $saldoRow->tanggal_transaksi : null;
|
||||||
|
$santri->pemasukan_bulan = $bulan ? (float)$bulan->pemasukan_bulan : 0;
|
||||||
|
$santri->pengeluaran_bulan = $bulan ? (float)$bulan->pengeluaran_bulan : 0;
|
||||||
|
$santri->transaksi_bulan_ini = $bulan ? (int)$bulan->total_bulan : 0;
|
||||||
$santri->transaksi_terbaru = $transaksiMap[$santri->id_santri] ?? collect();
|
$santri->transaksi_terbaru = $transaksiMap[$santri->id_santri] ?? collect();
|
||||||
|
return $santri;
|
||||||
});
|
});
|
||||||
|
|
||||||
return view('admin.uang-saku.index', compact('santriList'));
|
// ── Re-sort collection setelah attach ───────────────────────
|
||||||
|
$sorted = match($sort) {
|
||||||
|
'saldo_asc' => $collection->sortBy('saldo_terakhir'),
|
||||||
|
'saldo_desc' => $collection->sortByDesc('saldo_terakhir'),
|
||||||
|
'transaksi_desc' => $collection->sortByDesc('transaksi_bulan_ini'),
|
||||||
|
'terakhir' => $collection->sortByDesc('transaksi_terakhir_tgl'),
|
||||||
|
default => $collection->sortBy('nama_lengkap'),
|
||||||
|
};
|
||||||
|
|
||||||
|
$santriList->setCollection($sorted->values());
|
||||||
|
|
||||||
|
return view('admin.uang-saku.index', compact('santriList', 'kpi', 'dari', 'sampai', 'sort'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,15 +140,13 @@ public function santriInfo($id_santri)
|
||||||
|
|
||||||
$bulanIni = now();
|
$bulanIni = now();
|
||||||
|
|
||||||
// Saldo terakhir
|
|
||||||
$lastTx = UangSaku::where('id_santri', $id_santri)
|
$lastTx = UangSaku::where('id_santri', $id_santri)
|
||||||
->orderByDesc('tanggal_transaksi')
|
->orderByDesc('tanggal_transaksi')
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$saldo = $lastTx ? $lastTx->saldo_sesudah : 0;
|
$saldo = $lastTx ? (float)$lastTx->saldo_sesudah : 0;
|
||||||
|
|
||||||
// Total pemasukan & pengeluaran bulan ini
|
|
||||||
$pemasukanBulanIni = UangSaku::where('id_santri', $id_santri)
|
$pemasukanBulanIni = UangSaku::where('id_santri', $id_santri)
|
||||||
->where('jenis_transaksi', 'pemasukan')
|
->where('jenis_transaksi', 'pemasukan')
|
||||||
->whereMonth('tanggal_transaksi', $bulanIni->month)
|
->whereMonth('tanggal_transaksi', $bulanIni->month)
|
||||||
|
|
@ -98,13 +159,12 @@ public function santriInfo($id_santri)
|
||||||
->whereYear('tanggal_transaksi', $bulanIni->year)
|
->whereYear('tanggal_transaksi', $bulanIni->year)
|
||||||
->sum('nominal');
|
->sum('nominal');
|
||||||
|
|
||||||
// 3 transaksi terakhir
|
|
||||||
$transaksiTerakhir = UangSaku::where('id_santri', $id_santri)
|
$transaksiTerakhir = UangSaku::where('id_santri', $id_santri)
|
||||||
->orderByDesc('tanggal_transaksi')
|
->orderByDesc('tanggal_transaksi')
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->limit(3)
|
->limit(3)
|
||||||
->get()
|
->get()
|
||||||
->map(fn ($t) => [
|
->map(fn($t) => [
|
||||||
'tanggal' => $t->tanggal_transaksi->format('d/m/Y'),
|
'tanggal' => $t->tanggal_transaksi->format('d/m/Y'),
|
||||||
'jenis' => $t->jenis_transaksi,
|
'jenis' => $t->jenis_transaksi,
|
||||||
'nominal' => number_format($t->nominal, 0, ',', '.'),
|
'nominal' => number_format($t->nominal, 0, ',', '.'),
|
||||||
|
|
@ -121,9 +181,6 @@ public function santriInfo($id_santri)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Form tambah transaksi
|
|
||||||
*/
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
$santriList = Santri::where('status', 'Aktif')
|
$santriList = Santri::where('status', 'Aktif')
|
||||||
|
|
@ -134,9 +191,6 @@ public function create()
|
||||||
return view('admin.uang-saku.create', compact('santriList'));
|
return view('admin.uang-saku.create', compact('santriList'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simpan transaksi baru
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
|
|
@ -145,117 +199,84 @@ public function store(Request $request)
|
||||||
'nominal' => 'required|numeric|min:1|max:99999999',
|
'nominal' => 'required|numeric|min:1|max:99999999',
|
||||||
'keterangan' => 'nullable|string|max:500',
|
'keterangan' => 'nullable|string|max:500',
|
||||||
'tanggal_transaksi' => 'required|date',
|
'tanggal_transaksi' => 'required|date',
|
||||||
], [
|
|
||||||
'id_santri.required' => 'Santri wajib dipilih.',
|
|
||||||
'id_santri.exists' => 'Santri tidak ditemukan.',
|
|
||||||
'jenis_transaksi.required' => 'Jenis transaksi wajib dipilih.',
|
|
||||||
'nominal.required' => 'Nominal wajib diisi.',
|
|
||||||
'nominal.numeric' => 'Nominal harus berupa angka.',
|
|
||||||
'nominal.min' => 'Nominal minimal Rp 1.',
|
|
||||||
'tanggal_transaksi.required' => 'Tanggal transaksi wajib diisi.',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
try {
|
try {
|
||||||
UangSaku::create($validated);
|
UangSaku::create($validated);
|
||||||
|
|
||||||
// Update saldo transaksi berikutnya jika ada
|
|
||||||
$this->recalculateSaldoAfter($validated['id_santri'], $validated['tanggal_transaksi']);
|
$this->recalculateSaldoAfter($validated['id_santri'], $validated['tanggal_transaksi']);
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
Cache::forget('santri_aktif_uang_saku');
|
Cache::forget('santri_aktif_uang_saku');
|
||||||
|
|
||||||
return redirect()->route('admin.uang-saku.index')
|
return redirect()->route('admin.uang-saku.index')
|
||||||
->with('success', 'Transaksi uang saku berhasil ditambahkan.');
|
->with('success', 'Transaksi uang saku berhasil ditambahkan.');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
return back()->withInput()
|
return back()->withInput()->with('error', 'Gagal menambahkan transaksi: ' . $e->getMessage());
|
||||||
->with('error', 'Gagal menambahkan transaksi: ' . $e->getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tampilkan detail transaksi
|
|
||||||
*/
|
|
||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
$transaksi = UangSaku::with('santri')->findOrFail($id);
|
$transaksi = UangSaku::with('santri')->findOrFail($id);
|
||||||
return view('admin.uang-saku.show', compact('transaksi'));
|
return view('admin.uang-saku.show', compact('transaksi'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Form edit transaksi
|
|
||||||
*/
|
|
||||||
public function edit($id)
|
public function edit($id)
|
||||||
{
|
{
|
||||||
$transaksi = UangSaku::with('santri')->findOrFail($id);
|
$transaksi = UangSaku::with('santri')->findOrFail($id);
|
||||||
|
|
||||||
$santriList = Santri::where('status', 'Aktif')
|
$santriList = Santri::where('status', 'Aktif')
|
||||||
->select('id_santri', 'nama_lengkap')
|
->select('id_santri', 'nama_lengkap')
|
||||||
->orderBy('nama_lengkap')
|
->orderBy('nama_lengkap')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('admin.uang-saku.edit', compact('transaksi', 'santriList'));
|
return view('admin.uang-saku.edit', compact('transaksi', 'santriList'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update transaksi
|
|
||||||
*/
|
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
$transaksi = UangSaku::findOrFail($id);
|
$transaksi = UangSaku::findOrFail($id);
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'jenis_transaksi' => 'required|in:pemasukan,pengeluaran',
|
'jenis_transaksi' => 'required|in:pemasukan,pengeluaran',
|
||||||
'nominal' => 'required|numeric|min:1|max:99999999',
|
'nominal' => 'required|numeric|min:1|max:99999999',
|
||||||
'keterangan' => 'nullable|string|max:500',
|
'keterangan' => 'nullable|string|max:500',
|
||||||
'tanggal_transaksi' => 'required|date',
|
'tanggal_transaksi' => 'required|date',
|
||||||
], [
|
|
||||||
'jenis_transaksi.required' => 'Jenis transaksi wajib dipilih.',
|
|
||||||
'nominal.required' => 'Nominal wajib diisi.',
|
|
||||||
'nominal.numeric' => 'Nominal harus berupa angka.',
|
|
||||||
'nominal.min' => 'Nominal minimal Rp 1.',
|
|
||||||
'tanggal_transaksi.required' => 'Tanggal transaksi wajib diisi.',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Simpan tanggal lama sebelum update, agar recalculate dimulai dari yang paling awal
|
||||||
|
$tanggalLama = $transaksi->tanggal_transaksi->format('Y-m-d');
|
||||||
|
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
try {
|
try {
|
||||||
$transaksi->update($validated);
|
// Gunakan saveQuietly agar model boot (updating) tidak ikut menghitung ulang saldo
|
||||||
|
// — recalculate akan dikerjakan secara menyeluruh oleh recalculateSaldoAfter()
|
||||||
|
$transaksi->fill($validated)->saveQuietly();
|
||||||
|
|
||||||
// Recalculate semua saldo setelah transaksi ini
|
// Recalculate dari tanggal yang paling awal antara tanggal lama dan baru
|
||||||
$this->recalculateSaldoAfter($transaksi->id_santri, $transaksi->tanggal_transaksi);
|
$tanggalBaru = $validated['tanggal_transaksi'];
|
||||||
|
$tanggalMulai = min($tanggalLama, $tanggalBaru);
|
||||||
|
|
||||||
|
$this->recalculateSaldoAfter($transaksi->id_santri, $tanggalMulai);
|
||||||
DB::commit();
|
DB::commit();
|
||||||
Cache::forget('santri_aktif_uang_saku');
|
Cache::forget('santri_aktif_uang_saku');
|
||||||
|
|
||||||
return redirect()->route('admin.uang-saku.index')
|
return redirect()->route('admin.uang-saku.index')
|
||||||
->with('success', 'Transaksi berhasil diperbarui.');
|
->with('success', 'Transaksi berhasil diperbarui.');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
return back()->withInput()
|
return back()->withInput()->with('error', 'Gagal memperbarui transaksi: ' . $e->getMessage());
|
||||||
->with('error', 'Gagal memperbarui transaksi: ' . $e->getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hapus transaksi
|
|
||||||
*/
|
|
||||||
public function destroy($id)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
$transaksi = UangSaku::findOrFail($id);
|
$transaksi = UangSaku::findOrFail($id);
|
||||||
$idSantri = $transaksi->id_santri;
|
$idSantri = $transaksi->id_santri;
|
||||||
$tanggal = $transaksi->tanggal_transaksi;
|
$tanggal = $transaksi->tanggal_transaksi->format('Y-m-d');
|
||||||
|
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
try {
|
try {
|
||||||
$transaksi->delete();
|
$transaksi->delete();
|
||||||
|
|
||||||
// Recalculate saldo setelah transaksi dihapus
|
|
||||||
$this->recalculateSaldoAfter($idSantri, $tanggal);
|
$this->recalculateSaldoAfter($idSantri, $tanggal);
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
Cache::forget('santri_aktif_uang_saku');
|
Cache::forget('santri_aktif_uang_saku');
|
||||||
|
|
||||||
return redirect()->route('admin.uang-saku.index')
|
return redirect()->route('admin.uang-saku.index')
|
||||||
->with('success', 'Transaksi berhasil dihapus.');
|
->with('success', 'Transaksi berhasil dihapus.');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
@ -264,35 +285,21 @@ public function destroy($id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tampilkan riwayat uang saku per santri dengan filter tanggal
|
|
||||||
*/
|
|
||||||
public function riwayat(Request $request, $id_santri)
|
public function riwayat(Request $request, $id_santri)
|
||||||
{
|
{
|
||||||
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
|
||||||
|
|
||||||
// Default: bulan ini
|
$tanggalDari = $request->filled('tanggal_dari') ? $request->tanggal_dari : now()->startOfMonth()->format('Y-m-d');
|
||||||
$tanggalDari = $request->filled('tanggal_dari')
|
$tanggalSampai = $request->filled('tanggal_sampai') ? $request->tanggal_sampai : now()->endOfMonth()->format('Y-m-d');
|
||||||
? $request->tanggal_dari
|
|
||||||
: now()->startOfMonth()->format('Y-m-d');
|
|
||||||
|
|
||||||
$tanggalSampai = $request->filled('tanggal_sampai')
|
$query = UangSaku::where('id_santri', $id_santri)
|
||||||
? $request->tanggal_sampai
|
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai]);
|
||||||
: now()->endOfMonth()->format('Y-m-d');
|
|
||||||
|
|
||||||
// Query transaksi dengan filter tanggal
|
|
||||||
$query = UangSaku::where('id_santri', $id_santri);
|
|
||||||
|
|
||||||
if ($tanggalDari && $tanggalSampai) {
|
|
||||||
$query->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$transaksi = $query->orderBy('tanggal_transaksi', 'desc')
|
$transaksi = $query->orderBy('tanggal_transaksi', 'desc')
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
->paginate(20)
|
->paginate(20)
|
||||||
->appends($request->query());
|
->appends($request->query());
|
||||||
|
|
||||||
// Statistik dengan filter tanggal
|
|
||||||
$totalPemasukan = UangSaku::where('id_santri', $id_santri)
|
$totalPemasukan = UangSaku::where('id_santri', $id_santri)
|
||||||
->where('jenis_transaksi', 'pemasukan')
|
->where('jenis_transaksi', 'pemasukan')
|
||||||
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
|
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
|
||||||
|
|
@ -303,82 +310,79 @@ public function riwayat(Request $request, $id_santri)
|
||||||
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
|
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
|
||||||
->sum('nominal');
|
->sum('nominal');
|
||||||
|
|
||||||
// Saldo terakhir tetap dari keseluruhan transaksi
|
// Ambil saldo aktual dari transaksi TERAKHIR santri ini (real-time, bukan dari filter)
|
||||||
$saldoTerakhir = $santri->saldo_uang_saku;
|
$lastTx = UangSaku::where('id_santri', $id_santri)
|
||||||
|
->orderByDesc('tanggal_transaksi')
|
||||||
|
->orderByDesc('created_at')
|
||||||
|
->first();
|
||||||
|
$saldoTerakhir = $lastTx ? (float)$lastTx->saldo_sesudah : 0;
|
||||||
|
|
||||||
// Data untuk grafik dengan filter tanggal
|
|
||||||
$dataGrafik = UangSaku::where('id_santri', $id_santri)
|
$dataGrafik = UangSaku::where('id_santri', $id_santri)
|
||||||
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
|
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
|
||||||
->select(
|
->select(
|
||||||
DB::raw('DATE(tanggal_transaksi) as tanggal'),
|
DB::raw('DATE(tanggal_transaksi) as tanggal'),
|
||||||
DB::raw('SUM(CASE WHEN jenis_transaksi = "pemasukan" THEN nominal ELSE 0 END) as pemasukan'),
|
DB::raw('SUM(CASE WHEN jenis_transaksi="pemasukan" THEN nominal ELSE 0 END) as pemasukan'),
|
||||||
DB::raw('SUM(CASE WHEN jenis_transaksi = "pengeluaran" THEN nominal ELSE 0 END) as pengeluaran')
|
DB::raw('SUM(CASE WHEN jenis_transaksi="pengeluaran" THEN nominal ELSE 0 END) as pengeluaran')
|
||||||
)
|
)
|
||||||
->groupBy('tanggal')
|
->groupBy('tanggal')
|
||||||
->orderBy('tanggal')
|
->orderBy('tanggal')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Jika tidak ada transaksi di rentang tanggal, buat data kosong
|
|
||||||
if ($dataGrafik->isEmpty()) {
|
if ($dataGrafik->isEmpty()) {
|
||||||
$dataGrafik = collect([
|
$dataGrafik = collect([(object)['tanggal' => $tanggalDari, 'pemasukan' => 0, 'pengeluaran' => 0]]);
|
||||||
(object)[
|
|
||||||
'tanggal' => $tanggalDari,
|
|
||||||
'pemasukan' => 0,
|
|
||||||
'pengeluaran' => 0
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info periode
|
$periodeDari = Carbon::parse($tanggalDari);
|
||||||
$periodeDari = \Carbon\Carbon::parse($tanggalDari);
|
$periodeSampai = Carbon::parse($tanggalSampai);
|
||||||
$periodeSampai = \Carbon\Carbon::parse($tanggalSampai);
|
|
||||||
|
|
||||||
return view('admin.uang-saku.riwayat', compact(
|
return view('admin.uang-saku.riwayat', compact(
|
||||||
'santri',
|
'santri', 'transaksi',
|
||||||
'transaksi',
|
'totalPemasukan', 'totalPengeluaran', 'saldoTerakhir',
|
||||||
'totalPemasukan',
|
'dataGrafik', 'tanggalDari', 'tanggalSampai',
|
||||||
'totalPengeluaran',
|
'periodeDari', 'periodeSampai'
|
||||||
'saldoTerakhir',
|
|
||||||
'dataGrafik',
|
|
||||||
'tanggalDari',
|
|
||||||
'tanggalSampai',
|
|
||||||
'periodeDari',
|
|
||||||
'periodeSampai'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper: Recalculate saldo untuk transaksi setelah tanggal tertentu
|
* Hitung ulang saldo_sebelum & saldo_sesudah untuk semua transaksi
|
||||||
|
* milik $idSantri yang tanggalnya >= $tanggal.
|
||||||
|
*
|
||||||
|
* Dipanggil setelah store / update / destroy agar urutan saldo
|
||||||
|
* tetap konsisten meski transaksi disisipkan di tengah.
|
||||||
*/
|
*/
|
||||||
private function recalculateSaldoAfter($idSantri, $tanggal)
|
private function recalculateSaldoAfter($idSantri, $tanggal)
|
||||||
{
|
{
|
||||||
|
// Pastikan format tanggal string (bukan Carbon object)
|
||||||
|
$tanggal = $tanggal instanceof \Carbon\Carbon
|
||||||
|
? $tanggal->format('Y-m-d')
|
||||||
|
: $tanggal;
|
||||||
|
|
||||||
$transaksiSetelah = UangSaku::where('id_santri', $idSantri)
|
$transaksiSetelah = UangSaku::where('id_santri', $idSantri)
|
||||||
->where('tanggal_transaksi', '>=', $tanggal)
|
->where('tanggal_transaksi', '>=', $tanggal)
|
||||||
->orderBy('tanggal_transaksi')
|
->orderBy('tanggal_transaksi')
|
||||||
->orderBy('created_at')
|
->orderBy('created_at')
|
||||||
|
->orderBy('id')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
foreach ($transaksiSetelah as $index => $trans) {
|
foreach ($transaksiSetelah as $index => $trans) {
|
||||||
if ($index === 0) {
|
if ($index === 0) {
|
||||||
// Transaksi pertama: ambil saldo dari transaksi sebelumnya
|
// Cari saldo_sesudah transaksi tepat sebelum batch ini
|
||||||
$saldoSebelumnya = UangSaku::where('id_santri', $idSantri)
|
$prev = UangSaku::where('id_santri', $idSantri)
|
||||||
->where('id', '<', $trans->id)
|
->where('tanggal_transaksi', '<', $tanggal)
|
||||||
->orderBy('tanggal_transaksi', 'desc')
|
->orderByDesc('tanggal_transaksi')
|
||||||
->orderBy('created_at', 'desc')
|
->orderByDesc('created_at')
|
||||||
|
->orderByDesc('id')
|
||||||
->first();
|
->first();
|
||||||
|
$trans->saldo_sebelum = $prev ? (float)$prev->saldo_sesudah : 0;
|
||||||
$trans->saldo_sebelum = $saldoSebelumnya ? $saldoSebelumnya->saldo_sesudah : 0;
|
|
||||||
} else {
|
} else {
|
||||||
$trans->saldo_sebelum = $transaksiSetelah[$index - 1]->saldo_sesudah;
|
$trans->saldo_sebelum = (float)$transaksiSetelah[$index - 1]->saldo_sesudah;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($trans->jenis_transaksi === 'pemasukan') {
|
$trans->saldo_sesudah = $trans->jenis_transaksi === 'pemasukan'
|
||||||
$trans->saldo_sesudah = $trans->saldo_sebelum + $trans->nominal;
|
? $trans->saldo_sebelum + (float)$trans->nominal
|
||||||
} else {
|
: $trans->saldo_sebelum - (float)$trans->nominal;
|
||||||
$trans->saldo_sesudah = $trans->saldo_sebelum - $trans->nominal;
|
|
||||||
}
|
|
||||||
|
|
||||||
$trans->saveQuietly(); // Save tanpa trigger event
|
$trans->saveQuietly();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,183 +6,363 @@
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
|
use App\Models\SantriAccount;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
|
// ══════════════════ AKUN SANTRI (WEB) ══════════════════
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan daftar akun Santri.
|
* Daftar akun santri
|
||||||
*/
|
*/
|
||||||
public function santriAccounts()
|
public function santriAccounts()
|
||||||
{
|
{
|
||||||
$users = User::where('role', 'santri')->with('santri')->get();
|
$users = SantriAccount::where('role', 'santri')->with('santri')->get();
|
||||||
$santris_tanpa_akun = Santri::whereDoesntHave('user', function($query) {
|
|
||||||
$query->where('role', 'santri');
|
$santris_tanpa_akun = Santri::whereDoesntHave('santriAccount', function ($q) {
|
||||||
|
$q->where('role', 'santri');
|
||||||
})->get();
|
})->get();
|
||||||
|
|
||||||
return view('admin.users.santri_accounts', compact('users', 'santris_tanpa_akun'));
|
return view('admin.users.santri_accounts', compact('users', 'santris_tanpa_akun'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan daftar akun Wali Santri.
|
* Buat akun santri untuk satu santri langsung (1 klik)
|
||||||
|
*/
|
||||||
|
public function buatAkunSantri(Request $request, string $idSantri)
|
||||||
|
{
|
||||||
|
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
|
||||||
|
|
||||||
|
if (!$santri->nis) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('error', 'Santri ' . $santri->nama_lengkap . ' belum memiliki NIS.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sudahAda = SantriAccount::where('role', 'santri')
|
||||||
|
->where('id_santri', $idSantri)->exists();
|
||||||
|
|
||||||
|
if ($sudahAda) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('error', 'Santri ' . $santri->nama_lengkap . ' sudah memiliki akun.');
|
||||||
|
}
|
||||||
|
|
||||||
|
SantriAccount::create([
|
||||||
|
'id_santri' => $santri->id_santri,
|
||||||
|
'username' => $santri->nama_lengkap,
|
||||||
|
'password' => Hash::make($santri->nis),
|
||||||
|
'role' => 'santri',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->with('success', 'Akun santri ' . $santri->nama_lengkap . ' berhasil dibuat. Username: ' . $santri->nama_lengkap . ' | Password: ' . $santri->nis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buat akun santri untuk semua santri yang belum punya akun (1 klik massal)
|
||||||
|
*/
|
||||||
|
public function buatSemuaAkunSantri(Request $request)
|
||||||
|
{
|
||||||
|
$santriList = Santri::whereDoesntHave('santriAccount', function ($q) {
|
||||||
|
$q->where('role', 'santri');
|
||||||
|
})->whereNotNull('nis')->get();
|
||||||
|
|
||||||
|
if ($santriList->isEmpty()) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('info', 'Semua santri sudah memiliki akun.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$berhasil = 0;
|
||||||
|
|
||||||
|
foreach ($santriList as $santri) {
|
||||||
|
SantriAccount::create([
|
||||||
|
'id_santri' => $santri->id_santri,
|
||||||
|
'username' => $santri->nama_lengkap,
|
||||||
|
'password' => Hash::make($santri->nis),
|
||||||
|
'role' => 'santri',
|
||||||
|
]);
|
||||||
|
$berhasil++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->with('success', $berhasil . ' akun santri berhasil dibuat sekaligus.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hapus akun santri
|
||||||
|
*/
|
||||||
|
public function destroySantriAccount(string $id)
|
||||||
|
{
|
||||||
|
$account = SantriAccount::where('role', 'santri')->findOrFail($id);
|
||||||
|
$nama = $account->santri ? $account->santri->nama_lengkap : $account->username;
|
||||||
|
$account->delete();
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->with('success', 'Akun santri ' . $nama . ' berhasil dihapus.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════ AKUN WALI (MOBILE) ══════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daftar akun wali
|
||||||
*/
|
*/
|
||||||
public function waliAccounts()
|
public function waliAccounts()
|
||||||
{
|
{
|
||||||
$users = User::where('role', 'wali')->with('santri')->get();
|
$users = SantriAccount::where('role', 'wali')->with('santri')->get();
|
||||||
|
|
||||||
$santris_tanpa_wali = Santri::whereDoesntHave('waliUser')->get();
|
$santris_tanpa_wali = Santri::whereDoesntHave('santriAccount', function ($q) {
|
||||||
|
$q->where('role', 'wali');
|
||||||
|
})->get();
|
||||||
|
|
||||||
return view('admin.users.wali_accounts', compact('users', 'santris_tanpa_wali'));
|
return view('admin.users.wali_accounts', compact('users', 'santris_tanpa_wali'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan form untuk membuat akun baru.
|
* Resolve username untuk akun wali.
|
||||||
|
*
|
||||||
|
* Aturan:
|
||||||
|
* - Default : nama_orang_tua (sama seperti sebelumnya, username = nama ortu)
|
||||||
|
* - Fallback : "nama_orang_tua - nama_santri"
|
||||||
|
* → hanya dipakai jika nama_orang_tua sudah dipakai
|
||||||
|
* akun wali lain (cek DB + array in-memory untuk proses massal).
|
||||||
|
*
|
||||||
|
* @param Santri $santri
|
||||||
|
* @param array $usernameYangSudahDipakai username yang sudah dibuat dalam iterasi massal saat ini
|
||||||
*/
|
*/
|
||||||
public function createAccount(string $role)
|
private function resolveUsernameWali(Santri $santri, array $usernameYangSudahDipakai = []): string
|
||||||
{
|
{
|
||||||
if (!in_array($role, ['santri', 'wali'])) {
|
$usernameDefault = $santri->nama_orang_tua;
|
||||||
abort(404);
|
|
||||||
|
// Cek di database: apakah nama ortu ini sudah jadi username wali lain?
|
||||||
|
$sudahDiDbOlehLain = SantriAccount::where('role', 'wali')
|
||||||
|
->where('username', $usernameDefault)
|
||||||
|
->where('id_santri', '!=', $santri->id_santri)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
// Cek di array in-memory (untuk proses massal dalam 1 request)
|
||||||
|
$sudahDiMemoriOlehLain = in_array($usernameDefault, $usernameYangSudahDipakai);
|
||||||
|
|
||||||
|
if ($sudahDiDbOlehLain || $sudahDiMemoriOlehLain) {
|
||||||
|
// Fallback: tambahkan nama santri agar unik
|
||||||
|
return $usernameDefault . ' - ' . $santri->nama_lengkap;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($role === 'santri') {
|
// Normal: cukup nama orang tua saja
|
||||||
$list_data = Santri::whereDoesntHave('user', function($query) {
|
return $usernameDefault;
|
||||||
$query->where('role', 'santri');
|
|
||||||
})->get();
|
|
||||||
} else {
|
|
||||||
// Wali: ambil santri yang belum punya akun wali
|
|
||||||
$list_data = Santri::whereDoesntHave('waliUser')->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('admin.users.create_account', compact('role', 'list_data'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simpan akun baru.
|
* Buat akun wali untuk satu santri langsung (1 klik)
|
||||||
*/
|
*/
|
||||||
public function storeAccount(Request $request, string $role)
|
public function buatAkunWali(Request $request, string $idSantri)
|
||||||
{
|
{
|
||||||
if (!in_array($role, ['santri', 'wali'])) {
|
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
|
||||||
abort(404);
|
|
||||||
|
if (!$santri->nis) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('error', 'Santri ' . $santri->nama_lengkap . ' belum memiliki NIS.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validasi berbeda untuk santri dan wali
|
if (!$santri->nama_orang_tua) {
|
||||||
$rules = [
|
return redirect()->back()
|
||||||
'role_id' => [
|
->with('error', 'Santri ' . $santri->nama_lengkap . ' belum memiliki data nama orang tua.');
|
||||||
'required',
|
|
||||||
Rule::exists('santris', 'id_santri'),
|
|
||||||
function ($attribute, $value, $fail) use ($role) {
|
|
||||||
$exists = User::where('role', $role)
|
|
||||||
->where('role_id', $value)
|
|
||||||
->exists();
|
|
||||||
if ($exists) {
|
|
||||||
$fail("Santri ini sudah memiliki akun {$role}.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'username' => 'required|string|max:255|unique:users,username',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Untuk wali: password tidak perlu min karena otomatis dari NIS
|
|
||||||
// Untuk santri: password minimal 8 karakter
|
|
||||||
if ($role === 'wali') {
|
|
||||||
$rules['password'] = 'required|string|confirmed';
|
|
||||||
} else {
|
|
||||||
$rules['password'] = 'required|string|min:8|confirmed';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$messages = [
|
$sudahAda = SantriAccount::where('role', 'wali')
|
||||||
'role_id.required' => 'Wajib memilih santri.',
|
->where('id_santri', $idSantri)->exists();
|
||||||
'role_id.exists' => 'Data santri tidak ditemukan.',
|
|
||||||
'username.unique' => 'Username sudah digunakan.',
|
|
||||||
'username.required' => 'Username wajib diisi.',
|
|
||||||
'password.required' => 'Password wajib diisi.',
|
|
||||||
'password.min' => 'Password minimal 8 karakter.',
|
|
||||||
'password.confirmed' => 'Konfirmasi password tidak cocok.',
|
|
||||||
];
|
|
||||||
|
|
||||||
$validated = $request->validate($rules, $messages);
|
if ($sudahAda) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('error', 'Wali santri ' . $santri->nama_lengkap . ' sudah memiliki akun.');
|
||||||
|
}
|
||||||
|
|
||||||
// Ambil data santri
|
$username = $this->resolveUsernameWali($santri);
|
||||||
$santri = Santri::where('id_santri', $validated['role_id'])->firstOrFail();
|
|
||||||
|
|
||||||
// Untuk wali: name = nama orang tua (jika ada) atau nama santri
|
SantriAccount::create([
|
||||||
// Untuk santri: name = nama santri
|
'id_santri' => $santri->id_santri,
|
||||||
$name = ($role === 'wali')
|
'username' => $username,
|
||||||
? ($santri->nama_orang_tua ?? $santri->nama_lengkap)
|
'password' => Hash::make($santri->nis),
|
||||||
: $santri->nama_lengkap;
|
'role' => 'wali',
|
||||||
|
|
||||||
// Simpan User
|
|
||||||
User::create([
|
|
||||||
'name' => $name,
|
|
||||||
'username' => $validated['username'],
|
|
||||||
'password' => Hash::make($validated['password']),
|
|
||||||
'role' => $role,
|
|
||||||
'role_id' => $validated['role_id'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$successMsg = $role === 'wali'
|
return redirect()->back()
|
||||||
? "Akun wali untuk santri {$santri->nama_lengkap} berhasil dibuat. Login: Username={$validated['username']}, Password=NIS"
|
->with('success', 'Akun wali untuk ' . $santri->nama_lengkap . ' berhasil dibuat. Username: ' . $username . ' | Password: ' . $santri->nis);
|
||||||
: "Akun santri {$santri->nama_lengkap} berhasil dibuat.";
|
|
||||||
|
|
||||||
return redirect()->route('admin.users.'.$role.'_accounts')
|
|
||||||
->with('success', $successMsg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hapus akun santri/wali.
|
* Buat akun wali untuk semua santri yang belum punya akun wali (1 klik massal)
|
||||||
*/
|
*/
|
||||||
public function destroyAccount(string $role, string $userId)
|
public function buatSemuaAkunWali(Request $request)
|
||||||
{
|
{
|
||||||
if (!in_array($role, ['santri', 'wali'])) {
|
$santriList = Santri::whereDoesntHave('santriAccount', function ($q) {
|
||||||
abort(404);
|
$q->where('role', 'wali');
|
||||||
|
})->whereNotNull('nis')->whereNotNull('nama_orang_tua')->get();
|
||||||
|
|
||||||
|
if ($santriList->isEmpty()) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('info', 'Semua santri sudah memiliki akun wali.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cari user berdasarkan ID
|
$berhasil = 0;
|
||||||
$user = User::findOrFail($userId);
|
$gagal = 0;
|
||||||
|
|
||||||
// Pastikan user yang akan dihapus adalah role yang sesuai
|
// Lacak username yg dibuat dalam iterasi ini agar
|
||||||
if ($user->role !== $role) {
|
// santri berikut dg nama ortu sama langsung dapat fallback
|
||||||
return redirect()->back()->with('error', 'Akun tidak valid.');
|
$usernameYangSudahDipakai = [];
|
||||||
|
|
||||||
|
foreach ($santriList as $santri) {
|
||||||
|
if (!$santri->nama_orang_tua) {
|
||||||
|
$gagal++;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$userName = $user->name;
|
$username = $this->resolveUsernameWali($santri, $usernameYangSudahDipakai);
|
||||||
$user->delete();
|
|
||||||
|
|
||||||
return redirect()->route('admin.users.'.$role.'_accounts')
|
SantriAccount::create([
|
||||||
->with('success', "Akun {$role} {$userName} berhasil dihapus.");
|
'id_santri' => $santri->id_santri,
|
||||||
|
'username' => $username,
|
||||||
|
'password' => Hash::make($santri->nis),
|
||||||
|
'role' => 'wali',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$usernameYangSudahDipakai[] = $username;
|
||||||
|
$berhasil++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pesan = $berhasil . ' akun wali berhasil dibuat.';
|
||||||
|
if ($gagal > 0) {
|
||||||
|
$pesan .= ' ' . $gagal . ' dilewati karena data orang tua tidak lengkap.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', $pesan);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset password akun santri/wali ke default (NIS).
|
* Hapus akun wali
|
||||||
*/
|
*/
|
||||||
public function resetPassword(string $role, string $userId)
|
public function destroyWaliAccount(string $id)
|
||||||
{
|
{
|
||||||
if (!in_array($role, ['santri', 'wali'])) {
|
$account = SantriAccount::where('role', 'wali')->findOrFail($id);
|
||||||
abort(404);
|
$nama = $account->santri ? $account->santri->nama_lengkap : $account->username;
|
||||||
|
$account->delete();
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->with('success', 'Akun wali ' . $nama . ' berhasil dihapus.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cari user berdasarkan ID
|
// ══════════════════ AKUN ADMIN ══════════════════
|
||||||
$user = User::findOrFail($userId);
|
|
||||||
|
|
||||||
// Pastikan user adalah role yang sesuai
|
/**
|
||||||
if ($user->role !== $role) {
|
* Daftar akun admin
|
||||||
return redirect()->back()->with('error', 'Akun tidak valid.');
|
*/
|
||||||
|
public function adminAccounts()
|
||||||
|
{
|
||||||
|
$admins = User::whereIn('role', ['super_admin', 'akademik', 'pamong'])
|
||||||
|
->orderByRaw("FIELD(role, 'super_admin', 'akademik', 'pamong')")
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.users.admin_accounts', compact('admins'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ambil santri terkait
|
/**
|
||||||
$santri = Santri::where('id_santri', $user->role_id)->first();
|
* Form buat akun admin baru
|
||||||
|
*/
|
||||||
if (!$santri || !$santri->nis) {
|
public function createAdminAccount()
|
||||||
return redirect()->back()->with('error', 'NIS santri tidak ditemukan. Tidak dapat mereset password.');
|
{
|
||||||
|
return view('admin.users.admin_form', [
|
||||||
|
'admin' => null,
|
||||||
|
'action' => route('admin.users.admin_store'),
|
||||||
|
'method' => 'POST',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset password ke NIS
|
/**
|
||||||
$user->password = Hash::make($santri->nis);
|
* Simpan akun admin baru
|
||||||
$user->save();
|
*/
|
||||||
|
public function storeAdminAccount(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|email|unique:users,email',
|
||||||
|
'role' => 'required|in:akademik,pamong',
|
||||||
|
'password' => 'required|string|min:8|confirmed',
|
||||||
|
], [
|
||||||
|
'name.required' => 'Nama wajib diisi.',
|
||||||
|
'email.required' => 'Email wajib diisi.',
|
||||||
|
'email.unique' => 'Email sudah digunakan.',
|
||||||
|
'role.required' => 'Role wajib dipilih.',
|
||||||
|
'password.required' => 'Password wajib diisi.',
|
||||||
|
'password.min' => 'Password minimal 8 karakter.',
|
||||||
|
'password.confirmed'=> 'Konfirmasi password tidak cocok.',
|
||||||
|
]);
|
||||||
|
|
||||||
return redirect()->route('admin.users.'.$role.'_accounts')
|
User::create([
|
||||||
->with('success', "Password akun {$user->name} berhasil direset ke NIS: {$santri->nis}");
|
'name' => $validated['name'],
|
||||||
|
'email' => $validated['email'],
|
||||||
|
'username' => $validated['email'],
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
'role' => $validated['role'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.users.admin_accounts')
|
||||||
|
->with('success', 'Akun ' . $validated['role'] . ' untuk ' . $validated['name'] . ' berhasil dibuat.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form edit akun admin
|
||||||
|
*/
|
||||||
|
public function editAdminAccount(string $userId)
|
||||||
|
{
|
||||||
|
$admin = User::whereIn('role', ['akademik', 'pamong'])->findOrFail($userId);
|
||||||
|
|
||||||
|
return view('admin.users.admin_form', [
|
||||||
|
'admin' => $admin,
|
||||||
|
'action' => route('admin.users.admin_update', $userId),
|
||||||
|
'method' => 'PUT',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update akun admin
|
||||||
|
*/
|
||||||
|
public function updateAdminAccount(Request $request, string $userId)
|
||||||
|
{
|
||||||
|
$admin = User::whereIn('role', ['akademik', 'pamong'])->findOrFail($userId);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|email|unique:users,email,' . $userId,
|
||||||
|
'role' => 'required|in:akademik,pamong',
|
||||||
|
'password' => 'nullable|string|min:8|confirmed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$admin->name = $validated['name'];
|
||||||
|
$admin->email = $validated['email'];
|
||||||
|
$admin->username = $validated['email'];
|
||||||
|
$admin->role = $validated['role'];
|
||||||
|
|
||||||
|
if (!empty($validated['password'])) {
|
||||||
|
$admin->password = Hash::make($validated['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$admin->save();
|
||||||
|
|
||||||
|
return redirect()->route('admin.users.admin_accounts')
|
||||||
|
->with('success', 'Akun ' . $admin->name . ' berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hapus akun admin
|
||||||
|
*/
|
||||||
|
public function destroyAdminAccount(string $userId)
|
||||||
|
{
|
||||||
|
$admin = User::whereIn('role', ['akademik', 'pamong'])->findOrFail($userId);
|
||||||
|
$nama = $admin->name;
|
||||||
|
$admin->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.users.admin_accounts')
|
||||||
|
->with('success', 'Akun ' . $nama . ' berhasil dihapus.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ public function today(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$idSantri = $user->role_id; // Santri atau wali punya role_id = id_santri
|
$idSantri = $user->id_santri; // id_santri dari santri_accounts
|
||||||
|
|
||||||
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
|
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
|
||||||
$selectedDate = Carbon::parse($tanggal);
|
$selectedDate = Carbon::parse($tanggal);
|
||||||
|
|
@ -120,7 +120,7 @@ public function week(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$idSantri = $user->role_id;
|
$idSantri = $user->id_santri;
|
||||||
|
|
||||||
$startDate = Carbon::now()->startOfWeek();
|
$startDate = Carbon::now()->startOfWeek();
|
||||||
$endDate = Carbon::now()->endOfWeek();
|
$endDate = Carbon::now()->endOfWeek();
|
||||||
|
|
@ -221,7 +221,7 @@ public function month(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$idSantri = $user->role_id;
|
$idSantri = $user->id_santri;
|
||||||
|
|
||||||
$bulan = $request->get('bulan', now()->format('Y-m'));
|
$bulan = $request->get('bulan', now()->format('Y-m'));
|
||||||
$date = Carbon::parse($bulan . '-01');
|
$date = Carbon::parse($bulan . '-01');
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\SantriAccount;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
@ -13,16 +13,16 @@
|
||||||
class ApiAuthController extends Controller
|
class ApiAuthController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Login Santri/Wali via Mobile
|
* Login Wali via Mobile (Sanctum token)
|
||||||
*
|
*
|
||||||
* Request:
|
* Request:
|
||||||
* - id_santri (username)
|
* - username
|
||||||
* - password
|
* - password
|
||||||
*
|
*
|
||||||
* Response:
|
* Response:
|
||||||
* - token
|
* - token
|
||||||
* - user (name, role, role_id)
|
* - user (role, id_santri)
|
||||||
* - santri (data lengkap santri jika role=santri)
|
* - santri (data lengkap)
|
||||||
*/
|
*/
|
||||||
public function login(Request $request)
|
public function login(Request $request)
|
||||||
{
|
{
|
||||||
|
|
@ -31,47 +31,39 @@ public function login(Request $request)
|
||||||
'password' => 'required|string',
|
'password' => 'required|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Cari user berdasarkan username (id_santri)
|
// -- Cari akun di santri_accounts --
|
||||||
$user = User::where('username', $request->id_santri)->first();
|
$account = SantriAccount::where('username', $request->id_santri)->first();
|
||||||
|
|
||||||
// Validasi user dan password
|
if (!$account || !Hash::check($request->password, $account->password)) {
|
||||||
if (!$user || !Hash::check($request->password, $user->password)) {
|
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'id_santri' => ['ID Santri atau password salah.'],
|
'id_santri' => ['ID Santri atau password salah.'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cek apakah user adalah santri atau wali
|
// -- Hapus token lama --
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
$account->tokens()->delete();
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Akun ini tidak memiliki akses ke aplikasi mobile.',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hapus token lama (optional, untuk keamanan)
|
// -- Buat token baru --
|
||||||
$user->tokens()->delete();
|
$token = $account->createToken('mobile-app')->plainTextToken;
|
||||||
|
|
||||||
// Buat token baru
|
// -- Update last_login --
|
||||||
$token = $user->createToken('mobile-app')->plainTextToken;
|
$account->update(['last_login' => now()]);
|
||||||
|
|
||||||
// Prepare response data
|
// -- Response data --
|
||||||
$responseData = [
|
$responseData = [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Login berhasil',
|
'message' => 'Login berhasil',
|
||||||
'token' => $token,
|
'token' => $token,
|
||||||
'user' => [
|
'user' => [
|
||||||
'name' => $user->name,
|
'name' => $account->santri->nama_lengkap ?? '-',
|
||||||
'role' => $user->role,
|
'role' => $account->role,
|
||||||
'role_id' => $user->role_id,
|
'role_id' => $account->id_santri,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Jika santri atau wali, sertakan data santri
|
// -- Sertakan data santri --
|
||||||
// Untuk wali, role_id menyimpan id_santri yang diwali (anaknya)
|
|
||||||
if (in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
$santri = Santri::with(['kelasSantri.kelas.kelompok', 'kelasPrimary.kelas'])
|
$santri = Santri::with(['kelasSantri.kelas.kelompok', 'kelasPrimary.kelas'])
|
||||||
->where('id_santri', $user->role_id)
|
->where('id_santri', $account->id_santri)
|
||||||
->select([
|
->select([
|
||||||
'id_santri',
|
'id_santri',
|
||||||
'nis',
|
'nis',
|
||||||
|
|
@ -87,10 +79,8 @@ public function login(Request $request)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($santri) {
|
if ($santri) {
|
||||||
// Build kelas_list grouped by kelompok
|
|
||||||
$kelasList = $this->buildKelasListGrouped($santri);
|
$kelasList = $this->buildKelasListGrouped($santri);
|
||||||
|
|
||||||
// Get primary kelas name for backward compatibility
|
|
||||||
$kelasName = 'Belum Ada Kelas';
|
$kelasName = 'Belum Ada Kelas';
|
||||||
if ($santri->kelasPrimary && $santri->kelasPrimary->kelas) {
|
if ($santri->kelasPrimary && $santri->kelasPrimary->kelas) {
|
||||||
$kelasName = $santri->kelasPrimary->kelas->nama_kelas;
|
$kelasName = $santri->kelasPrimary->kelas->nama_kelas;
|
||||||
|
|
@ -110,13 +100,12 @@ public function login(Request $request)
|
||||||
'nomor_hp_ortu' => $santri->nomor_hp_ortu,
|
'nomor_hp_ortu' => $santri->nomor_hp_ortu,
|
||||||
'foto' => $santri->foto,
|
'foto' => $santri->foto,
|
||||||
'foto_url' => $santri->foto_url,
|
'foto_url' => $santri->foto_url,
|
||||||
'kelas' => $kelasName, // Backward compatibility
|
'kelas' => $kelasName,
|
||||||
'kelas_list' => $kelasList, // NEW: Multiple kelas grouped
|
'kelas_list' => $kelasList,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
$responseData['santri'] = null;
|
$responseData['santri'] = null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($responseData, 200);
|
return response()->json($responseData, 200);
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +115,6 @@ public function login(Request $request)
|
||||||
*/
|
*/
|
||||||
public function logout(Request $request)
|
public function logout(Request $request)
|
||||||
{
|
{
|
||||||
// Hapus token yang sedang digunakan
|
|
||||||
$request->user()->currentAccessToken()->delete();
|
$request->user()->currentAccessToken()->delete();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
@ -137,24 +125,13 @@ public function logout(Request $request)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Profile Santri yang sedang login
|
* Get Profile Santri yang sedang login
|
||||||
* Untuk role santri: tampilkan data diri sendiri
|
|
||||||
* Untuk role wali: tampilkan data santri yang diwali (anaknya)
|
|
||||||
*/
|
*/
|
||||||
public function profile(Request $request)
|
public function profile(Request $request)
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$account = $request->user();
|
||||||
|
|
||||||
// Hanya santri dan wali yang bisa akses profil
|
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Hanya santri/wali yang bisa mengakses profil.',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Untuk santri dan wali, role_id menyimpan id_santri
|
|
||||||
$santri = Santri::with(['kelasSantri.kelas.kelompok', 'kelasPrimary.kelas'])
|
$santri = Santri::with(['kelasSantri.kelas.kelompok', 'kelasPrimary.kelas'])
|
||||||
->where('id_santri', $user->role_id)
|
->where('id_santri', $account->id_santri)
|
||||||
->select([
|
->select([
|
||||||
'id_santri',
|
'id_santri',
|
||||||
'nis',
|
'nis',
|
||||||
|
|
@ -177,10 +154,8 @@ public function profile(Request $request)
|
||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build kelas_list grouped by kelompok
|
|
||||||
$kelasList = $this->buildKelasListGrouped($santri);
|
$kelasList = $this->buildKelasListGrouped($santri);
|
||||||
|
|
||||||
// Get primary kelas name for backward compatibility
|
|
||||||
$kelasName = 'Belum Ada Kelas';
|
$kelasName = 'Belum Ada Kelas';
|
||||||
if ($santri->kelasPrimary && $santri->kelasPrimary->kelas) {
|
if ($santri->kelasPrimary && $santri->kelasPrimary->kelas) {
|
||||||
$kelasName = $santri->kelasPrimary->kelas->nama_kelas;
|
$kelasName = $santri->kelasPrimary->kelas->nama_kelas;
|
||||||
|
|
@ -200,10 +175,10 @@ public function profile(Request $request)
|
||||||
'daerah_asal' => $santri->daerah_asal,
|
'daerah_asal' => $santri->daerah_asal,
|
||||||
'nama_orang_tua' => $santri->nama_orang_tua,
|
'nama_orang_tua' => $santri->nama_orang_tua,
|
||||||
'nomor_hp_ortu' => $santri->nomor_hp_ortu,
|
'nomor_hp_ortu' => $santri->nomor_hp_ortu,
|
||||||
'foto_url' => $santri->foto_url, // Accessor dari Model Santri
|
'foto_url' => $santri->foto_url,
|
||||||
'bergabung_sejak' => $santri->created_at->format('d F Y'),
|
'bergabung_sejak' => $santri->created_at->format('d F Y'),
|
||||||
'kelas' => $kelasName, // Backward compatibility
|
'kelas' => $kelasName,
|
||||||
'kelas_list' => $kelasList, // NEW: Multiple kelas grouped
|
'kelas_list' => $kelasList,
|
||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class ApiBeritaController extends Controller
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
$santri = Santri::with('kelasPrimary.kelas')->where('id_santri', $idSantri)->first();
|
$santri = Santri::with('kelasPrimary.kelas')->where('id_santri', $idSantri)->first();
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ public function index(Request $request)
|
||||||
public function show(Request $request, $idBerita)
|
public function show(Request $request, $idBerita)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
$berita = Berita::where('id_berita', $idBerita)
|
$berita = Berita::where('id_berita', $idBerita)
|
||||||
->where('status', 'published')
|
->where('status', 'published')
|
||||||
|
|
|
||||||
|
|
@ -108,16 +108,7 @@ public function overview(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
$idSantri = $user->id_santri;
|
||||||
// Validasi role
|
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Akses ditolak. Role: ' . $user->role,
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$idSantri = $user->role_id;
|
|
||||||
|
|
||||||
$santri = Santri::with(['kelasPrimary.kelas.kelompok', 'kelasSantri.kelas.kelompok'])
|
$santri = Santri::with(['kelasPrimary.kelas.kelompok', 'kelasSantri.kelas.kelompok'])
|
||||||
->where('id_santri', $idSantri)
|
->where('id_santri', $idSantri)
|
||||||
|
|
@ -221,7 +212,7 @@ public function listMateriByKategori(Request $request, $kategori)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$idSantri = $user->role_id;
|
$idSantri = $user->id_santri;
|
||||||
|
|
||||||
$validKategori = ['Al-Qur\'an', 'Hadist', 'Materi Tambahan'];
|
$validKategori = ['Al-Qur\'an', 'Hadist', 'Materi Tambahan'];
|
||||||
if (!in_array($kategori, $validKategori)) {
|
if (!in_array($kategori, $validKategori)) {
|
||||||
|
|
@ -304,7 +295,7 @@ public function detailCapaian(Request $request, $idCapaian)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$idSantri = $user->role_id;
|
$idSantri = $user->id_santri;
|
||||||
|
|
||||||
$capaian = Capaian::where('id_capaian', $idCapaian)
|
$capaian = Capaian::where('id_capaian', $idCapaian)
|
||||||
->where('id_santri', $idSantri)
|
->where('id_santri', $idSantri)
|
||||||
|
|
@ -386,7 +377,7 @@ public function grafikProgress(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$idSantri = $user->role_id;
|
$idSantri = $user->id_santri;
|
||||||
|
|
||||||
$semesters = Semester::orderBy('tahun_ajaran')
|
$semesters = Semester::orderBy('tahun_ajaran')
|
||||||
->orderBy('periode')
|
->orderBy('periode')
|
||||||
|
|
@ -435,12 +426,7 @@ public function trendSemester(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
$idSantri = $user->id_santri;
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
return response()->json(['success' => false, 'message' => 'Akses ditolak'], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$idSantri = $user->role_id;
|
|
||||||
|
|
||||||
$santri = Santri::with(['kelasPrimary.kelas.kelompok'])->where('id_santri', $idSantri)->first();
|
$santri = Santri::with(['kelasPrimary.kelas.kelompok'])->where('id_santri', $idSantri)->first();
|
||||||
if (!$santri) {
|
if (!$santri) {
|
||||||
|
|
@ -514,12 +500,7 @@ public function dashboard(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
$idSantri = $user->id_santri;
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
return response()->json(['success' => false, 'message' => 'Akses ditolak'], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$idSantri = $user->role_id;
|
|
||||||
$santri = Santri::with(['kelasPrimary.kelas.kelompok', 'kelasSantri.kelas.kelompok'])
|
$santri = Santri::with(['kelasPrimary.kelas.kelompok', 'kelasSantri.kelas.kelompok'])
|
||||||
->where('id_santri', $idSantri)
|
->where('id_santri', $idSantri)
|
||||||
->first();
|
->first();
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,8 @@ public function index(Request $request)
|
||||||
try {
|
try {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Pastikan user adalah santri atau wali
|
// Ambil id_santri dari akun yang login
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
$idSantri = $user->id_santri;
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Akses ditolak. Hanya santri/wali yang dapat mengakses.',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ambil id_santri dari role_id (untuk santri dan wali, role_id = id_santri)
|
|
||||||
$idSantri = $user->role_id;
|
|
||||||
|
|
||||||
if (!$idSantri) {
|
if (!$idSantri) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
@ -70,7 +62,7 @@ public function index(Request $request)
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Data kepulangan berhasil diambil.',
|
'message' => 'Data kepulangan berhasil diambil.',
|
||||||
'data' => [
|
'data' => [
|
||||||
'kepulangan' => $kepulangan->map(function($item) {
|
'kepulangan' => collect($kepulangan->items())->map(function($item) {
|
||||||
return [
|
return [
|
||||||
'id_kepulangan' => $item->id_kepulangan,
|
'id_kepulangan' => $item->id_kepulangan,
|
||||||
'tanggal_izin' => $item->tanggal_izin->format('Y-m-d'),
|
'tanggal_izin' => $item->tanggal_izin->format('Y-m-d'),
|
||||||
|
|
@ -129,15 +121,7 @@ public function show($idKepulangan)
|
||||||
try {
|
try {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Pastikan user adalah santri atau wali
|
$idSantri = $user->id_santri;
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Akses ditolak.',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$idSantri = $user->role_id;
|
|
||||||
|
|
||||||
// Get kepulangan dengan validasi kepemilikan
|
// Get kepulangan dengan validasi kepemilikan
|
||||||
$kepulangan = Kepulangan::with('santri')
|
$kepulangan = Kepulangan::with('santri')
|
||||||
|
|
@ -220,7 +204,7 @@ public function kuota(Request $request)
|
||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$idSantri = $user->role_id;
|
$idSantri = $user->id_santri;
|
||||||
|
|
||||||
if (!$idSantri) {
|
if (!$idSantri) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
@ -275,4 +259,67 @@ public function kuota(Request $request)
|
||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifikasi status kepulangan santri saat ini
|
||||||
|
* GET /api/v1/kepulangan/notifikasi
|
||||||
|
*/
|
||||||
|
public function notifikasiKepulangan(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if (!in_array($user->role, ['santri', 'wali'])) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Akses ditolak.',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$idSantri = $user->id_santri;
|
||||||
|
$today = Carbon::today();
|
||||||
|
|
||||||
|
// Cari kepulangan yang sedang aktif (tanggal hari ini ada di antara tanggal_pulang dan tanggal_kembali)
|
||||||
|
$kepulangan = Kepulangan::where('id_santri', $idSantri)
|
||||||
|
->where('status', 'Disetujui')
|
||||||
|
->where('tanggal_pulang', '<=', $today)
|
||||||
|
->where('tanggal_kembali', '>=', $today)
|
||||||
|
->orderBy('tanggal_kembali', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$kepulangan) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'sedang_pulang' => false,
|
||||||
|
'tanggal_kembali' => null,
|
||||||
|
'sisa_hari' => 0,
|
||||||
|
'status' => null,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tanggalKembali = Carbon::parse($kepulangan->tanggal_kembali);
|
||||||
|
$sisaHari = $today->diffInDays($tanggalKembali, false); // negatif jika sudah lewat
|
||||||
|
$statusKepulangan = $sisaHari < 0 ? 'terlambat' : 'aktif';
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'sedang_pulang' => true,
|
||||||
|
'tanggal_kembali' => $tanggalKembali->format('Y-m-d'),
|
||||||
|
'tanggal_kembali_formatted' => $tanggalKembali->locale('id')->isoFormat('D MMMM Y'),
|
||||||
|
'sisa_hari' => (int) $sisaHari,
|
||||||
|
'status' => $statusKepulangan,
|
||||||
|
'alasan' => $kepulangan->alasan,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public function index(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// Ambil id_santri dari user yang login (wali)
|
// Ambil id_santri dari user yang login (wali)
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
// Cek santri exist
|
// Cek santri exist
|
||||||
$santri = Santri::where('id_santri', $idSantri)->first();
|
$santri = Santri::where('id_santri', $idSantri)->first();
|
||||||
|
|
@ -91,7 +91,7 @@ public function index(Request $request)
|
||||||
public function show(Request $request, $idKesehatan)
|
public function show(Request $request, $idKesehatan)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
// Cari data kesehatan
|
// Cari data kesehatan
|
||||||
$kesehatan = KesehatanSantri::where('id_kesehatan', $idKesehatan)
|
$kesehatan = KesehatanSantri::where('id_kesehatan', $idKesehatan)
|
||||||
|
|
@ -134,7 +134,7 @@ public function show(Request $request, $idKesehatan)
|
||||||
public function statistik(Request $request)
|
public function statistik(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
// Hitung total per status
|
// Hitung total per status
|
||||||
$totalDirawat = KesehatanSantri::where('id_santri', $idSantri)
|
$totalDirawat = KesehatanSantri::where('id_santri', $idSantri)
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,7 @@ public function store(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
$idSantri = $user->id_santri;
|
||||||
// Validasi role
|
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Akses ditolak.',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$idSantri = $user->role_id;
|
|
||||||
|
|
||||||
// Validasi input
|
// Validasi input
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
|
|
@ -98,15 +89,7 @@ public function index(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
$idSantri = $user->id_santri;
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Akses ditolak.',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$idSantri = $user->role_id;
|
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
$page = $request->input('page', 1);
|
$page = $request->input('page', 1);
|
||||||
|
|
@ -176,7 +159,7 @@ public function preview(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$idSantri = $user->role_id;
|
$idSantri = $user->id_santri;
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'tanggal_pulang' => 'required|date',
|
'tanggal_pulang' => 'required|date',
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class ApiSppController extends Controller
|
||||||
public function statusBulanIni(Request $request)
|
public function statusBulanIni(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
$bulanIni = date('n');
|
$bulanIni = date('n');
|
||||||
$tahunIni = date('Y');
|
$tahunIni = date('Y');
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ public function statusBulanIni(Request $request)
|
||||||
public function tunggakan(Request $request)
|
public function tunggakan(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
// Hitung tunggakan
|
// Hitung tunggakan
|
||||||
$tunggakanList = PembayaranSpp::where('id_santri', $idSantri)
|
$tunggakanList = PembayaranSpp::where('id_santri', $idSantri)
|
||||||
|
|
@ -104,7 +104,7 @@ public function tunggakan(Request $request)
|
||||||
public function riwayat(Request $request)
|
public function riwayat(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
// Query riwayat
|
// Query riwayat
|
||||||
$query = PembayaranSpp::where('id_santri', $idSantri)
|
$query = PembayaranSpp::where('id_santri', $idSantri)
|
||||||
|
|
@ -174,7 +174,7 @@ public function riwayat(Request $request)
|
||||||
public function statistik(Request $request)
|
public function statistik(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
$totalLunas = PembayaranSpp::where('id_santri', $idSantri)
|
$totalLunas = PembayaranSpp::where('id_santri', $idSantri)
|
||||||
->where('status', 'Lunas')
|
->where('status', 'Lunas')
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public function saldo(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// Ambil id_santri dari user yang login (wali)
|
// Ambil id_santri dari user yang login (wali)
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
// Ambil data santri
|
// Ambil data santri
|
||||||
$santri = Santri::where('id_santri', $idSantri)->first();
|
$santri = Santri::where('id_santri', $idSantri)->first();
|
||||||
|
|
@ -77,7 +77,7 @@ public function index(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// Ambil id_santri dari user yang login (wali)
|
// Ambil id_santri dari user yang login (wali)
|
||||||
$idSantri = $request->user()->role_id;
|
$idSantri = $request->user()->id_santri;
|
||||||
|
|
||||||
// Query transaksi uang saku
|
// Query transaksi uang saku
|
||||||
$query = UangSaku::where('id_santri', $idSantri)
|
$query = UangSaku::where('id_santri', $idSantri)
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ public function getRiwayatPelanggaran(Request $request)
|
||||||
try {
|
try {
|
||||||
// Ambil id_santri dari user yang login
|
// Ambil id_santri dari user yang login
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$idSantri = $user->role_id; // role_id menyimpan id_santri
|
$idSantri = $user->id_santri; // id_santri dari santri_accounts
|
||||||
|
|
||||||
// Query dengan pagination
|
// Query dengan pagination
|
||||||
$perPage = $request->input('per_page', 10);
|
$perPage = $request->input('per_page', 10);
|
||||||
|
|
@ -168,7 +168,7 @@ public function getStatistik(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$idSantri = $user->role_id;
|
$idSantri = $user->id_santri;
|
||||||
|
|
||||||
// Hanya hitung yang sudah dipublish
|
// Hanya hitung yang sudah dipublish
|
||||||
$totalPelanggaran = RiwayatPelanggaran::where('id_santri', $idSantri)
|
$totalPelanggaran = RiwayatPelanggaran::where('id_santri', $idSantri)
|
||||||
|
|
@ -221,7 +221,7 @@ public function getDetailRiwayat(Request $request, $idRiwayat)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$idSantri = $user->role_id;
|
$idSantri = $user->id_santri;
|
||||||
|
|
||||||
$riwayat = RiwayatPelanggaran::with([
|
$riwayat = RiwayatPelanggaran::with([
|
||||||
'kategori:id_kategori,nama_pelanggaran,poin,kafaroh,id_klasifikasi',
|
'kategori:id_kategori,nama_pelanggaran,poin,kafaroh,id_klasifikasi',
|
||||||
|
|
|
||||||
|
|
@ -33,21 +33,25 @@ public function authenticate(Request $request)
|
||||||
'password' => ['required', 'string'],
|
'password' => ['required', 'string'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Clear session lama sebelum login
|
// -- Coba login dengan username --
|
||||||
$request->session()->invalidate();
|
|
||||||
$request->session()->regenerateToken();
|
|
||||||
|
|
||||||
// Start session baru
|
|
||||||
$request->session()->start();
|
|
||||||
|
|
||||||
// Coba login dengan username DAN role harus 'admin'
|
|
||||||
if (Auth::attempt([
|
if (Auth::attempt([
|
||||||
'username' => $credentials['username'],
|
'username' => $credentials['username'],
|
||||||
'password' => $credentials['password'],
|
'password' => $credentials['password'],
|
||||||
'role' => 'admin'
|
|
||||||
], $request->boolean('remember'))) {
|
], $request->boolean('remember'))) {
|
||||||
|
|
||||||
// Regenerate session untuk keamanan
|
$user = Auth::user();
|
||||||
|
$adminRoles = ['super_admin', 'akademik', 'pamong'];
|
||||||
|
|
||||||
|
// -- Pastikan hanya role admin yang bisa login via form admin --
|
||||||
|
if (!in_array($user->role, $adminRoles)) {
|
||||||
|
Auth::logout();
|
||||||
|
$request->session()->invalidate();
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'username' => 'Akun ini bukan akun admin.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Regenerate session untuk keamanan --
|
||||||
$request->session()->regenerate();
|
$request->session()->regenerate();
|
||||||
|
|
||||||
return redirect()->intended(route('admin.dashboard'));
|
return redirect()->intended(route('admin.dashboard'));
|
||||||
|
|
@ -112,8 +116,8 @@ public function storeRegister(Request $request)
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
'name' => 'Administrator',
|
'name' => 'Administrator',
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
'username' => $request->email, // WAJIB: Gunakan email sebagai username untuk login
|
'username' => $request->email,
|
||||||
'role' => 'admin',
|
'role' => 'super_admin',
|
||||||
'password' => Hash::make($request->password),
|
'password' => Hash::make($request->password),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
<?php
|
||||||
|
// app/Http/Controllers/Auth/AdminForgotPasswordController.php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\PasswordResetOtp;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Mail\OtpMail;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
||||||
|
class AdminForgotPasswordController extends Controller
|
||||||
|
{
|
||||||
|
// ══════════════════ STEP 1 : FORM EMAIL ══════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tampilkan form input email
|
||||||
|
*/
|
||||||
|
public function showEmailForm()
|
||||||
|
{
|
||||||
|
return view('admin.auth.forgot_password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kirim OTP ke email super admin
|
||||||
|
*/
|
||||||
|
public function sendOtp(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => 'required|email',
|
||||||
|
], [
|
||||||
|
'email.required' => 'Email wajib diisi.',
|
||||||
|
'email.email' => 'Format email tidak valid.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cek apakah email terdaftar sebagai super_admin
|
||||||
|
$user = User::where('email', $request->email)
|
||||||
|
->where('role', 'super_admin')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'email' => 'Email tidak ditemukan atau bukan akun Super Admin.',
|
||||||
|
])->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus OTP lama untuk email ini
|
||||||
|
PasswordResetOtp::where('email', $request->email)->delete();
|
||||||
|
|
||||||
|
// Generate OTP 6 digit
|
||||||
|
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
// Simpan ke database dengan expired 10 menit
|
||||||
|
PasswordResetOtp::create([
|
||||||
|
'email' => $request->email,
|
||||||
|
'otp' => $otp,
|
||||||
|
'expired_at' => now()->addMinutes(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Kirim email OTP
|
||||||
|
Mail::to($request->email)->send(new OtpMail($otp, $user->name));
|
||||||
|
|
||||||
|
// Redirect ke form verifikasi OTP
|
||||||
|
return redirect()
|
||||||
|
->route('admin.forgot.verify_form', ['email' => $request->email])
|
||||||
|
->with('success', 'Kode OTP telah dikirim ke email Anda. Berlaku 10 menit.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════ STEP 2 : VERIFIKASI OTP ══════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tampilkan form input OTP
|
||||||
|
*/
|
||||||
|
public function showVerifyForm(Request $request)
|
||||||
|
{
|
||||||
|
$email = $request->query('email');
|
||||||
|
|
||||||
|
if (!$email) {
|
||||||
|
return redirect()->route('admin.forgot.email_form');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.auth.verify_otp', compact('email'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proses verifikasi OTP
|
||||||
|
*/
|
||||||
|
public function verifyOtp(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => 'required|email',
|
||||||
|
'otp' => 'required|string|size:6',
|
||||||
|
], [
|
||||||
|
'otp.required' => 'Kode OTP wajib diisi.',
|
||||||
|
'otp.size' => 'Kode OTP harus 6 digit.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$record = PasswordResetOtp::where('email', $request->email)
|
||||||
|
->where('otp', $request->otp)
|
||||||
|
->where('is_verified', false)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$record) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'otp' => 'Kode OTP tidak valid.',
|
||||||
|
])->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($record->isExpired()) {
|
||||||
|
$record->delete();
|
||||||
|
return back()->withErrors([
|
||||||
|
'otp' => 'Kode OTP sudah expired. Silakan kirim ulang.',
|
||||||
|
])->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tandai OTP sebagai terverifikasi
|
||||||
|
$record->update(['is_verified' => true]);
|
||||||
|
|
||||||
|
// Redirect ke form reset password
|
||||||
|
return redirect()
|
||||||
|
->route('admin.forgot.reset_form', ['email' => $request->email])
|
||||||
|
->with('success', 'Kode OTP valid. Silakan buat password baru.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kirim ulang OTP
|
||||||
|
*/
|
||||||
|
public function resendOtp(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => 'required|email',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::where('email', $request->email)
|
||||||
|
->where('role', 'super_admin')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'email' => 'Email tidak ditemukan.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus OTP lama
|
||||||
|
PasswordResetOtp::where('email', $request->email)->delete();
|
||||||
|
|
||||||
|
// Generate OTP baru
|
||||||
|
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
PasswordResetOtp::create([
|
||||||
|
'email' => $request->email,
|
||||||
|
'otp' => $otp,
|
||||||
|
'expired_at' => now()->addMinutes(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Mail::to($request->email)->send(new OtpMail($otp, $user->name));
|
||||||
|
|
||||||
|
return back()->with('success', 'Kode OTP baru telah dikirim ke email Anda.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════ STEP 3 : RESET PASSWORD ══════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tampilkan form reset password (hanya jika OTP sudah diverifikasi)
|
||||||
|
*/
|
||||||
|
public function showResetForm(Request $request)
|
||||||
|
{
|
||||||
|
$email = $request->query('email');
|
||||||
|
|
||||||
|
if (!$email) {
|
||||||
|
return redirect()->route('admin.forgot.email_form');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pastikan OTP sudah diverifikasi
|
||||||
|
$verified = PasswordResetOtp::where('email', $email)
|
||||||
|
->where('is_verified', true)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (!$verified) {
|
||||||
|
return redirect()->route('admin.forgot.email_form')
|
||||||
|
->withErrors(['email' => 'Silakan verifikasi OTP terlebih dahulu.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.auth.reset_password', compact('email'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proses reset password
|
||||||
|
*/
|
||||||
|
public function resetPassword(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => 'required|email',
|
||||||
|
'password' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'min:8',
|
||||||
|
'confirmed',
|
||||||
|
'regex:/[A-Z]/', // minimal 1 huruf besar
|
||||||
|
'regex:/[a-z]/', // minimal 1 huruf kecil
|
||||||
|
'regex:/[0-9]/', // minimal 1 angka
|
||||||
|
'regex:/[^A-Za-z0-9]/', // minimal 1 simbol
|
||||||
|
],
|
||||||
|
], [
|
||||||
|
'password.required' => 'Password baru wajib diisi.',
|
||||||
|
'password.min' => 'Password minimal 8 karakter.',
|
||||||
|
'password.confirmed' => 'Konfirmasi password tidak cocok.',
|
||||||
|
'password.regex' => 'Password harus mengandung huruf besar, huruf kecil, angka, dan simbol.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cek ulang apakah OTP sudah terverifikasi
|
||||||
|
$verified = PasswordResetOtp::where('email', $request->email)
|
||||||
|
->where('is_verified', true)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (!$verified) {
|
||||||
|
return redirect()->route('admin.forgot.email_form')
|
||||||
|
->withErrors(['email' => 'Sesi tidak valid. Silakan ulangi proses.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password user
|
||||||
|
$user = User::where('email', $request->email)
|
||||||
|
->where('role', 'super_admin')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return redirect()->route('admin.forgot.email_form')
|
||||||
|
->withErrors(['email' => 'Akun tidak ditemukan.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->password = Hash::make($request->password);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
// Hapus semua record OTP untuk email ini
|
||||||
|
PasswordResetOtp::where('email', $request->email)->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.login')
|
||||||
|
->with('success', 'Password berhasil diubah! Silakan login dengan password baru.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,24 @@
|
||||||
<?php
|
<?php
|
||||||
// app/Http/Controllers/Auth/SantriAuthController.php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class SantriAuthController extends Controller
|
class SantriAuthController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Tampilkan halaman login santri/wali
|
|
||||||
*/
|
|
||||||
public function login()
|
public function login()
|
||||||
{
|
{
|
||||||
|
if (Auth::guard('santri')->check()) {
|
||||||
|
return redirect()->route('santri.dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
return view('santri.auth.login');
|
return view('santri.auth.login');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Proses login santri/wali dengan auto-clear session on failed
|
|
||||||
*/
|
|
||||||
public function authenticate(Request $request)
|
public function authenticate(Request $request)
|
||||||
{
|
{
|
||||||
$credentials = $request->validate([
|
$credentials = $request->validate([
|
||||||
|
|
@ -31,63 +29,50 @@ public function authenticate(Request $request)
|
||||||
'password.required' => 'Password wajib diisi.',
|
'password.required' => 'Password wajib diisi.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ✅ TAMBAHAN 1: Clear old session data
|
$request->session()->forget(['login_attempts']);
|
||||||
$request->session()->forget(['login_attempts', 'last_attempt_time']);
|
|
||||||
|
|
||||||
// Coba login dengan guard default
|
if (Auth::guard('santri')->attempt($credentials, $request->boolean('remember'))) {
|
||||||
if (Auth::attempt($credentials, $request->boolean('remember'))) {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
// Cek apakah user adalah santri atau wali
|
|
||||||
if ($user->role === 'santri' || $user->role === 'wali') {
|
|
||||||
// ✅ TAMBAHAN 2: Regenerate & clear
|
|
||||||
$request->session()->regenerate();
|
|
||||||
$request->session()->forget(['login_attempts', 'last_attempt_time']);
|
|
||||||
|
|
||||||
return redirect()->intended(route('santri.dashboard'))
|
|
||||||
->with('success', 'Selamat datang, ' . $user->name . '!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ TAMBAHAN 3: Role tidak sesuai - clear session
|
|
||||||
Auth::logout();
|
|
||||||
$request->session()->invalidate();
|
|
||||||
$request->session()->regenerate();
|
$request->session()->regenerate();
|
||||||
|
|
||||||
return redirect()->back()->withErrors([
|
// Gunakan DB::table langsung — hindari masalah model cast/mutator
|
||||||
'username' => 'Akun Anda tidak memiliki akses ke halaman ini. Gunakan login Admin jika Anda admin.'
|
$account = Auth::guard('santri')->user();
|
||||||
])->withInput($request->except('password'));
|
|
||||||
|
DB::table('santri_accounts')
|
||||||
|
->where('id', $account->id)
|
||||||
|
->update(['last_login' => now()]);
|
||||||
|
|
||||||
|
$nama = $account->santri
|
||||||
|
? $account->santri->nama_lengkap
|
||||||
|
: $account->username;
|
||||||
|
|
||||||
|
return redirect()->route('santri.dashboard')
|
||||||
|
->with('success', 'Selamat datang, ' . $nama . '!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ TAMBAHAN 4: Track & auto-flush
|
|
||||||
$attempts = $request->session()->get('login_attempts', 0) + 1;
|
$attempts = $request->session()->get('login_attempts', 0) + 1;
|
||||||
$request->session()->put('login_attempts', $attempts);
|
$request->session()->put('login_attempts', $attempts);
|
||||||
$request->session()->put('last_attempt_time', now());
|
|
||||||
|
|
||||||
if ($attempts >= 3) {
|
if ($attempts >= 3) {
|
||||||
$request->session()->flush();
|
$request->session()->flush();
|
||||||
$request->session()->regenerate();
|
$request->session()->regenerate();
|
||||||
|
|
||||||
return redirect()->back()->withErrors([
|
return redirect()->back()->withErrors([
|
||||||
'username' => 'Terlalu banyak percobaan login gagal. Session telah direset. Silakan coba lagi.'
|
'username' => 'Terlalu banyak percobaan. Session direset, silakan coba lagi.',
|
||||||
])->withInput($request->except('password'));
|
])->withInput($request->except('password'));
|
||||||
}
|
}
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'username' => "Login gagal (Percobaan ke-{$attempts}/3). Username/Password salah atau akun tidak terdaftar.",
|
'username' => 'Login gagal (Percobaan ke-' . $attempts . '/3). Username atau password salah.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Logout santri/wali
|
|
||||||
*/
|
|
||||||
public function logout(Request $request)
|
public function logout(Request $request)
|
||||||
{
|
{
|
||||||
Auth::logout();
|
Auth::guard('santri')->logout();
|
||||||
|
|
||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
$request->session()->regenerateToken();
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
return redirect()->route('santri.login')
|
return redirect()->route('santri.login')
|
||||||
->with('success', 'Anda berhasil logout.');
|
->with('success', 'Berhasil logout.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
|
use App\Models\SantriKelas;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Kegiatan;
|
use App\Models\Kegiatan;
|
||||||
use App\Models\AbsensiKegiatan;
|
use App\Models\AbsensiKegiatan;
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
use App\Models\Kepulangan;
|
use App\Models\Kepulangan;
|
||||||
use App\Models\PengajuanKepulangan;
|
use App\Models\PengajuanKepulangan;
|
||||||
use App\Models\PembayaranSpp;
|
use App\Models\PembayaranSpp;
|
||||||
|
use App\Models\Keuangan; // ← TAMBAHAN: untuk data kas pondok
|
||||||
use App\Models\UangSaku;
|
use App\Models\UangSaku;
|
||||||
use App\Models\Capaian;
|
use App\Models\Capaian;
|
||||||
use App\Models\Semester;
|
use App\Models\Semester;
|
||||||
|
|
@ -48,16 +50,23 @@ public function admin()
|
||||||
$tahunIni = (int) $today->format('Y');
|
$tahunIni = (int) $today->format('Y');
|
||||||
|
|
||||||
// ────────────────────────── KPI CARDS ──────────────────────────
|
// ────────────────────────── KPI CARDS ──────────────────────────
|
||||||
$totalSantriAktif = Cache::remember('dash_santri_aktif', 300, fn () => Santri::aktif()->count());
|
$user = Auth::user();
|
||||||
|
$totalSantriAktif = Cache::remember('dash_santri_aktif', 300, function () {
|
||||||
|
return Santri::aktif()->count();
|
||||||
|
});
|
||||||
|
|
||||||
// Kegiatan hari ini + status absensi
|
// Kegiatan hari ini + status absensi
|
||||||
$kegiatanHariIni = Kegiatan::with(['kategori', 'absensis' => fn ($q) => $q->whereDate('tanggal', $today)])
|
$kegiatanHariIni = Kegiatan::with(['kategori', 'absensis' => function ($q) use ($today) {
|
||||||
|
$q->whereDate('tanggal', $today);
|
||||||
|
}])
|
||||||
->where('hari', $hariIni)
|
->where('hari', $hariIni)
|
||||||
->orderBy('waktu_mulai')
|
->orderBy('waktu_mulai')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$totalKegiatan = $kegiatanHariIni->count();
|
$totalKegiatan = $kegiatanHariIni->count();
|
||||||
$sudahAbsensi = $kegiatanHariIni->filter(fn ($k) => $k->absensis->isNotEmpty())->count();
|
$sudahAbsensi = $kegiatanHariIni->filter(function ($k) {
|
||||||
|
return $k->absensis->isNotEmpty();
|
||||||
|
})->count();
|
||||||
$belumAbsensi = $totalKegiatan - $sudahAbsensi;
|
$belumAbsensi = $totalKegiatan - $sudahAbsensi;
|
||||||
|
|
||||||
// Santri di UKP (sedang dirawat)
|
// Santri di UKP (sedang dirawat)
|
||||||
|
|
@ -66,10 +75,13 @@ public function admin()
|
||||||
// Pengajuan kepulangan menunggu approval
|
// Pengajuan kepulangan menunggu approval
|
||||||
$kepulanganMenunggu = PengajuanKepulangan::where('status', 'Menunggu')->count();
|
$kepulanganMenunggu = PengajuanKepulangan::where('status', 'Menunggu')->count();
|
||||||
|
|
||||||
// Santri aktif yang belum punya akun wali
|
// Santri aktif yang belum punya akun wali (super_admin only)
|
||||||
|
$santriTanpaWali = 0;
|
||||||
|
if ($user->role === 'super_admin') {
|
||||||
$santriTanpaWali = Santri::aktif()
|
$santriTanpaWali = Santri::aktif()
|
||||||
->whereDoesntHave('waliUser')
|
->whereDoesntHave('waliUser')
|
||||||
->count();
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
$kpiCards = compact(
|
$kpiCards = compact(
|
||||||
'totalSantriAktif', 'totalKegiatan', 'sudahAbsensi',
|
'totalSantriAktif', 'totalKegiatan', 'sudahAbsensi',
|
||||||
|
|
@ -95,16 +107,19 @@ public function admin()
|
||||||
});
|
});
|
||||||
|
|
||||||
// ────────────────────────── ALERT PANEL ──────────────────────────
|
// ────────────────────────── ALERT PANEL ──────────────────────────
|
||||||
// 1) Santri alpa beruntun (≥3 hari berturut-turut dalam 7 hari terakhir)
|
// 1) Santri alpa beruntun (semua role bisa lihat)
|
||||||
$santriAlpaBeruntun = $this->getSantriAlpaBeruntun();
|
$santriAlpaBeruntun = $this->getSantriAlpaBeruntun();
|
||||||
|
|
||||||
// 2) SPP jatuh tempo (belum lunas & batas_bayar sudah lewat)
|
// 2) SPP jatuh tempo (super_admin only)
|
||||||
|
$sppJatuhTempo = collect([]);
|
||||||
|
if ($user->role === 'super_admin') {
|
||||||
$sppJatuhTempo = PembayaranSpp::telat()
|
$sppJatuhTempo = PembayaranSpp::telat()
|
||||||
->with('santri:id_santri,nama_lengkap')
|
->with('santri:id_santri,nama_lengkap')
|
||||||
->select('id_pembayaran', 'id_santri', 'bulan', 'tahun', 'nominal', 'batas_bayar')
|
->select('id_pembayaran', 'id_santri', 'bulan', 'tahun', 'nominal', 'batas_bayar')
|
||||||
->orderBy('batas_bayar')
|
->orderBy('batas_bayar')
|
||||||
->limit(10)
|
->limit(10)
|
||||||
->get();
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
// 3) Pengajuan kepulangan menunggu review
|
// 3) Pengajuan kepulangan menunggu review
|
||||||
$kepulanganPending = PengajuanKepulangan::where('status', 'Menunggu')
|
$kepulanganPending = PengajuanKepulangan::where('status', 'Menunggu')
|
||||||
|
|
@ -119,22 +134,45 @@ public function admin()
|
||||||
// ──────────────── GRAFIK TREN KEHADIRAN (4 MINGGU) ────────────────
|
// ──────────────── GRAFIK TREN KEHADIRAN (4 MINGGU) ────────────────
|
||||||
$trenKehadiran = $this->getTrenKehadiran($today);
|
$trenKehadiran = $this->getTrenKehadiran($today);
|
||||||
|
|
||||||
// ──────────────── RINGKASAN SPP BULAN INI ────────────────
|
// ──────────────── RINGKASAN SPP + KEUANGAN BULAN INI ─────────────
|
||||||
$sppBulanIni = Cache::remember("dash_spp_{$bulanIni}_{$tahunIni}", 300, function () use ($bulanIni, $tahunIni) {
|
// Default (untuk non super_admin atau jika query gagal)
|
||||||
|
$sppBulanIni = [
|
||||||
|
'lunas' => 0,
|
||||||
|
'belum' => 0,
|
||||||
|
'terkumpul' => 0,
|
||||||
|
'totalTagihan' => 0,
|
||||||
|
'pemasukanLain' => 0, // pemasukan kas pondok selain SPP
|
||||||
|
'pengeluaran' => 0, // pengeluaran kas pondok
|
||||||
|
];
|
||||||
|
|
||||||
|
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();
|
$lunas = PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->lunas()->count();
|
||||||
$belum = PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->belumLunas()->count();
|
$belum = PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->belumLunas()->count();
|
||||||
$terkumpul = PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->lunas()->sum('nominal');
|
$terkumpul = (float) PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->lunas()->sum('nominal');
|
||||||
$totalTagihan = PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->sum('nominal');
|
$totalTagihan = (float) PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->sum('nominal');
|
||||||
|
|
||||||
return compact('lunas', 'belum', 'terkumpul', 'totalTagihan');
|
// ── Data Keuangan Pondok (non-SPP) ──
|
||||||
|
$pemasukanLain = (float) Keuangan::pemasukan()
|
||||||
|
->whereMonth('tanggal', $bulanIni)
|
||||||
|
->whereYear('tanggal', $tahunIni)
|
||||||
|
->sum('nominal');
|
||||||
|
|
||||||
|
$pengeluaran = (float) Keuangan::pengeluaran()
|
||||||
|
->whereMonth('tanggal', $bulanIni)
|
||||||
|
->whereYear('tanggal', $tahunIni)
|
||||||
|
->sum('nominal');
|
||||||
|
|
||||||
|
return compact('lunas', 'belum', 'terkumpul', 'totalTagihan', 'pemasukanLain', 'pengeluaran');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
// ──────────────── FEED AKTIVITAS TERBARU ────────────────
|
|
||||||
$feedAktivitas = $this->getFeedAktivitas($today);
|
|
||||||
|
|
||||||
return view('admin.dashboardAdmin', compact(
|
return view('admin.dashboardAdmin', compact(
|
||||||
'kpiCards', 'kegiatanHariIni', 'alerts',
|
'kpiCards', 'kegiatanHariIni', 'alerts',
|
||||||
'trenKehadiran', 'sppBulanIni', 'feedAktivitas',
|
'trenKehadiran', 'sppBulanIni',
|
||||||
'hariIni', 'today'
|
'hariIni', 'today'
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
@ -156,7 +194,6 @@ private function getSantriAlpaBeruntun(int $threshold = 3): \Illuminate\Support\
|
||||||
{
|
{
|
||||||
$weekAgo = Carbon::today()->subDays(7);
|
$weekAgo = Carbon::today()->subDays(7);
|
||||||
|
|
||||||
// Ambil data alpa per santri 7 hari terakhir
|
|
||||||
$alpaData = AbsensiKegiatan::where('status', 'Alpa')
|
$alpaData = AbsensiKegiatan::where('status', 'Alpa')
|
||||||
->whereDate('tanggal', '>=', $weekAgo)
|
->whereDate('tanggal', '>=', $weekAgo)
|
||||||
->select('id_santri')
|
->select('id_santri')
|
||||||
|
|
@ -190,7 +227,6 @@ private function getTrenKehadiran(Carbon $today): array
|
||||||
|
|
||||||
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
||||||
|
|
||||||
// 4 minggu terakhir → label "Mg 1" s.d "Mg 4"
|
|
||||||
for ($i = 3; $i >= 0; $i--) {
|
for ($i = 3; $i >= 0; $i--) {
|
||||||
$start = $today->copy()->subWeeks($i)->startOfWeek(Carbon::MONDAY);
|
$start = $today->copy()->subWeeks($i)->startOfWeek(Carbon::MONDAY);
|
||||||
$end = $start->copy()->endOfWeek(Carbon::SUNDAY);
|
$end = $start->copy()->endOfWeek(Carbon::SUNDAY);
|
||||||
|
|
@ -216,13 +252,12 @@ private function getTrenKehadiran(Carbon $today): array
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feed aktivitas terbaru: absensi, pelanggaran, pembayaran SPP, transaksi uang saku
|
* Feed aktivitas terbaru
|
||||||
*/
|
*/
|
||||||
private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
|
private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
|
||||||
{
|
{
|
||||||
$items = collect();
|
$items = collect();
|
||||||
|
|
||||||
// Absensi terbaru
|
|
||||||
AbsensiKegiatan::with(['santri:id_santri,nama_lengkap', 'kegiatan:kegiatan_id,nama_kegiatan'])
|
AbsensiKegiatan::with(['santri:id_santri,nama_lengkap', 'kegiatan:kegiatan_id,nama_kegiatan'])
|
||||||
->whereDate('tanggal', $today)
|
->whereDate('tanggal', $today)
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
|
|
@ -235,7 +270,6 @@ private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
|
||||||
'time' => $a->created_at,
|
'time' => $a->created_at,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// Pelanggaran terbaru (7 hari)
|
|
||||||
RiwayatPelanggaran::with(['santri:id_santri,nama_lengkap', 'kategori:id_kategori,nama_pelanggaran'])
|
RiwayatPelanggaran::with(['santri:id_santri,nama_lengkap', 'kategori:id_kategori,nama_pelanggaran'])
|
||||||
->whereDate('tanggal', '>=', $today->copy()->subDays(7))
|
->whereDate('tanggal', '>=', $today->copy()->subDays(7))
|
||||||
->terbaru()
|
->terbaru()
|
||||||
|
|
@ -248,7 +282,6 @@ private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
|
||||||
'time' => $p->created_at,
|
'time' => $p->created_at,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// Pembayaran SPP terbaru (7 hari)
|
|
||||||
PembayaranSpp::with('santri:id_santri,nama_lengkap')
|
PembayaranSpp::with('santri:id_santri,nama_lengkap')
|
||||||
->lunas()
|
->lunas()
|
||||||
->whereNotNull('tanggal_bayar')
|
->whereNotNull('tanggal_bayar')
|
||||||
|
|
@ -267,41 +300,38 @@ private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dashboard Santri/Wali - FIXED VERSION ✅
|
* Dashboard Santri
|
||||||
*/
|
*/
|
||||||
public function santri()
|
public function santri()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = Auth::user();
|
$account = auth('santri')->user();
|
||||||
|
|
||||||
Log::info('=== DASHBOARD SANTRI START ===');
|
Log::info('=== DASHBOARD SANTRI START ===');
|
||||||
Log::info('User ID: ' . $user->id);
|
Log::info('Account ID: ' . $account->id);
|
||||||
Log::info('Role: ' . $user->role);
|
Log::info('Role: ' . $account->role);
|
||||||
Log::info('Role ID: ' . $user->role_id);
|
Log::info('ID Santri: ' . $account->id_santri);
|
||||||
|
|
||||||
// Validasi role
|
$santri = Santri::with([
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
'kelasPrimary.kelas.kelompok',
|
||||||
Log::error('Role tidak sesuai: ' . $user->role);
|
])
|
||||||
abort(403, 'Akses ditolak. Role Anda: ' . $user->role);
|
->where('id_santri', $account->id_santri)
|
||||||
}
|
->select('id_santri', 'nama_lengkap')
|
||||||
|
|
||||||
// ✅ Ambil data santri
|
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
|
||||||
->select('id_santri', 'nama_lengkap', 'kelas')
|
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if (!$santri) {
|
if (!$santri) {
|
||||||
Log::error('Santri tidak ditemukan dengan role_id: ' . $user->role_id);
|
Log::error('Santri tidak ditemukan dengan id_santri: ' . $account->id_santri);
|
||||||
abort(404, 'Data santri tidak ditemukan.');
|
abort(404, 'Data santri tidak ditemukan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::info('Santri ditemukan: ' . $santri->nama_lengkap);
|
Log::info('Santri ditemukan: ' . $santri->nama_lengkap);
|
||||||
|
|
||||||
|
$namaKelas = $santri->kelas;
|
||||||
$idSantri = $santri->id_santri;
|
$idSantri = $santri->id_santri;
|
||||||
$today = Carbon::today();
|
$today = Carbon::today();
|
||||||
$weekAgo = Carbon::now()->subDays(7);
|
$weekAgo = Carbon::now()->subDays(7);
|
||||||
|
|
||||||
// ✅ Ambil semester aktif dengan FALLBACK
|
// Ambil semester aktif dengan FALLBACK
|
||||||
$semesterAktif = null;
|
$semesterAktif = null;
|
||||||
try {
|
try {
|
||||||
$semesterAktif = Semester::aktif()
|
$semesterAktif = Semester::aktif()
|
||||||
|
|
@ -321,67 +351,58 @@ public function santri()
|
||||||
$semesterAktif = null;
|
$semesterAktif = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ AMBIL PROGRES AL-QUR'AN dengan FALLBACK
|
// Progres Al-Qur'an
|
||||||
$progresAlquran = 0;
|
$progresAlquran = 0;
|
||||||
try {
|
try {
|
||||||
$query = Capaian::where('id_santri', $idSantri);
|
$query = Capaian::where('id_santri', $idSantri);
|
||||||
|
|
||||||
if ($semesterAktif) {
|
if ($semesterAktif) {
|
||||||
$query->where('id_semester', $semesterAktif->id_semester);
|
$query->where('id_semester', $semesterAktif->id_semester);
|
||||||
}
|
}
|
||||||
|
$progresAlquran = $query->whereHas('materi', function ($q) {
|
||||||
$progresAlquran = $query->whereHas('materi', function($q) {
|
|
||||||
$q->where('kategori', 'Al-Qur\'an');
|
$q->where('kategori', 'Al-Qur\'an');
|
||||||
})->avg('persentase') ?? 0;
|
})->avg('persentase') ?? 0;
|
||||||
|
|
||||||
Log::info('Progres Al-Quran: ' . $progresAlquran);
|
Log::info('Progres Al-Quran: ' . $progresAlquran);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::warning('Error progres Al-Quran: ' . $e->getMessage());
|
Log::warning('Error progres Al-Quran: ' . $e->getMessage());
|
||||||
$progresAlquran = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ AMBIL PROGRES HADIST dengan FALLBACK
|
// Progres Hadist
|
||||||
$progresHadist = 0;
|
$progresHadist = 0;
|
||||||
try {
|
try {
|
||||||
$query = Capaian::where('id_santri', $idSantri);
|
$query = Capaian::where('id_santri', $idSantri);
|
||||||
|
|
||||||
if ($semesterAktif) {
|
if ($semesterAktif) {
|
||||||
$query->where('id_semester', $semesterAktif->id_semester);
|
$query->where('id_semester', $semesterAktif->id_semester);
|
||||||
}
|
}
|
||||||
|
$progresHadist = $query->whereHas('materi', function ($q) {
|
||||||
$progresHadist = $query->whereHas('materi', function($q) {
|
|
||||||
$q->where('kategori', 'Hadist');
|
$q->where('kategori', 'Hadist');
|
||||||
})->avg('persentase') ?? 0;
|
})->avg('persentase') ?? 0;
|
||||||
|
|
||||||
Log::info('Progres Hadist: ' . $progresHadist);
|
Log::info('Progres Hadist: ' . $progresHadist);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::warning('Error progres Hadist: ' . $e->getMessage());
|
Log::warning('Error progres Hadist: ' . $e->getMessage());
|
||||||
$progresHadist = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ AMBIL PROGRES MATERI TAMBAHAN dengan FALLBACK
|
// Progres Materi Tambahan
|
||||||
$progresMateriTambahan = 0;
|
$progresMateriTambahan = 0;
|
||||||
try {
|
try {
|
||||||
$query = Capaian::where('id_santri', $idSantri);
|
$query = Capaian::where('id_santri', $idSantri);
|
||||||
|
|
||||||
if ($semesterAktif) {
|
if ($semesterAktif) {
|
||||||
$query->where('id_semester', $semesterAktif->id_semester);
|
$query->where('id_semester', $semesterAktif->id_semester);
|
||||||
}
|
}
|
||||||
|
$progresMateriTambahan = $query->whereHas('materi', function ($q) {
|
||||||
$progresMateriTambahan = $query->whereHas('materi', function($q) {
|
|
||||||
$q->where('kategori', 'Materi Tambahan');
|
$q->where('kategori', 'Materi Tambahan');
|
||||||
})->avg('persentase') ?? 0;
|
})->avg('persentase') ?? 0;
|
||||||
|
|
||||||
Log::info('Progres Materi Tambahan: ' . $progresMateriTambahan);
|
Log::info('Progres Materi Tambahan: ' . $progresMateriTambahan);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::warning('Error progres Materi Tambahan: ' . $e->getMessage());
|
Log::warning('Error progres Materi Tambahan: ' . $e->getMessage());
|
||||||
$progresMateriTambahan = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ DATA UNTUK GRAFIK 1: Progress per Materi dengan FALLBACK
|
// Data untuk grafik: Progress per Materi
|
||||||
$capaianPerMateri = collect([]);
|
$capaianPerMateri = collect([]);
|
||||||
try {
|
try {
|
||||||
$query = Capaian::with(['materi' => function($q) {
|
$query = Capaian::with(['materi' => function ($q) {
|
||||||
$q->select('id_materi', 'nama_kitab', 'kategori', 'total_halaman');
|
$q->select('id_materi', 'nama_kitab', 'kategori', 'total_halaman');
|
||||||
}])
|
}])
|
||||||
->where('id_santri', $idSantri);
|
->where('id_santri', $idSantri);
|
||||||
|
|
@ -401,17 +422,15 @@ public function santri()
|
||||||
$capaianPerMateri = collect([]);
|
$capaianPerMateri = collect([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ DATA UNTUK GRAFIK 2: Distribusi Status dengan FALLBACK
|
// Data untuk grafik: Distribusi Status
|
||||||
$distribusiStatus = [
|
$distribusiStatus = [
|
||||||
'selesai' => 0,
|
'selesai' => 0,
|
||||||
'hampir_selesai' => 0,
|
'hampir_selesai' => 0,
|
||||||
'sedang_berjalan' => 0,
|
'sedang_berjalan' => 0,
|
||||||
'baru_dimulai' => 0,
|
'baru_dimulai' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$baseQuery = Capaian::where('id_santri', $idSantri);
|
$baseQuery = Capaian::where('id_santri', $idSantri);
|
||||||
|
|
||||||
if ($semesterAktif) {
|
if ($semesterAktif) {
|
||||||
$baseQuery->where('id_semester', $semesterAktif->id_semester);
|
$baseQuery->where('id_semester', $semesterAktif->id_semester);
|
||||||
}
|
}
|
||||||
|
|
@ -428,22 +447,19 @@ public function santri()
|
||||||
Log::warning('Error distribusi status: ' . $e->getMessage());
|
Log::warning('Error distribusi status: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Data dashboard utama
|
|
||||||
$data = [
|
$data = [
|
||||||
'nama_santri' => $santri->nama_lengkap,
|
'nama_santri' => $santri->nama_lengkap,
|
||||||
'kelas' => $santri->kelas,
|
'kelas' => $namaKelas,
|
||||||
'progres_quran' => round($progresAlquran, 1),
|
'progres_quran' => round($progresAlquran, 1),
|
||||||
'progres_hadist' => round($progresHadist, 1),
|
'progres_hadist' => round($progresHadist, 1),
|
||||||
'progres_materi_tambahan' => round($progresMateriTambahan, 1),
|
'progres_materi_tambahan' => round($progresMateriTambahan, 1),
|
||||||
'saldo_uang_saku' => method_exists($santri, 'getSaldoUangSakuAttribute')
|
'saldo_uang_saku' => $santri->saldo_uang_saku ?? 0,
|
||||||
? $santri->saldo_uang_saku
|
|
||||||
: 0,
|
|
||||||
'poin_pelanggaran' => RiwayatPelanggaran::where('id_santri', $idSantri)->sum('poin') ?? 0,
|
'poin_pelanggaran' => RiwayatPelanggaran::where('id_santri', $idSantri)->sum('poin') ?? 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
Log::info('Data array: ' . json_encode($data));
|
Log::info('Data array: ' . json_encode($data));
|
||||||
|
|
||||||
// ✅ Query status kesehatan dengan FALLBACK
|
// Status kesehatan
|
||||||
$statusKesehatan = null;
|
$statusKesehatan = null;
|
||||||
try {
|
try {
|
||||||
$statusKesehatan = KesehatanSantri::where('id_santri', $idSantri)
|
$statusKesehatan = KesehatanSantri::where('id_santri', $idSantri)
|
||||||
|
|
@ -455,7 +471,7 @@ public function santri()
|
||||||
Log::warning('Error status kesehatan: ' . $e->getMessage());
|
Log::warning('Error status kesehatan: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Query kepulangan aktif dengan FALLBACK
|
// Kepulangan aktif
|
||||||
$kepulanganAktif = null;
|
$kepulanganAktif = null;
|
||||||
try {
|
try {
|
||||||
$kepulanganAktif = Kepulangan::where('id_santri', $idSantri)
|
$kepulanganAktif = Kepulangan::where('id_santri', $idSantri)
|
||||||
|
|
@ -468,17 +484,17 @@ public function santri()
|
||||||
Log::warning('Error kepulangan aktif: ' . $e->getMessage());
|
Log::warning('Error kepulangan aktif: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Query berita terbaru dengan FALLBACK
|
// Berita terbaru
|
||||||
$beritaTerbaru = collect([]);
|
$beritaTerbaru = collect([]);
|
||||||
try {
|
try {
|
||||||
$beritaTerbaru = Berita::select('id_berita', 'judul', 'created_at')
|
$beritaTerbaru = Berita::select('id_berita', 'judul', 'created_at')
|
||||||
->where('status', 'published')
|
->where('status', 'published')
|
||||||
->where('created_at', '>=', $weekAgo)
|
->where('created_at', '>=', $weekAgo)
|
||||||
->where(function($query) use ($santri) {
|
->where(function ($query) use ($namaKelas) {
|
||||||
$query->where('target_berita', 'semua')
|
$query->where('target_berita', 'semua')
|
||||||
->orWhere(function($q) use ($santri) {
|
->orWhere(function ($q) use ($namaKelas) {
|
||||||
$q->where('target_berita', 'kelas_tertentu')
|
$q->where('target_berita', 'kelas_tertentu')
|
||||||
->whereJsonContains('target_kelas', $santri->kelas);
|
->whereJsonContains('target_kelas', $namaKelas);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
|
|
@ -493,11 +509,10 @@ public function santri()
|
||||||
|
|
||||||
Log::info('=== DASHBOARD SANTRI SUCCESS ===');
|
Log::info('=== DASHBOARD SANTRI SUCCESS ===');
|
||||||
|
|
||||||
// Return view dengan semua data
|
|
||||||
return view('santri.dashboardSantri', compact(
|
return view('santri.dashboardSantri', compact(
|
||||||
'data',
|
'data',
|
||||||
'santri',
|
'santri',
|
||||||
'user',
|
'account',
|
||||||
'beritaTerbaru',
|
'beritaTerbaru',
|
||||||
'statusKesehatan',
|
'statusKesehatan',
|
||||||
'kepulanganAktif',
|
'kepulanganAktif',
|
||||||
|
|
@ -513,12 +528,10 @@ public function santri()
|
||||||
Log::error('Line: ' . $e->getLine());
|
Log::error('Line: ' . $e->getLine());
|
||||||
Log::error('Trace: ' . $e->getTraceAsString());
|
Log::error('Trace: ' . $e->getTraceAsString());
|
||||||
|
|
||||||
// Tampilkan error detail jika debug mode
|
|
||||||
if (config('app.debug')) {
|
if (config('app.debug')) {
|
||||||
abort(500, 'Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
abort(500, 'Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
||||||
} else {
|
}
|
||||||
abort(500, 'Terjadi kesalahan saat memuat dashboard. Silakan hubungi administrator.');
|
abort(500, 'Terjadi kesalahan saat memuat dashboard. Silakan hubungi administrator.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,153 +7,329 @@
|
||||||
use App\Models\Kegiatan;
|
use App\Models\Kegiatan;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class RiwayatKegiatanSantriController extends Controller
|
class RiwayatKegiatanSantriController extends Controller
|
||||||
{
|
{
|
||||||
|
private function getSantriId()
|
||||||
|
{
|
||||||
|
return auth('santri')->user()->id_santri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Halaman utama: Jadwal Harian + Riwayat Absensi
|
* 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);
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
switch ($preset) {
|
||||||
|
case 'today':
|
||||||
|
return [$now->copy()->startOfDay(), $now->copy()->endOfDay(), 'today'];
|
||||||
|
case 'this_week':
|
||||||
|
return [$now->copy()->startOfWeek(), $now->copy()->endOfWeek(), 'this_week'];
|
||||||
|
case 'last_30':
|
||||||
|
return [$now->copy()->subDays(29)->startOfDay(), $now->copy()->endOfDay(), 'last_30'];
|
||||||
|
case 'this_month':
|
||||||
|
return [$now->copy()->startOfMonth(), $now->copy()->endOfMonth(), 'this_month'];
|
||||||
|
case 'last_month':
|
||||||
|
$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();
|
||||||
|
$to = $request->filled('date_to')
|
||||||
|
? Carbon::parse($request->date_to)->endOfDay()
|
||||||
|
: $now->copy()->endOfDay();
|
||||||
|
if ($from->gt($to)) [$from, $to] = [$to, $from];
|
||||||
|
return [$from, $to, 'custom'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
if ($user->role !== 'santri') {
|
// ✅ FIX: No 'kelas' column, use relasi
|
||||||
abort(403, 'Akses ditolak.');
|
$santri = Santri::where('id_santri', $idSantri)
|
||||||
}
|
->with(['kelasPrimary.kelas'])
|
||||||
|
->select('id_santri', 'nama_lengkap', 'nis', 'status')
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
|
||||||
->select('id_santri', 'nama_lengkap', 'kelas')
|
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$idSantri = $santri->id_santri;
|
$namaKelas = optional(optional($santri->kelasPrimary)->kelas)->nama_kelas ?? '-';
|
||||||
$today = Carbon::today();
|
$kelasSantriId = optional($santri->kelasPrimary)->id_kelas;
|
||||||
$hariIni = Carbon::now()->locale('id')->dayName; // Senin, Selasa, etc.
|
|
||||||
|
|
||||||
// ✅ JADWAL KEGIATAN HARI INI (Tetap)
|
// -- Aktif tab (dari request, default: statistik) --
|
||||||
$jadwalHariIni = Kegiatan::with('kategori')
|
$activeTab = $request->input('tab', 'statistik');
|
||||||
->where('hari', ucfirst($hariIni))
|
|
||||||
->select('kegiatan_id', 'kategori_id', 'nama_kegiatan', 'waktu_mulai', 'waktu_selesai', 'materi')
|
|
||||||
->orderBy('waktu_mulai')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
// ✅ CEK STATUS ABSENSI HARI INI
|
// -- Tiap tab punya preset/range masing-masing --
|
||||||
$absensiHariIni = AbsensiKegiatan::where('id_santri', $idSantri)
|
// Statistik: default this_week
|
||||||
->whereDate('tanggal', $today)
|
// Jadwal & Riwayat: default today
|
||||||
->pluck('status', 'kegiatan_id')
|
// Request bisa bawa preset_stat, preset_jadwal, preset_riwayat
|
||||||
->toArray();
|
// atau preset global (backward compat)
|
||||||
|
|
||||||
// ✅ RIWAYAT ABSENSI (dengan filter)
|
// Statistik range
|
||||||
$query = AbsensiKegiatan::with('kegiatan.kategori')
|
$statPresetReq = $request->input('preset_stat', $request->input('preset', 'this_week'));
|
||||||
->where('id_santri', $idSantri);
|
[$statFrom, $statTo, $statPreset] = $this->resolveDateRange(
|
||||||
|
$request->merge(['preset' => $statPresetReq,
|
||||||
// Filter Bulan
|
'date_from' => $request->input('stat_date_from'),
|
||||||
if ($request->filled('bulan')) {
|
'date_to' => $request->input('stat_date_to')]),
|
||||||
$bulan = Carbon::parse($request->bulan);
|
'this_week'
|
||||||
$query->whereMonth('tanggal', $bulan->month)
|
);
|
||||||
->whereYear('tanggal', $bulan->year);
|
if ($statPreset === 'custom') {
|
||||||
|
$statFrom = $request->filled('stat_date_from') ? Carbon::parse($request->stat_date_from)->startOfDay() : $statFrom;
|
||||||
|
$statTo = $request->filled('stat_date_to') ? Carbon::parse($request->stat_date_to)->endOfDay() : $statTo;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter Status
|
// Jadwal range
|
||||||
if ($request->filled('status')) {
|
$jadPresetReq = $request->input('preset_jad', $request->input('preset', 'today'));
|
||||||
$query->where('status', $request->status);
|
[$jadFrom, $jadTo, $jadPreset] = $this->resolveDateRange(
|
||||||
}
|
$request->merge(['preset' => $jadPresetReq,
|
||||||
|
'date_from' => $request->input('jad_date_from'),
|
||||||
|
'date_to' => $request->input('jad_date_to')]),
|
||||||
|
'today'
|
||||||
|
);
|
||||||
|
|
||||||
$riwayats = $query->orderBy('tanggal', 'desc')
|
// Riwayat range
|
||||||
->orderBy('waktu_absen', 'desc')
|
$riwPresetReq = $request->input('preset_riw', $request->input('preset', 'today'));
|
||||||
->paginate(15)
|
[$riwFrom, $riwTo, $riwPreset] = $this->resolveDateRange(
|
||||||
->appends(request()->query());
|
$request->merge(['preset' => $riwPresetReq,
|
||||||
|
'date_from' => $request->input('riw_date_from'),
|
||||||
|
'date_to' => $request->input('riw_date_to')]),
|
||||||
|
'today'
|
||||||
|
);
|
||||||
|
|
||||||
// ✅ STATISTIK KEHADIRAN (30 HARI TERAKHIR)
|
// -- Mapping hari --
|
||||||
$stats30Hari = AbsensiKegiatan::where('id_santri', $idSantri)
|
$hariMapDb = [
|
||||||
->whereDate('tanggal', '>=', Carbon::now()->subDays(30))
|
'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) ──────────────────────────────────
|
||||||
|
$statFromStr = $statFrom->format('Y-m-d');
|
||||||
|
$statToStr = $statTo->format('Y-m-d');
|
||||||
|
|
||||||
|
$statsRange = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||||
|
->whereBetween('tanggal', [$statFromStr, $statToStr])
|
||||||
->select('status', DB::raw('count(*) as total'))
|
->select('status', DB::raw('count(*) as total'))
|
||||||
->groupBy('status')
|
->groupBy('status')
|
||||||
->pluck('total', 'status')
|
->pluck('total', 'status')
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
$totalKegiatan30Hari = array_sum($stats30Hari);
|
$totalRange = array_sum($statsRange);
|
||||||
$persentaseKehadiran = $totalKegiatan30Hari > 0
|
$hadirRange = $statsRange['Hadir'] ?? 0;
|
||||||
? round(($stats30Hari['Hadir'] ?? 0) / $totalKegiatan30Hari * 100, 1)
|
$izinRange = $statsRange['Izin'] ?? 0;
|
||||||
: 0;
|
$sakitRange = $statsRange['Sakit'] ?? 0;
|
||||||
|
$alpaRange = $statsRange['Alpa'] ?? 0;
|
||||||
|
$persentaseKehadiran = $totalRange > 0 ? round($hadirRange / $totalRange * 100, 1) : 0;
|
||||||
|
|
||||||
// ✅ DATA GRAFIK: Kehadiran per Minggu (4 Minggu Terakhir)
|
// ── JADWAL ────────────────────────────────────────────────────────
|
||||||
$dataGrafikMingguan = [];
|
$hariDalamRange = [];
|
||||||
for ($i = 3; $i >= 0; $i--) {
|
$cursor = $jadFrom->copy();
|
||||||
$startWeek = Carbon::now()->subWeeks($i)->startOfWeek();
|
while ($cursor->lte($jadTo)) {
|
||||||
$endWeek = Carbon::now()->subWeeks($i)->endOfWeek();
|
$hariDb = $hariMapDb[$cursor->locale('id')->dayName] ?? $cursor->locale('id')->dayName;
|
||||||
|
$hariDalamRange[$hariDb] = true;
|
||||||
|
$cursor->addDay();
|
||||||
|
}
|
||||||
|
$hariDalamRange = array_keys($hariDalamRange);
|
||||||
|
|
||||||
$hadir = AbsensiKegiatan::where('id_santri', $idSantri)
|
$jadwalDalamRange = Kegiatan::with('kategori')
|
||||||
->whereBetween('tanggal', [$startWeek, $endWeek])
|
->whereIn('hari', $hariDalamRange)
|
||||||
->where('status', 'Hadir')
|
->where(function ($q) use ($kelasSantriId) {
|
||||||
->count();
|
$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();
|
||||||
|
|
||||||
$total = AbsensiKegiatan::where('id_santri', $idSantri)
|
// Status absensi per kegiatan dalam range jadwal
|
||||||
->whereBetween('tanggal', [$startWeek, $endWeek])
|
$absensiDalamRange = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||||
->count();
|
->whereBetween('tanggal', [$jadFrom->format('Y-m-d'), $jadTo->format('Y-m-d')])
|
||||||
|
->pluck('status', 'kegiatan_id')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
$dataGrafikMingguan[] = [
|
// Status khusus hari ini (untuk badge)
|
||||||
'minggu' => 'Minggu ' . (4 - $i),
|
$absensiHariIni = AbsensiKegiatan::where('id_santri', $idSantri)
|
||||||
'hadir' => $hadir,
|
->whereDate('tanggal', Carbon::today())
|
||||||
'total' => $total,
|
->pluck('status', 'kegiatan_id')
|
||||||
'persentase' => $total > 0 ? round($hadir / $total * 100, 1) : 0,
|
->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));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ STATISTIK PER KATEGORI KEGIATAN
|
$riwayats = $queryRiwayat->orderBy('tanggal', 'desc')
|
||||||
$statsByKategori = AbsensiKegiatan::where('id_santri', $idSantri)
|
->orderBy('waktu_absen', 'desc')
|
||||||
|
->paginate(15)
|
||||||
|
->appends(request()->query());
|
||||||
|
|
||||||
|
// ── 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++;
|
||||||
|
else return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── GRAFIK TREN (stat range) ──────────────────────────────────────
|
||||||
|
$diffDays = $statFrom->diffInDays($statTo);
|
||||||
|
$dataGrafik = [];
|
||||||
|
|
||||||
|
if ($diffDays <= 31) {
|
||||||
|
$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();
|
||||||
|
$dataGrafik[] = ['label' => $cur->format('d/m'), 'hadir' => $hadir, 'total' => $total];
|
||||||
|
$cur->addDay();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$cur = $statFrom->copy()->startOfWeek();
|
||||||
|
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];
|
||||||
|
$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('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
|
||||||
->join('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id')
|
->join('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id')
|
||||||
->select(
|
->select(
|
||||||
|
'kegiatans.kegiatan_id',
|
||||||
|
'kegiatans.nama_kegiatan',
|
||||||
'kategori_kegiatans.nama_kategori',
|
'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 = "Hadir" THEN 1 ELSE 0 END) as hadir'),
|
||||||
DB::raw('COUNT(*) as total')
|
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('kategori_kegiatans.nama_kategori')
|
->groupBy('kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan', 'kategori_kegiatans.nama_kategori')
|
||||||
->get();
|
->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();
|
||||||
|
|
||||||
|
// ── HEATMAP: kalender bulan aktif (stat range, max tampil 1 bulan) ─
|
||||||
|
// Kita buat kalender bulan-bulan dalam stat range, dengan angka tanggal
|
||||||
|
$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
|
||||||
|
|
||||||
|
$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));
|
||||||
|
}
|
||||||
|
$days[] = [
|
||||||
|
'day' => $d,
|
||||||
|
'date' => $date,
|
||||||
|
'level' => $level,
|
||||||
|
'count' => $rows->where('status', 'Hadir')->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,
|
||||||
|
];
|
||||||
|
|
||||||
|
$cur->addMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
$kategoriList = \App\Models\KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
||||||
|
|
||||||
return view('santri.kegiatan.index', compact(
|
return view('santri.kegiatan.index', compact(
|
||||||
'santri',
|
'santri', 'namaKelas',
|
||||||
'jadwalHariIni',
|
'jadwalDalamRange', 'absensiDalamRange', 'absensiHariIni', 'hariIni',
|
||||||
'absensiHariIni',
|
'jadPreset', 'jadFrom', 'jadTo',
|
||||||
'riwayats',
|
'riwayats', 'riwPreset', 'riwFrom', 'riwTo',
|
||||||
'stats30Hari',
|
'statsRange', 'totalRange', 'hadirRange', 'izinRange', 'sakitRange', 'alpaRange',
|
||||||
'totalKegiatan30Hari',
|
'persentaseKehadiran', 'streak',
|
||||||
'persentaseKehadiran',
|
'dataGrafik', 'statPreset', 'statFrom', 'statTo', 'statFromStr', 'statToStr', 'diffDays',
|
||||||
'dataGrafikMingguan',
|
'consistencyScores',
|
||||||
'statsByKategori',
|
'heatmapMonths',
|
||||||
'hariIni'
|
'kategoriList',
|
||||||
|
'activeTab', 'hariIni'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function show($kegiatan_id, Request $request)
|
||||||
* Detail Riwayat Absensi per Kegiatan
|
|
||||||
*/
|
|
||||||
public function show($kegiatan_id)
|
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
if ($user->role !== 'santri') {
|
$santri = Santri::where('id_santri', $idSantri)
|
||||||
abort(403, 'Akses ditolak.');
|
->with(['kelasPrimary.kelas'])
|
||||||
}
|
->select('id_santri', 'nama_lengkap', 'nis', 'status')
|
||||||
|
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
|
||||||
->select('id_santri', 'nama_lengkap')
|
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$kegiatan = Kegiatan::with('kategori')
|
$kegiatan = Kegiatan::with('kategori')
|
||||||
->where('kegiatan_id', $kegiatan_id)
|
->where('kegiatan_id', $kegiatan_id)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
// Riwayat absensi untuk kegiatan ini
|
|
||||||
$riwayats = AbsensiKegiatan::where('id_santri', $santri->id_santri)
|
$riwayats = AbsensiKegiatan::where('id_santri', $santri->id_santri)
|
||||||
->where('kegiatan_id', $kegiatan_id)
|
->where('kegiatan_id', $kegiatan_id)
|
||||||
->orderBy('tanggal', 'desc')
|
->orderBy('tanggal', 'desc')
|
||||||
->paginate(20);
|
->paginate(20);
|
||||||
|
|
||||||
// Statistik kehadiran untuk kegiatan ini
|
|
||||||
$stats = AbsensiKegiatan::where('id_santri', $santri->id_santri)
|
$stats = AbsensiKegiatan::where('id_santri', $santri->id_santri)
|
||||||
->where('kegiatan_id', $kegiatan_id)
|
->where('kegiatan_id', $kegiatan_id)
|
||||||
->select('status', DB::raw('count(*) as total'))
|
->select('status', DB::raw('count(*) as total'))
|
||||||
|
|
@ -163,16 +339,33 @@ public function show($kegiatan_id)
|
||||||
|
|
||||||
$totalAbsensi = array_sum($stats);
|
$totalAbsensi = array_sum($stats);
|
||||||
$persentaseHadir = $totalAbsensi > 0
|
$persentaseHadir = $totalAbsensi > 0
|
||||||
? round(($stats['Hadir'] ?? 0) / $totalAbsensi * 100, 1)
|
? round(($stats['Hadir'] ?? 0) / $totalAbsensi * 100, 1) : 0;
|
||||||
: 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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Referrer tab untuk tombol kembali
|
||||||
|
$fromTab = $request->input('from_tab', 'riwayat');
|
||||||
|
|
||||||
return view('santri.kegiatan.show', compact(
|
return view('santri.kegiatan.show', compact(
|
||||||
'santri',
|
'santri', 'kegiatan', 'riwayats',
|
||||||
'kegiatan',
|
'stats', 'totalAbsensi', 'persentaseHadir',
|
||||||
'riwayats',
|
'trendBulanan', 'fromTab'
|
||||||
'stats',
|
|
||||||
'totalAbsensi',
|
|
||||||
'persentaseHadir'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
// app/Http/Controllers/Santri/SantriBeritaController.php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Santri;
|
namespace App\Http\Controllers\Santri;
|
||||||
|
|
||||||
|
|
@ -7,22 +8,27 @@
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
use App\Models\SantriKelas;
|
use App\Models\SantriKelas;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
|
|
||||||
class SantriBeritaController extends Controller
|
class SantriBeritaController extends Controller
|
||||||
{
|
{
|
||||||
|
// -- Helper: Ambil id_santri dari akun yang login --
|
||||||
|
private function getSantriId()
|
||||||
|
{
|
||||||
|
return auth('santri')->user()->id_santri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan daftar berita yang bisa diakses santri
|
* Tampilkan daftar berita yang bisa diakses santri
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
$santri = Santri::where('id_santri', $idSantri)
|
||||||
->select('id_santri')
|
->select('id_santri')
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
// Ambil id kelas santri
|
// -- Ambil id kelas santri --
|
||||||
$kelasIds = SantriKelas::where('id_santri', $santri->id_santri)
|
$kelasIds = SantriKelas::where('id_santri', $santri->id_santri)
|
||||||
->pluck('id_kelas')->toArray();
|
->pluck('id_kelas')->toArray();
|
||||||
|
|
||||||
|
|
@ -52,9 +58,9 @@ public function index(Request $request)
|
||||||
*/
|
*/
|
||||||
public function show($id_berita)
|
public function show($id_berita)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
$santri = Santri::where('id_santri', $idSantri)
|
||||||
->select('id_santri')
|
->select('id_santri')
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
// app/Http/Controllers/Santri/SantriCapaianController.php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Santri;
|
namespace App\Http\Controllers\Santri;
|
||||||
|
|
||||||
|
|
@ -6,105 +7,135 @@
|
||||||
use App\Models\Capaian;
|
use App\Models\Capaian;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
use App\Models\Semester;
|
use App\Models\Semester;
|
||||||
|
use App\Services\CapaianAccessService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class SantriCapaianController extends Controller
|
class SantriCapaianController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
private function getSantriId()
|
||||||
* Tampilkan daftar capaian santri yang sedang login
|
|
||||||
*/
|
|
||||||
public function index(Request $request)
|
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
return auth('santri')->user()->id_santri;
|
||||||
|
|
||||||
// Validasi role
|
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
abort(403, 'Unauthorized access');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$idSantri = $user->role_id;
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
// Cache data santri selama 10 menit
|
// Ambil data santri
|
||||||
$santri = Cache::remember("santri_capaian_{$idSantri}", 600, function () use ($idSantri) {
|
$santri = Cache::remember("santri_{$idSantri}_profile", 600, function () use ($idSantri) {
|
||||||
return Santri::where('id_santri', $idSantri)
|
return Santri::where('id_santri', $idSantri)
|
||||||
->select('id_santri', 'nama_lengkap', 'kelas', 'nis')
|
->with(['kelasPrimary.kelas'])
|
||||||
|
->select('id_santri', 'nama_lengkap', 'nis', 'status')
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get semester aktif
|
|
||||||
$semesterAktif = Semester::aktif()->first();
|
$semesterAktif = Semester::aktif()->first();
|
||||||
$selectedSemester = $request->input('id_semester', $semesterAktif?->id_semester);
|
$selectedSemester = $request->input('id_semester',
|
||||||
|
$semesterAktif ? $semesterAktif->id_semester : null
|
||||||
|
);
|
||||||
|
|
||||||
// Query capaian dengan relasi
|
// Capaian untuk tab Ringkasan / Daftar / Grafik (filter semester)
|
||||||
$query = Capaian::with(['materi:id_materi,nama_kitab,kategori,total_halaman', 'semester:id_semester,nama_semester'])
|
$query = Capaian::with([
|
||||||
|
'materi:id_materi,nama_kitab,kategori,total_halaman,halaman_mulai,halaman_akhir',
|
||||||
|
'semester:id_semester,nama_semester',
|
||||||
|
])
|
||||||
->where('id_santri', $idSantri)
|
->where('id_santri', $idSantri)
|
||||||
->select('id', 'id_capaian', 'id_santri', 'id_materi', 'id_semester', 'halaman_selesai', 'persentase', 'tanggal_input');
|
->select('id', 'id_capaian', 'id_santri', 'id_materi',
|
||||||
|
'id_semester', 'halaman_selesai', 'persentase', 'tanggal_input');
|
||||||
|
|
||||||
// Filter semester
|
|
||||||
if ($selectedSemester) {
|
if ($selectedSemester) {
|
||||||
$query->where('id_semester', $selectedSemester);
|
$query->where('id_semester', $selectedSemester);
|
||||||
}
|
}
|
||||||
|
|
||||||
$capaians = $query->orderBy('tanggal_input', 'desc')->get();
|
$capaians = $query->orderBy('tanggal_input', 'desc')->get();
|
||||||
|
|
||||||
// Statistik Umum
|
// Statistik umum
|
||||||
$totalCapaian = $capaians->count();
|
$totalCapaian = $capaians->count();
|
||||||
$rataRataPersentase = $capaians->avg('persentase') ?? 0;
|
$rataRataPersentase = $capaians->avg('persentase') ?? 0;
|
||||||
$materiSelesai = $capaians->where('persentase', '>=', 100)->count();
|
$materiSelesai = $capaians->where('persentase', '>=', 100)->count();
|
||||||
|
|
||||||
// Statistik per Kategori
|
// Statistik per kategori
|
||||||
$statistikKategori = [
|
$statistikKategori = [
|
||||||
'Al-Qur\'an' => [
|
"Al-Qur'an" => ['count' => 0, 'avg' => 0, 'selesai' => 0],
|
||||||
'count' => 0,
|
'Hadist' => ['count' => 0, 'avg' => 0, 'selesai' => 0],
|
||||||
'avg' => 0,
|
'Materi Tambahan' => ['count' => 0, 'avg' => 0, 'selesai' => 0],
|
||||||
'selesai' => 0,
|
|
||||||
],
|
|
||||||
'Hadist' => [
|
|
||||||
'count' => 0,
|
|
||||||
'avg' => 0,
|
|
||||||
'selesai' => 0,
|
|
||||||
],
|
|
||||||
'Materi Tambahan' => [
|
|
||||||
'count' => 0,
|
|
||||||
'avg' => 0,
|
|
||||||
'selesai' => 0,
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($capaians as $capaian) {
|
foreach ($capaians as $capaian) {
|
||||||
$kategori = $capaian->materi->kategori;
|
$kat = $capaian->materi->kategori ?? 'Materi Tambahan';
|
||||||
$statistikKategori[$kategori]['count']++;
|
if (!isset($statistikKategori[$kat])) continue;
|
||||||
$statistikKategori[$kategori]['avg'] += $capaian->persentase;
|
$statistikKategori[$kat]['count']++;
|
||||||
if ($capaian->persentase >= 100) {
|
$statistikKategori[$kat]['avg'] += $capaian->persentase;
|
||||||
$statistikKategori[$kategori]['selesai']++;
|
if ($capaian->persentase >= 100) $statistikKategori[$kat]['selesai']++;
|
||||||
}
|
}
|
||||||
}
|
foreach ($statistikKategori as $kat => $data) {
|
||||||
|
|
||||||
// Hitung rata-rata
|
|
||||||
foreach ($statistikKategori as $kategori => $data) {
|
|
||||||
if ($data['count'] > 0) {
|
if ($data['count'] > 0) {
|
||||||
$statistikKategori[$kategori]['avg'] = $data['avg'] / $data['count'];
|
$statistikKategori[$kat]['avg'] = round($data['avg'] / $data['count'], 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Distribusi persentase untuk chart
|
// Distribusi persentase
|
||||||
$distribusiPersentase = [
|
$distribusiPersentase = [
|
||||||
'0-25%' => $capaians->whereBetween('persentase', [0, 25])->count(),
|
'0-25%' => $capaians->filter(fn($c) => $c->persentase >= 0 && $c->persentase <= 25)->count(),
|
||||||
'26-50%' => $capaians->whereBetween('persentase', [26, 50])->count(),
|
'26-50%' => $capaians->filter(fn($c) => $c->persentase > 25 && $c->persentase <= 50)->count(),
|
||||||
'51-75%' => $capaians->whereBetween('persentase', [51, 75])->count(),
|
'51-75%' => $capaians->filter(fn($c) => $c->persentase > 50 && $c->persentase <= 75)->count(),
|
||||||
'76-99%' => $capaians->whereBetween('persentase', [76, 99])->count(),
|
'76-99%' => $capaians->filter(fn($c) => $c->persentase > 75 && $c->persentase < 100)->count(),
|
||||||
'100%' => $capaians->where('persentase', '>=', 100)->count(),
|
'100%' => $capaians->where('persentase', '>=', 100)->count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Data untuk semester dropdown
|
// PREDIKSI: ambil SEMUA capaian tanpa filter semester
|
||||||
|
$allCapaians = Capaian::with([
|
||||||
|
'materi:id_materi,nama_kitab,kategori',
|
||||||
|
'semester:id_semester,nama_semester,tahun_ajaran,periode',
|
||||||
|
])
|
||||||
|
->where('id_santri', $idSantri)
|
||||||
|
->select('id', 'id_santri', 'id_materi', 'id_semester', 'persentase')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Susun history per semester (urut cronologis)
|
||||||
|
$allSemesters = Semester::orderBy('tahun_ajaran')->orderBy('periode')->get();
|
||||||
|
|
||||||
|
$historyData = [];
|
||||||
|
foreach ($allSemesters as $sem) {
|
||||||
|
$semCap = $allCapaians->where('id_semester', $sem->id_semester);
|
||||||
|
if ($semCap->isNotEmpty()) {
|
||||||
|
$historyData[] = [
|
||||||
|
'sem' => $sem->nama_semester,
|
||||||
|
'avg' => round($semCap->avg('persentase'), 2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung growth rate (rata-rata kenaikan antar semester)
|
||||||
|
$growthRate = 0;
|
||||||
|
if (count($historyData) >= 2) {
|
||||||
|
$diffs = [];
|
||||||
|
for ($i = 1; $i < count($historyData); $i++) {
|
||||||
|
$diffs[] = $historyData[$i]['avg'] - $historyData[$i - 1]['avg'];
|
||||||
|
}
|
||||||
|
$growthRate = round(array_sum($diffs) / count($diffs), 2);
|
||||||
|
} elseif (count($historyData) === 1) {
|
||||||
|
$growthRate = round($historyData[0]['avg'], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressHistory = [
|
||||||
|
'history' => $historyData,
|
||||||
|
'growth_rate' => $growthRate,
|
||||||
|
'all_capaians' => $allCapaians,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Semester dropdown
|
||||||
$semesters = Semester::select('id_semester', 'nama_semester', 'tahun_ajaran')
|
$semesters = Semester::select('id_semester', 'nama_semester', 'tahun_ajaran')
|
||||||
->orderBy('tahun_ajaran', 'desc')
|
->orderBy('tahun_ajaran', 'desc')
|
||||||
->orderBy('periode', 'desc')
|
->orderBy('periode', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
// Status akses input capaian mandiri
|
||||||
|
$capaianAccessOpen = CapaianAccessService::isOpen();
|
||||||
|
$capaianAccessConfig = CapaianAccessService::getConfig();
|
||||||
|
$capaianSisaWaktu = CapaianAccessService::getSisaWaktu();
|
||||||
|
|
||||||
return view('santri.capaian.index', compact(
|
return view('santri.capaian.index', compact(
|
||||||
'santri',
|
'santri',
|
||||||
'capaians',
|
'capaians',
|
||||||
|
|
@ -113,102 +144,28 @@ public function index(Request $request)
|
||||||
'materiSelesai',
|
'materiSelesai',
|
||||||
'statistikKategori',
|
'statistikKategori',
|
||||||
'distribusiPersentase',
|
'distribusiPersentase',
|
||||||
|
'progressHistory',
|
||||||
'semesters',
|
'semesters',
|
||||||
'selectedSemester',
|
'selectedSemester',
|
||||||
'semesterAktif'
|
'semesterAktif',
|
||||||
|
'capaianAccessOpen',
|
||||||
|
'capaianAccessConfig',
|
||||||
|
'capaianSisaWaktu'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tampilkan detail capaian tertentu
|
|
||||||
*/
|
|
||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
abort(403, 'Unauthorized access');
|
|
||||||
}
|
|
||||||
|
|
||||||
$capaian = Capaian::with([
|
$capaian = Capaian::with([
|
||||||
'materi:id_materi,nama_kitab,kategori,halaman_mulai,halaman_akhir,total_halaman',
|
'materi:id_materi,nama_kitab,kategori,halaman_mulai,halaman_akhir,total_halaman',
|
||||||
'semester:id_semester,nama_semester,tahun_ajaran',
|
'semester:id_semester,nama_semester,tahun_ajaran',
|
||||||
'santri:id_santri,nama_lengkap,kelas'
|
'santri:id_santri,nama_lengkap,nis',
|
||||||
])
|
])
|
||||||
->where('id_santri', $user->role_id)
|
->where('id_santri', $idSantri)
|
||||||
->findOrFail($id);
|
->findOrFail($id);
|
||||||
|
|
||||||
return view('santri.capaian.show', compact('capaian'));
|
return view('santri.capaian.show', compact('capaian'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* API untuk data grafik (AJAX)
|
|
||||||
*/
|
|
||||||
public function apiGrafikData(Request $request)
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
$type = $request->input('type', 'kategori');
|
|
||||||
$idSemester = $request->input('id_semester');
|
|
||||||
|
|
||||||
$query = Capaian::with('materi:id_materi,kategori')
|
|
||||||
->where('id_santri', $user->role_id)
|
|
||||||
->select('id', 'id_materi', 'persentase', 'id_semester');
|
|
||||||
|
|
||||||
if ($idSemester) {
|
|
||||||
$query->where('id_semester', $idSemester);
|
|
||||||
}
|
|
||||||
|
|
||||||
$capaians = $query->get();
|
|
||||||
$data = [];
|
|
||||||
|
|
||||||
switch ($type) {
|
|
||||||
case 'kategori':
|
|
||||||
$avgAlquran = $capaians->filter(fn($c) => $c->materi->kategori == 'Al-Qur\'an')->avg('persentase') ?? 0;
|
|
||||||
$avgHadist = $capaians->filter(fn($c) => $c->materi->kategori == 'Hadist')->avg('persentase') ?? 0;
|
|
||||||
$avgTambahan = $capaians->filter(fn($c) => $c->materi->kategori == 'Materi Tambahan')->avg('persentase') ?? 0;
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
'labels' => ['Al-Qur\'an', 'Hadist', 'Materi Tambahan'],
|
|
||||||
'datasets' => [[
|
|
||||||
'label' => 'Rata-rata Progress (%)',
|
|
||||||
'data' => [
|
|
||||||
round($avgAlquran, 2),
|
|
||||||
round($avgHadist, 2),
|
|
||||||
round($avgTambahan, 2)
|
|
||||||
],
|
|
||||||
'backgroundColor' => [
|
|
||||||
'rgba(111, 186, 157, 0.8)',
|
|
||||||
'rgba(129, 198, 232, 0.8)',
|
|
||||||
'rgba(255, 213, 107, 0.8)',
|
|
||||||
],
|
|
||||||
]]
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'distribusi':
|
|
||||||
$data = [
|
|
||||||
'labels' => ['0-25%', '26-50%', '51-75%', '76-99%', '100%'],
|
|
||||||
'datasets' => [[
|
|
||||||
'label' => 'Jumlah Materi',
|
|
||||||
'data' => [
|
|
||||||
$capaians->whereBetween('persentase', [0, 25])->count(),
|
|
||||||
$capaians->whereBetween('persentase', [26, 50])->count(),
|
|
||||||
$capaians->whereBetween('persentase', [51, 75])->count(),
|
|
||||||
$capaians->whereBetween('persentase', [76, 99])->count(),
|
|
||||||
$capaians->where('persentase', '>=', 100)->count(),
|
|
||||||
],
|
|
||||||
'backgroundColor' => [
|
|
||||||
'rgba(255, 139, 148, 0.8)',
|
|
||||||
'rgba(255, 171, 145, 0.8)',
|
|
||||||
'rgba(255, 213, 107, 0.8)',
|
|
||||||
'rgba(129, 198, 232, 0.8)',
|
|
||||||
'rgba(111, 186, 157, 0.8)',
|
|
||||||
],
|
|
||||||
]]
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
<?php
|
||||||
|
// app/Http/Controllers/Santri/SantriCapaianInputController.php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Santri;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Capaian;
|
||||||
|
use App\Models\Materi;
|
||||||
|
use App\Models\Semester;
|
||||||
|
use App\Models\Santri;
|
||||||
|
use App\Services\CapaianAccessService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SantriCapaianInputController extends Controller
|
||||||
|
{
|
||||||
|
private function getSantri(): Santri
|
||||||
|
{
|
||||||
|
$idSantri = auth('santri')->user()->id_santri;
|
||||||
|
return Santri::where('id_santri', $idSantri)
|
||||||
|
->with(['kelasSantri.kelas'])
|
||||||
|
->firstOrFail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form input capaian untuk santri.
|
||||||
|
* GET /santri/capaian/input
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
// Cek apakah akses sedang dibuka
|
||||||
|
if (!CapaianAccessService::isOpen()) {
|
||||||
|
return redirect()->route('santri.capaian.index')
|
||||||
|
->with('error', 'Saat ini belum ada jadwal input capaian. Silakan tunggu informasi dari admin.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$santri = $this->getSantri();
|
||||||
|
$accessConfig = CapaianAccessService::getConfig();
|
||||||
|
$sisaWaktu = CapaianAccessService::getSisaWaktu();
|
||||||
|
|
||||||
|
// Ambil semester yang berlaku
|
||||||
|
$idSemesterConfig = $accessConfig['id_semester'] ?? null;
|
||||||
|
if ($idSemesterConfig) {
|
||||||
|
$semesterAktif = Semester::where('id_semester', $idSemesterConfig)->first();
|
||||||
|
} else {
|
||||||
|
$semesterAktif = Semester::aktif()->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Materi sesuai kelas santri
|
||||||
|
$kelasNames = $santri->kelasSantri->map(fn($sk) => $sk->kelas?->nama_kelas)->filter()->unique()->toArray();
|
||||||
|
$materiOptions = Materi::whereIn('kelas', $kelasNames ?: [''])
|
||||||
|
->orderBy('kategori')->orderBy('nama_kitab')->get();
|
||||||
|
|
||||||
|
// Capaian yang sudah ada di semester ini
|
||||||
|
$existingCapaians = [];
|
||||||
|
if ($semesterAktif) {
|
||||||
|
$existingCapaians = Capaian::where('id_santri', $santri->id_santri)
|
||||||
|
->where('id_semester', $semesterAktif->id_semester)
|
||||||
|
->pluck('persentase', 'id_materi')
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
|
||||||
|
|
||||||
|
return view('santri.capaian.input', compact(
|
||||||
|
'santri', 'semesterAktif', 'semesters', 'materiOptions',
|
||||||
|
'existingCapaians', 'accessConfig', 'sisaWaktu'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simpan/update capaian oleh santri.
|
||||||
|
* POST /santri/capaian/input
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
// Double-check akses masih terbuka
|
||||||
|
if (!CapaianAccessService::isOpen()) {
|
||||||
|
return redirect()->route('santri.capaian.index')
|
||||||
|
->with('error', 'Waktu input capaian telah berakhir.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$santri = $this->getSantri();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'id_materi' => 'required|exists:materi,id_materi',
|
||||||
|
'id_semester' => 'required|exists:semester,id_semester',
|
||||||
|
'halaman_selesai'=> 'required|string',
|
||||||
|
'catatan' => 'nullable|string|max:500',
|
||||||
|
'tanggal_input' => 'required|date',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Pastikan semester yang dikirim sesuai dengan yang diizinkan
|
||||||
|
$accessConfig = CapaianAccessService::getConfig();
|
||||||
|
if (!empty($accessConfig['id_semester']) && $accessConfig['id_semester'] !== $validated['id_semester']) {
|
||||||
|
return back()->with('error', 'Semester tidak sesuai dengan jadwal input yang dibuka admin.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi materi sesuai kelas santri
|
||||||
|
$kelasNames = $santri->kelasSantri->map(fn($sk) => $sk->kelas?->nama_kelas)->filter()->unique()->toArray();
|
||||||
|
$materi = Materi::where('id_materi', $validated['id_materi'])
|
||||||
|
->whereIn('kelas', $kelasNames ?: [''])->first();
|
||||||
|
|
||||||
|
if (!$materi) {
|
||||||
|
return back()->with('error', 'Materi tidak sesuai dengan kelas Anda.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert capaian (create or update)
|
||||||
|
$existing = Capaian::where('id_santri', $santri->id_santri)
|
||||||
|
->where('id_materi', $validated['id_materi'])
|
||||||
|
->where('id_semester', $validated['id_semester'])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$existing->update([
|
||||||
|
'halaman_selesai' => $validated['halaman_selesai'],
|
||||||
|
'catatan' => $validated['catatan'],
|
||||||
|
'tanggal_input' => $validated['tanggal_input'],
|
||||||
|
]);
|
||||||
|
$msg = "Capaian {$materi->nama_kitab} berhasil diperbarui.";
|
||||||
|
} else {
|
||||||
|
Capaian::create([
|
||||||
|
'id_santri' => $santri->id_santri,
|
||||||
|
'id_materi' => $validated['id_materi'],
|
||||||
|
'id_semester' => $validated['id_semester'],
|
||||||
|
'halaman_selesai'=> $validated['halaman_selesai'],
|
||||||
|
'catatan' => $validated['catatan'],
|
||||||
|
'tanggal_input' => $validated['tanggal_input'],
|
||||||
|
]);
|
||||||
|
$msg = "Capaian {$materi->nama_kitab} berhasil disimpan.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('santri.capaian.input.create')
|
||||||
|
->with('success', $msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Ambil detail materi + existing capaian santri ini.
|
||||||
|
* POST /santri/capaian/input/ajax/detail-materi
|
||||||
|
*/
|
||||||
|
public function ajaxDetailMateri(Request $request)
|
||||||
|
{
|
||||||
|
$santri = $this->getSantri();
|
||||||
|
|
||||||
|
$materi = Materi::where('id_materi', $request->id_materi)->first();
|
||||||
|
if (!$materi) return response()->json(['error' => 'Materi tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
$existing = null;
|
||||||
|
if ($request->filled('id_semester')) {
|
||||||
|
$existing = Capaian::where('id_santri', $santri->id_santri)
|
||||||
|
->where('id_materi', $request->id_materi)
|
||||||
|
->where('id_semester', $request->id_semester)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'materi' => $materi,
|
||||||
|
'existing_capaian' => $existing,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Hitung persentase preview.
|
||||||
|
*/
|
||||||
|
public function ajaxHitungPersentase(Request $request)
|
||||||
|
{
|
||||||
|
if (empty($request->halaman_selesai) || empty($request->id_materi)) {
|
||||||
|
return response()->json(['persentase' => 0, 'jumlah' => 0]);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$persentase = Capaian::calculatePersentase($request->halaman_selesai, $request->id_materi);
|
||||||
|
$pages = Capaian::parseHalamanSelesai($request->halaman_selesai);
|
||||||
|
return response()->json([
|
||||||
|
'persentase' => number_format($persentase, 2),
|
||||||
|
'jumlah' => count($pages),
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['error' => $e->getMessage()], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
// app/Http/Controllers/Santri/SantriKepulanganController.php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Santri;
|
namespace App\Http\Controllers\Santri;
|
||||||
|
|
||||||
|
|
@ -6,78 +7,76 @@
|
||||||
use App\Models\Kepulangan;
|
use App\Models\Kepulangan;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class SantriKepulanganController extends Controller
|
class SantriKepulanganController extends Controller
|
||||||
{
|
{
|
||||||
|
// -- Helper: Ambil id_santri dari akun yang login --
|
||||||
|
private function getSantriId()
|
||||||
|
{
|
||||||
|
return auth('santri')->user()->id_santri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan riwayat kepulangan santri yang sedang login
|
* Tampilkan riwayat kepulangan santri yang sedang login
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
// Ambil data santri
|
// -- Ambil data santri (tanpa kolom 'kelas' yang mungkin tidak ada) --
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
|
||||||
->select('id_santri', 'nama_lengkap', 'kelas')
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
// Tahun untuk filter
|
// -- Tahun untuk filter --
|
||||||
$tahunSekarang = $request->filled('tahun') ? $request->tahun : Carbon::now()->year;
|
$tahunSekarang = $request->filled('tahun') ? $request->tahun : Carbon::now()->year;
|
||||||
|
|
||||||
// Query riwayat kepulangan
|
// -- Query riwayat kepulangan --
|
||||||
$query = Kepulangan::query()
|
$query = Kepulangan::query()
|
||||||
->select([
|
|
||||||
'id',
|
|
||||||
'id_kepulangan',
|
|
||||||
'id_santri',
|
|
||||||
'tanggal_izin',
|
|
||||||
'tanggal_pulang',
|
|
||||||
'tanggal_kembali',
|
|
||||||
'durasi_izin',
|
|
||||||
'alasan',
|
|
||||||
'status',
|
|
||||||
'approved_at',
|
|
||||||
'created_at'
|
|
||||||
])
|
|
||||||
->where('id_santri', $santri->id_santri)
|
->where('id_santri', $santri->id_santri)
|
||||||
->whereYear('tanggal_pulang', $tahunSekarang);
|
->whereYear('tanggal_pulang', $tahunSekarang);
|
||||||
|
|
||||||
// Filter status jika ada
|
// -- Filter status jika ada --
|
||||||
if ($request->filled('status')) {
|
if ($request->filled('status')) {
|
||||||
$query->where('status', $request->status);
|
$query->where('status', $request->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Urutkan terbaru dan paginate
|
// -- Urutkan terbaru dan paginate --
|
||||||
$riwayatKepulangan = $query->orderBy('tanggal_pulang', 'desc')
|
$riwayatKepulangan = $query->orderBy('tanggal_pulang', 'desc')
|
||||||
->paginate(10)
|
->paginate(10)
|
||||||
->appends($request->all());
|
->appends($request->all());
|
||||||
|
|
||||||
// Hitung statistik tahun ini
|
// -- Hitung statistik tahun ini --
|
||||||
|
$allKepulanganTahunIni = Kepulangan::where('id_santri', $santri->id_santri)
|
||||||
|
->whereYear('tanggal_pulang', $tahunSekarang)
|
||||||
|
->get();
|
||||||
|
|
||||||
$statistik = [
|
$statistik = [
|
||||||
'total_izin' => Kepulangan::where('id_santri', $santri->id_santri)
|
'total_izin' => $allKepulanganTahunIni->count(),
|
||||||
->whereYear('tanggal_pulang', $tahunSekarang)
|
'disetujui' => $allKepulanganTahunIni->where('status', 'Disetujui')->count(),
|
||||||
->count(),
|
'ditolak' => $allKepulanganTahunIni->where('status', 'Ditolak')->count(),
|
||||||
'disetujui' => Kepulangan::where('id_santri', $santri->id_santri)
|
'menunggu' => $allKepulanganTahunIni->where('status', 'Menunggu')->count(),
|
||||||
->where('status', 'Disetujui')
|
'selesai' => $allKepulanganTahunIni->where('status', 'Selesai')->count(),
|
||||||
->whereYear('tanggal_pulang', $tahunSekarang)
|
'total_hari' => $allKepulanganTahunIni->whereIn('status', ['Disetujui', 'Selesai'])->sum('durasi_izin'),
|
||||||
->count(),
|
|
||||||
'total_hari' => Kepulangan::where('id_santri', $santri->id_santri)
|
|
||||||
->where('status', 'Disetujui')
|
|
||||||
->whereYear('tanggal_pulang', $tahunSekarang)
|
|
||||||
->sum('durasi_izin'),
|
|
||||||
'menunggu' => Kepulangan::where('id_santri', $santri->id_santri)
|
|
||||||
->where('status', 'Menunggu')
|
|
||||||
->whereYear('tanggal_pulang', $tahunSekarang)
|
|
||||||
->count(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Hitung sisa kuota (maksimal 12 hari/tahun)
|
|
||||||
$statistik['sisa_kuota'] = max(0, 12 - $statistik['total_hari']);
|
$statistik['sisa_kuota'] = max(0, 12 - $statistik['total_hari']);
|
||||||
$statistik['over_limit'] = $statistik['total_hari'] > 12;
|
$statistik['over_limit'] = $statistik['total_hari'] > 12;
|
||||||
|
$statistik['persen_kuota'] = min(100, round(($statistik['total_hari'] / 12) * 100));
|
||||||
|
|
||||||
// Data untuk filter
|
// -- Cek apakah sedang aktif pulang --
|
||||||
|
$sedangPulang = Kepulangan::where('id_santri', $santri->id_santri)
|
||||||
|
->where('status', 'Disetujui')
|
||||||
|
->whereDate('tanggal_pulang', '<=', Carbon::today())
|
||||||
|
->whereDate('tanggal_kembali', '>=', Carbon::today())
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// -- Cek apakah ada yang terlambat --
|
||||||
|
$terlambat = Kepulangan::where('id_santri', $santri->id_santri)
|
||||||
|
->where('status', 'Disetujui')
|
||||||
|
->whereDate('tanggal_kembali', '<', Carbon::today())
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// -- Data untuk filter --
|
||||||
$statusOptions = [
|
$statusOptions = [
|
||||||
'Menunggu' => 'Menunggu Approval',
|
'Menunggu' => 'Menunggu Approval',
|
||||||
'Disetujui' => 'Disetujui',
|
'Disetujui' => 'Disetujui',
|
||||||
|
|
@ -85,7 +84,7 @@ public function index(Request $request)
|
||||||
'Selesai' => 'Selesai'
|
'Selesai' => 'Selesai'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Tahun options (5 tahun terakhir)
|
// -- Tahun options (5 tahun terakhir) --
|
||||||
$tahunOptions = range(Carbon::now()->year, Carbon::now()->year - 4);
|
$tahunOptions = range(Carbon::now()->year, Carbon::now()->year - 4);
|
||||||
|
|
||||||
return view('santri.kepulangan.index', compact(
|
return view('santri.kepulangan.index', compact(
|
||||||
|
|
@ -94,7 +93,9 @@ public function index(Request $request)
|
||||||
'statistik',
|
'statistik',
|
||||||
'statusOptions',
|
'statusOptions',
|
||||||
'tahunOptions',
|
'tahunOptions',
|
||||||
'tahunSekarang'
|
'tahunSekarang',
|
||||||
|
'sedangPulang',
|
||||||
|
'terlambat'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,26 +104,41 @@ public function index(Request $request)
|
||||||
*/
|
*/
|
||||||
public function show($id_kepulangan)
|
public function show($id_kepulangan)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
|
||||||
->select('id_santri', 'nama_lengkap', 'kelas')
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
// Ambil data kepulangan dengan validasi kepemilikan
|
// -- Ambil data kepulangan dengan validasi kepemilikan --
|
||||||
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)
|
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)
|
||||||
->where('id_santri', $santri->id_santri)
|
->where('id_santri', $santri->id_santri)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
// Hitung total hari izin tahun ini
|
// -- Hitung total hari izin tahun ini --
|
||||||
$tahunSekarang = Carbon::now()->year;
|
$tahunSekarang = Carbon::now()->year;
|
||||||
$totalHariTahunIni = Kepulangan::where('id_santri', $santri->id_santri)
|
$totalHariTahunIni = Kepulangan::where('id_santri', $santri->id_santri)
|
||||||
->where('status', 'Disetujui')
|
->whereIn('status', ['Disetujui', 'Selesai'])
|
||||||
->whereYear('tanggal_pulang', $tahunSekarang)
|
->whereYear('tanggal_pulang', $tahunSekarang)
|
||||||
->sum('durasi_izin');
|
->sum('durasi_izin');
|
||||||
|
|
||||||
$sisaKuota = max(0, 12 - $totalHariTahunIni);
|
$sisaKuota = max(0, 12 - $totalHariTahunIni);
|
||||||
|
$persenKuota = min(100, round(($totalHariTahunIni / 12) * 100));
|
||||||
|
|
||||||
return view('santri.kepulangan.show', compact('kepulangan', 'santri', 'totalHariTahunIni', 'sisaKuota'));
|
// -- Riwayat kepulangan lain tahun ini --
|
||||||
|
$riwayatLain = Kepulangan::where('id_santri', $santri->id_santri)
|
||||||
|
->where('id_kepulangan', '!=', $id_kepulangan)
|
||||||
|
->whereYear('tanggal_pulang', $tahunSekarang)
|
||||||
|
->whereIn('status', ['Disetujui', 'Selesai'])
|
||||||
|
->orderBy('tanggal_pulang', 'desc')
|
||||||
|
->limit(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('santri.kepulangan.show', compact(
|
||||||
|
'kepulangan',
|
||||||
|
'santri',
|
||||||
|
'totalHariTahunIni',
|
||||||
|
'sisaKuota',
|
||||||
|
'persenKuota',
|
||||||
|
'riwayatLain'
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
// app/Http/Controllers/Santri/SantriKesehatanController.php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Santri;
|
namespace App\Http\Controllers\Santri;
|
||||||
|
|
||||||
|
|
@ -6,25 +7,30 @@
|
||||||
use App\Models\KesehatanSantri;
|
use App\Models\KesehatanSantri;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class SantriKesehatanController extends Controller
|
class SantriKesehatanController extends Controller
|
||||||
{
|
{
|
||||||
|
private function getSantriId()
|
||||||
|
{
|
||||||
|
return auth('santri')->user()->id_santri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan riwayat kesehatan santri yang sedang login dengan filter tanggal
|
* Tampilkan riwayat kesehatan santri yang sedang login
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
// Ambil data santri
|
// ✅ Fix: hapus 'kelas' dari select, tambah eager load kelasPrimary
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
$santri = Santri::with('kelasPrimary.kelas')
|
||||||
->select('id_santri', 'nama_lengkap', 'kelas')
|
->where('id_santri', $idSantri)
|
||||||
|
->select('id_santri', 'nama_lengkap', 'jenis_kelamin', 'status')
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
// ✅ TENTUKAN RANGE TANGGAL
|
// -- Tentukan range tanggal --
|
||||||
// Jika tidak ada filter, default bulan ini
|
|
||||||
$tanggalDari = $request->filled('tanggal_dari')
|
$tanggalDari = $request->filled('tanggal_dari')
|
||||||
? Carbon::parse($request->tanggal_dari)
|
? Carbon::parse($request->tanggal_dari)
|
||||||
: Carbon::now()->startOfMonth();
|
: Carbon::now()->startOfMonth();
|
||||||
|
|
@ -33,21 +39,20 @@ public function index(Request $request)
|
||||||
? Carbon::parse($request->tanggal_sampai)
|
? Carbon::parse($request->tanggal_sampai)
|
||||||
: Carbon::now()->endOfMonth();
|
: Carbon::now()->endOfMonth();
|
||||||
|
|
||||||
// Validasi: tanggal_sampai tidak boleh lebih kecil dari tanggal_dari
|
// -- Validasi tanggal --
|
||||||
if ($tanggalSampai->lt($tanggalDari)) {
|
if ($tanggalSampai->lt($tanggalDari)) {
|
||||||
return back()->withErrors([
|
return back()->withErrors([
|
||||||
'tanggal_sampai' => 'Tanggal sampai harus lebih besar atau sama dengan tanggal dari.'
|
'tanggal_sampai' => 'Tanggal sampai harus lebih besar dari tanggal dari.'
|
||||||
])->withInput();
|
])->withInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ QUERY DASAR DENGAN FILTER TANGGAL
|
// -- Statistik berdasarkan filter tanggal --
|
||||||
$baseQuery = KesehatanSantri::where('id_santri', $santri->id_santri)
|
$baseQuery = KesehatanSantri::where('id_santri', $idSantri)
|
||||||
->whereBetween('tanggal_masuk', [
|
->whereBetween('tanggal_masuk', [
|
||||||
$tanggalDari->format('Y-m-d'),
|
$tanggalDari->format('Y-m-d'),
|
||||||
$tanggalSampai->format('Y-m-d')
|
$tanggalSampai->format('Y-m-d'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ✅ HITUNG STATISTIK BERDASARKAN FILTER TANGGAL
|
|
||||||
$statistik = [
|
$statistik = [
|
||||||
'total_kunjungan' => (clone $baseQuery)->count(),
|
'total_kunjungan' => (clone $baseQuery)->count(),
|
||||||
'sedang_dirawat' => (clone $baseQuery)->where('status', 'dirawat')->count(),
|
'sedang_dirawat' => (clone $baseQuery)->where('status', 'dirawat')->count(),
|
||||||
|
|
@ -55,39 +60,60 @@ public function index(Request $request)
|
||||||
'izin' => (clone $baseQuery)->where('status', 'izin')->count(),
|
'izin' => (clone $baseQuery)->where('status', 'izin')->count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// ✅ QUERY RIWAYAT KESEHATAN UNTUK TABEL
|
// -- Cek apakah SAAT INI sedang dirawat (semua waktu, bukan filter) --
|
||||||
$query = KesehatanSantri::query()
|
$sedangDirawatSekarang = KesehatanSantri::where('id_santri', $idSantri)
|
||||||
->select([
|
->where('status', 'dirawat')
|
||||||
'id',
|
->latest('tanggal_masuk')
|
||||||
'id_kesehatan',
|
->first();
|
||||||
'id_santri',
|
|
||||||
'tanggal_masuk',
|
// -- Data grafik: kunjungan per bulan (6 bulan terakhir) --
|
||||||
'tanggal_keluar',
|
$dataGrafik = KesehatanSantri::where('id_santri', $idSantri)
|
||||||
'keluhan',
|
->where('tanggal_masuk', '>=', Carbon::now()->subMonths(6)->startOfMonth())
|
||||||
'status',
|
->select(
|
||||||
'created_at'
|
DB::raw('YEAR(tanggal_masuk) as tahun'),
|
||||||
])
|
DB::raw('MONTH(tanggal_masuk) as bulan'),
|
||||||
->where('id_santri', $santri->id_santri)
|
DB::raw('COUNT(*) as total'),
|
||||||
->whereBetween('tanggal_masuk', [
|
DB::raw('SUM(CASE WHEN status = "sembuh" THEN 1 ELSE 0 END) as sembuh'),
|
||||||
$tanggalDari->format('Y-m-d'),
|
DB::raw('SUM(CASE WHEN status = "dirawat" THEN 1 ELSE 0 END) as dirawat'),
|
||||||
$tanggalSampai->format('Y-m-d')
|
DB::raw('SUM(CASE WHEN status = "izin" THEN 1 ELSE 0 END) as izin')
|
||||||
|
)
|
||||||
|
->groupBy('tahun', 'bulan')
|
||||||
|
->orderBy('tahun')
|
||||||
|
->orderBy('bulan')
|
||||||
|
->get()
|
||||||
|
->map(fn($item) => [
|
||||||
|
'label' => Carbon::createFromDate($item->tahun, $item->bulan, 1)
|
||||||
|
->locale('id')->isoFormat('MMM YY'),
|
||||||
|
'total' => $item->total,
|
||||||
|
'sembuh' => $item->sembuh,
|
||||||
|
'dirawat' => $item->dirawat,
|
||||||
|
'izin' => $item->izin,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// -- Statistik total keseluruhan (all time) --
|
||||||
|
$totalAllTime = KesehatanSantri::where('id_santri', $idSantri)->count();
|
||||||
|
$totalHariDirawat = KesehatanSantri::where('id_santri', $idSantri)->get()
|
||||||
|
->sum('lama_dirawat');
|
||||||
|
|
||||||
|
// -- Query riwayat dengan filter --
|
||||||
|
$query = KesehatanSantri::where('id_santri', $idSantri)
|
||||||
|
->whereBetween('tanggal_masuk', [
|
||||||
|
$tanggalDari->format('Y-m-d'),
|
||||||
|
$tanggalSampai->format('Y-m-d'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Filter status jika ada
|
|
||||||
if ($request->filled('status')) {
|
if ($request->filled('status')) {
|
||||||
$query->where('status', $request->status);
|
$query->where('status', $request->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Urutkan terbaru dan paginate
|
|
||||||
$riwayatKesehatan = $query->orderBy('tanggal_masuk', 'desc')
|
$riwayatKesehatan = $query->orderBy('tanggal_masuk', 'desc')
|
||||||
->paginate(10)
|
->paginate(10)
|
||||||
->appends($request->all()); // Append query string untuk pagination
|
->appends($request->all());
|
||||||
|
|
||||||
// Data untuk filter
|
|
||||||
$statusOptions = [
|
$statusOptions = [
|
||||||
'dirawat' => 'Sedang Dirawat',
|
'dirawat' => 'Sedang Dirawat',
|
||||||
'sembuh' => 'Sembuh',
|
'sembuh' => 'Sembuh',
|
||||||
'izin' => 'Izin Sakit'
|
'izin' => 'Izin Sakit',
|
||||||
];
|
];
|
||||||
|
|
||||||
return view('santri.kesehatan.index', compact(
|
return view('santri.kesehatan.index', compact(
|
||||||
|
|
@ -96,7 +122,11 @@ public function index(Request $request)
|
||||||
'statistik',
|
'statistik',
|
||||||
'statusOptions',
|
'statusOptions',
|
||||||
'tanggalDari',
|
'tanggalDari',
|
||||||
'tanggalSampai'
|
'tanggalSampai',
|
||||||
|
'sedangDirawatSekarang',
|
||||||
|
'dataGrafik',
|
||||||
|
'totalAllTime',
|
||||||
|
'totalHariDirawat'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,17 +135,29 @@ public function index(Request $request)
|
||||||
*/
|
*/
|
||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
// ✅ Fix: hapus 'kelas' dari select
|
||||||
->select('id_santri', 'nama_lengkap', 'kelas')
|
$santri = Santri::with('kelasPrimary.kelas')
|
||||||
|
->where('id_santri', $idSantri)
|
||||||
|
->select('id_santri', 'nama_lengkap', 'jenis_kelamin', 'status')
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
// Ambil data kesehatan dengan validasi kepemilikan
|
|
||||||
$kesehatanSantri = KesehatanSantri::where('id', $id)
|
$kesehatanSantri = KesehatanSantri::where('id', $id)
|
||||||
->where('id_santri', $santri->id_santri)
|
->where('id_santri', $idSantri)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
return view('santri.kesehatan.show', compact('kesehatanSantri', 'santri'));
|
// -- Riwayat lain santri ini (untuk konteks) --
|
||||||
|
$riwayatLain = KesehatanSantri::where('id_santri', $idSantri)
|
||||||
|
->where('id', '!=', $id)
|
||||||
|
->orderBy('tanggal_masuk', 'desc')
|
||||||
|
->take(3)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('santri.kesehatan.show', compact(
|
||||||
|
'kesehatanSantri',
|
||||||
|
'santri',
|
||||||
|
'riwayatLain'
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,26 +7,26 @@
|
||||||
use App\Models\RiwayatPelanggaran;
|
use App\Models\RiwayatPelanggaran;
|
||||||
use App\Models\KategoriPelanggaran;
|
use App\Models\KategoriPelanggaran;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class SantriPelanggaranController extends Controller
|
class SantriPelanggaranController extends Controller
|
||||||
{
|
{
|
||||||
|
// -- Helper: Ambil id_santri dari akun yang login --
|
||||||
|
private function getSantriId()
|
||||||
|
{
|
||||||
|
return auth('santri')->user()->id_santri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan daftar riwayat pelanggaran santri yang sedang login
|
* Tampilkan daftar riwayat pelanggaran santri yang sedang login
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
// Validasi role
|
// -- Query riwayat pelanggaran dengan relasi --
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
|
||||||
abort(403, 'Akses ditolak.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query riwayat pelanggaran dengan relasi
|
|
||||||
$query = RiwayatPelanggaran::with(['kategori:id,id_kategori,nama_pelanggaran,poin'])
|
$query = RiwayatPelanggaran::with(['kategori:id,id_kategori,nama_pelanggaran,poin'])
|
||||||
->where('id_santri', $user->role_id)
|
->where('id_santri', $idSantri)
|
||||||
->select([
|
->select([
|
||||||
'id',
|
'id',
|
||||||
'id_riwayat',
|
'id_riwayat',
|
||||||
|
|
@ -38,7 +38,7 @@ public function index(Request $request)
|
||||||
'created_at'
|
'created_at'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Filter berdasarkan tanggal (opsional)
|
// -- Filter berdasarkan tanggal --
|
||||||
if ($request->filled('tanggal_mulai')) {
|
if ($request->filled('tanggal_mulai')) {
|
||||||
$query->whereDate('tanggal', '>=', $request->tanggal_mulai);
|
$query->whereDate('tanggal', '>=', $request->tanggal_mulai);
|
||||||
}
|
}
|
||||||
|
|
@ -47,18 +47,18 @@ public function index(Request $request)
|
||||||
$query->whereDate('tanggal', '<=', $request->tanggal_selesai);
|
$query->whereDate('tanggal', '<=', $request->tanggal_selesai);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter bulan ini (jika ada parameter)
|
// -- Filter bulan ini --
|
||||||
if ($request->has('bulan_ini') && $request->bulan_ini == '1') {
|
if ($request->has('bulan_ini') && $request->bulan_ini == '1') {
|
||||||
$query->bulanIni();
|
$query->bulanIni();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Urutkan dari terbaru
|
// -- Urutkan dari terbaru --
|
||||||
$riwayat = $query->terbaru()->paginate(15);
|
$riwayat = $query->terbaru()->paginate(15);
|
||||||
|
|
||||||
// Statistik pelanggaran santri
|
// -- Statistik pelanggaran santri --
|
||||||
$totalPelanggaran = RiwayatPelanggaran::where('id_santri', $user->role_id)->count();
|
$totalPelanggaran = RiwayatPelanggaran::where('id_santri', $idSantri)->count();
|
||||||
$totalPoin = RiwayatPelanggaran::where('id_santri', $user->role_id)->sum('poin');
|
$totalPoin = RiwayatPelanggaran::where('id_santri', $idSantri)->sum('poin');
|
||||||
$pelanggaranBulanIni = RiwayatPelanggaran::where('id_santri', $user->role_id)
|
$pelanggaranBulanIni = RiwayatPelanggaran::where('id_santri', $idSantri)
|
||||||
->bulanIni()
|
->bulanIni()
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
|
|
@ -75,14 +75,12 @@ public function index(Request $request)
|
||||||
*/
|
*/
|
||||||
public function show(RiwayatPelanggaran $riwayatPelanggaran)
|
public function show(RiwayatPelanggaran $riwayatPelanggaran)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
// -- Validasi: pastikan pelanggaran milik santri yang login --
|
||||||
|
if ($riwayatPelanggaran->id_santri !== $this->getSantriId()) {
|
||||||
// Validasi: pastikan pelanggaran milik santri yang login
|
|
||||||
if ($riwayatPelanggaran->id_santri !== $user->role_id) {
|
|
||||||
abort(403, 'Anda tidak memiliki akses ke data ini.');
|
abort(403, 'Anda tidak memiliki akses ke data ini.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load relasi kategori
|
// -- Load relasi kategori --
|
||||||
$riwayatPelanggaran->load('kategori:id,id_kategori,nama_pelanggaran,poin');
|
$riwayatPelanggaran->load('kategori:id,id_kategori,nama_pelanggaran,poin');
|
||||||
|
|
||||||
return view('santri.pelanggaran.show', compact('riwayatPelanggaran'));
|
return view('santri.pelanggaran.show', compact('riwayatPelanggaran'));
|
||||||
|
|
@ -93,7 +91,7 @@ public function show(RiwayatPelanggaran $riwayatPelanggaran)
|
||||||
*/
|
*/
|
||||||
public function kategoriList()
|
public function kategoriList()
|
||||||
{
|
{
|
||||||
// Cache daftar kategori selama 1 jam
|
// -- Cache daftar kategori selama 1 jam --
|
||||||
$kategoriList = Cache::remember('kategori_pelanggaran_list', 3600, function () {
|
$kategoriList = Cache::remember('kategori_pelanggaran_list', 3600, function () {
|
||||||
return KategoriPelanggaran::select([
|
return KategoriPelanggaran::select([
|
||||||
'id',
|
'id',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
// app/Http/Controllers/Santri/SantriPembinaanController.php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Santri;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\PembinaanSanksi;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class SantriPembinaanController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Tampilkan daftar konten pembinaan & sanksi
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Cache 30 menit karena konten jarang berubah
|
||||||
|
$pembinaanList = Cache::remember('pembinaan_sanksi_aktif', 1800, function () {
|
||||||
|
return PembinaanSanksi::aktif()
|
||||||
|
->byUrutan()
|
||||||
|
->select(['id', 'id_pembinaan', 'judul', 'konten', 'urutan', 'updated_at'])
|
||||||
|
->get();
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('santri.pembinaan.index', compact('pembinaanList'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tampilkan detail satu konten pembinaan & sanksi
|
||||||
|
*/
|
||||||
|
public function show($id_pembinaan)
|
||||||
|
{
|
||||||
|
$pembinaan = PembinaanSanksi::aktif()
|
||||||
|
->where('id_pembinaan', $id_pembinaan)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
// Konten sebelum dan sesudah untuk navigasi
|
||||||
|
$prev = PembinaanSanksi::aktif()
|
||||||
|
->byUrutan()
|
||||||
|
->where('urutan', '<', $pembinaan->urutan)
|
||||||
|
->orWhere(function ($q) use ($pembinaan) {
|
||||||
|
$q->where('urutan', $pembinaan->urutan)
|
||||||
|
->where('id', '<', $pembinaan->id);
|
||||||
|
})
|
||||||
|
->orderBy('urutan', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$next = PembinaanSanksi::aktif()
|
||||||
|
->byUrutan()
|
||||||
|
->where('urutan', '>', $pembinaan->urutan)
|
||||||
|
->orWhere(function ($q) use ($pembinaan) {
|
||||||
|
$q->where('urutan', $pembinaan->urutan)
|
||||||
|
->where('id', '>', $pembinaan->id);
|
||||||
|
})
|
||||||
|
->orderBy('urutan', 'asc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Semua konten untuk sidebar
|
||||||
|
$pembinaanList = Cache::remember('pembinaan_sanksi_aktif', 1800, function () {
|
||||||
|
return PembinaanSanksi::aktif()->byUrutan()
|
||||||
|
->select(['id', 'id_pembinaan', 'judul', 'urutan'])
|
||||||
|
->get();
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('santri.pembinaan.show', compact(
|
||||||
|
'pembinaan',
|
||||||
|
'pembinaanList',
|
||||||
|
'prev',
|
||||||
|
'next'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,46 +5,43 @@
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class SantriProfileController extends Controller
|
class SantriProfileController extends Controller
|
||||||
{
|
{
|
||||||
|
private function getSantriId()
|
||||||
|
{
|
||||||
|
return auth('santri')->user()->id_santri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan halaman profil santri yang sedang login
|
* Tampilkan halaman profil santri yang sedang login (READ ONLY)
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
// Ambil data user yang sedang login
|
$idSantri = $this->getSantriId();
|
||||||
$user = Auth::guard('web')->user();
|
|
||||||
|
|
||||||
// Pastikan user adalah santri
|
|
||||||
if ($user->role !== 'santri') {
|
|
||||||
abort(403, 'Unauthorized access');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache data santri selama 10 menit untuk mengurangi query database
|
|
||||||
$santri = Cache::remember(
|
$santri = Cache::remember(
|
||||||
'santri_profile_' . $user->role_id,
|
'santri_profile_' . $idSantri,
|
||||||
600, // 10 menit
|
600,
|
||||||
function () use ($user) {
|
function () use ($idSantri) {
|
||||||
return Santri::where('id_santri', $user->role_id)
|
return Santri::with(['kelasSantri.kelas.kelompok', 'kelasPrimary.kelas.kelompok'])
|
||||||
|
->where('id_santri', $idSantri)
|
||||||
->select([
|
->select([
|
||||||
'id',
|
'id',
|
||||||
'id_santri',
|
'id_santri',
|
||||||
'nis',
|
'nis',
|
||||||
'nama_lengkap',
|
'nama_lengkap',
|
||||||
'jenis_kelamin',
|
'jenis_kelamin',
|
||||||
'kelas',
|
|
||||||
'status',
|
'status',
|
||||||
'alamat_santri',
|
'alamat_santri',
|
||||||
'daerah_asal',
|
'daerah_asal',
|
||||||
'nama_orang_tua',
|
'nama_orang_tua',
|
||||||
'nomor_hp_ortu',
|
'nomor_hp_ortu',
|
||||||
'rfid_uid',
|
'rfid_uid',
|
||||||
'foto', // ✅ TAMBAHAN INI - PENTING!
|
'foto',
|
||||||
'created_at'
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
])
|
])
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
}
|
}
|
||||||
|
|
@ -52,59 +49,4 @@ function () use ($user) {
|
||||||
|
|
||||||
return view('santri.profil.index', compact('santri'));
|
return view('santri.profil.index', compact('santri'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tampilkan form edit profil (data terbatas yang bisa diedit santri)
|
|
||||||
*/
|
|
||||||
public function edit()
|
|
||||||
{
|
|
||||||
$user = Auth::guard('web')->user();
|
|
||||||
|
|
||||||
if ($user->role !== 'santri') {
|
|
||||||
abort(403, 'Unauthorized access');
|
|
||||||
}
|
|
||||||
|
|
||||||
$santri = Santri::where('id_santri', $user->role_id)
|
|
||||||
->select([
|
|
||||||
'id',
|
|
||||||
'id_santri',
|
|
||||||
'nama_lengkap',
|
|
||||||
'jenis_kelamin', // ✅ TAMBAHAN untuk fallback foto default
|
|
||||||
'alamat_santri',
|
|
||||||
'nomor_hp_ortu',
|
|
||||||
'foto' // ✅ TAMBAHAN INI - PENTING!
|
|
||||||
])
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
return view('santri.profil.edit', compact('santri'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update profil santri (hanya field tertentu yang boleh diedit)
|
|
||||||
*/
|
|
||||||
public function update(Request $request)
|
|
||||||
{
|
|
||||||
$user = Auth::guard('web')->user();
|
|
||||||
|
|
||||||
if ($user->role !== 'santri') {
|
|
||||||
abort(403, 'Unauthorized access');
|
|
||||||
}
|
|
||||||
|
|
||||||
$validated = $request->validate([
|
|
||||||
'alamat_santri' => 'nullable|string|max:500',
|
|
||||||
'nomor_hp_ortu' => 'nullable|string|max:20|regex:/^[0-9+\-\s()]+$/',
|
|
||||||
], [
|
|
||||||
'nomor_hp_ortu.regex' => 'Format nomor HP tidak valid. Hanya boleh berisi angka, +, -, spasi, dan tanda kurung.',
|
|
||||||
'alamat_santri.max' => 'Alamat maksimal 500 karakter.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$santri = Santri::where('id_santri', $user->role_id)->firstOrFail();
|
|
||||||
$santri->update($validated);
|
|
||||||
|
|
||||||
// Clear cache setelah update
|
|
||||||
Cache::forget('santri_profile_' . $user->role_id);
|
|
||||||
|
|
||||||
return redirect()->route('santri.profil.index')
|
|
||||||
->with('success', 'Profil berhasil diperbarui.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -5,71 +5,62 @@
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use App\Models\UangSaku;
|
use App\Models\UangSaku;
|
||||||
use App\Models\Santri;
|
use App\Models\Santri;
|
||||||
|
|
||||||
class SantriUangSakuController extends Controller
|
class SantriUangSakuController extends Controller
|
||||||
{
|
{
|
||||||
|
private function getSantriId()
|
||||||
|
{
|
||||||
|
return auth('santri')->user()->id_santri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan riwayat uang saku santri yang sedang login
|
* Tampilkan riwayat uang saku santri yang sedang login
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
// Validasi role
|
$santri = Santri::with(['kelasPrimary.kelas'])
|
||||||
if (!in_array($user->role, ['santri', 'wali'])) {
|
->where('id_santri', $idSantri)
|
||||||
abort(403, 'Akses ditolak');
|
->firstOrFail();
|
||||||
}
|
|
||||||
|
|
||||||
// Ambil data santri
|
// -- Query uang saku --
|
||||||
$santri = Santri::where('id_santri', $user->role_id)->first();
|
$query = UangSaku::where('id_santri', $idSantri);
|
||||||
|
|
||||||
if (!$santri) {
|
// -- Filter jenis transaksi --
|
||||||
abort(404, 'Data santri tidak ditemukan');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query uang saku dengan pagination dan filter
|
|
||||||
$query = UangSaku::where('id_santri', $santri->id_santri)
|
|
||||||
->with('santri:id_santri,nama_lengkap,kelas');
|
|
||||||
|
|
||||||
// Filter berdasarkan jenis transaksi
|
|
||||||
if ($request->filled('jenis_transaksi')) {
|
if ($request->filled('jenis_transaksi')) {
|
||||||
$query->where('jenis_transaksi', $request->jenis_transaksi);
|
$query->where('jenis_transaksi', $request->jenis_transaksi);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter berdasarkan tanggal
|
// -- Filter tanggal --
|
||||||
if ($request->filled('tanggal_dari')) {
|
if ($request->filled('tanggal_dari')) {
|
||||||
$query->whereDate('tanggal_transaksi', '>=', $request->tanggal_dari);
|
$query->whereDate('tanggal_transaksi', '>=', $request->tanggal_dari);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('tanggal_sampai')) {
|
if ($request->filled('tanggal_sampai')) {
|
||||||
$query->whereDate('tanggal_transaksi', '<=', $request->tanggal_sampai);
|
$query->whereDate('tanggal_transaksi', '<=', $request->tanggal_sampai);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search
|
// -- Search keterangan --
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$search = $request->search;
|
$search = $request->search;
|
||||||
$query->where(function($q) use ($search) {
|
$query->where(function ($q) use ($search) {
|
||||||
$q->where('keterangan', 'like', "%{$search}%")
|
$q->where('keterangan', 'like', "%{$search}%")
|
||||||
->orWhere('id_uang_saku', 'like', "%{$search}%");
|
->orWhere('id_uang_saku', 'like', "%{$search}%");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Urutkan dari yang terbaru
|
$riwayatUangSaku = $query->orderBy('tanggal_transaksi', 'desc')
|
||||||
$query->orderBy('tanggal_transaksi', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
->orderBy('created_at', 'desc');
|
->paginate(15)
|
||||||
|
->withQueryString();
|
||||||
|
|
||||||
// Pagination
|
// -- Statistik: bulan ini atau sesuai filter tanggal --
|
||||||
$riwayatUangSaku = $query->paginate(15)->withQueryString();
|
$statistikQuery = UangSaku::where('id_santri', $idSantri);
|
||||||
|
|
||||||
// ✅ Hitung statistik berdasarkan filter atau bulan ini
|
|
||||||
$statistikQuery = UangSaku::where('id_santri', $santri->id_santri);
|
|
||||||
|
|
||||||
// Jika ada filter tanggal, gunakan filter tersebut
|
|
||||||
if ($request->filled('tanggal_dari') || $request->filled('tanggal_sampai')) {
|
if ($request->filled('tanggal_dari') || $request->filled('tanggal_sampai')) {
|
||||||
if ($request->filled('tanggal_dari')) {
|
if ($request->filled('tanggal_dari')) {
|
||||||
$statistikQuery->whereDate('tanggal_transaksi', '>=', $request->tanggal_dari);
|
$statistikQuery->whereDate('tanggal_transaksi', '>=', $request->tanggal_dari);
|
||||||
|
|
@ -78,32 +69,14 @@ public function index(Request $request)
|
||||||
$statistikQuery->whereDate('tanggal_transaksi', '<=', $request->tanggal_sampai);
|
$statistikQuery->whereDate('tanggal_transaksi', '<=', $request->tanggal_sampai);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Jika tidak ada filter, tampilkan data bulan ini saja
|
|
||||||
$statistikQuery->whereMonth('tanggal_transaksi', now()->month)
|
$statistikQuery->whereMonth('tanggal_transaksi', now()->month)
|
||||||
->whereYear('tanggal_transaksi', now()->year);
|
->whereYear('tanggal_transaksi', now()->year);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone query untuk menghitung pemasukan dan pengeluaran
|
|
||||||
$totalPemasukan = (clone $statistikQuery)->where('jenis_transaksi', 'pemasukan')->sum('nominal');
|
$totalPemasukan = (clone $statistikQuery)->where('jenis_transaksi', 'pemasukan')->sum('nominal');
|
||||||
$totalPengeluaran = (clone $statistikQuery)->where('jenis_transaksi', 'pengeluaran')->sum('nominal');
|
$totalPengeluaran = (clone $statistikQuery)->where('jenis_transaksi', 'pengeluaran')->sum('nominal');
|
||||||
|
|
||||||
// Saldo terakhir tetap dari data terbaru (tidak terpengaruh filter)
|
|
||||||
$saldoTerakhir = $santri->saldo_uang_saku;
|
$saldoTerakhir = $santri->saldo_uang_saku;
|
||||||
|
|
||||||
// Info periode untuk ditampilkan di view
|
|
||||||
if ($request->filled('tanggal_dari') || $request->filled('tanggal_sampai')) {
|
|
||||||
$periodeTeks = 'Periode: ';
|
|
||||||
if ($request->filled('tanggal_dari')) {
|
|
||||||
$periodeTeks .= \Carbon\Carbon::parse($request->tanggal_dari)->format('d/m/Y');
|
|
||||||
}
|
|
||||||
$periodeTeks .= ' - ';
|
|
||||||
if ($request->filled('tanggal_sampai')) {
|
|
||||||
$periodeTeks .= \Carbon\Carbon::parse($request->tanggal_sampai)->format('d/m/Y');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$periodeTeks = 'Bulan Ini: ' . now()->isoFormat('MMMM YYYY');
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('santri.uang-saku.index', compact(
|
return view('santri.uang-saku.index', compact(
|
||||||
'riwayatUangSaku',
|
'riwayatUangSaku',
|
||||||
'santri',
|
'santri',
|
||||||
|
|
@ -113,39 +86,33 @@ public function index(Request $request)
|
||||||
));
|
));
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Error di Riwayat Uang Saku Santri: ' . $e->getMessage());
|
Log::error('Error Riwayat Uang Saku: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Terjadi kesalahan saat memuat data uang saku.');
|
||||||
return back()->with('error', 'Terjadi kesalahan saat memuat riwayat uang saku');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan detail transaksi
|
* Tampilkan detail satu transaksi
|
||||||
*/
|
*/
|
||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$user = Auth::user();
|
$idSantri = $this->getSantriId();
|
||||||
|
|
||||||
// Ambil data santri
|
// Pastikan transaksi ini milik santri yang login
|
||||||
$santri = Santri::where('id_santri', $user->role_id)->first();
|
|
||||||
|
|
||||||
if (!$santri) {
|
|
||||||
abort(404, 'Data santri tidak ditemukan');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ambil transaksi dengan validasi kepemilikan
|
|
||||||
$transaksi = UangSaku::where('id', $id)
|
$transaksi = UangSaku::where('id', $id)
|
||||||
->where('id_santri', $santri->id_santri)
|
->where('id_santri', $idSantri)
|
||||||
->with('santri:id_santri,nama_lengkap,kelas')
|
->with(['santri' => function ($q) {
|
||||||
|
$q->with('kelasPrimary.kelas')
|
||||||
|
->select('id_santri', 'nama_lengkap');
|
||||||
|
}])
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
return view('santri.uang-saku.show', compact('transaksi', 'santri'));
|
return view('santri.uang-saku.show', compact('transaksi'));
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Error di Detail Uang Saku: ' . $e->getMessage());
|
Log::error('Error Detail Uang Saku: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Transaksi tidak ditemukan atau Anda tidak memiliki akses.');
|
||||||
return back()->with('error', 'Transaksi tidak ditemukan atau Anda tidak memiliki akses');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,8 +36,11 @@ class Kernel extends HttpKernel
|
||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
\Illuminate\Session\Middleware\AuthenticateSession::class,
|
// AuthenticateSession dipindah dari global ke alias 'auth.session'
|
||||||
\App\Http\Middleware\ClearStuckSession::class,
|
// agar tidak menyebabkan redirect loop pada user tanpa remember_token
|
||||||
|
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||||
|
// ClearStuckSession dihapus karena menyebabkan session flush setelah login
|
||||||
|
// \App\Http\Middleware\ClearStuckSession::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'api' => [
|
'api' => [
|
||||||
|
|
@ -67,5 +70,6 @@ class Kernel extends HttpKernel
|
||||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||||
'role' => \App\Http\Middleware\Role::class,
|
'role' => \App\Http\Middleware\Role::class,
|
||||||
|
'santri.auth' => \App\Http\Middleware\CheckSantriAuth::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
// app/Http/Middleware/CheckSantriAuth.php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CheckSantriAuth
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (!Auth::guard('santri')->check()) {
|
||||||
|
// Jika request AJAX/API, return JSON
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
return response()->json(['message' => 'Unauthenticated.'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('santri.login')
|
||||||
|
->with('error', 'Silakan login terlebih dahulu.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = Auth::guard('santri')->user();
|
||||||
|
|
||||||
|
// PERBAIKAN: pastikan akun masih valid dan punya id_santri
|
||||||
|
if (!$account || !$account->id_santri) {
|
||||||
|
Auth::guard('santri')->logout();
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect()->route('santri.login')
|
||||||
|
->with('error', 'Akun tidak valid. Silakan login ulang.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use App\Providers\RouteServiceProvider;
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
@ -10,18 +9,19 @@
|
||||||
|
|
||||||
class RedirectIfAuthenticated
|
class RedirectIfAuthenticated
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Handle an incoming request.
|
|
||||||
*
|
|
||||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||||
{
|
{
|
||||||
$guards = empty($guards) ? [null] : $guards;
|
$path = $request->path();
|
||||||
|
|
||||||
foreach ($guards as $guard) {
|
if (str_starts_with($path, 'santri')) {
|
||||||
if (Auth::guard($guard)->check()) {
|
// Halaman guest santri → redirect hanya jika guard santri aktif
|
||||||
return redirect(RouteServiceProvider::HOME);
|
if (Auth::guard('santri')->check()) {
|
||||||
|
return redirect()->route('santri.dashboard');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Halaman guest admin → redirect hanya jika guard web aktif
|
||||||
|
if (Auth::check()) {
|
||||||
|
return redirect()->route('admin.dashboard');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,45 +9,27 @@
|
||||||
|
|
||||||
class Role
|
class Role
|
||||||
{
|
{
|
||||||
public function handle(Request $request, Closure $next, string $roles): Response
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
* Menerima daftar role yang diizinkan sebagai parameter middleware.
|
||||||
|
* Contoh: role:super_admin,akademik,pamong
|
||||||
|
*
|
||||||
|
* Laravel memecah parameter setelah ':' menjadi argumen terpisah per koma,
|
||||||
|
* sehingga kita harus gunakan variadic (...$roles) bukan string tunggal.
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, string ...$roles): Response
|
||||||
{
|
{
|
||||||
// 1. Cek apakah pengguna sudah login
|
// -- Cek apakah pengguna sudah login --
|
||||||
if (!Auth::check()) {
|
if (!Auth::check()) {
|
||||||
// Clear session jika belum login tapi masih ada session
|
return redirect()->route('admin.login');
|
||||||
$request->session()->flush();
|
|
||||||
$request->session()->regenerate();
|
|
||||||
|
|
||||||
return redirect('/admin/login');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ambil role pengguna saat ini
|
// -- Cek apakah role pengguna termasuk dalam daftar yang diizinkan --
|
||||||
$currentRole = Auth::user()->role;
|
if (!in_array(Auth::user()->role, $roles)) {
|
||||||
|
|
||||||
// Pisahkan daftar role yang diizinkan
|
|
||||||
$allowedRoles = explode(',', $roles);
|
|
||||||
|
|
||||||
// 2. Cek apakah role pengguna termasuk dalam daftar yang diizinkan
|
|
||||||
if (!in_array($currentRole, $allowedRoles)) {
|
|
||||||
// ✅ TAMBAHAN: Redirect ke dashboard yang sesuai, jangan abort
|
|
||||||
if ($currentRole === 'admin') {
|
|
||||||
return redirect()->route('admin.dashboard')
|
return redirect()->route('admin.dashboard')
|
||||||
->with('error', 'Anda tidak memiliki akses ke halaman tersebut.');
|
->with('error', 'Anda tidak memiliki akses ke halaman tersebut.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($currentRole === 'santri' || $currentRole === 'wali') {
|
|
||||||
return redirect()->route('santri.dashboard')
|
|
||||||
->with('error', 'Anda tidak memiliki akses ke halaman tersebut.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jika role tidak dikenali, logout paksa
|
|
||||||
Auth::logout();
|
|
||||||
$request->session()->invalidate();
|
|
||||||
$request->session()->regenerate();
|
|
||||||
|
|
||||||
return redirect('/admin/login')
|
|
||||||
->with('error', 'Role tidak valid. Silakan login kembali.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
// app/Mail/OtpMail.php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class OtpMail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public string $otp;
|
||||||
|
public string $nama;
|
||||||
|
|
||||||
|
public function __construct(string $otp, string $nama)
|
||||||
|
{
|
||||||
|
$this->otp = $otp;
|
||||||
|
$this->nama = $nama;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: 'Kode OTP Reset Password - SIM PKPPS',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
view: 'emails.otp',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachments(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -85,6 +85,8 @@ public function getStatusBadgeAttribute()
|
||||||
'Izin' => '<span class="badge badge-warning"><i class="fas fa-info-circle"></i> Izin</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>',
|
'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>',
|
'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;
|
return $badges[$this->status] ?? $this->status;
|
||||||
|
|
@ -120,6 +122,8 @@ public function getStatusBadgeClassAttribute()
|
||||||
'Izin' => 'badge-info',
|
'Izin' => 'badge-info',
|
||||||
'Sakit' => 'badge-warning',
|
'Sakit' => 'badge-warning',
|
||||||
'Alpa' => 'badge-danger',
|
'Alpa' => 'badge-danger',
|
||||||
|
'Terlambat' => 'badge-warning',
|
||||||
|
'Pulang' => 'badge-secondary',
|
||||||
default => 'badge-secondary',
|
default => 'badge-secondary',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PasswordResetOtp extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'password_reset_otps';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'email',
|
||||||
|
'otp',
|
||||||
|
'expired_at',
|
||||||
|
'is_verified',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'expired_at' => 'datetime',
|
||||||
|
'is_verified' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah OTP sudah expired
|
||||||
|
*/
|
||||||
|
public function isExpired(): bool
|
||||||
|
{
|
||||||
|
return now()->greaterThan($this->expired_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,9 +33,10 @@ class PembayaranSpp extends Model
|
||||||
'updated_at' => 'datetime',
|
'updated_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
// ══════════════════════════════════════════════════════
|
||||||
* Boot method untuk auto-generate ID
|
// BOOT
|
||||||
*/
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
protected static function boot()
|
protected static function boot()
|
||||||
{
|
{
|
||||||
parent::boot();
|
parent::boot();
|
||||||
|
|
@ -49,47 +50,152 @@ protected static function boot()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ══════════════════════════════════════════════════════
|
||||||
* Relasi: Pembayaran SPP milik satu Santri
|
// RELASI
|
||||||
*/
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
public function santri()
|
public function santri()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
|
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// CICILAN HELPERS
|
||||||
|
//
|
||||||
|
// Status di DB tetap "Belum Lunas" (tidak ubah enum).
|
||||||
|
// Cicilan dideteksi dari keterangan berformat JSON:
|
||||||
|
// {"terbayar": 150000, "catatan": "Cicilan ke-1"}
|
||||||
|
//
|
||||||
|
// Keterangan teks biasa (non-JSON) tetap terbaca normal.
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accessor: Nama bulan dalam bahasa Indonesia
|
* Cek apakah record ini berstatus cicilan
|
||||||
|
* (status Belum Lunas + ada data terbayar di keterangan).
|
||||||
*/
|
*/
|
||||||
public function getBulanNamaAttribute()
|
public function isCicilan(): bool
|
||||||
|
{
|
||||||
|
if ($this->status !== 'Belum Lunas') return false;
|
||||||
|
$data = $this->getCicilanData();
|
||||||
|
return $data !== null && ($data['terbayar'] ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil array cicilan dari keterangan, atau null jika bukan JSON cicilan.
|
||||||
|
*/
|
||||||
|
public function getCicilanData(): ?array
|
||||||
|
{
|
||||||
|
if (!$this->keterangan) return null;
|
||||||
|
$decoded = json_decode($this->keterangan, true);
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) return null;
|
||||||
|
if (!array_key_exists('terbayar', $decoded)) return null;
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nominal yang sudah dibayar.
|
||||||
|
*/
|
||||||
|
public function getNominalTerbayarAttribute(): float
|
||||||
|
{
|
||||||
|
if ($this->status === 'Lunas') return (float) $this->nominal;
|
||||||
|
$data = $this->getCicilanData();
|
||||||
|
return $data ? (float) ($data['terbayar'] ?? 0) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sisa yang belum dibayar.
|
||||||
|
*/
|
||||||
|
public function getNominalSisaAttribute(): float
|
||||||
|
{
|
||||||
|
return max(0, (float) $this->nominal - $this->nominal_terbayar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persentase cicilan (0–100).
|
||||||
|
*/
|
||||||
|
public function getPorsentaseCicilanAttribute(): int
|
||||||
|
{
|
||||||
|
if (!$this->nominal || (float) $this->nominal == 0) return 0;
|
||||||
|
return (int) min(100, round(($this->nominal_terbayar / (float) $this->nominal) * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simpan progres cicilan ke keterangan (JSON).
|
||||||
|
* Status DB tidak diubah — tetap "Belum Lunas".
|
||||||
|
*/
|
||||||
|
public function setCicilan(float $terbayar, ?string $catatan = null): void
|
||||||
|
{
|
||||||
|
// Jika keterangan sebelumnya teks biasa, pindahkan sebagai catatan
|
||||||
|
if ($this->keterangan && !$this->getCicilanData()) {
|
||||||
|
$catatan = $catatan ?? $this->keterangan;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = ['terbayar' => $terbayar];
|
||||||
|
if ($catatan) $data['catatan'] = $catatan;
|
||||||
|
|
||||||
|
$this->keterangan = json_encode($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baca catatan teks (dari JSON atau teks biasa).
|
||||||
|
*/
|
||||||
|
public function getCatatanTeksAttribute(): ?string
|
||||||
|
{
|
||||||
|
if (!$this->keterangan) return null;
|
||||||
|
$data = $this->getCicilanData();
|
||||||
|
if ($data) return $data['catatan'] ?? null;
|
||||||
|
return $this->keterangan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// ACCESSORS
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public function getBulanNamaAttribute(): string
|
||||||
{
|
{
|
||||||
$bulanIndo = [
|
$bulanIndo = [
|
||||||
1 => 'Januari', 2 => 'Februari', 3 => 'Maret',
|
1 => 'Januari', 2 => 'Februari', 3 => 'Maret',
|
||||||
4 => 'April', 5 => 'Mei', 6 => 'Juni',
|
4 => 'April', 5 => 'Mei', 6 => 'Juni',
|
||||||
7 => 'Juli', 8 => 'Agustus', 9 => 'September',
|
7 => 'Juli', 8 => 'Agustus', 9 => 'September',
|
||||||
10 => 'Oktober', 11 => 'November', 12 => 'Desember'
|
10 => 'Oktober',11 => 'November', 12 => 'Desember'
|
||||||
];
|
];
|
||||||
|
|
||||||
return $bulanIndo[$this->bulan] ?? '-';
|
return $bulanIndo[$this->bulan] ?? '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function getPeriodeLengkapAttribute(): string
|
||||||
* Accessor: Periode lengkap (Januari 2024)
|
|
||||||
*/
|
|
||||||
public function getPeriodeLengkapAttribute()
|
|
||||||
{
|
{
|
||||||
return $this->bulan_nama . ' ' . $this->tahun;
|
return $this->bulan_nama . ' ' . $this->tahun;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getNominalFormatAttribute(): string
|
||||||
|
{
|
||||||
|
return 'Rp ' . number_format($this->nominal, 0, ',', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNominalTerbayarFormatAttribute(): string
|
||||||
|
{
|
||||||
|
return 'Rp ' . number_format($this->nominal_terbayar, 0, ',', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNominalSisaFormatAttribute(): string
|
||||||
|
{
|
||||||
|
return 'Rp ' . number_format($this->nominal_sisa, 0, ',', '.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accessor: Status Badge HTML
|
* Status Badge HTML — mengenali cicilan dari keterangan JSON,
|
||||||
|
* bukan dari nilai kolom status.
|
||||||
*/
|
*/
|
||||||
public function getStatusBadgeAttribute()
|
public function getStatusBadgeAttribute(): string
|
||||||
{
|
{
|
||||||
if ($this->status === 'Lunas') {
|
if ($this->status === 'Lunas') {
|
||||||
return '<span class="badge badge-success"><i class="fas fa-check-circle"></i> Lunas</span>';
|
return '<span class="badge badge-success"><i class="fas fa-check-circle"></i> Lunas</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cek apakah telat
|
if ($this->isCicilan()) {
|
||||||
|
return '<span class="badge badge-cicilan"><i class="fas fa-coins"></i> Cicilan ' . $this->porsentase_cicilan . '%</span>';
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isTelat()) {
|
if ($this->isTelat()) {
|
||||||
return '<span class="badge badge-danger"><i class="fas fa-exclamation-triangle"></i> Belum Lunas (Telat)</span>';
|
return '<span class="badge badge-danger"><i class="fas fa-exclamation-triangle"></i> Belum Lunas (Telat)</span>';
|
||||||
}
|
}
|
||||||
|
|
@ -97,73 +203,49 @@ public function getStatusBadgeAttribute()
|
||||||
return '<span class="badge badge-warning"><i class="fas fa-clock"></i> Belum Lunas</span>';
|
return '<span class="badge badge-warning"><i class="fas fa-clock"></i> Belum Lunas</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ══════════════════════════════════════════════════════
|
||||||
* Cek apakah pembayaran sudah telat
|
// HELPERS
|
||||||
*/
|
// ══════════════════════════════════════════════════════
|
||||||
public function isTelat()
|
|
||||||
{
|
|
||||||
if ($this->status === 'Lunas') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public function isTelat(): bool
|
||||||
|
{
|
||||||
|
if ($this->status === 'Lunas') return false;
|
||||||
return Carbon::now()->isAfter($this->batas_bayar);
|
return Carbon::now()->isAfter($this->batas_bayar);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ══════════════════════════════════════════════════════
|
||||||
* Accessor: Nominal format Rupiah
|
// SCOPES
|
||||||
*/
|
// ══════════════════════════════════════════════════════
|
||||||
public function getNominalFormatAttribute()
|
|
||||||
{
|
|
||||||
return 'Rp ' . number_format($this->nominal, 0, ',', '.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope: Filter pembayaran belum lunas
|
|
||||||
*/
|
|
||||||
public function scopeBelumLunas($query)
|
public function scopeBelumLunas($query)
|
||||||
{
|
{
|
||||||
return $query->where('status', 'Belum Lunas');
|
return $query->where('status', 'Belum Lunas');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope: Filter pembayaran lunas
|
|
||||||
*/
|
|
||||||
public function scopeLunas($query)
|
public function scopeLunas($query)
|
||||||
{
|
{
|
||||||
return $query->where('status', 'Lunas');
|
return $query->where('status', 'Lunas');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope: Filter pembayaran telat
|
|
||||||
*/
|
|
||||||
public function scopeTelat($query)
|
public function scopeTelat($query)
|
||||||
{
|
{
|
||||||
return $query->where('status', 'Belum Lunas')
|
return $query->where('status', 'Belum Lunas')
|
||||||
->where('batas_bayar', '<', Carbon::now());
|
->where('batas_bayar', '<', Carbon::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope: Filter by tahun
|
|
||||||
*/
|
|
||||||
public function scopeTahun($query, $tahun)
|
public function scopeTahun($query, $tahun)
|
||||||
{
|
{
|
||||||
return $query->where('tahun', $tahun);
|
return $query->where('tahun', $tahun);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope: Filter by bulan
|
|
||||||
*/
|
|
||||||
public function scopeBulan($query, $bulan)
|
public function scopeBulan($query, $bulan)
|
||||||
{
|
{
|
||||||
return $query->where('bulan', $bulan);
|
return $query->where('bulan', $bulan);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope: Search
|
|
||||||
*/
|
|
||||||
public function scopeSearch($query, $search)
|
public function scopeSearch($query, $search)
|
||||||
{
|
{
|
||||||
return $query->whereHas('santri', function($q) use ($search) {
|
return $query->whereHas('santri', function ($q) use ($search) {
|
||||||
$q->where('nama_lengkap', 'like', "%{$search}%")
|
$q->where('nama_lengkap', 'like', "%{$search}%")
|
||||||
->orWhere('id_santri', 'like', "%{$search}%")
|
->orWhere('id_santri', 'like', "%{$search}%")
|
||||||
->orWhere('nis', 'like', "%{$search}%");
|
->orWhere('nis', 'like', "%{$search}%");
|
||||||
|
|
|
||||||
|
|
@ -52,20 +52,28 @@ protected static function boot()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relasi: Santri memiliki satu User Account (hasOne)
|
* Relasi: Santri memiliki banyak akun (santri_accounts)
|
||||||
|
*/
|
||||||
|
public function santriAccount()
|
||||||
|
{
|
||||||
|
return $this->hasMany(SantriAccount::class, 'id_santri', 'id_santri');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relasi: Santri memiliki satu User Account (hasOne) - LEGACY
|
||||||
*/
|
*/
|
||||||
public function user()
|
public function user()
|
||||||
{
|
{
|
||||||
return $this->hasOne(User::class, 'role_id', 'id_santri')
|
return $this->hasOne(SantriAccount::class, 'id_santri', 'id_santri')
|
||||||
->where('role', 'santri');
|
->where('role', 'santri');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relasi: Santri memiliki satu akun Wali (orang tua)
|
* Relasi: Santri memiliki satu akun Wali (orang tua) - LEGACY
|
||||||
*/
|
*/
|
||||||
public function waliUser()
|
public function waliUser()
|
||||||
{
|
{
|
||||||
return $this->hasOne(User::class, 'role_id', 'id_santri')
|
return $this->hasOne(SantriAccount::class, 'id_santri', 'id_santri')
|
||||||
->where('role', 'wali');
|
->where('role', 'wali');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
|
class SantriAccount extends Authenticatable
|
||||||
|
{
|
||||||
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
|
|
||||||
|
protected $table = 'santri_accounts';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'id_santri',
|
||||||
|
'username',
|
||||||
|
'password',
|
||||||
|
'role',
|
||||||
|
'last_login',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
// HAPUS 'password' => 'hashed' — hanya Laravel 10+
|
||||||
|
protected $casts = [
|
||||||
|
'last_login' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ganti dengan mutator manual
|
||||||
|
public function setPasswordAttribute(string $value): void
|
||||||
|
{
|
||||||
|
// Cegah double hash
|
||||||
|
if (
|
||||||
|
!str_starts_with($value, '$2y$') &&
|
||||||
|
!str_starts_with($value, '$argon2i$') &&
|
||||||
|
!str_starts_with($value, '$argon2id$')
|
||||||
|
) {
|
||||||
|
$this->attributes['password'] = bcrypt($value);
|
||||||
|
} else {
|
||||||
|
$this->attributes['password'] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function santri()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSantri(): bool
|
||||||
|
{
|
||||||
|
return $this->role === 'santri';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isWali(): bool
|
||||||
|
{
|
||||||
|
return $this->role === 'wali';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,6 @@ class User extends Authenticatable
|
||||||
'username',
|
'username',
|
||||||
'password',
|
'password',
|
||||||
'role',
|
'role',
|
||||||
'role_id',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
|
|
@ -31,35 +30,47 @@ class User extends Authenticatable
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
// ══════════════════ HELPER METHODS ══════════════════
|
||||||
* Relasi ke Santri
|
|
||||||
*/
|
|
||||||
public function santri()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Santri::class, 'role_id', 'id_santri');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relasi ke Wali
|
* Cek apakah user adalah admin (semua role admin)
|
||||||
*/
|
*/
|
||||||
public function wali()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Wali::class, 'role_id', 'id_wali');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
public function isAdmin()
|
public function isAdmin()
|
||||||
{
|
{
|
||||||
return $this->role === 'admin';
|
return in_array($this->role, ['super_admin', 'akademik', 'pamong']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isSantri()
|
/**
|
||||||
|
* Cek apakah user adalah super admin
|
||||||
|
*/
|
||||||
|
public function isSuperAdmin()
|
||||||
{
|
{
|
||||||
return $this->role === 'santri';
|
return $this->role === 'super_admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isWali()
|
/**
|
||||||
|
* Cek apakah user adalah akademik
|
||||||
|
*/
|
||||||
|
public function isAkademik()
|
||||||
{
|
{
|
||||||
return $this->role === 'wali';
|
return $this->role === 'akademik';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah user adalah pamong
|
||||||
|
*/
|
||||||
|
public function isPamong()
|
||||||
|
{
|
||||||
|
return $this->role === 'pamong';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah user memiliki salah satu role yang diberikan.
|
||||||
|
* Contoh: $user->hasRole('super_admin', 'akademik')
|
||||||
|
*/
|
||||||
|
public function hasRole()
|
||||||
|
{
|
||||||
|
$roles = func_get_args();
|
||||||
|
return in_array($this->role, $roles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Pagination\Paginator;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -19,6 +20,7 @@ public function register(): void
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
Paginator::defaultView('vendor.pagination.custom');
|
||||||
|
Paginator::defaultSimpleView('vendor.pagination.custom');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
// app/Services/CapaianAccessService.php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service untuk mengelola akses input capaian oleh santri.
|
||||||
|
* Menggunakan Laravel Cache (no migration needed).
|
||||||
|
* Data disimpan di cache dengan key 'capaian_access_config'.
|
||||||
|
*/
|
||||||
|
class CapaianAccessService
|
||||||
|
{
|
||||||
|
const CACHE_KEY = 'capaian_access_config';
|
||||||
|
const CACHE_TTL = 60 * 24 * 30; // 30 hari (dalam menit)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil konfigurasi akses saat ini.
|
||||||
|
*/
|
||||||
|
public static function getConfig(): array
|
||||||
|
{
|
||||||
|
return Cache::get(self::CACHE_KEY, [
|
||||||
|
'is_open' => false,
|
||||||
|
'opened_by' => null,
|
||||||
|
'opened_at' => null,
|
||||||
|
'closed_at' => null,
|
||||||
|
'catatan' => null,
|
||||||
|
'id_semester' => null,
|
||||||
|
// Opsional: auto-close setelah X jam (null = manual)
|
||||||
|
'auto_close_at'=> null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buka akses input capaian untuk santri.
|
||||||
|
*/
|
||||||
|
public static function open(array $params = []): void
|
||||||
|
{
|
||||||
|
$config = self::getConfig();
|
||||||
|
|
||||||
|
$autoCloseAt = null;
|
||||||
|
if (!empty($params['durasi_jam'])) {
|
||||||
|
$autoCloseAt = now()->addHours((int) $params['durasi_jam'])->toIso8601String();
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = array_merge($config, [
|
||||||
|
'is_open' => true,
|
||||||
|
'opened_by' => $params['opened_by'] ?? auth()->user()?->name,
|
||||||
|
'opened_at' => now()->toIso8601String(),
|
||||||
|
'closed_at' => null,
|
||||||
|
'catatan' => $params['catatan'] ?? null,
|
||||||
|
'id_semester' => $params['id_semester'] ?? null,
|
||||||
|
'auto_close_at' => $autoCloseAt,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cache::put(self::CACHE_KEY, $config, now()->addMinutes(self::CACHE_TTL));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tutup akses input capaian.
|
||||||
|
*/
|
||||||
|
public static function close(): void
|
||||||
|
{
|
||||||
|
$config = self::getConfig();
|
||||||
|
$config['is_open'] = false;
|
||||||
|
$config['closed_at'] = now()->toIso8601String();
|
||||||
|
Cache::put(self::CACHE_KEY, $config, now()->addMinutes(self::CACHE_TTL));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah akses sedang dibuka.
|
||||||
|
* Otomatis tutup jika sudah melewati auto_close_at.
|
||||||
|
*/
|
||||||
|
public static function isOpen(): bool
|
||||||
|
{
|
||||||
|
$config = self::getConfig();
|
||||||
|
|
||||||
|
if (!$config['is_open']) return false;
|
||||||
|
|
||||||
|
// Auto-close check
|
||||||
|
if (!empty($config['auto_close_at'])) {
|
||||||
|
if (now()->isAfter($config['auto_close_at'])) {
|
||||||
|
self::close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah semester yang dibuka cocok dengan semester tertentu.
|
||||||
|
* Jika id_semester di config null, berarti semua semester boleh.
|
||||||
|
*/
|
||||||
|
public static function isOpenForSemester(?string $idSemester): bool
|
||||||
|
{
|
||||||
|
if (!self::isOpen()) return false;
|
||||||
|
|
||||||
|
$config = self::getConfig();
|
||||||
|
if (empty($config['id_semester'])) return true; // semua semester
|
||||||
|
|
||||||
|
return $config['id_semester'] === $idSemester;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil sisa waktu auto-close dalam format human readable.
|
||||||
|
*/
|
||||||
|
public static function getSisaWaktu(): ?string
|
||||||
|
{
|
||||||
|
$config = self::getConfig();
|
||||||
|
if (empty($config['auto_close_at'])) return null;
|
||||||
|
|
||||||
|
$close = \Carbon\Carbon::parse($config['auto_close_at']);
|
||||||
|
if (now()->isAfter($close)) return 'Sudah berakhir';
|
||||||
|
|
||||||
|
return now()->diffForHumans($close, ['parts' => 2, 'syntax' => \Carbon\CarbonInterface::DIFF_ABSOLUTE]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
*
|
|
||||||
!.gitignore
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php return array (
|
||||||
|
'barryvdh/laravel-dompdf' =>
|
||||||
|
array (
|
||||||
|
'aliases' =>
|
||||||
|
array (
|
||||||
|
'PDF' => 'Barryvdh\\DomPDF\\Facade\\Pdf',
|
||||||
|
'Pdf' => 'Barryvdh\\DomPDF\\Facade\\Pdf',
|
||||||
|
),
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'Barryvdh\\DomPDF\\ServiceProvider',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'laravel/breeze' =>
|
||||||
|
array (
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'Laravel\\Breeze\\BreezeServiceProvider',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'laravel/sail' =>
|
||||||
|
array (
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'Laravel\\Sail\\SailServiceProvider',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'laravel/sanctum' =>
|
||||||
|
array (
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'Laravel\\Sanctum\\SanctumServiceProvider',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'laravel/tinker' =>
|
||||||
|
array (
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'nesbot/carbon' =>
|
||||||
|
array (
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'Carbon\\Laravel\\ServiceProvider',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'nunomaduro/collision' =>
|
||||||
|
array (
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'nunomaduro/termwind' =>
|
||||||
|
array (
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'Termwind\\Laravel\\TermwindServiceProvider',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'spatie/laravel-ignition' =>
|
||||||
|
array (
|
||||||
|
'aliases' =>
|
||||||
|
array (
|
||||||
|
'Flare' => 'Spatie\\LaravelIgnition\\Facades\\Flare',
|
||||||
|
),
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
<?php return array (
|
||||||
|
'providers' =>
|
||||||
|
array (
|
||||||
|
0 => 'Illuminate\\Auth\\AuthServiceProvider',
|
||||||
|
1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||||
|
2 => 'Illuminate\\Bus\\BusServiceProvider',
|
||||||
|
3 => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||||
|
4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
5 => 'Illuminate\\Cookie\\CookieServiceProvider',
|
||||||
|
6 => 'Illuminate\\Database\\DatabaseServiceProvider',
|
||||||
|
7 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
|
||||||
|
8 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
|
||||||
|
9 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
|
||||||
|
10 => 'Illuminate\\Hashing\\HashServiceProvider',
|
||||||
|
11 => 'Illuminate\\Mail\\MailServiceProvider',
|
||||||
|
12 => 'Illuminate\\Notifications\\NotificationServiceProvider',
|
||||||
|
13 => 'Illuminate\\Pagination\\PaginationServiceProvider',
|
||||||
|
14 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
|
||||||
|
15 => 'Illuminate\\Pipeline\\PipelineServiceProvider',
|
||||||
|
16 => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||||
|
17 => 'Illuminate\\Redis\\RedisServiceProvider',
|
||||||
|
18 => 'Illuminate\\Session\\SessionServiceProvider',
|
||||||
|
19 => 'Illuminate\\Translation\\TranslationServiceProvider',
|
||||||
|
20 => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||||
|
21 => 'Illuminate\\View\\ViewServiceProvider',
|
||||||
|
22 => 'Barryvdh\\DomPDF\\ServiceProvider',
|
||||||
|
23 => 'Laravel\\Breeze\\BreezeServiceProvider',
|
||||||
|
24 => 'Laravel\\Sail\\SailServiceProvider',
|
||||||
|
25 => 'Laravel\\Sanctum\\SanctumServiceProvider',
|
||||||
|
26 => 'Laravel\\Tinker\\TinkerServiceProvider',
|
||||||
|
27 => 'Carbon\\Laravel\\ServiceProvider',
|
||||||
|
28 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
|
||||||
|
29 => 'Termwind\\Laravel\\TermwindServiceProvider',
|
||||||
|
30 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider',
|
||||||
|
31 => 'App\\Providers\\AppServiceProvider',
|
||||||
|
32 => 'App\\Providers\\AuthServiceProvider',
|
||||||
|
33 => 'App\\Providers\\EventServiceProvider',
|
||||||
|
34 => 'App\\Providers\\RouteServiceProvider',
|
||||||
|
),
|
||||||
|
'eager' =>
|
||||||
|
array (
|
||||||
|
0 => 'Illuminate\\Auth\\AuthServiceProvider',
|
||||||
|
1 => 'Illuminate\\Cookie\\CookieServiceProvider',
|
||||||
|
2 => 'Illuminate\\Database\\DatabaseServiceProvider',
|
||||||
|
3 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
|
||||||
|
4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
|
||||||
|
5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
|
||||||
|
6 => 'Illuminate\\Notifications\\NotificationServiceProvider',
|
||||||
|
7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
|
||||||
|
8 => 'Illuminate\\Session\\SessionServiceProvider',
|
||||||
|
9 => 'Illuminate\\View\\ViewServiceProvider',
|
||||||
|
10 => 'Barryvdh\\DomPDF\\ServiceProvider',
|
||||||
|
11 => 'Laravel\\Sanctum\\SanctumServiceProvider',
|
||||||
|
12 => 'Carbon\\Laravel\\ServiceProvider',
|
||||||
|
13 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
|
||||||
|
14 => 'Termwind\\Laravel\\TermwindServiceProvider',
|
||||||
|
15 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider',
|
||||||
|
16 => 'App\\Providers\\AppServiceProvider',
|
||||||
|
17 => 'App\\Providers\\AuthServiceProvider',
|
||||||
|
18 => 'App\\Providers\\EventServiceProvider',
|
||||||
|
19 => 'App\\Providers\\RouteServiceProvider',
|
||||||
|
),
|
||||||
|
'deferred' =>
|
||||||
|
array (
|
||||||
|
'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||||
|
'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||||
|
'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||||
|
'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||||
|
'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||||
|
'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||||
|
'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||||
|
'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||||
|
'cache' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||||
|
'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||||
|
'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||||
|
'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||||
|
'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||||
|
'hash' => 'Illuminate\\Hashing\\HashServiceProvider',
|
||||||
|
'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider',
|
||||||
|
'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider',
|
||||||
|
'mailer' => 'Illuminate\\Mail\\MailServiceProvider',
|
||||||
|
'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider',
|
||||||
|
'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
|
||||||
|
'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
|
||||||
|
'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
|
||||||
|
'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
|
||||||
|
'queue' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||||
|
'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||||
|
'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||||
|
'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||||
|
'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||||
|
'redis' => 'Illuminate\\Redis\\RedisServiceProvider',
|
||||||
|
'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider',
|
||||||
|
'translator' => 'Illuminate\\Translation\\TranslationServiceProvider',
|
||||||
|
'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider',
|
||||||
|
'validator' => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||||
|
'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||||
|
'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||||
|
'Laravel\\Breeze\\Console\\InstallCommand' => 'Laravel\\Breeze\\BreezeServiceProvider',
|
||||||
|
'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider',
|
||||||
|
'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider',
|
||||||
|
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
|
||||||
|
),
|
||||||
|
'when' =>
|
||||||
|
array (
|
||||||
|
'Illuminate\\Broadcasting\\BroadcastServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Bus\\BusServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Cache\\CacheServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Hashing\\HashServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Mail\\MailServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Pipeline\\PipelineServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Queue\\QueueServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Redis\\RedisServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Translation\\TranslationServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Illuminate\\Validation\\ValidationServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Laravel\\Breeze\\BreezeServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Laravel\\Sail\\SailServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
'Laravel\\Tinker\\TinkerServiceProvider' =>
|
||||||
|
array (
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -7,10 +7,12 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"barryvdh/laravel-dompdf": "^3.1",
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
|
"doctrine/dbal": "^3.10",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"laravel/framework": "^10.10",
|
"laravel/framework": "^10.10",
|
||||||
"laravel/sanctum": "^3.3",
|
"laravel/sanctum": "^3.3",
|
||||||
"laravel/tinker": "^2.8"
|
"laravel/tinker": "^2.8",
|
||||||
|
"mpdf/mpdf": "^8.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.9.1",
|
"fakerphp/faker": "^1.9.1",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "3b130f909d8c7acd5973043a958c243d",
|
"content-hash": "a470717879bee7fca3c22f312f8d5be7",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "barryvdh/laravel-dompdf",
|
"name": "barryvdh/laravel-dompdf",
|
||||||
|
|
@ -287,6 +287,259 @@
|
||||||
},
|
},
|
||||||
"time": "2024-07-08T12:26:09+00:00"
|
"time": "2024-07-08T12:26:09+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "doctrine/dbal",
|
||||||
|
"version": "3.10.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/doctrine/dbal.git",
|
||||||
|
"reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/doctrine/dbal/zipball/63a46cb5aa6f60991186cc98c1d1b50c09311868",
|
||||||
|
"reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer-runtime-api": "^2",
|
||||||
|
"doctrine/deprecations": "^0.5.3|^1",
|
||||||
|
"doctrine/event-manager": "^1|^2",
|
||||||
|
"php": "^7.4 || ^8.0",
|
||||||
|
"psr/cache": "^1|^2|^3",
|
||||||
|
"psr/log": "^1|^2|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/cache": "< 1.11"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/cache": "^1.11|^2.0",
|
||||||
|
"doctrine/coding-standard": "14.0.0",
|
||||||
|
"fig/log-test": "^1",
|
||||||
|
"jetbrains/phpstorm-stubs": "2023.1",
|
||||||
|
"phpstan/phpstan": "2.1.30",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2",
|
||||||
|
"phpunit/phpunit": "9.6.29",
|
||||||
|
"slevomat/coding-standard": "8.24.0",
|
||||||
|
"squizlabs/php_codesniffer": "4.0.0",
|
||||||
|
"symfony/cache": "^5.4|^6.0|^7.0|^8.0",
|
||||||
|
"symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"symfony/console": "For helpful console commands such as SQL execution and import of files."
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/doctrine-dbal"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Doctrine\\DBAL\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Guilherme Blanco",
|
||||||
|
"email": "guilhermeblanco@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Roman Borschel",
|
||||||
|
"email": "roman@code-factory.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Benjamin Eberlei",
|
||||||
|
"email": "kontakt@beberlei.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jonathan Wage",
|
||||||
|
"email": "jonwage@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
|
||||||
|
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
|
||||||
|
"keywords": [
|
||||||
|
"abstraction",
|
||||||
|
"database",
|
||||||
|
"db2",
|
||||||
|
"dbal",
|
||||||
|
"mariadb",
|
||||||
|
"mssql",
|
||||||
|
"mysql",
|
||||||
|
"oci8",
|
||||||
|
"oracle",
|
||||||
|
"pdo",
|
||||||
|
"pgsql",
|
||||||
|
"postgresql",
|
||||||
|
"queryobject",
|
||||||
|
"sasql",
|
||||||
|
"sql",
|
||||||
|
"sqlite",
|
||||||
|
"sqlserver",
|
||||||
|
"sqlsrv"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/doctrine/dbal/issues",
|
||||||
|
"source": "https://github.com/doctrine/dbal/tree/3.10.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.doctrine-project.org/sponsorship.html",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/phpdoctrine",
|
||||||
|
"type": "patreon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-11-29T10:46:08+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "doctrine/deprecations",
|
||||||
|
"version": "1.1.6",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/doctrine/deprecations.git",
|
||||||
|
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||||
|
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"phpunit/phpunit": "<=7.5 || >=14"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/coding-standard": "^9 || ^12 || ^14",
|
||||||
|
"phpstan/phpstan": "1.4.10 || 2.1.30",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
|
||||||
|
"psr/log": "^1 || ^2 || ^3"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Doctrine\\Deprecations\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
|
||||||
|
"homepage": "https://www.doctrine-project.org/",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/doctrine/deprecations/issues",
|
||||||
|
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
|
||||||
|
},
|
||||||
|
"time": "2026-02-07T07:09:04+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "doctrine/event-manager",
|
||||||
|
"version": "2.1.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/doctrine/event-manager.git",
|
||||||
|
"reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf",
|
||||||
|
"reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/common": "<2.9"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/coding-standard": "^14",
|
||||||
|
"phpdocumentor/guides-cli": "^1.4",
|
||||||
|
"phpstan/phpstan": "^2.1.32",
|
||||||
|
"phpunit/phpunit": "^10.5.58"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Doctrine\\Common\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Guilherme Blanco",
|
||||||
|
"email": "guilhermeblanco@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Roman Borschel",
|
||||||
|
"email": "roman@code-factory.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Benjamin Eberlei",
|
||||||
|
"email": "kontakt@beberlei.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jonathan Wage",
|
||||||
|
"email": "jonwage@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Johannes Schmitt",
|
||||||
|
"email": "schmittjoh@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Marco Pivetta",
|
||||||
|
"email": "ocramius@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
|
||||||
|
"homepage": "https://www.doctrine-project.org/projects/event-manager.html",
|
||||||
|
"keywords": [
|
||||||
|
"event",
|
||||||
|
"event dispatcher",
|
||||||
|
"event manager",
|
||||||
|
"event system",
|
||||||
|
"events"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/doctrine/event-manager/issues",
|
||||||
|
"source": "https://github.com/doctrine/event-manager/tree/2.1.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.doctrine-project.org/sponsorship.html",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/phpdoctrine",
|
||||||
|
"type": "patreon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-01-29T07:11:08+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/inflector",
|
"name": "doctrine/inflector",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
|
@ -2291,6 +2544,239 @@
|
||||||
],
|
],
|
||||||
"time": "2025-03-24T10:02:05+00:00"
|
"time": "2025-03-24T10:02:05+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/mpdf",
|
||||||
|
"version": "v8.2.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/mpdf.git",
|
||||||
|
"reference": "b59670a09498689c33ce639bac8f5ba26721dab3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3",
|
||||||
|
"reference": "b59670a09498689c33ce639bac8f5ba26721dab3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"mpdf/psr-http-message-shim": "^1.0 || ^2.0",
|
||||||
|
"mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
|
||||||
|
"myclabs/deep-copy": "^1.7",
|
||||||
|
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
|
||||||
|
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||||
|
"psr/http-message": "^1.0 || ^2.0",
|
||||||
|
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"setasign/fpdi": "^2.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.3.0",
|
||||||
|
"mpdf/qrcode": "^1.1.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5.0",
|
||||||
|
"tracy/tracy": "~2.5",
|
||||||
|
"yoast/phpunit-polyfills": "^1.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "Needed for generation of some types of barcodes",
|
||||||
|
"ext-xml": "Needed mainly for SVG manipulation",
|
||||||
|
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"GPL-2.0-only"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matěj Humpál",
|
||||||
|
"role": "Developer, maintainer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ian Back",
|
||||||
|
"role": "Developer (retired)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
|
||||||
|
"homepage": "https://mpdf.github.io",
|
||||||
|
"keywords": [
|
||||||
|
"pdf",
|
||||||
|
"php",
|
||||||
|
"utf-8"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"docs": "https://mpdf.github.io",
|
||||||
|
"issues": "https://github.com/mpdf/mpdf/issues",
|
||||||
|
"source": "https://github.com/mpdf/mpdf"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.paypal.me/mpdf",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-12-01T10:18:02+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/psr-http-message-shim",
|
||||||
|
"version": "v2.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/psr-http-message-shim.git",
|
||||||
|
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
|
||||||
|
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"psr/http-message": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\PsrHttpMessageShim\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Dorison",
|
||||||
|
"email": "mark@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kristofer Widholm",
|
||||||
|
"email": "kristofer@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nigel Cunningham",
|
||||||
|
"email": "nigel.cunningham@technocrat.com.au"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Shim to allow support of different psr/message versions.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/mpdf/psr-http-message-shim/issues",
|
||||||
|
"source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
|
||||||
|
},
|
||||||
|
"time": "2023-10-02T14:34:03+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/psr-log-aware-trait",
|
||||||
|
"version": "v3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/psr-log-aware-trait.git",
|
||||||
|
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78",
|
||||||
|
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"psr/log": "^3.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\PsrLogAwareTrait\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Dorison",
|
||||||
|
"email": "mark@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kristofer Widholm",
|
||||||
|
"email": "kristofer@chromatichq.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Trait to allow support of different psr/log versions.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
|
||||||
|
"source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2023-05-03T06:19:36+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "myclabs/deep-copy",
|
||||||
|
"version": "1.13.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||||
|
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||||
|
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/collections": "<1.6.8",
|
||||||
|
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/collections": "^1.6.8",
|
||||||
|
"doctrine/common": "^2.13.3 || ^3.2.2",
|
||||||
|
"phpspec/prophecy": "^1.10",
|
||||||
|
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/DeepCopy/deep_copy.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"DeepCopy\\": "src/DeepCopy/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Create deep copies (clones) of your objects",
|
||||||
|
"keywords": [
|
||||||
|
"clone",
|
||||||
|
"copy",
|
||||||
|
"duplicate",
|
||||||
|
"object",
|
||||||
|
"object graph"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||||
|
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-01T08:46:24+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nesbot/carbon",
|
"name": "nesbot/carbon",
|
||||||
"version": "2.73.0",
|
"version": "2.73.0",
|
||||||
|
|
@ -2692,6 +3178,56 @@
|
||||||
],
|
],
|
||||||
"time": "2024-11-21T10:36:35+00:00"
|
"time": "2024-11-21T10:36:35+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "paragonie/random_compat",
|
||||||
|
"version": "v9.99.100",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/paragonie/random_compat.git",
|
||||||
|
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||||
|
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">= 7"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "4.*|5.*",
|
||||||
|
"vimeo/psalm": "^1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Paragon Initiative Enterprises",
|
||||||
|
"email": "security@paragonie.com",
|
||||||
|
"homepage": "https://paragonie.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
|
||||||
|
"keywords": [
|
||||||
|
"csprng",
|
||||||
|
"polyfill",
|
||||||
|
"pseudorandom",
|
||||||
|
"random"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"email": "info@paragonie.com",
|
||||||
|
"issues": "https://github.com/paragonie/random_compat/issues",
|
||||||
|
"source": "https://github.com/paragonie/random_compat"
|
||||||
|
},
|
||||||
|
"time": "2020-10-15T08:29:30+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoption/phpoption",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
|
|
@ -2767,6 +3303,55 @@
|
||||||
],
|
],
|
||||||
"time": "2025-08-21T11:53:16+00:00"
|
"time": "2025-08-21T11:53:16+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/cache",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/cache.git",
|
||||||
|
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||||
|
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Cache\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for caching libraries",
|
||||||
|
"keywords": [
|
||||||
|
"cache",
|
||||||
|
"psr",
|
||||||
|
"psr-6"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/cache/tree/3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2021-02-03T23:26:27+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/clock",
|
"name": "psr/clock",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
@ -3521,6 +4106,78 @@
|
||||||
},
|
},
|
||||||
"time": "2025-07-11T13:20:48+00:00"
|
"time": "2025-07-11T13:20:48+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "setasign/fpdi",
|
||||||
|
"version": "v2.6.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Setasign/FPDI.git",
|
||||||
|
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
|
||||||
|
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"setasign/tfpdf": "<1.31"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7",
|
||||||
|
"setasign/fpdf": "~1.8.6",
|
||||||
|
"setasign/tfpdf": "~1.33",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5",
|
||||||
|
"tecnickcom/tcpdf": "^6.8"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"setasign\\Fpdi\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jan Slabon",
|
||||||
|
"email": "jan.slabon@setasign.com",
|
||||||
|
"homepage": "https://www.setasign.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Maximilian Kresse",
|
||||||
|
"email": "maximilian.kresse@setasign.com",
|
||||||
|
"homepage": "https://www.setasign.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
|
||||||
|
"homepage": "https://www.setasign.com/fpdi",
|
||||||
|
"keywords": [
|
||||||
|
"fpdf",
|
||||||
|
"fpdi",
|
||||||
|
"pdf"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Setasign/FPDI/issues",
|
||||||
|
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-05T09:57:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/console",
|
"name": "symfony/console",
|
||||||
"version": "v6.4.25",
|
"version": "v6.4.25",
|
||||||
|
|
@ -6500,66 +7157,6 @@
|
||||||
},
|
},
|
||||||
"time": "2024-05-16T03:13:13+00:00"
|
"time": "2024-05-16T03:13:13+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "myclabs/deep-copy",
|
|
||||||
"version": "1.13.4",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
|
||||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
|
||||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.1 || ^8.0"
|
|
||||||
},
|
|
||||||
"conflict": {
|
|
||||||
"doctrine/collections": "<1.6.8",
|
|
||||||
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"doctrine/collections": "^1.6.8",
|
|
||||||
"doctrine/common": "^2.13.3 || ^3.2.2",
|
|
||||||
"phpspec/prophecy": "^1.10",
|
|
||||||
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"src/DeepCopy/deep_copy.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"DeepCopy\\": "src/DeepCopy/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "Create deep copies (clones) of your objects",
|
|
||||||
"keywords": [
|
|
||||||
"clone",
|
|
||||||
"copy",
|
|
||||||
"duplicate",
|
|
||||||
"object",
|
|
||||||
"object graph"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
|
||||||
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2025-08-01T08:46:24+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "nunomaduro/collision",
|
"name": "nunomaduro/collision",
|
||||||
"version": "v7.12.0",
|
"version": "v7.12.0",
|
||||||
|
|
@ -8674,5 +9271,5 @@
|
||||||
"php": "^8.1"
|
"php": "^8.1"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'Asia/Jakarta',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@
|
||||||
'driver' => 'session',
|
'driver' => 'session',
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
],
|
],
|
||||||
|
'santri' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'santri_accounts',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -64,6 +68,10 @@
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => App\Models\User::class,
|
'model' => App\Models\User::class,
|
||||||
],
|
],
|
||||||
|
'santri_accounts' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => App\Models\SantriAccount::class,
|
||||||
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// 'users' => [
|
||||||
// 'driver' => 'database',
|
// 'driver' => 'database',
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'guard' => ['web'],
|
'guard' => ['santri'],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
// database/migrations/2026_02_24_000001_update_users_role_enum.php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// -- Langkah 1: Perluas enum dengan semua nilai (lama + baru) --
|
||||||
|
DB::statement("ALTER TABLE `users` MODIFY `role` ENUM('admin','super_admin','akademik','pamong','santri','wali') NOT NULL DEFAULT 'admin'");
|
||||||
|
|
||||||
|
// -- Langkah 2: Update data lama 'admin' → 'super_admin' --
|
||||||
|
DB::table('users')
|
||||||
|
->where('role', 'admin')
|
||||||
|
->update(['role' => 'super_admin']);
|
||||||
|
|
||||||
|
// -- Langkah 3: Hapus 'admin' dari enum, set default baru --
|
||||||
|
DB::statement("ALTER TABLE `users` MODIFY `role` ENUM('super_admin','akademik','pamong','santri','wali') NOT NULL DEFAULT 'super_admin'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// -- Langkah 1: Perluas enum kembali dengan 'admin' --
|
||||||
|
DB::statement("ALTER TABLE `users` MODIFY `role` ENUM('admin','super_admin','akademik','pamong','santri','wali') NOT NULL DEFAULT 'super_admin'");
|
||||||
|
|
||||||
|
// -- Langkah 2: Kembalikan role admin roles ke 'admin' --
|
||||||
|
DB::table('users')
|
||||||
|
->whereIn('role', ['super_admin', 'akademik', 'pamong'])
|
||||||
|
->update(['role' => 'admin']);
|
||||||
|
|
||||||
|
// -- Langkah 3: Kembalikan ke enum lama --
|
||||||
|
DB::statement("ALTER TABLE `users` MODIFY `role` ENUM('admin','santri','wali') NOT NULL DEFAULT 'admin'");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Modify enum to add Terlambat and Pulang
|
||||||
|
DB::statement("ALTER TABLE absensi_kegiatans MODIFY COLUMN status ENUM('Hadir', 'Izin', 'Sakit', 'Alpa', 'Terlambat', 'Pulang') NOT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::statement("ALTER TABLE absensi_kegiatans MODIFY COLUMN status ENUM('Hadir', 'Izin', 'Sakit', 'Alpa') NOT NULL");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
// database/migrations/2026_02_25_100001_create_santri_accounts_table.php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('santri_accounts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('id_santri');
|
||||||
|
$table->string('username')->unique();
|
||||||
|
$table->string('password');
|
||||||
|
$table->enum('role', ['santri', 'wali'])->default('santri');
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamp('last_login')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('id_santri')
|
||||||
|
->references('id_santri')
|
||||||
|
->on('santris')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('santri_accounts');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
// database/migrations/2026_02_25_100002_remove_santri_wali_from_users_table.php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// -- Hapus semua user dengan role santri/wali --
|
||||||
|
DB::table('users')->whereIn('role', ['santri', 'wali'])->delete();
|
||||||
|
|
||||||
|
// -- Ubah enum role hanya untuk admin (raw SQL karena DBAL tidak support enum) --
|
||||||
|
DB::statement("ALTER TABLE users MODIFY COLUMN role ENUM('super_admin','akademik','pamong') NOT NULL DEFAULT 'akademik'");
|
||||||
|
|
||||||
|
// -- Hapus kolom role_id jika ada --
|
||||||
|
if (Schema::hasColumn('users', 'role_id')) {
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('role_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::statement("ALTER TABLE users MODIFY COLUMN role ENUM('super_admin','akademik','pamong','santri','wali') NOT NULL DEFAULT 'akademik'");
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('users', 'role_id')) {
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('role_id')->nullable()->after('role');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Already applied manually — ensures 'Khatam' is a valid status value
|
||||||
|
DB::statement("ALTER TABLE santris MODIFY COLUMN status ENUM('Aktif','Lulus','Tidak Aktif','Khatam') NOT NULL DEFAULT 'Aktif'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::statement("ALTER TABLE santris MODIFY COLUMN status ENUM('Aktif','Lulus','Tidak Aktif') NOT NULL DEFAULT 'Aktif'");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('password_reset_otps', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('email')->index();
|
||||||
|
$table->string('otp', 6);
|
||||||
|
$table->timestamp('expired_at');
|
||||||
|
$table->boolean('is_verified')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('password_reset_otps');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -12,11 +12,8 @@ class DatabaseSeeder extends Seeder
|
||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// \App\Models\User::factory(10)->create();
|
$this->call([
|
||||||
|
SantriSeeder::class,
|
||||||
// \App\Models\User::factory()->create([
|
]);
|
||||||
// 'name' => 'Test User',
|
|
||||||
// 'email' => 'test@example.com',
|
|
||||||
// ]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,329 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class SantriSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* ============================================================
|
||||||
|
* REFERENSI KODE KELAS (dari KelasSeeder)
|
||||||
|
* ============================================================
|
||||||
|
* Kelas Pondok (KEL001):
|
||||||
|
* KLS001 = PB
|
||||||
|
* KLS002 = Lambatan
|
||||||
|
* KLS003 = Cepatan
|
||||||
|
*
|
||||||
|
* Sekolah Formal (KEL002):
|
||||||
|
* KLS004 = SD 1 | KLS005 = SD 2 | KLS006 = SD 3
|
||||||
|
* KLS007 = SD 4 | KLS008 = SD 5 | KLS009 = SD 6
|
||||||
|
* KLS010 = SMP 7 | KLS011 = SMP 8 | KLS012 = SMP 9
|
||||||
|
* KLS013 = SMA 10 | KLS014 = SMA 11 | KLS015 = SMA 12
|
||||||
|
* ============================================================
|
||||||
|
*
|
||||||
|
* CARA EDIT KELAS SANTRI:
|
||||||
|
* Setiap santri punya array 'kelas' berisi kode kelas yang diikuti.
|
||||||
|
* - is_primary: true = kelas utama (kelas pondok)
|
||||||
|
* - is_primary: false = kelas tambahan (kelas formal)
|
||||||
|
* Ubah kode kelas di bagian 'kelas' sesuai data asli!
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Hanya hapus data S003 ke atas, S001 & S002 tetap aman
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||||
|
DB::table('santri_kelas')
|
||||||
|
->whereIn('id_santri', array_map(fn($n) => 'S' . str_pad($n, 3, '0', STR_PAD_LEFT), range(3, 99)))
|
||||||
|
->delete();
|
||||||
|
DB::table('santris')
|
||||||
|
->whereIn('id_santri', array_map(fn($n) => 'S' . str_pad($n, 3, '0', STR_PAD_LEFT), range(3, 99)))
|
||||||
|
->delete();
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||||
|
|
||||||
|
$tahunAjaran = '2024/2025';
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
$santriList = [
|
||||||
|
// ============================================================
|
||||||
|
// S003 - S034: Edit bagian 'kelas' sesuai data asli!
|
||||||
|
// ============================================================
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S003', 'nis' => '510035160089253003', 'nama_lengkap' => 'Altaf Baihaqi Amrullah', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Tiebuk RT 006 RW 003 Wiyu, Pacet, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Baihaqi', 'nomor_hp_ortu' => '+6281234560003', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH SESUAI DATA
|
||||||
|
['kode' => 'KLS004', 'is_primary' => false], // SD 1 ← UBAH SESUAI DATA
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S004', 'nis' => '510035160089253004', 'nama_lengkap' => 'Aminati Yusrin Isnaini', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Brangkal RT 002 RW 001 Brangkal, Sooko, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Yusrin', 'nomor_hp_ortu' => '+6281234560004', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS005', 'is_primary' => false], // SD 2 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S005', 'nis' => '510035160089253005', 'nama_lengkap' => 'Ananda Novreandis', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Jampirogo RT 006 RW 001 Jampirogo, Sooko, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Novreandis Sr.', 'nomor_hp_ortu' => '+6281234560005', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS004', 'is_primary' => false], // SD 1 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S006', 'nis' => '510035160089253006', 'nama_lengkap' => 'Andika Maulana Ishaq', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Jemparing RT 001 RW 001 Pakel, Bareng, Jombang', 'daerah_asal' => 'Jombang Selatan', 'nama_orang_tua' => 'Ishaq', 'nomor_hp_ortu' => '+6281234560006', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS010', 'is_primary' => false], // SMP 7 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S007', 'nis' => '510035160089253007', 'nama_lengkap' => 'Anggraini Nur Dina Fahma', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Plosarejo RT 002 RW 001 Tlogoagung, Kedungadem, Bojonegoro', 'daerah_asal' => 'Bojonegoro Timur', 'nama_orang_tua' => 'Nur Fahma', 'nomor_hp_ortu' => '+6281234560007', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS011', 'is_primary' => false], // SMP 8 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S008', 'nis' => '510035160089253008', 'nama_lengkap' => 'Azalia Calysta Salsabila', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Bulu RT 002 RW 001 Gedangan, Kutorejo, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Calysta Sr.', 'nomor_hp_ortu' => '+6281234560008', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS006', 'is_primary' => false], // SD 3 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S009', 'nis' => '510035160089253009', 'nama_lengkap' => 'Bustomi Firman Amrulloh', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Kuwik RT 001 RW 001 Bareng, Bareng, Jombang', 'daerah_asal' => 'Jombang Selatan', 'nama_orang_tua' => 'Firman', 'nomor_hp_ortu' => '+6281234560009', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS007', 'is_primary' => false], // SD 4 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S010', 'nis' => '510035160089253010', 'nama_lengkap' => 'Cresya Nirva Arvenda', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Kedungmaling II RT 014 RW 006 Kedungmaling, Sooko, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Nirva Sr.', 'nomor_hp_ortu' => '+6281234560010', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS003', 'is_primary' => true], // Cepatan ← UBAH
|
||||||
|
['kode' => 'KLS013', 'is_primary' => false], // SMA 10 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S011', 'nis' => '510035160089253011', 'nama_lengkap' => 'Daud Fasal', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Plumpang RT 017 RW 004 Penambangan, Balongbendo, Sidoarjo', 'daerah_asal' => 'Sidoarjo Utara', 'nama_orang_tua' => 'Fasal Sr.', 'nomor_hp_ortu' => '+6281234560011', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS010', 'is_primary' => false], // SMP 7 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S012', 'nis' => '510035160089253012', 'nama_lengkap' => 'Dwi Melviana Putri', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Panggih RT 003 RW 003 Panggih, Trowulan, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Melviana Sr.', 'nomor_hp_ortu' => '+6281234560012', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS014', 'is_primary' => false], // SMA 11 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S013', 'nis' => '510035160089253013', 'nama_lengkap' => 'Fina Yusrina Jannah', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Kepuhanyar RT 001 RW 001 Kepuhanyar, Mojoanyar, Mojokerto', 'daerah_asal' => 'Mojokerto Kota', 'nama_orang_tua' => 'Yusrina Sr.', 'nomor_hp_ortu' => '+6281234560013', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS003', 'is_primary' => true], // Cepatan ← UBAH
|
||||||
|
['kode' => 'KLS015', 'is_primary' => false], // SMA 12 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S014', 'nis' => '510035160089253014', 'nama_lengkap' => 'Gadis Sholikhah', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Brangkal RT 002 RW 001 Brangkal, Sooko, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Sholikhah Sr.', 'nomor_hp_ortu' => '+6281234560014', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS008', 'is_primary' => false], // SD 5 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S015', 'nis' => '510035160089253015', 'nama_lengkap' => 'Gilang Aswin Nahar', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Panggih RT 001 RW 003 Panggih, Trowulan, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Aswin', 'nomor_hp_ortu' => '+6281234560015', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS009', 'is_primary' => false], // SD 6 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S016', 'nis' => '510035160089253016', 'nama_lengkap' => 'Gustiyar Abdullah Manshurin', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Setro RT 007 RW 001 Jatirejo, Kasreman, Ngawi', 'daerah_asal' => 'Ngawi Kota', 'nama_orang_tua' => 'Abdullah', 'nomor_hp_ortu' => '+6281234560016', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS012', 'is_primary' => false], // SMP 9 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S017', 'nis' => '510035160089253017', 'nama_lengkap' => 'Ilham Maulana Abdillah', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Tundungan RT 004 RW 002 Sidomojo, Krian, Sidoarjo', 'daerah_asal' => 'Sidoarjo Utara', 'nama_orang_tua' => 'Maulana', 'nomor_hp_ortu' => '+6281234560017', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS003', 'is_primary' => true], // Cepatan ← UBAH
|
||||||
|
['kode' => 'KLS013', 'is_primary' => false], // SMA 10 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S018', 'nis' => '510035160089253018', 'nama_lengkap' => 'Kafa Septian Ramdan Efendi', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Brangkal RT 002 RW001 Brangkal, Sooko, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Septian', 'nomor_hp_ortu' => '+6281234560018', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS011', 'is_primary' => false], // SMP 8 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S019', 'nis' => '510035160089253019', 'nama_lengkap' => "Khalisa Syifa'ul Aini", 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Tiebuk RT 007 RW 003 Wiyu, Pacet, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => "Syifa'ul Sr.", 'nomor_hp_ortu' => '+6281234560019', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS005', 'is_primary' => false], // SD 2 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S020', 'nis' => '510035160089253020', 'nama_lengkap' => 'Kharisa Nur Qalbi', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Pasinan Lemah Putih RT 011 RW 003, Wringinanom, Gresik', 'daerah_asal' => 'Gresik Selatan', 'nama_orang_tua' => 'Nur Qalbi Sr.', 'nomor_hp_ortu' => '+6281234560020', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS003', 'is_primary' => true], // Cepatan ← UBAH
|
||||||
|
['kode' => 'KLS014', 'is_primary' => false], // SMA 11 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S021', 'nis' => '510035160089253021', 'nama_lengkap' => 'Lana Novpriyanto', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Parengan RT 019 RW 004 Kraton, Krian, Sidoarjo', 'daerah_asal' => 'Sidoarjo Utara', 'nama_orang_tua' => 'Novpriyanto Sr.', 'nomor_hp_ortu' => '+6281234560021', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS010', 'is_primary' => false], // SMP 7 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S022', 'nis' => '510035160089253022', 'nama_lengkap' => 'M. Reyhan Firdaus', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Jampirogo RT 005 RW 001 Jampirogo, Sooko, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Reyhan Sr.', 'nomor_hp_ortu' => '+6281234560022', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS006', 'is_primary' => false], // SD 3 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S023', 'nis' => '510035160089253023', 'nama_lengkap' => 'Masrurotin Fatma Ayu Wulandari', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Pakis Kulon RT 003 RW 003 Pakis, Trowulan, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Fatma Sr.', 'nomor_hp_ortu' => '+6281234560023', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS003', 'is_primary' => true], // Cepatan ← UBAH
|
||||||
|
['kode' => 'KLS015', 'is_primary' => false], // SMA 12 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S024', 'nis' => '510035160089253024', 'nama_lengkap' => 'Mochammad Adam Madinata', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Kepuhanyar RT 001 RW001 Kepuhanyar, Mojoanyar, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Adam Sr.', 'nomor_hp_ortu' => '+6281234560024', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS007', 'is_primary' => false], // SD 4 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S025', 'nis' => '510035160089253025', 'nama_lengkap' => "Muchammad Fachrizal Ta'awamu Insan", 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Sidokepung RT 015 RW 003 Buduran, Sidoarjo', 'daerah_asal' => 'Sidoarjo Tengah', 'nama_orang_tua' => 'Fachrizal', 'nomor_hp_ortu' => '+6281234560025', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS011', 'is_primary' => false], // SMP 8 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S026', 'nis' => '510035160089253026', 'nama_lengkap' => 'Muhammad Revano Fadillah Ramadhan', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Kemlagi Timur RT 001 RW 003 Kemlagi, Kemlagi, Mojokerto', 'daerah_asal' => 'Mojokerto Kota', 'nama_orang_tua' => 'Fadillah', 'nomor_hp_ortu' => '+6281234560026', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS003', 'is_primary' => true], // Cepatan ← UBAH
|
||||||
|
['kode' => 'KLS012', 'is_primary' => false], // SMP 9 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S027', 'nis' => '510035160089253027', 'nama_lengkap' => 'Muhammad Ibrahim Try Aji', 'jenis_kelamin' => 'Laki-laki', 'status' => 'Aktif', 'alamat_santri' => 'Jln. Patimura RT 002 RW 002 Keboan, Ngusikan, Jombang', 'daerah_asal' => 'Mojokerto Kota', 'nama_orang_tua' => 'Ibrahim', 'nomor_hp_ortu' => '+6281234560027', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS013', 'is_primary' => false], // SMA 10 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S028', 'nis' => '510035160089253028', 'nama_lengkap' => 'Mutiara Dira Ardiana', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Tiebuk RT 008 RW 001 Wiyu, Pacet, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Dira Sr.', 'nomor_hp_ortu' => '+6281234560028', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS008', 'is_primary' => false], // SD 5 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S029', 'nis' => '510035160089253029', 'nama_lengkap' => 'Nerissa Arviana Maharani', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Ngangkrik Kidul RT 001 RW 003 Gebangangkrik, Ngimbang, Lamongan', 'daerah_asal' => 'Kambangan', 'nama_orang_tua' => 'Arviana Sr.', 'nomor_hp_ortu' => '+6281234560029', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS003', 'is_primary' => true], // Cepatan ← UBAH
|
||||||
|
['kode' => 'KLS009', 'is_primary' => false], // SD 6 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S030', 'nis' => '510035160089253030', 'nama_lengkap' => 'Prisca Zuzin Firdaus', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Ngrayung RT 004 RW 001 Brayung, Puri, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Zuzin Sr.', 'nomor_hp_ortu' => '+6281234560030', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS004', 'is_primary' => false], // SD 1 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S031', 'nis' => '510035160089253031', 'nama_lengkap' => 'Shoffiya Fitriani Az Zahra', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Dadi RT 027 RW 014 Dadi, Plaosan, Magetan', 'daerah_asal' => 'Magetan', 'nama_orang_tua' => 'Fitriani Sr.', 'nomor_hp_ortu' => '+6281234560031', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS005', 'is_primary' => false], // SD 2 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S032', 'nis' => '510035160089253032', 'nama_lengkap' => 'Syifa Putri Ramahdani', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Sumbertempur Perum Puri Kencana RT 003 RW003 Sumbergirang, Puri, Mojokerto', 'daerah_asal' => 'Mojokerto Barat', 'nama_orang_tua' => 'Putri Sr.', 'nomor_hp_ortu' => '+6281234560032', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS003', 'is_primary' => true], // Cepatan ← UBAH
|
||||||
|
['kode' => 'KLS006', 'is_primary' => false], // SD 3 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S033', 'nis' => '510035160089253033', 'nama_lengkap' => 'Tiara Rahmadhani Faradilah', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Bulurejo RT 001 RW 002 Kepuhkajang, Perak, Jombang', 'daerah_asal' => 'Mojokerto Kota', 'nama_orang_tua' => 'Rahmadhani Sr.', 'nomor_hp_ortu' => '+6281234560033', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS001', 'is_primary' => true], // PB ← UBAH
|
||||||
|
['kode' => 'KLS014', 'is_primary' => false], // SMA 11 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'santri' => ['id_santri' => 'S034', 'nis' => '510035160089253034', 'nama_lengkap' => 'Virlye Andyra Zahra', 'jenis_kelamin' => 'Perempuan', 'status' => 'Aktif', 'alamat_santri' => 'Dsn. Randuwates RT 005 RW 003 Mojowatesrejo, Kemlagi, Mojokerto', 'daerah_asal' => 'Mojokerto Kota', 'nama_orang_tua' => 'Andyra Sr.', 'nomor_hp_ortu' => '+6281234560034', 'rfid_uid' => null, 'foto' => null, 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
'kelas' => [
|
||||||
|
['kode' => 'KLS002', 'is_primary' => true], // Lambatan ← UBAH
|
||||||
|
['kode' => 'KLS015', 'is_primary' => false], // SMA 12 ← UBAH
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// PROSES INSERT — jangan ubah bagian ini
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Ambil mapping kode_kelas -> id dari DB
|
||||||
|
$kelasMap = DB::table('kelas')->pluck('id', 'kode_kelas')->toArray();
|
||||||
|
|
||||||
|
$totalSantri = 0;
|
||||||
|
$totalKelas = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($santriList as $item) {
|
||||||
|
DB::table('santris')->insert($item['santri']);
|
||||||
|
$totalSantri++;
|
||||||
|
|
||||||
|
$idSantri = $item['santri']['id_santri'];
|
||||||
|
|
||||||
|
foreach ($item['kelas'] as $kelasItem) {
|
||||||
|
$kode = $kelasItem['kode'];
|
||||||
|
|
||||||
|
if (!isset($kelasMap[$kode])) {
|
||||||
|
$errors[] = "⚠️ Kelas [{$kode}] tidak ada di DB! Santri {$idSantri} dilewati untuk kelas ini.";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('santri_kelas')->insert([
|
||||||
|
'id_santri' => $idSantri,
|
||||||
|
'id_kelas' => $kelasMap[$kode],
|
||||||
|
'tahun_ajaran' => $tahunAjaran,
|
||||||
|
'is_primary' => $kelasItem['is_primary'],
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
$totalKelas++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->command->info("✅ SantriSeeder selesai!");
|
||||||
|
$this->command->info(" → {$totalSantri} santri ditambahkan (S003–S034)");
|
||||||
|
$this->command->info(" → {$totalKelas} record santri_kelas ditambahkan");
|
||||||
|
$this->command->info(" → Tahun ajaran: {$tahunAjaran}");
|
||||||
|
$this->command->info(" → S001 & S002 tidak tersentuh ✓");
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
$this->command->warn("\nAda masalah:");
|
||||||
|
foreach ($errors as $err) {
|
||||||
|
$this->command->warn(" " . $err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,311 @@
|
||||||
|
{{-- resources/views/admin/auth/forgot_password.blade.php --}}
|
||||||
|
@extends('auth.auth_layout')
|
||||||
|
|
||||||
|
@section('title', 'Lupa Password')
|
||||||
|
|
||||||
|
@section('auth-content')
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:opsz,wght@9..40,300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body.auth-page {
|
||||||
|
background: #F8FDFB !important;
|
||||||
|
font-family: 'DM Sans', sans-serif !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
}
|
||||||
|
.auth-container {
|
||||||
|
width: 100vw !important; max-width: 100vw !important;
|
||||||
|
min-height: 100vh !important; background: transparent !important;
|
||||||
|
padding: 0 !important; border-radius: 0 !important; box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fp-wrap {
|
||||||
|
position: relative; width: 100%; min-height: 100vh;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
overflow: hidden; font-family: 'DM Sans', sans-serif;
|
||||||
|
}
|
||||||
|
.fp-bg {
|
||||||
|
position: absolute; inset: 0; z-index: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 60% at 70% 50%, rgba(111,186,157,.12) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse 50% 80% at 10% 80%, rgba(111,186,157,.08) 0%, transparent 55%),
|
||||||
|
#F8FDFB;
|
||||||
|
}
|
||||||
|
.fp-bg::before {
|
||||||
|
content: ''; position: absolute; inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(111,186,157,.055) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(111,186,157,.055) 1px, transparent 1px);
|
||||||
|
background-size: 48px 48px;
|
||||||
|
}
|
||||||
|
.fp-ring { position: absolute; border-radius: 50%; border: 1.5px solid rgba(111,186,157,.18); pointer-events: none; }
|
||||||
|
.fp-ring.r1 { width:420px; height:420px; top:-110px; right:-110px; }
|
||||||
|
.fp-ring.r2 { width:270px; height:270px; bottom:50px; left:60px; border-color:rgba(111,186,157,.10); }
|
||||||
|
.fp-dot { position:absolute; border-radius:50%; background:#6FBA9D; pointer-events:none; }
|
||||||
|
.fp-dot.d1 { width:8px; height:8px; top:22%; right:18%; opacity:.14; }
|
||||||
|
.fp-dot.d2 { width:11px; height:11px; top:55%; left:15%; opacity:.08; }
|
||||||
|
.fp-line { position:absolute; height:1px; background:linear-gradient(90deg,transparent,rgba(111,186,157,.14),transparent); pointer-events:none; }
|
||||||
|
.fp-line.l1 { width:280px; top:28%; left:-60px; transform:rotate(-15deg); }
|
||||||
|
|
||||||
|
.fp-layout {
|
||||||
|
position: relative; z-index: 2;
|
||||||
|
display: flex; align-items: center;
|
||||||
|
width: 100%; max-width: 1100px;
|
||||||
|
padding: 40px 60px; gap: 80px;
|
||||||
|
animation: fpIn .6s ease both;
|
||||||
|
}
|
||||||
|
@keyframes fpIn {
|
||||||
|
from { opacity:0; transform:translateY(20px); }
|
||||||
|
to { opacity:1; transform:translateY(0); }
|
||||||
|
}
|
||||||
|
.fp-brand { flex: 0 0 340px; order: 1; }
|
||||||
|
.fp-form-panel { flex: 1; max-width: 430px; order: 2; }
|
||||||
|
|
||||||
|
/* Brand */
|
||||||
|
.fp-logo { width:72px; height:72px; margin-bottom:20px; border-radius:16px; box-shadow:0 4px 20px rgba(111,186,157,.2); object-fit:contain; background:#fff; }
|
||||||
|
.fp-eyebrow {
|
||||||
|
display:inline-flex; align-items:center; gap:8px;
|
||||||
|
font-size:.68rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#6FBA9D; margin-bottom:18px;
|
||||||
|
}
|
||||||
|
.fp-eyebrow::before { content:''; display:inline-block; width:22px; height:2px; background:#6FBA9D; border-radius:2px; }
|
||||||
|
.fp-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:3.2rem; line-height:1.05; color:#0F2118; margin-bottom:6px;
|
||||||
|
}
|
||||||
|
.fp-title em { font-style:italic; color:#5EA98C; }
|
||||||
|
.fp-sub { font-size:.9rem; font-weight:500; color:#8AADA0; margin-bottom:32px; line-height:1.6; }
|
||||||
|
.fp-divider { width:44px; height:3px; background:linear-gradient(90deg,#6FBA9D,#A8D8C6); border-radius:3px; margin-bottom:24px; }
|
||||||
|
.fp-desc { font-size:.81rem; color:#8AADA0; line-height:1.8; max-width:290px; margin-bottom:32px; }
|
||||||
|
|
||||||
|
/* Steps */
|
||||||
|
.fp-steps { display:flex; flex-direction:column; gap:14px; }
|
||||||
|
.fp-step { display:flex; align-items:flex-start; gap:12px; }
|
||||||
|
.fp-step-num {
|
||||||
|
width:28px; height:28px; border-radius:50%;
|
||||||
|
background:linear-gradient(135deg,#6FBA9D,#5EA98C);
|
||||||
|
color:#fff; font-size:.72rem; font-weight:800;
|
||||||
|
display:flex; align-items:center; justify-content:center; flex-shrink:0;
|
||||||
|
}
|
||||||
|
.fp-step-num.active { box-shadow:0 0 0 4px rgba(111,186,157,.2); }
|
||||||
|
.fp-step-text { font-size:.78rem; color:#2A4235; font-weight:500; line-height:1.5; padding-top:3px; }
|
||||||
|
.fp-step-text small { display:block; color:#8AADA0; font-weight:400; font-size:.72rem; }
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.fp-card {
|
||||||
|
background: #fff; border-radius: 24px;
|
||||||
|
padding: 42px 38px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(111,186,157,.1),
|
||||||
|
0 4px 6px rgba(15,33,24,.03),
|
||||||
|
0 20px 44px rgba(15,33,24,.08);
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.fp-card::before {
|
||||||
|
content: ''; position:absolute; top:0; left:0; right:0; height:3px;
|
||||||
|
background: linear-gradient(90deg, #e57373, #ef9a9a, #e57373);
|
||||||
|
}
|
||||||
|
.fp-card::after {
|
||||||
|
content: ''; position:absolute; bottom:-50px; right:-50px;
|
||||||
|
width:140px; height:140px; border-radius:50%;
|
||||||
|
background: radial-gradient(circle, rgba(229,115,115,.06) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
.fp-card-icon {
|
||||||
|
width:54px; height:54px; border-radius:14px;
|
||||||
|
background:linear-gradient(135deg,#FFEBEE,#FFCDD2);
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
font-size:1.3rem; color:#e53935; margin-bottom:16px;
|
||||||
|
}
|
||||||
|
.fp-card-lbl {
|
||||||
|
font-size:.67rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#e57373; margin-bottom:7px;
|
||||||
|
}
|
||||||
|
.fp-card-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:1.85rem; color:#0F2118; line-height:1.1; margin-bottom:5px;
|
||||||
|
}
|
||||||
|
.fp-card-desc { font-size:.79rem; color:#8AADA0; line-height:1.6; margin-bottom:26px; }
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
.fp-alert-danger {
|
||||||
|
border-radius:10px; padding:10px 13px; font-size:.79rem;
|
||||||
|
margin-bottom:18px; background:#FFF3F3; color:#c62828; border-left:3px solid #e53935;
|
||||||
|
display:flex; align-items:center; gap:7px;
|
||||||
|
}
|
||||||
|
.fp-alert-success {
|
||||||
|
border-radius:10px; padding:10px 13px; font-size:.79rem;
|
||||||
|
margin-bottom:18px; background:#F0FFF4; color:#2E7D32; border-left:3px solid #43A047;
|
||||||
|
display:flex; align-items:center; gap:7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Field */
|
||||||
|
.fp-field { margin-bottom:18px; }
|
||||||
|
.fp-lbl { display:block; font-size:.7rem; font-weight:700; letter-spacing:.8px; text-transform:uppercase; color:#2A4235; margin-bottom:7px; }
|
||||||
|
.fp-shell { position:relative; display:flex; align-items:center; }
|
||||||
|
.fp-shell .fi { position:absolute; left:15px; color:#A8D8C6; font-size:.8rem; pointer-events:none; transition:color .2s; }
|
||||||
|
.fp-shell input {
|
||||||
|
width:100%; padding:12px 15px 12px 40px;
|
||||||
|
background:#EBF7F2; border:1.5px solid transparent;
|
||||||
|
border-radius:11px; font-family:inherit; font-size:.87rem; color:#0F2118; outline:none;
|
||||||
|
transition:all .2s;
|
||||||
|
}
|
||||||
|
.fp-shell input::placeholder { color:#8AADA0; font-size:.83rem; }
|
||||||
|
.fp-shell input:focus { background:#fff; border-color:#e57373; box-shadow:0 0 0 4px rgba(229,115,115,.12); }
|
||||||
|
.fp-shell .fi.active { color:#e57373; }
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
.fp-btn {
|
||||||
|
width:100%; padding:13px;
|
||||||
|
background:linear-gradient(135deg, #e57373, #ef5350);
|
||||||
|
color:#fff; border:none; border-radius:12px;
|
||||||
|
font-family:inherit; font-size:.89rem; font-weight:700;
|
||||||
|
cursor:pointer; letter-spacing:.3px;
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:8px;
|
||||||
|
box-shadow:0 4px 18px rgba(229,115,115,.35);
|
||||||
|
transition:all .25s;
|
||||||
|
}
|
||||||
|
.fp-btn:hover { transform:translateY(-2px); box-shadow:0 8px 26px rgba(229,115,115,.45); }
|
||||||
|
.fp-btn:active { transform:none; }
|
||||||
|
|
||||||
|
.fp-back {
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:6px;
|
||||||
|
margin-top:20px; font-size:.78rem; color:#5EA98C; font-weight:600; text-decoration:none;
|
||||||
|
transition:color .2s;
|
||||||
|
}
|
||||||
|
.fp-back:hover { color:#3D8A6E; text-decoration:underline; }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.fp-layout { gap:48px; padding:32px 36px; }
|
||||||
|
.fp-brand { flex:0 0 260px; }
|
||||||
|
.fp-title { font-size:2.7rem; }
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
body.auth-page { align-items:flex-start !important; overflow-y:auto !important; }
|
||||||
|
.fp-wrap { align-items:flex-start; min-height:auto; padding:24px 0 40px; }
|
||||||
|
.fp-layout { flex-direction:column; padding:0 20px; gap:28px; }
|
||||||
|
.fp-form-panel { order:2; max-width:100%; }
|
||||||
|
.fp-brand { order:1; flex:none; text-align:center; }
|
||||||
|
.fp-title { font-size:2.2rem; }
|
||||||
|
.fp-steps, .fp-desc, .fp-divider { display:none; }
|
||||||
|
.fp-sub { margin-bottom:0; }
|
||||||
|
.fp-card { padding:28px 20px; }
|
||||||
|
.fp-logo { width:56px; height:56px; margin:0 auto 14px; display:block; }
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.fp-title { font-size:1.85rem; }
|
||||||
|
.fp-card { padding:24px 16px; border-radius:16px; }
|
||||||
|
.fp-card-title { font-size:1.5rem; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.fp-layout { max-width:1160px; padding:40px 80px; }
|
||||||
|
.fp-brand { flex:0 0 360px; }
|
||||||
|
.fp-title { font-size:3.6rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="fp-wrap">
|
||||||
|
<div class="fp-bg"></div>
|
||||||
|
<div class="fp-ring r1"></div>
|
||||||
|
<div class="fp-ring r2"></div>
|
||||||
|
<div class="fp-dot d1"></div>
|
||||||
|
<div class="fp-dot d2"></div>
|
||||||
|
<div class="fp-line l1"></div>
|
||||||
|
|
||||||
|
<div class="fp-layout">
|
||||||
|
|
||||||
|
<!-- ═══ Brand (kiri) ═══ -->
|
||||||
|
<div class="fp-brand">
|
||||||
|
<img src="{{ asset('images/logo.png') }}" alt="Logo PKPPS" class="fp-logo">
|
||||||
|
<div class="fp-eyebrow">Reset Akses</div>
|
||||||
|
<h1 class="fp-title">Lupa<br><em>Password?</em></h1>
|
||||||
|
<p class="fp-sub">Jangan khawatir, kami bantu pulihkan.</p>
|
||||||
|
<div class="fp-divider"></div>
|
||||||
|
<p class="fp-desc">Ikuti langkah berikut untuk mengatur ulang password akun Super Admin Anda.</p>
|
||||||
|
|
||||||
|
<div class="fp-steps">
|
||||||
|
<div class="fp-step">
|
||||||
|
<div class="fp-step-num active">1</div>
|
||||||
|
<div class="fp-step-text">Masukkan email terdaftar <small>Kami kirim kode OTP 6 digit</small></div>
|
||||||
|
</div>
|
||||||
|
<div class="fp-step">
|
||||||
|
<div class="fp-step-num">2</div>
|
||||||
|
<div class="fp-step-text">Verifikasi kode OTP <small>Cek email masuk / spam</small></div>
|
||||||
|
</div>
|
||||||
|
<div class="fp-step">
|
||||||
|
<div class="fp-step-num">3</div>
|
||||||
|
<div class="fp-step-text">Buat password baru <small>Minimal 8 karakter</small></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Form (kanan) ═══ -->
|
||||||
|
<div class="fp-form-panel">
|
||||||
|
<div class="fp-card">
|
||||||
|
<div class="fp-card-icon">
|
||||||
|
<i class="fas fa-envelope-open-text"></i>
|
||||||
|
</div>
|
||||||
|
<div class="fp-card-lbl">Langkah 1 dari 3</div>
|
||||||
|
<div class="fp-card-title">Masukkan Email</div>
|
||||||
|
<div class="fp-card-desc">Masukkan email Super Admin yang terdaftar di sistem. Kami akan mengirim kode OTP ke email tersebut.</div>
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="fp-alert-danger">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
{{ $errors->first() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="fp-alert-success" id="fpSuccessAlert">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
{{ session('success') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.forgot.send_otp') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="fp-field">
|
||||||
|
<label class="fp-lbl">Email Super Admin</label>
|
||||||
|
<div class="fp-shell">
|
||||||
|
<i class="fas fa-envelope fi" id="ico-email"></i>
|
||||||
|
<input type="email" id="email" name="email"
|
||||||
|
value="{{ old('email') }}"
|
||||||
|
placeholder="contoh@email.com"
|
||||||
|
required autofocus
|
||||||
|
onfocus="document.getElementById('ico-email').classList.add('active')"
|
||||||
|
onblur="document.getElementById('ico-email').classList.remove('active')">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="fp-btn">
|
||||||
|
<i class="fas fa-paper-plane"></i>
|
||||||
|
Kirim Kode OTP
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.login') }}" class="fp-back">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali ke Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const sa = document.getElementById('fpSuccessAlert');
|
||||||
|
if (sa) {
|
||||||
|
setTimeout(() => {
|
||||||
|
sa.style.transition = 'opacity .5s ease';
|
||||||
|
sa.style.opacity = '0';
|
||||||
|
setTimeout(() => sa.remove(), 500);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -4,158 +4,386 @@
|
||||||
@section('title', 'Login Admin')
|
@section('title', 'Login Admin')
|
||||||
|
|
||||||
@section('auth-content')
|
@section('auth-content')
|
||||||
<div class="auth-header">
|
|
||||||
<h2><i class="fas fa-lock"></i> Admin Login</h2>
|
|
||||||
<p>Sistem Informasi Monitoring Santri</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Alert Error --}}
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:opsz,wght@9..40,300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
@if ($errors->any())
|
|
||||||
<div class="alert alert-danger">
|
<style>
|
||||||
|
body.auth-page {
|
||||||
|
background: #F8FDFB !important;
|
||||||
|
font-family: 'DM Sans', sans-serif !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
}
|
||||||
|
.auth-container {
|
||||||
|
width: 100vw !important; max-width: 100vw !important;
|
||||||
|
min-height: 100vh !important; background: transparent !important;
|
||||||
|
padding: 0 !important; border-radius: 0 !important; box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Wrapper & Background ── */
|
||||||
|
.lg-wrap {
|
||||||
|
position: relative; width: 100%; min-height: 100vh;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
overflow: hidden; font-family: 'DM Sans', sans-serif;
|
||||||
|
}
|
||||||
|
.lg-bg {
|
||||||
|
position: absolute; inset: 0; z-index: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 60% at 70% 50%, rgba(111,186,157,.12) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse 50% 80% at 10% 80%, rgba(111,186,157,.08) 0%, transparent 55%),
|
||||||
|
#F8FDFB;
|
||||||
|
}
|
||||||
|
.lg-bg::before {
|
||||||
|
content: ''; position: absolute; inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(111,186,157,.055) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(111,186,157,.055) 1px, transparent 1px);
|
||||||
|
background-size: 48px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decorations */
|
||||||
|
.lg-ring { position: absolute; border-radius: 50%; border: 1.5px solid rgba(111,186,157,.18); pointer-events: none; }
|
||||||
|
.lg-ring.r1 { width:420px; height:420px; top:-110px; right:-110px; }
|
||||||
|
.lg-ring.r2 { width:270px; height:270px; bottom:50px; right:60px; border-color:rgba(111,186,157,.10); }
|
||||||
|
.lg-ring.r3 { width:200px; height:200px; bottom:-50px; left:60px; border-color:rgba(111,186,157,.14); }
|
||||||
|
.lg-dot { position:absolute; border-radius:50%; background:#6FBA9D; pointer-events:none; }
|
||||||
|
.lg-dot.d1 { width:8px; height:8px; top:22%; right:18%; opacity:.14; }
|
||||||
|
.lg-dot.d2 { width:5px; height:5px; bottom:40%; right:30%; opacity:.09; }
|
||||||
|
.lg-dot.d3 { width:11px; height:11px; top:55%; left:15%; opacity:.08; }
|
||||||
|
.lg-line { position:absolute; height:1px; background:linear-gradient(90deg,transparent,rgba(111,186,157,.14),transparent); pointer-events:none; }
|
||||||
|
.lg-line.l1 { width:280px; top:28%; left:-60px; transform:rotate(-15deg); }
|
||||||
|
.lg-line.l2 { width:220px; bottom:30%; right:-40px; transform:rotate(18deg); }
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.lg-layout {
|
||||||
|
position: relative; z-index: 2;
|
||||||
|
display: flex; align-items: center;
|
||||||
|
width: 100%; max-width: 1100px;
|
||||||
|
padding: 40px 60px; gap: 80px;
|
||||||
|
animation: lgIn .6s ease both;
|
||||||
|
}
|
||||||
|
@keyframes lgIn {
|
||||||
|
from { opacity:0; transform:translateY(20px); }
|
||||||
|
to { opacity:1; transform:translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brand left, Form right */
|
||||||
|
.lg-brand { flex: 0 0 340px; order: 1; }
|
||||||
|
.lg-form-panel { flex: 1; max-width: 430px; order: 2; }
|
||||||
|
|
||||||
|
/* ── Brand section ── */
|
||||||
|
.lg-logo { width:72px; height:72px; margin-bottom:20px; border-radius:16px; box-shadow:0 4px 20px rgba(111,186,157,.2); object-fit:contain; background:#fff; }
|
||||||
|
.lg-eyebrow {
|
||||||
|
display:inline-flex; align-items:center; gap:8px;
|
||||||
|
font-size:.68rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#6FBA9D; margin-bottom:18px;
|
||||||
|
}
|
||||||
|
.lg-eyebrow::before {
|
||||||
|
content:''; display:inline-block; width:22px; height:2px;
|
||||||
|
background:#6FBA9D; border-radius:2px;
|
||||||
|
}
|
||||||
|
.lg-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:3.2rem; line-height:1.05; color:#0F2118; margin-bottom:6px;
|
||||||
|
}
|
||||||
|
.lg-title em { font-style:italic; color:#5EA98C; }
|
||||||
|
.lg-sub { font-size:.9rem; font-weight:500; color:#8AADA0; margin-bottom:32px; line-height:1.6; }
|
||||||
|
.lg-divider { width:44px; height:3px; background:linear-gradient(90deg,#6FBA9D,#A8D8C6); border-radius:3px; margin-bottom:24px; }
|
||||||
|
.lg-desc { font-size:.81rem; color:#8AADA0; line-height:1.8; max-width:290px; margin-bottom:32px; }
|
||||||
|
.lg-features { display:flex; flex-direction:column; gap:11px; }
|
||||||
|
.lg-feat { display:flex; align-items:center; gap:11px; font-size:.79rem; color:#2A4235; font-weight:500; }
|
||||||
|
.lg-feat-ico {
|
||||||
|
width:30px; height:30px; border-radius:8px; background:#EBF7F2;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
color:#3D8A6E; font-size:.73rem; flex-shrink:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card ── */
|
||||||
|
.lg-card {
|
||||||
|
background: #fff; border-radius: 24px;
|
||||||
|
padding: 42px 38px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(111,186,157,.1),
|
||||||
|
0 4px 6px rgba(15,33,24,.03),
|
||||||
|
0 20px 44px rgba(15,33,24,.08);
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.lg-card::before {
|
||||||
|
content: ''; position:absolute; top:0; left:0; right:0; height:3px;
|
||||||
|
background: linear-gradient(90deg, #6FBA9D, #A8D8C6, #6FBA9D);
|
||||||
|
}
|
||||||
|
.lg-card::after {
|
||||||
|
content: ''; position:absolute; bottom:-50px; right:-50px;
|
||||||
|
width:140px; height:140px; border-radius:50%;
|
||||||
|
background: radial-gradient(circle, rgba(111,186,157,.06) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
.lg-card-lbl {
|
||||||
|
font-size:.67rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#6FBA9D; margin-bottom:7px;
|
||||||
|
}
|
||||||
|
.lg-card-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:1.85rem; color:#0F2118; line-height:1.1; margin-bottom:5px;
|
||||||
|
}
|
||||||
|
.lg-card-desc { font-size:.79rem; color:#8AADA0; line-height:1.6; margin-bottom:26px; }
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
.lg-alert-danger {
|
||||||
|
border-radius:10px; padding:10px 13px; font-size:.79rem;
|
||||||
|
margin-bottom:18px; background:#FFF3F3; color:#c62828; border-left:3px solid #e53935;
|
||||||
|
display:flex; align-items:center; gap:7px;
|
||||||
|
}
|
||||||
|
.lg-alert-success {
|
||||||
|
border-radius:10px; padding:10px 13px; font-size:.79rem;
|
||||||
|
margin-bottom:18px; background:#F0FFF4; color:#2E7D32; border-left:3px solid #43A047;
|
||||||
|
display:flex; align-items:center; gap:7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fields */
|
||||||
|
.lg-field { margin-bottom:15px; }
|
||||||
|
.lg-lbl {
|
||||||
|
display:block; font-size:.7rem; font-weight:700;
|
||||||
|
letter-spacing:.8px; text-transform:uppercase; color:#2A4235; margin-bottom:7px;
|
||||||
|
}
|
||||||
|
.lg-shell { position:relative; display:flex; align-items:center; }
|
||||||
|
.lg-shell .fi { position:absolute; left:15px; color:#A8D8C6; font-size:.8rem; pointer-events:none; transition:color .2s; }
|
||||||
|
.lg-shell input {
|
||||||
|
width:100%; padding:12px 15px 12px 40px;
|
||||||
|
background:#EBF7F2; border:1.5px solid transparent;
|
||||||
|
border-radius:11px; font-family:inherit; font-size:.87rem; color:#0F2118; outline:none;
|
||||||
|
transition:all .2s;
|
||||||
|
}
|
||||||
|
.lg-shell input::placeholder { color:#8AADA0; font-size:.83rem; }
|
||||||
|
.lg-shell input:focus {
|
||||||
|
background:#fff; border-color:#6FBA9D;
|
||||||
|
box-shadow:0 0 0 4px rgba(111,186,157,.12);
|
||||||
|
}
|
||||||
|
.lg-shell .fi.active { color:#6FBA9D; }
|
||||||
|
.lg-show {
|
||||||
|
position:absolute; right:13px;
|
||||||
|
background:none; border:none; font-size:.68rem; font-weight:800;
|
||||||
|
letter-spacing:.8px; color:#5EA98C; cursor:pointer; font-family:inherit;
|
||||||
|
}
|
||||||
|
.lg-show:hover { color:#3D8A6E; }
|
||||||
|
|
||||||
|
/* Remember + Forgot row */
|
||||||
|
.lg-options {
|
||||||
|
display:flex; align-items:center; justify-content:space-between;
|
||||||
|
margin-bottom:18px; font-size:.78rem;
|
||||||
|
}
|
||||||
|
.lg-remember { display:flex; align-items:center; gap:7px; color:#2A4235; font-weight:500; cursor:pointer; }
|
||||||
|
.lg-remember input[type="checkbox"] {
|
||||||
|
width:16px; height:16px; accent-color:#6FBA9D; cursor:pointer;
|
||||||
|
}
|
||||||
|
.lg-forgot { color:#e57373; font-weight:600; text-decoration:none; transition:color .2s; }
|
||||||
|
.lg-forgot:hover { color:#c62828; text-decoration:underline; }
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.lg-btn {
|
||||||
|
width:100%; padding:13px;
|
||||||
|
background:linear-gradient(135deg, #6FBA9D, #5EA98C);
|
||||||
|
color:#fff; border:none; border-radius:12px;
|
||||||
|
font-family:inherit; font-size:.89rem; font-weight:700;
|
||||||
|
cursor:pointer; letter-spacing:.3px; margin-top:6px;
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:8px;
|
||||||
|
box-shadow:0 4px 18px rgba(94,169,140,.35);
|
||||||
|
transition:all .25s;
|
||||||
|
}
|
||||||
|
.lg-btn:hover { transform:translateY(-2px); box-shadow:0 8px 26px rgba(94,169,140,.45); }
|
||||||
|
.lg-btn:active { transform:none; }
|
||||||
|
|
||||||
|
.lg-foot { text-align:center; font-size:.77rem; color:#8AADA0; margin-top:20px; }
|
||||||
|
.lg-foot a { color:#5EA98C; font-weight:700; text-decoration:none; }
|
||||||
|
.lg-foot a:hover { text-decoration:underline; }
|
||||||
|
|
||||||
|
/* Santri separator */
|
||||||
|
.lg-sep {
|
||||||
|
display:flex; align-items:center; gap:12px; margin-top:22px;
|
||||||
|
font-size:.7rem; color:#B8D4C8; letter-spacing:1px; text-transform:uppercase; font-weight:600;
|
||||||
|
}
|
||||||
|
.lg-sep::before, .lg-sep::after {
|
||||||
|
content:''; flex:1; height:1px; background:linear-gradient(90deg,transparent,#D6EDE5,transparent);
|
||||||
|
}
|
||||||
|
.lg-santri-link {
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:8px;
|
||||||
|
margin-top:12px; padding:10px;
|
||||||
|
background:#EBF7F2; border:1.5px solid transparent; border-radius:10px;
|
||||||
|
font-size:.8rem; font-weight:600; color:#3D8A6E; text-decoration:none;
|
||||||
|
transition:all .2s;
|
||||||
|
}
|
||||||
|
.lg-santri-link:hover {
|
||||||
|
border-color:#6FBA9D; background:#fff; box-shadow:0 0 0 3px rgba(111,186,157,.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.lg-layout { gap:48px; padding:32px 36px; }
|
||||||
|
.lg-brand { flex:0 0 260px; }
|
||||||
|
.lg-title { font-size:2.7rem; }
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
body.auth-page { align-items:flex-start !important; overflow-y:auto !important; }
|
||||||
|
.lg-wrap { align-items:flex-start; min-height:auto; padding:24px 0 40px; }
|
||||||
|
.lg-layout { flex-direction:column; padding:0 20px; gap:28px; }
|
||||||
|
.lg-form-panel { order:2; max-width:100%; }
|
||||||
|
.lg-brand { order:1; flex:none; text-align:center; }
|
||||||
|
.lg-title { font-size:2.2rem; }
|
||||||
|
.lg-features, .lg-desc, .lg-divider { display:none; }
|
||||||
|
.lg-sub { margin-bottom:0; }
|
||||||
|
.lg-card { padding:28px 20px; }
|
||||||
|
.lg-ring.r1 { width:260px; height:260px; top:-70px; right:-70px; }
|
||||||
|
.lg-ring.r2 { display:none; }
|
||||||
|
.lg-logo { width:56px; height:56px; margin:0 auto 14px; display:block; }
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.lg-title { font-size:1.85rem; }
|
||||||
|
.lg-card { padding:24px 16px; border-radius:16px; }
|
||||||
|
.lg-card-title { font-size:1.5rem; }
|
||||||
|
.lg-options { flex-direction:column; align-items:flex-start; gap:10px; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.lg-layout { max-width:1160px; padding:40px 80px; }
|
||||||
|
.lg-brand { flex:0 0 360px; }
|
||||||
|
.lg-title { font-size:3.6rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="lg-wrap">
|
||||||
|
<div class="lg-bg"></div>
|
||||||
|
<div class="lg-ring r1"></div>
|
||||||
|
<div class="lg-ring r2"></div>
|
||||||
|
<div class="lg-ring r3"></div>
|
||||||
|
<div class="lg-dot d1"></div>
|
||||||
|
<div class="lg-dot d2"></div>
|
||||||
|
<div class="lg-dot d3"></div>
|
||||||
|
<div class="lg-line l1"></div>
|
||||||
|
<div class="lg-line l2"></div>
|
||||||
|
|
||||||
|
<div class="lg-layout">
|
||||||
|
|
||||||
|
<!-- ═══ Brand (kiri) ═══ -->
|
||||||
|
<div class="lg-brand">
|
||||||
|
<img src="{{ asset('images/logo.png') }}" alt="Logo PKPPS" class="lg-logo">
|
||||||
|
<div class="lg-eyebrow">Selamat Datang</div>
|
||||||
|
<h1 class="lg-title">Masuk ke<br>Panel<br><em>Admin.</em></h1>
|
||||||
|
<p class="lg-sub">PKPPS Riyadlul Jannah</p>
|
||||||
|
<div class="lg-divider"></div>
|
||||||
|
<p class="lg-desc">Kelola data santri, absensi, keuangan, dan seluruh aktivitas pesantren dalam satu sistem terpadu.</p>
|
||||||
|
<div class="lg-features">
|
||||||
|
<div class="lg-feat">
|
||||||
|
<div class="lg-feat-ico"><i class="fas fa-chart-line"></i></div>
|
||||||
|
<span>Dashboard monitoring real-time</span>
|
||||||
|
</div>
|
||||||
|
<div class="lg-feat">
|
||||||
|
<div class="lg-feat-ico"><i class="fas fa-shield-alt"></i></div>
|
||||||
|
<span>Akses aman berbasis role</span>
|
||||||
|
</div>
|
||||||
|
<div class="lg-feat">
|
||||||
|
<div class="lg-feat-ico"><i class="fas fa-mobile-alt"></i></div>
|
||||||
|
<span>Terintegrasi aplikasi mobile wali</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Form (kanan) ═══ -->
|
||||||
|
<div class="lg-form-panel">
|
||||||
|
<div class="lg-card">
|
||||||
|
<div class="lg-card-lbl">Login Admin</div>
|
||||||
|
<div class="lg-card-title">Masuk Akun</div>
|
||||||
|
<div class="lg-card-desc">Masukkan username dan password untuk mengakses panel admin.</div>
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="lg-alert-danger">
|
||||||
<i class="fas fa-exclamation-circle"></i>
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
{{ $errors->first() }}
|
{{ $errors->first() }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Alert Success (dari logout) --}}
|
@if(session('success'))
|
||||||
@if(session('success'))
|
<div class="lg-alert-success" id="lgSuccessAlert">
|
||||||
<div class="alert alert-success">
|
|
||||||
<i class="fas fa-check-circle"></i>
|
<i class="fas fa-check-circle"></i>
|
||||||
{{ session('success') }}
|
{{ session('success') }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<form method="POST"
|
<form method="POST" action="{{ route('admin.login') }}" id="adminLoginForm">
|
||||||
action="{{ route('admin.login') }}"
|
|
||||||
id="adminLoginForm"
|
|
||||||
class="data-form"
|
|
||||||
autocomplete="on">
|
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
{{-- Username Field --}}
|
<div class="lg-field">
|
||||||
<div class="form-group">
|
<label class="lg-lbl">Username</label>
|
||||||
<label for="username">
|
<div class="lg-shell">
|
||||||
<i class="fas fa-user form-icon"></i>
|
<i class="fas fa-user fi" id="ico-u"></i>
|
||||||
Username
|
<input type="text" id="username" name="username"
|
||||||
</label>
|
|
||||||
<input type="text"
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
value="{{ old('username') }}"
|
value="{{ old('username') }}"
|
||||||
class="form-control @error('username') is-invalid @enderror"
|
|
||||||
autocomplete="username"
|
|
||||||
placeholder="Masukkan username admin"
|
placeholder="Masukkan username admin"
|
||||||
required
|
autocomplete="username" required autofocus
|
||||||
autofocus>
|
onfocus="document.getElementById('ico-u').classList.add('active')"
|
||||||
@error('username')
|
onblur="document.getElementById('ico-u').classList.remove('active')">
|
||||||
<div class="invalid-feedback">{{ $message }}</div>
|
</div>
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Password Field --}}
|
<div class="lg-field">
|
||||||
<div class="form-group">
|
<label class="lg-lbl">Password</label>
|
||||||
<label for="password">
|
<div class="lg-shell">
|
||||||
<i class="fas fa-lock form-icon"></i>
|
<i class="fas fa-lock fi" id="ico-p"></i>
|
||||||
Password
|
<input type="password" id="password" name="password"
|
||||||
</label>
|
|
||||||
<div style="position: relative;">
|
|
||||||
<input type="password"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
class="form-control @error('password') is-invalid @enderror"
|
|
||||||
autocomplete="current-password"
|
|
||||||
placeholder="Masukkan password"
|
placeholder="Masukkan password"
|
||||||
style="padding-right: 40px;"
|
autocomplete="current-password" required
|
||||||
required>
|
onfocus="document.getElementById('ico-p').classList.add('active')"
|
||||||
<button type="button"
|
onblur="document.getElementById('ico-p').classList.remove('active')">
|
||||||
id="togglePassword"
|
<button type="button" class="lg-show" id="lgTglBtn">SHOW</button>
|
||||||
style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; color: #666; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;">
|
|
||||||
<i class="fas fa-eye" id="eyeIcon"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
@error('password')
|
|
||||||
<div class="invalid-feedback">{{ $message }}</div>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Remember Me Checkbox --}}
|
<div class="lg-options">
|
||||||
<div class="form-group" style="display:flex; align-items: center;">
|
<label class="lg-remember">
|
||||||
<input type="checkbox"
|
<input type="checkbox" name="remember" id="remember"> Ingat Saya
|
||||||
name="remember"
|
</label>
|
||||||
id="remember"
|
<a href="{{ route('admin.forgot.email_form') }}" class="lg-forgot">
|
||||||
style="width: auto; margin-right: 10px;">
|
<i class="fas fa-key"></i> Lupa Password?
|
||||||
<label for="remember" style="font-weight: normal; margin-bottom: 0;">Ingat Saya</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Submit Button --}}
|
|
||||||
<div class="form-group">
|
|
||||||
<button type="submit" class="btn btn-primary btn-full">
|
|
||||||
<i class="fas fa-sign-in-alt"></i> Login
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Link ke Register --}}
|
|
||||||
<p style="text-align: center; font-size: 0.9rem; margin-top: 15px;">
|
|
||||||
Admin baru? <a href="{{ route('admin.register') }}" class="link-primary">Daftar Sekarang</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{-- Link ke Login Santri --}}
|
|
||||||
<div style="text-align: center; margin-top: 20px; padding-top: 20px; border-top: 2px solid var(--primary-light);">
|
|
||||||
<p style="color: var(--text-light); margin-bottom: 10px;">
|
|
||||||
<i class="fas fa-info-circle"></i> Login sebagai santri/wali?
|
|
||||||
</p>
|
|
||||||
<a href="{{ route('santri.login') }}" class="link-primary" style="font-size: 0.95rem;">
|
|
||||||
<i class="fas fa-user-graduate"></i> Login Santri/Wali
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
{{-- JavaScript --}}
|
<button type="submit" class="lg-btn">
|
||||||
|
<i class="fas fa-sign-in-alt"></i>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="lg-foot">
|
||||||
|
Admin baru? <a href="{{ route('admin.register') }}">Daftar Sekarang</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg-sep">atau</div>
|
||||||
|
<a href="{{ route('santri.login') }}" class="lg-santri-link">
|
||||||
|
<i class="fas fa-user-graduate"></i> Login sebagai Santri / Wali
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// ========================================
|
// Toggle password
|
||||||
// 1. Toggle Password Visibility
|
const btn = document.getElementById('lgTglBtn');
|
||||||
// ========================================
|
const pw = document.getElementById('password');
|
||||||
const togglePassword = document.getElementById('togglePassword');
|
if (btn && pw) {
|
||||||
const password = document.getElementById('password');
|
btn.addEventListener('click', () => {
|
||||||
const eyeIcon = document.getElementById('eyeIcon');
|
const isP = pw.type === 'password';
|
||||||
|
pw.type = isP ? 'text' : 'password';
|
||||||
if (togglePassword && password && eyeIcon) {
|
btn.textContent = isP ? 'HIDE' : 'SHOW';
|
||||||
togglePassword.addEventListener('click', function() {
|
|
||||||
// Toggle tipe input
|
|
||||||
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
|
|
||||||
password.setAttribute('type', type);
|
|
||||||
|
|
||||||
// Toggle icon
|
|
||||||
if (type === 'password') {
|
|
||||||
eyeIcon.classList.remove('fa-eye-slash');
|
|
||||||
eyeIcon.classList.add('fa-eye');
|
|
||||||
} else {
|
|
||||||
eyeIcon.classList.remove('fa-eye');
|
|
||||||
eyeIcon.classList.add('fa-eye-slash');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// CSRF check
|
||||||
// 2. Auto-refresh CSRF Token (FIX 419)
|
|
||||||
// ========================================
|
|
||||||
const form = document.getElementById('adminLoginForm');
|
const form = document.getElementById('adminLoginForm');
|
||||||
if (form) {
|
if (form) {
|
||||||
form.addEventListener('submit', function(e) {
|
form.addEventListener('submit', function(e) {
|
||||||
const csrfInput = document.querySelector('input[name="_token"]');
|
const csrf = document.querySelector('input[name="_token"]');
|
||||||
|
if (!csrf || !csrf.value || csrf.value.length < 40) {
|
||||||
if (!csrfInput) {
|
|
||||||
e.preventDefault();
|
|
||||||
alert('CSRF token tidak ditemukan. Halaman akan dimuat ulang.');
|
|
||||||
window.location.reload();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = csrfInput.value;
|
|
||||||
|
|
||||||
// Cek apakah token valid (minimal 40 karakter)
|
|
||||||
if (!token || token.length < 40) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('Session expired. Halaman akan dimuat ulang.');
|
alert('Session expired. Halaman akan dimuat ulang.');
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|
@ -164,52 +392,32 @@ class="form-control @error('password') is-invalid @enderror"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// Clear error on input
|
||||||
// 3. Clear Error Message on Input
|
const alertBox = document.querySelector('.lg-alert-danger');
|
||||||
// ========================================
|
if (alertBox) {
|
||||||
const usernameInput = document.getElementById('username');
|
['username','password'].forEach(id => {
|
||||||
const passwordInput = document.getElementById('password');
|
const el = document.getElementById(id);
|
||||||
const alertBox = document.querySelector('.alert-danger');
|
if (el) el.addEventListener('input', () => alertBox.style.display = 'none');
|
||||||
|
|
||||||
if (usernameInput && passwordInput && alertBox) {
|
|
||||||
usernameInput.addEventListener('input', function() {
|
|
||||||
alertBox.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
passwordInput.addEventListener('input', function() {
|
|
||||||
alertBox.style.display = 'none';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// Auto-hide success
|
||||||
// 4. Auto-hide Success Alert (setelah 5 detik)
|
const sa = document.getElementById('lgSuccessAlert');
|
||||||
// ========================================
|
if (sa) {
|
||||||
const successAlert = document.querySelector('.alert-success');
|
setTimeout(() => {
|
||||||
if (successAlert) {
|
sa.style.transition = 'opacity .5s ease';
|
||||||
setTimeout(function() {
|
sa.style.opacity = '0';
|
||||||
successAlert.style.transition = 'opacity 0.5s ease';
|
setTimeout(() => sa.remove(), 500);
|
||||||
successAlert.style.opacity = '0';
|
}, 5000);
|
||||||
setTimeout(function() {
|
|
||||||
successAlert.remove();
|
|
||||||
}, 500);
|
|
||||||
}, 5000); // 5 detik
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// Focus management
|
||||||
// 5. Focus Management
|
const u = document.getElementById('username');
|
||||||
// ========================================
|
const p = document.getElementById('password');
|
||||||
// Auto-focus ke username saat halaman load
|
if (u && !u.value) u.focus();
|
||||||
if (usernameInput && !usernameInput.value) {
|
if (u && p) {
|
||||||
usernameInput.focus();
|
u.addEventListener('keypress', e => {
|
||||||
}
|
if (e.key === 'Enter') { e.preventDefault(); p.focus(); }
|
||||||
|
|
||||||
// Enter di username -> pindah ke password
|
|
||||||
if (usernameInput && passwordInput) {
|
|
||||||
usernameInput.addEventListener('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
passwordInput.focus();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,361 @@
|
||||||
|
{{-- resources/views/admin/auth/register.blade.php --}}
|
||||||
@extends('auth.auth_layout')
|
@extends('auth.auth_layout')
|
||||||
|
|
||||||
@section('title', 'Register Admin')
|
@section('title', 'Register Admin')
|
||||||
|
|
||||||
@section('auth-content')
|
@section('auth-content')
|
||||||
<div class="auth-header">
|
|
||||||
<div class="logo-circle">
|
|
||||||
<i class="fas fa-lock-open fa-2x"></i>
|
|
||||||
</div>
|
|
||||||
<h2>Pendaftaran Akun Admin</h2>
|
|
||||||
<p>Mohon gunakan email dan password yang kuat untuk keamanan sistem.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Tampilkan error dari validator --}}
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:opsz,wght@9..40,300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
@if ($errors->any())
|
|
||||||
<div class="alert alert-danger">
|
<style>
|
||||||
|
body.auth-page {
|
||||||
|
background: #F8FDFB !important;
|
||||||
|
font-family: 'DM Sans', sans-serif !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
}
|
||||||
|
.auth-container {
|
||||||
|
width: 100vw !important; max-width: 100vw !important;
|
||||||
|
min-height: 100vh !important; background: transparent !important;
|
||||||
|
padding: 0 !important; border-radius: 0 !important; box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv2-wrap {
|
||||||
|
position: relative; width: 100%; min-height: 100vh;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
overflow: hidden; font-family: 'DM Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv2-bg {
|
||||||
|
position: absolute; inset: 0; z-index: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 60% at 30% 50%, rgba(111,186,157,.12) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse 50% 80% at 90% 20%, rgba(111,186,157,.08) 0%, transparent 55%),
|
||||||
|
#F8FDFB;
|
||||||
|
}
|
||||||
|
.rv2-bg::before {
|
||||||
|
content: ''; position: absolute; inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(111,186,157,.055) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(111,186,157,.055) 1px, transparent 1px);
|
||||||
|
background-size: 48px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv2-ring {
|
||||||
|
position: absolute; border-radius: 50%;
|
||||||
|
border: 1.5px solid rgba(111,186,157,.18); pointer-events: none;
|
||||||
|
}
|
||||||
|
.rv2-ring.r1 { width:420px; height:420px; top:-110px; left:-110px; }
|
||||||
|
.rv2-ring.r2 { width:270px; height:270px; top:50px; left:60px; border-color:rgba(111,186,157,.10); }
|
||||||
|
.rv2-ring.r3 { width:200px; height:200px; bottom:-50px; right:60px; border-color:rgba(111,186,157,.14); }
|
||||||
|
.rv2-dot { position:absolute; border-radius:50%; background:#6FBA9D; pointer-events:none; }
|
||||||
|
.rv2-dot.d1 { width:8px; height:8px; top:22%; left:18%; opacity:.14; }
|
||||||
|
.rv2-dot.d2 { width:5px; height:5px; bottom:40%; left:30%; opacity:.09; }
|
||||||
|
.rv2-dot.d3 { width:11px; height:11px; top:55%; right:15%; opacity:.08; }
|
||||||
|
.rv2-line { position:absolute; height:1px; background:linear-gradient(90deg,transparent,rgba(111,186,157,.14),transparent); pointer-events:none; }
|
||||||
|
.rv2-line.l1 { width:280px; top:28%; right:-60px; transform:rotate(15deg); }
|
||||||
|
.rv2-line.l2 { width:220px; bottom:30%; left:-40px; transform:rotate(-18deg); }
|
||||||
|
|
||||||
|
.rv2-layout {
|
||||||
|
position: relative; z-index: 2;
|
||||||
|
display: flex; align-items: center;
|
||||||
|
width: 100%; max-width: 1100px;
|
||||||
|
padding: 40px 60px; gap: 80px;
|
||||||
|
animation: rv2In .6s ease both;
|
||||||
|
}
|
||||||
|
@keyframes rv2In {
|
||||||
|
from { opacity:0; transform:translateY(20px); }
|
||||||
|
to { opacity:1; transform:translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form kiri dulu di register */
|
||||||
|
.rv2-form-panel { flex: 1; max-width: 430px; order: 1; }
|
||||||
|
.rv2-brand { flex: 0 0 320px; order: 2; }
|
||||||
|
|
||||||
|
/* CARD */
|
||||||
|
.rv2-card {
|
||||||
|
background: #fff; border-radius: 24px;
|
||||||
|
padding: 42px 38px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(111,186,157,.1),
|
||||||
|
0 4px 6px rgba(15,33,24,.03),
|
||||||
|
0 20px 44px rgba(15,33,24,.08);
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.rv2-card::before {
|
||||||
|
content: ''; position:absolute; top:0; left:0; right:0; height:3px;
|
||||||
|
background: linear-gradient(90deg, #6FBA9D, #A8D8C6, #6FBA9D);
|
||||||
|
}
|
||||||
|
.rv2-card::after {
|
||||||
|
content: ''; position:absolute; bottom:-50px; left:-50px;
|
||||||
|
width:140px; height:140px; border-radius:50%;
|
||||||
|
background: radial-gradient(circle, rgba(111,186,157,.06) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv2-card-lbl {
|
||||||
|
font-size:.67rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#6FBA9D; margin-bottom:7px;
|
||||||
|
}
|
||||||
|
.rv2-card-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:1.85rem; color:#0F2118; line-height:1.1; margin-bottom:5px;
|
||||||
|
}
|
||||||
|
.rv2-card-desc { font-size:.79rem; color:#8AADA0; line-height:1.6; margin-bottom:26px; }
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
.rv2-alert {
|
||||||
|
border-radius:10px; padding:10px 13px; font-size:.79rem;
|
||||||
|
margin-bottom:18px; background:#FFF3F3; color:#c62828; border-left:3px solid #e53935;
|
||||||
|
}
|
||||||
|
.rv2-alert p { display:flex; align-items:center; gap:7px; margin:2px 0; }
|
||||||
|
|
||||||
|
/* Fields */
|
||||||
|
.rv2-field { margin-bottom:15px; }
|
||||||
|
.rv2-lbl {
|
||||||
|
display:block; font-size:.7rem; font-weight:700;
|
||||||
|
letter-spacing:.8px; text-transform:uppercase; color:#2A4235; margin-bottom:7px;
|
||||||
|
}
|
||||||
|
.rv2-shell { position:relative; display:flex; align-items:center; }
|
||||||
|
.rv2-shell .fi { position:absolute; left:15px; color:#A8D8C6; font-size:.8rem; pointer-events:none; transition:color .2s; }
|
||||||
|
.rv2-shell input {
|
||||||
|
width:100%; padding:12px 15px 12px 40px;
|
||||||
|
background:#EBF7F2; border:1.5px solid transparent;
|
||||||
|
border-radius:11px; font-family:inherit; font-size:.87rem; color:#0F2118; outline:none;
|
||||||
|
transition:all .2s;
|
||||||
|
}
|
||||||
|
.rv2-shell input::placeholder { color:#8AADA0; font-size:.83rem; }
|
||||||
|
.rv2-shell input:focus {
|
||||||
|
background:#fff; border-color:#6FBA9D;
|
||||||
|
box-shadow:0 0 0 4px rgba(111,186,157,.12);
|
||||||
|
}
|
||||||
|
.rv2-show {
|
||||||
|
position:absolute; right:13px;
|
||||||
|
background:none; border:none; font-size:.68rem; font-weight:800;
|
||||||
|
letter-spacing:.8px; color:#5EA98C; cursor:pointer; font-family:inherit;
|
||||||
|
}
|
||||||
|
.rv2-show:hover { color:#3D8A6E; }
|
||||||
|
.rv2-ferr { font-size:.72rem; color:#e53935; margin-top:4px; padding-left:3px; }
|
||||||
|
|
||||||
|
/* Strength */
|
||||||
|
.rv2-strength { display:flex; gap:4px; margin-top:7px; }
|
||||||
|
.rv2-bar { height:3px; flex:1; border-radius:3px; background:#D6EDE5; transition:background .3s; }
|
||||||
|
.rv2-bar.w { background:#e53935; }
|
||||||
|
.rv2-bar.m { background:#FB8C00; }
|
||||||
|
.rv2-bar.s { background:#6FBA9D; }
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.rv2-btn {
|
||||||
|
width:100%; padding:13px;
|
||||||
|
background:linear-gradient(135deg, #6FBA9D, #5EA98C);
|
||||||
|
color:#fff; border:none; border-radius:12px;
|
||||||
|
font-family:inherit; font-size:.89rem; font-weight:700;
|
||||||
|
cursor:pointer; letter-spacing:.3px; margin-top:6px;
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:8px;
|
||||||
|
box-shadow:0 4px 18px rgba(94,169,140,.35);
|
||||||
|
transition:all .25s;
|
||||||
|
}
|
||||||
|
.rv2-btn:hover { transform:translateY(-2px); box-shadow:0 8px 26px rgba(94,169,140,.45); }
|
||||||
|
.rv2-btn:active { transform:none; }
|
||||||
|
|
||||||
|
.rv2-foot { text-align:center; font-size:.77rem; color:#8AADA0; margin-top:20px; }
|
||||||
|
.rv2-foot a { color:#5EA98C; font-weight:700; text-decoration:none; }
|
||||||
|
.rv2-foot a:hover { text-decoration:underline; }
|
||||||
|
|
||||||
|
/* Brand */
|
||||||
|
.rv2-logo { width:72px; height:72px; margin-bottom:20px; border-radius:16px; box-shadow:0 4px 20px rgba(111,186,157,.2); object-fit:contain; background:#fff; }
|
||||||
|
.rv2-eyebrow {
|
||||||
|
display:inline-flex; align-items:center; gap:8px;
|
||||||
|
font-size:.68rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#6FBA9D; margin-bottom:18px;
|
||||||
|
}
|
||||||
|
.rv2-eyebrow::before {
|
||||||
|
content:''; display:inline-block; width:22px; height:2px;
|
||||||
|
background:#6FBA9D; border-radius:2px;
|
||||||
|
}
|
||||||
|
.rv2-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:3.2rem; line-height:1.05; color:#0F2118; margin-bottom:6px;
|
||||||
|
}
|
||||||
|
.rv2-title em { font-style:italic; color:#5EA98C; }
|
||||||
|
.rv2-sub { font-size:.9rem; font-weight:500; color:#8AADA0; margin-bottom:32px; line-height:1.6; }
|
||||||
|
.rv2-divider { width:44px; height:3px; background:linear-gradient(90deg,#6FBA9D,#A8D8C6); border-radius:3px; margin-bottom:24px; }
|
||||||
|
.rv2-desc { font-size:.81rem; color:#8AADA0; line-height:1.8; max-width:270px; margin-bottom:32px; }
|
||||||
|
.rv2-features { display:flex; flex-direction:column; gap:11px; }
|
||||||
|
.rv2-feat { display:flex; align-items:center; gap:11px; font-size:.79rem; color:#2A4235; font-weight:500; }
|
||||||
|
.rv2-feat-ico {
|
||||||
|
width:30px; height:30px; border-radius:8px; background:#EBF7F2;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
color:#3D8A6E; font-size:.73rem; flex-shrink:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.rv2-layout { gap:48px; padding:32px 36px; }
|
||||||
|
.rv2-brand { flex:0 0 260px; }
|
||||||
|
.rv2-title { font-size:2.7rem; }
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
body.auth-page { align-items:flex-start !important; overflow-y:auto !important; }
|
||||||
|
.rv2-wrap { align-items:flex-start; min-height:auto; padding:24px 0 40px; }
|
||||||
|
.rv2-layout { flex-direction:column; padding:0 20px; gap:28px; }
|
||||||
|
.rv2-form-panel { order:2; max-width:100%; }
|
||||||
|
.rv2-brand { order:1; flex:none; }
|
||||||
|
.rv2-title { font-size:2.2rem; }
|
||||||
|
.rv2-logo { width:56px; height:56px; margin:0 auto 14px; display:block; }
|
||||||
|
.rv2-features, .rv2-desc, .rv2-divider { display:none; }
|
||||||
|
.rv2-sub { margin-bottom:0; }
|
||||||
|
.rv2-card { padding:28px 20px; }
|
||||||
|
.rv2-ring.r1 { width:260px; height:260px; top:-70px; left:-70px; }
|
||||||
|
.rv2-ring.r2 { display:none; }
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.rv2-title { font-size:1.85rem; }
|
||||||
|
.rv2-card { padding:24px 16px; border-radius:16px; }
|
||||||
|
.rv2-card-title { font-size:1.5rem; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.rv2-layout { max-width:1160px; padding:40px 80px; }
|
||||||
|
.rv2-brand { flex:0 0 360px; }
|
||||||
|
.rv2-title { font-size:3.6rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="rv2-wrap">
|
||||||
|
<div class="rv2-bg"></div>
|
||||||
|
<div class="rv2-ring r1"></div>
|
||||||
|
<div class="rv2-ring r2"></div>
|
||||||
|
<div class="rv2-ring r3"></div>
|
||||||
|
<div class="rv2-dot d1"></div>
|
||||||
|
<div class="rv2-dot d2"></div>
|
||||||
|
<div class="rv2-dot d3"></div>
|
||||||
|
<div class="rv2-line l1"></div>
|
||||||
|
<div class="rv2-line l2"></div>
|
||||||
|
|
||||||
|
<div class="rv2-layout">
|
||||||
|
|
||||||
|
<!-- Form (kiri) -->
|
||||||
|
<div class="rv2-form-panel">
|
||||||
|
<div class="rv2-card">
|
||||||
|
<div class="rv2-card-lbl">Pendaftaran Admin</div>
|
||||||
|
<div class="rv2-card-title">Buat Akun Baru</div>
|
||||||
|
<div class="rv2-card-desc">Isi data berikut untuk mendaftarkan akun admin Anda.</div>
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="rv2-alert">
|
||||||
@foreach ($errors->all() as $error)
|
@foreach ($errors->all() as $error)
|
||||||
<p>{{ $error }}</p>
|
<p><i class="fas fa-circle-exclamation"></i> {{ $error }}</p>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<form method="POST" action="{{ route('admin.register') }}" class="data-form">
|
<form method="POST" action="{{ route('admin.register') }}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="rv2-field">
|
||||||
<label for="email"><i class="fas fa-envelope form-icon"></i> Email Admin</label>
|
<label class="rv2-lbl">Email Admin</label>
|
||||||
<input type="email" id="email" name="email" value="{{ old('email') }}" class="form-control @error('email') is-invalid @enderror" required autofocus>
|
<div class="rv2-shell">
|
||||||
@error('email')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
<i class="fas fa-envelope fi" id="ico-e"></i>
|
||||||
|
<input type="email" id="email" name="email"
|
||||||
|
value="{{ old('email') }}"
|
||||||
|
placeholder="nama@institusi.com"
|
||||||
|
autocomplete="email" required autofocus
|
||||||
|
onfocus="document.getElementById('ico-e').style.color='#6FBA9D'"
|
||||||
|
onblur="document.getElementById('ico-e').style.color=''">
|
||||||
|
</div>
|
||||||
|
@error('email')<div class="rv2-ferr">{{ $message }}</div>@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="rv2-field">
|
||||||
<label for="password"><i class="fas fa-key form-icon"></i> Password</label>
|
<label class="rv2-lbl">Password</label>
|
||||||
<input type="password" id="password" name="password" class="form-control @error('password') is-invalid @enderror" required>
|
<div class="rv2-shell">
|
||||||
@error('password')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
<i class="fas fa-lock fi" id="ico-pw"></i>
|
||||||
|
<input type="password" id="password" name="password"
|
||||||
|
placeholder="Buat password yang kuat"
|
||||||
|
oninput="rv2Str(this.value)" required
|
||||||
|
onfocus="document.getElementById('ico-pw').style.color='#6FBA9D'"
|
||||||
|
onblur="document.getElementById('ico-pw').style.color=''">
|
||||||
|
<button type="button" class="rv2-show" id="rv2TglBtn">SHOW</button>
|
||||||
|
</div>
|
||||||
|
<div class="rv2-strength">
|
||||||
|
<div class="rv2-bar" id="rv2b1"></div>
|
||||||
|
<div class="rv2-bar" id="rv2b2"></div>
|
||||||
|
<div class="rv2-bar" id="rv2b3"></div>
|
||||||
|
<div class="rv2-bar" id="rv2b4"></div>
|
||||||
|
</div>
|
||||||
|
@error('password')<div class="rv2-ferr">{{ $message }}</div>@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="rv2-field" style="margin-bottom:22px;">
|
||||||
<label for="password_confirmation"><i class="fas fa-lock form-icon"></i> Konfirmasi Password</label>
|
<label class="rv2-lbl">Konfirmasi Password</label>
|
||||||
<input type="password" id="password_confirmation" name="password_confirmation" class="form-control" required>
|
<div class="rv2-shell">
|
||||||
|
<i class="fas fa-lock-open fi" id="ico-c"></i>
|
||||||
|
<input type="password" id="password_confirmation"
|
||||||
|
name="password_confirmation"
|
||||||
|
placeholder="Ulangi password Anda" required
|
||||||
|
onfocus="document.getElementById('ico-c').style.color='#6FBA9D'"
|
||||||
|
onblur="document.getElementById('ico-c').style.color=''">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group action-group">
|
<button type="submit" class="rv2-btn">
|
||||||
<button type="submit" class="btn btn-success btn-full hover-shadow">
|
<i class="fas fa-user-plus"></i>
|
||||||
<i class="fas fa-paper-plane"></i> Daftarkan Admin
|
Daftarkan Akun Admin
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="rv2-foot">
|
||||||
|
Sudah punya akun? <a href="{{ route('admin.login') }}">Login di sini</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="text-align: center; font-size: 0.9rem; margin-top: 20px;">
|
<!-- Brand (kanan) -->
|
||||||
Sudah punya akun? <a href="{{ route('admin.login') }}" class="link-primary">Login di sini</a>
|
<div class="rv2-brand">
|
||||||
</p>
|
<img src="{{ asset('images/logo.png') }}" alt="Logo PKPPS" class="rv2-logo">
|
||||||
</form>
|
<div class="rv2-eyebrow">Bergabung Sekarang</div>
|
||||||
|
<h1 class="rv2-title">Bergabung<br>Bersama<br><em>Kami.</em></h1>
|
||||||
|
<p class="rv2-sub">PKPPS Riyadlul Jannah</p>
|
||||||
|
<div class="rv2-divider"></div>
|
||||||
|
<p class="rv2-desc">Daftarkan akun admin baru dengan aman. Gunakan email dan password kuat untuk menjaga keamanan sistem pesantren.</p>
|
||||||
|
<div class="rv2-features">
|
||||||
|
<div class="rv2-feat">
|
||||||
|
<div class="rv2-feat-ico"><i class="fas fa-envelope"></i></div>
|
||||||
|
<span>Gunakan email institusi yang valid</span>
|
||||||
|
</div>
|
||||||
|
<div class="rv2-feat">
|
||||||
|
<div class="rv2-feat-ico"><i class="fas fa-key"></i></div>
|
||||||
|
<span>Password minimal 8 karakter campuran</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const btn = document.getElementById('rv2TglBtn');
|
||||||
|
const pw = document.getElementById('password');
|
||||||
|
if (btn && pw) {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const isP = pw.type === 'password';
|
||||||
|
pw.type = isP ? 'text' : 'password';
|
||||||
|
btn.textContent = isP ? 'HIDE' : 'SHOW';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function rv2Str(v) {
|
||||||
|
const bars = ['rv2b1','rv2b2','rv2b3','rv2b4'].map(id => document.getElementById(id));
|
||||||
|
bars.forEach(b => b.className = 'rv2-bar');
|
||||||
|
let s = 0;
|
||||||
|
if (v.length >= 6) s++;
|
||||||
|
if (v.length >= 10) s++;
|
||||||
|
if (/[A-Z]/.test(v) && /[0-9]/.test(v)) s++;
|
||||||
|
if (/[^A-Za-z0-9]/.test(v)) s++;
|
||||||
|
const cls = s <= 1 ? 'w' : s <= 2 ? 'm' : 's';
|
||||||
|
for (let i = 0; i < s; i++) bars[i].classList.add(cls);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
@ -0,0 +1,521 @@
|
||||||
|
{{-- resources/views/admin/auth/reset_password.blade.php --}}
|
||||||
|
@extends('auth.auth_layout')
|
||||||
|
|
||||||
|
@section('title', 'Reset Password')
|
||||||
|
|
||||||
|
@section('auth-content')
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:opsz,wght@9..40,300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body.auth-page {
|
||||||
|
background: #F8FDFB !important;
|
||||||
|
font-family: 'DM Sans', sans-serif !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
}
|
||||||
|
.auth-container {
|
||||||
|
width: 100vw !important; max-width: 100vw !important;
|
||||||
|
min-height: 100vh !important; background: transparent !important;
|
||||||
|
padding: 0 !important; border-radius: 0 !important; box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rp-wrap {
|
||||||
|
position: relative; width: 100%; min-height: 100vh;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
overflow: hidden; font-family: 'DM Sans', sans-serif;
|
||||||
|
}
|
||||||
|
.rp-bg {
|
||||||
|
position: absolute; inset: 0; z-index: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 60% at 70% 50%, rgba(111,186,157,.12) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse 50% 80% at 10% 80%, rgba(111,186,157,.08) 0%, transparent 55%),
|
||||||
|
#F8FDFB;
|
||||||
|
}
|
||||||
|
.rp-bg::before {
|
||||||
|
content: ''; position: absolute; inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(111,186,157,.055) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(111,186,157,.055) 1px, transparent 1px);
|
||||||
|
background-size: 48px 48px;
|
||||||
|
}
|
||||||
|
.rp-ring { position: absolute; border-radius: 50%; border: 1.5px solid rgba(111,186,157,.18); pointer-events: none; }
|
||||||
|
.rp-ring.r1 { width:420px; height:420px; top:-110px; right:-110px; }
|
||||||
|
.rp-ring.r2 { width:200px; height:200px; bottom:-50px; left:60px; border-color:rgba(111,186,157,.14); }
|
||||||
|
.rp-dot { position:absolute; border-radius:50%; background:#6FBA9D; pointer-events:none; }
|
||||||
|
.rp-dot.d1 { width:8px; height:8px; top:22%; right:18%; opacity:.14; }
|
||||||
|
.rp-dot.d2 { width:11px; height:11px; top:55%; left:15%; opacity:.08; }
|
||||||
|
|
||||||
|
.rp-layout {
|
||||||
|
position: relative; z-index: 2;
|
||||||
|
display: flex; align-items: center;
|
||||||
|
width: 100%; max-width: 1100px;
|
||||||
|
padding: 40px 60px; gap: 80px;
|
||||||
|
animation: rpIn .6s ease both;
|
||||||
|
}
|
||||||
|
@keyframes rpIn {
|
||||||
|
from { opacity:0; transform:translateY(20px); }
|
||||||
|
to { opacity:1; transform:translateY(0); }
|
||||||
|
}
|
||||||
|
.rp-brand { flex: 0 0 340px; order: 1; }
|
||||||
|
.rp-form-panel { flex: 1; max-width: 460px; order: 2; }
|
||||||
|
|
||||||
|
/* Brand */
|
||||||
|
.rp-logo { width:72px; height:72px; margin-bottom:20px; border-radius:16px; box-shadow:0 4px 20px rgba(111,186,157,.2); object-fit:contain; background:#fff; }
|
||||||
|
.rp-eyebrow {
|
||||||
|
display:inline-flex; align-items:center; gap:8px;
|
||||||
|
font-size:.68rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#6FBA9D; margin-bottom:18px;
|
||||||
|
}
|
||||||
|
.rp-eyebrow::before { content:''; display:inline-block; width:22px; height:2px; background:#6FBA9D; border-radius:2px; }
|
||||||
|
.rp-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:3.2rem; line-height:1.05; color:#0F2118; margin-bottom:6px;
|
||||||
|
}
|
||||||
|
.rp-title em { font-style:italic; color:#5EA98C; }
|
||||||
|
.rp-sub { font-size:.9rem; font-weight:500; color:#8AADA0; margin-bottom:32px; line-height:1.6; }
|
||||||
|
.rp-divider { width:44px; height:3px; background:linear-gradient(90deg,#6FBA9D,#A8D8C6); border-radius:3px; margin-bottom:24px; }
|
||||||
|
.rp-desc { font-size:.81rem; color:#8AADA0; line-height:1.8; max-width:290px; margin-bottom:32px; }
|
||||||
|
|
||||||
|
/* Steps */
|
||||||
|
.rp-steps { display:flex; flex-direction:column; gap:14px; margin-bottom:28px; }
|
||||||
|
.rp-step { display:flex; align-items:flex-start; gap:12px; }
|
||||||
|
.rp-step-num {
|
||||||
|
width:28px; height:28px; border-radius:50%;
|
||||||
|
background:#D6EDE5; color:#5EA98C;
|
||||||
|
font-size:.72rem; font-weight:800;
|
||||||
|
display:flex; align-items:center; justify-content:center; flex-shrink:0;
|
||||||
|
}
|
||||||
|
.rp-step-num.done { background:linear-gradient(135deg,#6FBA9D,#5EA98C); color:#fff; }
|
||||||
|
.rp-step-num.active { background:linear-gradient(135deg,#6FBA9D,#5EA98C); color:#fff; box-shadow:0 0 0 4px rgba(111,186,157,.2); }
|
||||||
|
.rp-step-text { font-size:.78rem; color:#2A4235; font-weight:500; line-height:1.5; padding-top:3px; }
|
||||||
|
.rp-step-text small { display:block; color:#8AADA0; font-weight:400; font-size:.72rem; }
|
||||||
|
|
||||||
|
/* Password rules */
|
||||||
|
.rp-rules { margin-bottom:24px; }
|
||||||
|
.rp-rules-title { font-size:.72rem; font-weight:700; letter-spacing:.5px; color:#2A4235; margin-bottom:10px; text-transform:uppercase; }
|
||||||
|
.rp-rule {
|
||||||
|
display:flex; align-items:center; gap:8px;
|
||||||
|
font-size:.76rem; color:#8AADA0; font-weight:500;
|
||||||
|
padding:4px 0; transition:color .2s;
|
||||||
|
}
|
||||||
|
.rp-rule i { font-size:.65rem; width:16px; text-align:center; }
|
||||||
|
.rp-rule.pass { color:#2E7D32; }
|
||||||
|
.rp-rule.pass i { color:#43A047; }
|
||||||
|
.rp-rule.fail { color:#c62828; }
|
||||||
|
.rp-rule.fail i { color:#e53935; }
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.rp-card {
|
||||||
|
background: #fff; border-radius: 24px;
|
||||||
|
padding: 42px 38px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(111,186,157,.1),
|
||||||
|
0 4px 6px rgba(15,33,24,.03),
|
||||||
|
0 20px 44px rgba(15,33,24,.08);
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.rp-card::before {
|
||||||
|
content: ''; position:absolute; top:0; left:0; right:0; height:3px;
|
||||||
|
background: linear-gradient(90deg, #6FBA9D, #A8D8C6, #6FBA9D);
|
||||||
|
}
|
||||||
|
.rp-card::after {
|
||||||
|
content: ''; position:absolute; bottom:-50px; right:-50px;
|
||||||
|
width:140px; height:140px; border-radius:50%;
|
||||||
|
background: radial-gradient(circle, rgba(111,186,157,.06) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
.rp-card-icon {
|
||||||
|
width:54px; height:54px; border-radius:14px;
|
||||||
|
background:linear-gradient(135deg,#E8F5E9,#C8E6C9);
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
font-size:1.3rem; color:#2E7D32; margin-bottom:16px;
|
||||||
|
}
|
||||||
|
.rp-card-lbl {
|
||||||
|
font-size:.67rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#6FBA9D; margin-bottom:7px;
|
||||||
|
}
|
||||||
|
.rp-card-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:1.85rem; color:#0F2118; line-height:1.1; margin-bottom:5px;
|
||||||
|
}
|
||||||
|
.rp-card-desc { font-size:.79rem; color:#8AADA0; line-height:1.6; margin-bottom:26px; }
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
.rp-alert-danger {
|
||||||
|
border-radius:10px; padding:10px 13px; font-size:.79rem;
|
||||||
|
margin-bottom:18px; background:#FFF3F3; color:#c62828; border-left:3px solid #e53935;
|
||||||
|
display:flex; align-items:center; gap:7px;
|
||||||
|
}
|
||||||
|
.rp-alert-success {
|
||||||
|
border-radius:10px; padding:10px 13px; font-size:.79rem;
|
||||||
|
margin-bottom:18px; background:#F0FFF4; color:#2E7D32; border-left:3px solid #43A047;
|
||||||
|
display:flex; align-items:center; gap:7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fields */
|
||||||
|
.rp-field { margin-bottom:16px; }
|
||||||
|
.rp-lbl { display:block; font-size:.7rem; font-weight:700; letter-spacing:.8px; text-transform:uppercase; color:#2A4235; margin-bottom:7px; }
|
||||||
|
.rp-shell { position:relative; display:flex; align-items:center; }
|
||||||
|
.rp-shell .fi { position:absolute; left:15px; color:#A8D8C6; font-size:.8rem; pointer-events:none; transition:color .2s; }
|
||||||
|
.rp-shell input {
|
||||||
|
width:100%; padding:12px 50px 12px 40px;
|
||||||
|
background:#EBF7F2; border:1.5px solid transparent;
|
||||||
|
border-radius:11px; font-family:inherit; font-size:.87rem; color:#0F2118; outline:none;
|
||||||
|
transition:all .2s;
|
||||||
|
}
|
||||||
|
.rp-shell input::placeholder { color:#8AADA0; font-size:.83rem; }
|
||||||
|
.rp-shell input:focus { background:#fff; border-color:#6FBA9D; box-shadow:0 0 0 4px rgba(111,186,157,.12); }
|
||||||
|
.rp-shell .fi.active { color:#6FBA9D; }
|
||||||
|
.rp-show {
|
||||||
|
position:absolute; right:13px;
|
||||||
|
background:none; border:none; font-size:.68rem; font-weight:800;
|
||||||
|
letter-spacing:.8px; color:#5EA98C; cursor:pointer; font-family:inherit;
|
||||||
|
}
|
||||||
|
.rp-show:hover { color:#3D8A6E; }
|
||||||
|
|
||||||
|
/* Strength bar */
|
||||||
|
.rp-strength {
|
||||||
|
display:flex; gap:4px; margin-top:8px;
|
||||||
|
}
|
||||||
|
.rp-bar { height:4px; flex:1; border-radius:3px; background:#D6EDE5; transition:background .3s; }
|
||||||
|
.rp-str-text { font-size:.72rem; margin-top:4px; font-weight:600; transition:color .3s; }
|
||||||
|
|
||||||
|
/* Match */
|
||||||
|
.rp-match { font-size:.72rem; margin-top:6px; font-weight:600; }
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
.rp-btn {
|
||||||
|
width:100%; padding:13px;
|
||||||
|
background:linear-gradient(135deg, #6FBA9D, #5EA98C);
|
||||||
|
color:#fff; border:none; border-radius:12px;
|
||||||
|
font-family:inherit; font-size:.89rem; font-weight:700;
|
||||||
|
cursor:pointer; letter-spacing:.3px; margin-top:4px;
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:8px;
|
||||||
|
box-shadow:0 4px 18px rgba(94,169,140,.35);
|
||||||
|
transition:all .25s;
|
||||||
|
}
|
||||||
|
.rp-btn:hover { transform:translateY(-2px); box-shadow:0 8px 26px rgba(94,169,140,.45); }
|
||||||
|
.rp-btn:active { transform:none; }
|
||||||
|
.rp-btn:disabled {
|
||||||
|
background:#D6EDE5; color:#8AADA0; cursor:not-allowed;
|
||||||
|
box-shadow:none; transform:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rp-back {
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:6px;
|
||||||
|
margin-top:20px; font-size:.78rem; color:#5EA98C; font-weight:600; text-decoration:none;
|
||||||
|
}
|
||||||
|
.rp-back:hover { color:#3D8A6E; text-decoration:underline; }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.rp-layout { gap:48px; padding:32px 36px; }
|
||||||
|
.rp-brand { flex:0 0 260px; }
|
||||||
|
.rp-title { font-size:2.7rem; }
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
body.auth-page { align-items:flex-start !important; overflow-y:auto !important; }
|
||||||
|
.rp-wrap { align-items:flex-start; min-height:auto; padding:24px 0 40px; }
|
||||||
|
.rp-layout { flex-direction:column; padding:0 20px; gap:28px; }
|
||||||
|
.rp-form-panel { order:2; max-width:100%; }
|
||||||
|
.rp-brand { order:1; flex:none; text-align:center; }
|
||||||
|
.rp-title { font-size:2.2rem; }
|
||||||
|
.rp-steps, .rp-desc, .rp-divider, .rp-rules { display:none; }
|
||||||
|
.rp-sub { margin-bottom:0; }
|
||||||
|
.rp-card { padding:28px 20px; }
|
||||||
|
.rp-logo { width:56px; height:56px; margin:0 auto 14px; display:block; }
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.rp-title { font-size:1.85rem; }
|
||||||
|
.rp-card { padding:24px 16px; border-radius:16px; }
|
||||||
|
.rp-card-title { font-size:1.5rem; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.rp-layout { max-width:1160px; padding:40px 80px; }
|
||||||
|
.rp-brand { flex:0 0 360px; }
|
||||||
|
.rp-title { font-size:3.6rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="rp-wrap">
|
||||||
|
<div class="rp-bg"></div>
|
||||||
|
<div class="rp-ring r1"></div>
|
||||||
|
<div class="rp-ring r2"></div>
|
||||||
|
<div class="rp-dot d1"></div>
|
||||||
|
<div class="rp-dot d2"></div>
|
||||||
|
|
||||||
|
<div class="rp-layout">
|
||||||
|
|
||||||
|
<!-- ═══ Brand (kiri) ═══ -->
|
||||||
|
<div class="rp-brand">
|
||||||
|
<img src="{{ asset('images/logo.png') }}" alt="Logo PKPPS" class="rp-logo">
|
||||||
|
<div class="rp-eyebrow">Langkah Terakhir</div>
|
||||||
|
<h1 class="rp-title">Buat<br>Password<br><em>Baru.</em></h1>
|
||||||
|
<p class="rp-sub">PKPPS Riyadlul Jannah</p>
|
||||||
|
<div class="rp-divider"></div>
|
||||||
|
|
||||||
|
<div class="rp-steps">
|
||||||
|
<div class="rp-step">
|
||||||
|
<div class="rp-step-num done"><i class="fas fa-check" style="font-size:.6rem"></i></div>
|
||||||
|
<div class="rp-step-text" style="color:#8AADA0;">Email terkirim</div>
|
||||||
|
</div>
|
||||||
|
<div class="rp-step">
|
||||||
|
<div class="rp-step-num done"><i class="fas fa-check" style="font-size:.6rem"></i></div>
|
||||||
|
<div class="rp-step-text" style="color:#8AADA0;">OTP terverifikasi</div>
|
||||||
|
</div>
|
||||||
|
<div class="rp-step">
|
||||||
|
<div class="rp-step-num active">3</div>
|
||||||
|
<div class="rp-step-text">Buat password baru <small>Ikuti ketentuan di bawah</small></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Ketentuan Password --}}
|
||||||
|
<div class="rp-rules">
|
||||||
|
<div class="rp-rules-title"><i class="fas fa-info-circle"></i> Ketentuan Password</div>
|
||||||
|
<div class="rp-rule" id="rule-length">
|
||||||
|
<i class="fas fa-circle"></i> Minimal 8 karakter
|
||||||
|
</div>
|
||||||
|
<div class="rp-rule" id="rule-upper">
|
||||||
|
<i class="fas fa-circle"></i> Mengandung huruf besar (A-Z)
|
||||||
|
</div>
|
||||||
|
<div class="rp-rule" id="rule-lower">
|
||||||
|
<i class="fas fa-circle"></i> Mengandung huruf kecil (a-z)
|
||||||
|
</div>
|
||||||
|
<div class="rp-rule" id="rule-number">
|
||||||
|
<i class="fas fa-circle"></i> Mengandung angka (0-9)
|
||||||
|
</div>
|
||||||
|
<div class="rp-rule" id="rule-special">
|
||||||
|
<i class="fas fa-circle"></i> Mengandung simbol (!@#$%...)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Form (kanan) ═══ -->
|
||||||
|
<div class="rp-form-panel">
|
||||||
|
<div class="rp-card">
|
||||||
|
<div class="rp-card-icon">
|
||||||
|
<i class="fas fa-lock-open"></i>
|
||||||
|
</div>
|
||||||
|
<div class="rp-card-lbl">Langkah 3 dari 3</div>
|
||||||
|
<div class="rp-card-title">Password Baru</div>
|
||||||
|
<div class="rp-card-desc">Buat password baru yang kuat. Password harus memenuhi semua ketentuan keamanan berikut.</div>
|
||||||
|
|
||||||
|
{{-- Ketentuan Password (visible di mobile saja) --}}
|
||||||
|
<div class="rp-rules-mobile" style="display:none; margin-bottom:20px;">
|
||||||
|
<div class="rp-rules-title" style="font-size:.68rem;"><i class="fas fa-info-circle"></i> Ketentuan Password</div>
|
||||||
|
<div class="rp-rule" id="rule-length-m"><i class="fas fa-circle"></i> Minimal 8 karakter</div>
|
||||||
|
<div class="rp-rule" id="rule-upper-m"><i class="fas fa-circle"></i> Huruf besar (A-Z)</div>
|
||||||
|
<div class="rp-rule" id="rule-lower-m"><i class="fas fa-circle"></i> Huruf kecil (a-z)</div>
|
||||||
|
<div class="rp-rule" id="rule-number-m"><i class="fas fa-circle"></i> Angka (0-9)</div>
|
||||||
|
<div class="rp-rule" id="rule-special-m"><i class="fas fa-circle"></i> Simbol (!@#$%...)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="rp-alert-danger">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
{{ $errors->first() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="rp-alert-success" id="rpSuccessAlert">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
{{ session('success') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.forgot.reset_password') }}" id="resetForm">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="email" value="{{ $email }}">
|
||||||
|
|
||||||
|
<div class="rp-field">
|
||||||
|
<label class="rp-lbl">Password Baru</label>
|
||||||
|
<div class="rp-shell">
|
||||||
|
<i class="fas fa-lock fi" id="ico-pw"></i>
|
||||||
|
<input type="password" id="password" name="password"
|
||||||
|
placeholder="Buat password yang kuat"
|
||||||
|
required autofocus
|
||||||
|
onfocus="document.getElementById('ico-pw').classList.add('active')"
|
||||||
|
onblur="document.getElementById('ico-pw').classList.remove('active')">
|
||||||
|
<button type="button" class="rp-show" id="tglPw">SHOW</button>
|
||||||
|
</div>
|
||||||
|
<div class="rp-strength">
|
||||||
|
<div class="rp-bar" id="bar1"></div>
|
||||||
|
<div class="rp-bar" id="bar2"></div>
|
||||||
|
<div class="rp-bar" id="bar3"></div>
|
||||||
|
<div class="rp-bar" id="bar4"></div>
|
||||||
|
</div>
|
||||||
|
<div class="rp-str-text" id="strText"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rp-field">
|
||||||
|
<label class="rp-lbl">Konfirmasi Password</label>
|
||||||
|
<div class="rp-shell">
|
||||||
|
<i class="fas fa-lock-open fi" id="ico-pc"></i>
|
||||||
|
<input type="password" id="password_confirmation" name="password_confirmation"
|
||||||
|
placeholder="Ulangi password baru"
|
||||||
|
required
|
||||||
|
onfocus="document.getElementById('ico-pc').classList.add('active')"
|
||||||
|
onblur="document.getElementById('ico-pc').classList.remove('active')">
|
||||||
|
<button type="button" class="rp-show" id="tglPc">SHOW</button>
|
||||||
|
</div>
|
||||||
|
<div class="rp-match" id="matchText"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="rp-btn" id="submitBtn" disabled>
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
Ubah Password
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.login') }}" class="rp-back">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali ke Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.rp-rules-mobile { display:block !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const pw = document.getElementById('password');
|
||||||
|
const pc = document.getElementById('password_confirmation');
|
||||||
|
const bars = ['bar1','bar2','bar3','bar4'].map(id => document.getElementById(id));
|
||||||
|
const strText = document.getElementById('strText');
|
||||||
|
const matchText = document.getElementById('matchText');
|
||||||
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
|
||||||
|
// Toggle show/hide
|
||||||
|
function setupToggle(btnId, inputId) {
|
||||||
|
const btn = document.getElementById(btnId);
|
||||||
|
const inp = document.getElementById(inputId);
|
||||||
|
if (btn && inp) {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const isP = inp.type === 'password';
|
||||||
|
inp.type = isP ? 'text' : 'password';
|
||||||
|
btn.textContent = isP ? 'HIDE' : 'SHOW';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setupToggle('tglPw', 'password');
|
||||||
|
setupToggle('tglPc', 'password_confirmation');
|
||||||
|
|
||||||
|
// Rule checker
|
||||||
|
function updateRule(id, pass) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return;
|
||||||
|
const icon = el.querySelector('i');
|
||||||
|
if (pass) {
|
||||||
|
el.classList.add('pass');
|
||||||
|
el.classList.remove('fail');
|
||||||
|
icon.className = 'fas fa-check-circle';
|
||||||
|
} else {
|
||||||
|
el.classList.remove('pass');
|
||||||
|
el.classList.add('fail');
|
||||||
|
icon.className = 'fas fa-circle';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkRules(val) {
|
||||||
|
const rules = {
|
||||||
|
'length': val.length >= 8,
|
||||||
|
'upper': /[A-Z]/.test(val),
|
||||||
|
'lower': /[a-z]/.test(val),
|
||||||
|
'number': /[0-9]/.test(val),
|
||||||
|
'special': /[^A-Za-z0-9]/.test(val),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update both desktop and mobile rules
|
||||||
|
Object.entries(rules).forEach(([key, pass]) => {
|
||||||
|
updateRule('rule-' + key, pass);
|
||||||
|
updateRule('rule-' + key + '-m', pass);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.values(rules).filter(Boolean).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strength
|
||||||
|
pw.addEventListener('input', function() {
|
||||||
|
const val = this.value;
|
||||||
|
const passed = checkRules(val);
|
||||||
|
|
||||||
|
// Reset bars
|
||||||
|
bars.forEach(b => b.style.background = '#D6EDE5');
|
||||||
|
|
||||||
|
if (val.length === 0) {
|
||||||
|
strText.textContent = '';
|
||||||
|
validateForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color, label;
|
||||||
|
if (passed <= 2) { color = '#e53935'; label = 'Lemah'; }
|
||||||
|
else if (passed <= 3) { color = '#FB8C00'; label = 'Sedang'; }
|
||||||
|
else if (passed <= 4) { color = '#FFB74D'; label = 'Cukup Baik'; }
|
||||||
|
else { color = '#6FBA9D'; label = 'Kuat'; }
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(passed, 4); i++) bars[i].style.background = color;
|
||||||
|
strText.textContent = label;
|
||||||
|
strText.style.color = color;
|
||||||
|
|
||||||
|
checkMatch();
|
||||||
|
validateForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Match
|
||||||
|
function checkMatch() {
|
||||||
|
if (!pc.value) { matchText.textContent = ''; return; }
|
||||||
|
if (pw.value === pc.value) {
|
||||||
|
matchText.textContent = '✓ Password cocok';
|
||||||
|
matchText.style.color = '#6FBA9D';
|
||||||
|
} else {
|
||||||
|
matchText.textContent = '✗ Password tidak cocok';
|
||||||
|
matchText.style.color = '#e53935';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pc.addEventListener('input', function() { checkMatch(); validateForm(); });
|
||||||
|
|
||||||
|
// Validate form
|
||||||
|
function validateForm() {
|
||||||
|
const val = pw.value;
|
||||||
|
const allPass = val.length >= 8
|
||||||
|
&& /[A-Z]/.test(val)
|
||||||
|
&& /[a-z]/.test(val)
|
||||||
|
&& /[0-9]/.test(val)
|
||||||
|
&& /[^A-Za-z0-9]/.test(val)
|
||||||
|
&& pw.value === pc.value
|
||||||
|
&& pc.value.length > 0;
|
||||||
|
|
||||||
|
submitBtn.disabled = !allPass;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-hide success
|
||||||
|
const sa = document.getElementById('rpSuccessAlert');
|
||||||
|
if (sa) {
|
||||||
|
setTimeout(() => {
|
||||||
|
sa.style.transition = 'opacity .5s ease';
|
||||||
|
sa.style.opacity = '0';
|
||||||
|
setTimeout(() => sa.remove(), 500);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init rules to neutral
|
||||||
|
if (!pw.value) {
|
||||||
|
['length','upper','lower','number','special'].forEach(key => {
|
||||||
|
['rule-'+key, 'rule-'+key+'-m'].forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) { el.classList.remove('pass','fail'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,419 @@
|
||||||
|
{{-- resources/views/admin/auth/verify_otp.blade.php --}}
|
||||||
|
@extends('auth.auth_layout')
|
||||||
|
|
||||||
|
@section('title', 'Verifikasi OTP')
|
||||||
|
|
||||||
|
@section('auth-content')
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:opsz,wght@9..40,300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body.auth-page {
|
||||||
|
background: #F8FDFB !important;
|
||||||
|
font-family: 'DM Sans', sans-serif !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
}
|
||||||
|
.auth-container {
|
||||||
|
width: 100vw !important; max-width: 100vw !important;
|
||||||
|
min-height: 100vh !important; background: transparent !important;
|
||||||
|
padding: 0 !important; border-radius: 0 !important; box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vf-wrap {
|
||||||
|
position: relative; width: 100%; min-height: 100vh;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
overflow: hidden; font-family: 'DM Sans', sans-serif;
|
||||||
|
}
|
||||||
|
.vf-bg {
|
||||||
|
position: absolute; inset: 0; z-index: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 60% at 70% 50%, rgba(111,186,157,.12) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse 50% 80% at 10% 80%, rgba(111,186,157,.08) 0%, transparent 55%),
|
||||||
|
#F8FDFB;
|
||||||
|
}
|
||||||
|
.vf-bg::before {
|
||||||
|
content: ''; position: absolute; inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(111,186,157,.055) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(111,186,157,.055) 1px, transparent 1px);
|
||||||
|
background-size: 48px 48px;
|
||||||
|
}
|
||||||
|
.vf-ring { position: absolute; border-radius: 50%; border: 1.5px solid rgba(111,186,157,.18); pointer-events: none; }
|
||||||
|
.vf-ring.r1 { width:420px; height:420px; top:-110px; right:-110px; }
|
||||||
|
.vf-ring.r2 { width:200px; height:200px; bottom:-50px; left:60px; border-color:rgba(111,186,157,.14); }
|
||||||
|
.vf-dot { position:absolute; border-radius:50%; background:#6FBA9D; pointer-events:none; }
|
||||||
|
.vf-dot.d1 { width:8px; height:8px; top:22%; right:18%; opacity:.14; }
|
||||||
|
.vf-dot.d2 { width:11px; height:11px; top:55%; left:15%; opacity:.08; }
|
||||||
|
|
||||||
|
.vf-layout {
|
||||||
|
position: relative; z-index: 2;
|
||||||
|
display: flex; align-items: center;
|
||||||
|
width: 100%; max-width: 1100px;
|
||||||
|
padding: 40px 60px; gap: 80px;
|
||||||
|
animation: vfIn .6s ease both;
|
||||||
|
}
|
||||||
|
@keyframes vfIn {
|
||||||
|
from { opacity:0; transform:translateY(20px); }
|
||||||
|
to { opacity:1; transform:translateY(0); }
|
||||||
|
}
|
||||||
|
.vf-brand { flex: 0 0 340px; order: 1; }
|
||||||
|
.vf-form-panel { flex: 1; max-width: 460px; order: 2; }
|
||||||
|
|
||||||
|
/* Brand */
|
||||||
|
.vf-logo { width:72px; height:72px; margin-bottom:20px; border-radius:16px; box-shadow:0 4px 20px rgba(111,186,157,.2); object-fit:contain; background:#fff; }
|
||||||
|
.vf-eyebrow {
|
||||||
|
display:inline-flex; align-items:center; gap:8px;
|
||||||
|
font-size:.68rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#6FBA9D; margin-bottom:18px;
|
||||||
|
}
|
||||||
|
.vf-eyebrow::before { content:''; display:inline-block; width:22px; height:2px; background:#6FBA9D; border-radius:2px; }
|
||||||
|
.vf-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:3.2rem; line-height:1.05; color:#0F2118; margin-bottom:6px;
|
||||||
|
}
|
||||||
|
.vf-title em { font-style:italic; color:#5EA98C; }
|
||||||
|
.vf-sub { font-size:.9rem; font-weight:500; color:#8AADA0; margin-bottom:32px; line-height:1.6; }
|
||||||
|
.vf-divider { width:44px; height:3px; background:linear-gradient(90deg,#6FBA9D,#A8D8C6); border-radius:3px; margin-bottom:24px; }
|
||||||
|
.vf-desc { font-size:.81rem; color:#8AADA0; line-height:1.8; max-width:290px; margin-bottom:32px; }
|
||||||
|
|
||||||
|
/* Steps */
|
||||||
|
.vf-steps { display:flex; flex-direction:column; gap:14px; }
|
||||||
|
.vf-step { display:flex; align-items:flex-start; gap:12px; }
|
||||||
|
.vf-step-num {
|
||||||
|
width:28px; height:28px; border-radius:50%;
|
||||||
|
background:#D6EDE5; color:#5EA98C;
|
||||||
|
font-size:.72rem; font-weight:800;
|
||||||
|
display:flex; align-items:center; justify-content:center; flex-shrink:0;
|
||||||
|
}
|
||||||
|
.vf-step-num.done { background:linear-gradient(135deg,#6FBA9D,#5EA98C); color:#fff; }
|
||||||
|
.vf-step-num.active { background:linear-gradient(135deg,#6FBA9D,#5EA98C); color:#fff; box-shadow:0 0 0 4px rgba(111,186,157,.2); }
|
||||||
|
.vf-step-text { font-size:.78rem; color:#2A4235; font-weight:500; line-height:1.5; padding-top:3px; }
|
||||||
|
.vf-step-text small { display:block; color:#8AADA0; font-weight:400; font-size:.72rem; }
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.vf-card {
|
||||||
|
background: #fff; border-radius: 24px;
|
||||||
|
padding: 42px 38px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(111,186,157,.1),
|
||||||
|
0 4px 6px rgba(15,33,24,.03),
|
||||||
|
0 20px 44px rgba(15,33,24,.08);
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.vf-card::before {
|
||||||
|
content: ''; position:absolute; top:0; left:0; right:0; height:3px;
|
||||||
|
background: linear-gradient(90deg, #FFB74D, #FFA726, #FFB74D);
|
||||||
|
}
|
||||||
|
.vf-card::after {
|
||||||
|
content: ''; position:absolute; bottom:-50px; right:-50px;
|
||||||
|
width:140px; height:140px; border-radius:50%;
|
||||||
|
background: radial-gradient(circle, rgba(255,183,77,.06) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
.vf-card-icon {
|
||||||
|
width:54px; height:54px; border-radius:14px;
|
||||||
|
background:linear-gradient(135deg,#FFF3E0,#FFE0B2);
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
font-size:1.3rem; color:#F57C00; margin-bottom:16px;
|
||||||
|
}
|
||||||
|
.vf-card-lbl {
|
||||||
|
font-size:.67rem; font-weight:700; letter-spacing:2px;
|
||||||
|
text-transform:uppercase; color:#FFB74D; margin-bottom:7px;
|
||||||
|
}
|
||||||
|
.vf-card-title {
|
||||||
|
font-family:'DM Serif Display',serif;
|
||||||
|
font-size:1.85rem; color:#0F2118; line-height:1.1; margin-bottom:5px;
|
||||||
|
}
|
||||||
|
.vf-card-desc { font-size:.79rem; color:#8AADA0; line-height:1.6; margin-bottom:10px; }
|
||||||
|
.vf-email-badge {
|
||||||
|
display:inline-flex; align-items:center; gap:6px;
|
||||||
|
padding:6px 14px; border-radius:8px;
|
||||||
|
background:#EBF7F2; font-size:.78rem; font-weight:600; color:#3D8A6E;
|
||||||
|
margin-bottom:22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
.vf-alert-danger {
|
||||||
|
border-radius:10px; padding:10px 13px; font-size:.79rem;
|
||||||
|
margin-bottom:18px; background:#FFF3F3; color:#c62828; border-left:3px solid #e53935;
|
||||||
|
display:flex; align-items:center; gap:7px;
|
||||||
|
}
|
||||||
|
.vf-alert-success {
|
||||||
|
border-radius:10px; padding:10px 13px; font-size:.79rem;
|
||||||
|
margin-bottom:18px; background:#F0FFF4; color:#2E7D32; border-left:3px solid #43A047;
|
||||||
|
display:flex; align-items:center; gap:7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OTP Boxes */
|
||||||
|
.vf-otp-row {
|
||||||
|
display:flex; gap:10px; justify-content:center; margin:22px 0;
|
||||||
|
}
|
||||||
|
.vf-otp-box {
|
||||||
|
width:50px; height:60px; text-align:center;
|
||||||
|
font-size:1.6rem; font-weight:800; color:#0F2118;
|
||||||
|
font-family:'DM Sans',sans-serif;
|
||||||
|
border:2px solid #D6EDE5; border-radius:12px;
|
||||||
|
background:#EBF7F2; outline:none;
|
||||||
|
transition:all .2s;
|
||||||
|
}
|
||||||
|
.vf-otp-box:focus {
|
||||||
|
border-color:#FFB74D; background:#fff;
|
||||||
|
box-shadow:0 0 0 4px rgba(255,183,77,.15);
|
||||||
|
}
|
||||||
|
.vf-otp-box.filled {
|
||||||
|
border-color:#6FBA9D; background:#f0faf5;
|
||||||
|
}
|
||||||
|
.vf-otp-box.error {
|
||||||
|
border-color:#e53935; background:#FFF3F3;
|
||||||
|
animation: vfShake .4s ease;
|
||||||
|
}
|
||||||
|
@keyframes vfShake {
|
||||||
|
0%,100% { transform:translateX(0); }
|
||||||
|
20%,60% { transform:translateX(-4px); }
|
||||||
|
40%,80% { transform:translateX(4px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
.vf-btn {
|
||||||
|
width:100%; padding:13px;
|
||||||
|
background:linear-gradient(135deg, #FFB74D, #FFA726);
|
||||||
|
color:#fff; border:none; border-radius:12px;
|
||||||
|
font-family:inherit; font-size:.89rem; font-weight:700;
|
||||||
|
cursor:pointer; letter-spacing:.3px;
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:8px;
|
||||||
|
box-shadow:0 4px 18px rgba(255,183,77,.35);
|
||||||
|
transition:all .25s;
|
||||||
|
}
|
||||||
|
.vf-btn:hover { transform:translateY(-2px); box-shadow:0 8px 26px rgba(255,183,77,.45); }
|
||||||
|
.vf-btn:active { transform:none; }
|
||||||
|
|
||||||
|
/* Resend */
|
||||||
|
.vf-resend {
|
||||||
|
text-align:center; margin-top:20px; font-size:.8rem; color:#8AADA0;
|
||||||
|
}
|
||||||
|
.vf-resend a {
|
||||||
|
color:#5EA98C; font-weight:700; text-decoration:none; transition:color .2s;
|
||||||
|
}
|
||||||
|
.vf-resend a:hover { text-decoration:underline; color:#3D8A6E; }
|
||||||
|
.vf-resend a.disabled { color:#ccc; pointer-events:none; }
|
||||||
|
.vf-countdown { font-weight:700; color:#e53935; }
|
||||||
|
|
||||||
|
.vf-back {
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:6px;
|
||||||
|
margin-top:16px; font-size:.78rem; color:#5EA98C; font-weight:600; text-decoration:none;
|
||||||
|
}
|
||||||
|
.vf-back:hover { color:#3D8A6E; text-decoration:underline; }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.vf-layout { gap:48px; padding:32px 36px; }
|
||||||
|
.vf-brand { flex:0 0 260px; }
|
||||||
|
.vf-title { font-size:2.7rem; }
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
body.auth-page { align-items:flex-start !important; overflow-y:auto !important; }
|
||||||
|
.vf-wrap { align-items:flex-start; min-height:auto; padding:24px 0 40px; }
|
||||||
|
.vf-layout { flex-direction:column; padding:0 20px; gap:28px; }
|
||||||
|
.vf-form-panel { order:2; max-width:100%; }
|
||||||
|
.vf-brand { order:1; flex:none; text-align:center; }
|
||||||
|
.vf-title { font-size:2.2rem; }
|
||||||
|
.vf-steps, .vf-desc, .vf-divider { display:none; }
|
||||||
|
.vf-sub { margin-bottom:0; }
|
||||||
|
.vf-card { padding:28px 20px; }
|
||||||
|
.vf-logo { width:56px; height:56px; margin:0 auto 14px; display:block; }
|
||||||
|
.vf-otp-box { width:42px; height:50px; font-size:1.3rem; }
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.vf-title { font-size:1.85rem; }
|
||||||
|
.vf-card { padding:24px 16px; border-radius:16px; }
|
||||||
|
.vf-card-title { font-size:1.5rem; }
|
||||||
|
.vf-otp-row { gap:6px; }
|
||||||
|
.vf-otp-box { width:38px; height:46px; font-size:1.1rem; border-radius:9px; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.vf-layout { max-width:1160px; padding:40px 80px; }
|
||||||
|
.vf-brand { flex:0 0 360px; }
|
||||||
|
.vf-title { font-size:3.6rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="vf-wrap">
|
||||||
|
<div class="vf-bg"></div>
|
||||||
|
<div class="vf-ring r1"></div>
|
||||||
|
<div class="vf-ring r2"></div>
|
||||||
|
<div class="vf-dot d1"></div>
|
||||||
|
<div class="vf-dot d2"></div>
|
||||||
|
|
||||||
|
<div class="vf-layout">
|
||||||
|
|
||||||
|
<!-- ═══ Brand (kiri) ═══ -->
|
||||||
|
<div class="vf-brand">
|
||||||
|
<img src="{{ asset('images/logo.png') }}" alt="Logo PKPPS" class="vf-logo">
|
||||||
|
<div class="vf-eyebrow">Verifikasi</div>
|
||||||
|
<h1 class="vf-title">Cek<br><em>Email</em><br>Anda.</h1>
|
||||||
|
<p class="vf-sub">PKPPS Riyadlul Jannah</p>
|
||||||
|
<div class="vf-divider"></div>
|
||||||
|
<p class="vf-desc">Masukkan kode 6 digit yang telah kami kirim. Periksa juga folder spam jika belum menerima email.</p>
|
||||||
|
|
||||||
|
<div class="vf-steps">
|
||||||
|
<div class="vf-step">
|
||||||
|
<div class="vf-step-num done"><i class="fas fa-check" style="font-size:.6rem"></i></div>
|
||||||
|
<div class="vf-step-text" style="color:#8AADA0;">Email terkirim</div>
|
||||||
|
</div>
|
||||||
|
<div class="vf-step">
|
||||||
|
<div class="vf-step-num active">2</div>
|
||||||
|
<div class="vf-step-text">Verifikasi kode OTP <small>Masukkan 6 digit kode</small></div>
|
||||||
|
</div>
|
||||||
|
<div class="vf-step">
|
||||||
|
<div class="vf-step-num">3</div>
|
||||||
|
<div class="vf-step-text">Buat password baru</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Form (kanan) ═══ -->
|
||||||
|
<div class="vf-form-panel">
|
||||||
|
<div class="vf-card">
|
||||||
|
<div class="vf-card-icon">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
</div>
|
||||||
|
<div class="vf-card-lbl">Langkah 2 dari 3</div>
|
||||||
|
<div class="vf-card-title">Masukkan Kode OTP</div>
|
||||||
|
<div class="vf-card-desc">Kode verifikasi 6 digit telah dikirim ke:</div>
|
||||||
|
<div class="vf-email-badge">
|
||||||
|
<i class="fas fa-envelope"></i> {{ $email }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="vf-alert-danger">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
{{ $errors->first() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="vf-alert-success" id="vfSuccessAlert">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
{{ session('success') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.forgot.verify_otp') }}" id="otpForm">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="email" value="{{ $email }}">
|
||||||
|
<input type="hidden" name="otp" id="otpHidden" value="">
|
||||||
|
|
||||||
|
<div class="vf-otp-row" id="otpRow">
|
||||||
|
<input type="text" maxlength="1" class="vf-otp-box" data-index="0" inputmode="numeric" autofocus>
|
||||||
|
<input type="text" maxlength="1" class="vf-otp-box" data-index="1" inputmode="numeric">
|
||||||
|
<input type="text" maxlength="1" class="vf-otp-box" data-index="2" inputmode="numeric">
|
||||||
|
<input type="text" maxlength="1" class="vf-otp-box" data-index="3" inputmode="numeric">
|
||||||
|
<input type="text" maxlength="1" class="vf-otp-box" data-index="4" inputmode="numeric">
|
||||||
|
<input type="text" maxlength="1" class="vf-otp-box" data-index="5" inputmode="numeric">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="vf-btn" id="verifyBtn">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
Verifikasi Kode
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="vf-resend">
|
||||||
|
<p>Tidak menerima kode?</p>
|
||||||
|
<form method="POST" action="{{ route('admin.forgot.resend_otp') }}" id="resendForm" style="display:inline;">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="email" value="{{ $email }}">
|
||||||
|
<a href="#" id="resendLink" class="disabled" onclick="document.getElementById('resendForm').submit(); return false;">
|
||||||
|
Kirim Ulang OTP
|
||||||
|
</a>
|
||||||
|
<span id="timerText" class="vf-countdown"> (60s)</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.forgot.email_form') }}" class="vf-back">
|
||||||
|
<i class="fas fa-arrow-left"></i> Ganti Email
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const boxes = document.querySelectorAll('.vf-otp-box');
|
||||||
|
const hiddenOtp = document.getElementById('otpHidden');
|
||||||
|
const form = document.getElementById('otpForm');
|
||||||
|
|
||||||
|
function updateOtp() {
|
||||||
|
let otp = '';
|
||||||
|
boxes.forEach(b => otp += b.value);
|
||||||
|
hiddenOtp.value = otp;
|
||||||
|
}
|
||||||
|
|
||||||
|
boxes.forEach((box, idx) => {
|
||||||
|
box.addEventListener('input', function() {
|
||||||
|
this.value = this.value.replace(/[^0-9]/g, '');
|
||||||
|
if (this.value && idx < boxes.length - 1) boxes[idx + 1].focus();
|
||||||
|
this.classList.toggle('filled', !!this.value);
|
||||||
|
this.classList.remove('error');
|
||||||
|
updateOtp();
|
||||||
|
if (hiddenOtp.value.length === 6) form.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
box.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Backspace' && !this.value && idx > 0) {
|
||||||
|
boxes[idx - 1].focus();
|
||||||
|
boxes[idx - 1].value = '';
|
||||||
|
boxes[idx - 1].classList.remove('filled');
|
||||||
|
updateOtp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
box.addEventListener('paste', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const pasted = (e.clipboardData || window.clipboardData).getData('text').replace(/[^0-9]/g, '');
|
||||||
|
if (pasted.length >= 6) {
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
boxes[i].value = pasted[i] || '';
|
||||||
|
boxes[i].classList.toggle('filled', !!boxes[i].value);
|
||||||
|
}
|
||||||
|
boxes[5].focus();
|
||||||
|
updateOtp();
|
||||||
|
if (hiddenOtp.value.length === 6) form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there's an error, shake the boxes
|
||||||
|
@if ($errors->any())
|
||||||
|
boxes.forEach(b => b.classList.add('error'));
|
||||||
|
@endif
|
||||||
|
|
||||||
|
// Countdown
|
||||||
|
let secs = 60;
|
||||||
|
const timerText = document.getElementById('timerText');
|
||||||
|
const resendLink = document.getElementById('resendLink');
|
||||||
|
const cd = setInterval(() => {
|
||||||
|
secs--;
|
||||||
|
timerText.textContent = ' (' + secs + 's)';
|
||||||
|
if (secs <= 0) {
|
||||||
|
clearInterval(cd);
|
||||||
|
timerText.textContent = '';
|
||||||
|
resendLink.classList.remove('disabled');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Auto-hide success
|
||||||
|
const sa = document.getElementById('vfSuccessAlert');
|
||||||
|
if (sa) {
|
||||||
|
setTimeout(() => {
|
||||||
|
sa.style.transition = 'opacity .5s ease';
|
||||||
|
sa.style.opacity = '0';
|
||||||
|
setTimeout(() => sa.remove(), 500);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -149,8 +149,8 @@ class="form-control @error('status') is-invalid @enderror"
|
||||||
<i class="fas fa-graduation-cap form-icon"></i>
|
<i class="fas fa-graduation-cap form-icon"></i>
|
||||||
Pilih Kelas yang Akan Menerima Berita <span style="color: var(--danger-color);">*</span>
|
Pilih Kelas yang Akan Menerima Berita <span style="color: var(--danger-color);">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div style="border: 2px solid var(--primary-light); border-radius: var(--border-radius-sm); padding: 20px; background-color: var(--primary-light);">
|
<div style="border: 2px solid var(--primary-light); border-radius: var(--border-radius-sm); padding: 14px; background-color: var(--primary-light);">
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px;">
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 11px;">
|
||||||
@foreach($kelasOptions as $kelas)
|
@foreach($kelasOptions as $kelas)
|
||||||
<div style="background: white; padding: 12px; border-radius: var(--border-radius-sm); box-shadow: var(--shadow-sm);">
|
<div style="background: white; padding: 12px; border-radius: var(--border-radius-sm); box-shadow: var(--shadow-sm);">
|
||||||
<label style="display: flex; align-items: center; margin: 0; cursor: pointer;">
|
<label style="display: flex; align-items: center; margin: 0; cursor: pointer;">
|
||||||
|
|
@ -176,7 +176,7 @@ class="kelas-checkbox"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Buttons -->
|
<!-- Submit Buttons -->
|
||||||
<div style="display: flex; gap: 10px; margin-top: 30px; padding-top: 20px; border-top: 2px solid var(--primary-light);">
|
<div style="display: flex; gap: 10px; margin-top: 22px; padding-top: 20px; border-top: 2px solid var(--primary-light);">
|
||||||
<button type="submit" class="btn btn-success">
|
<button type="submit" class="btn btn-success">
|
||||||
<i class="fas fa-save"></i> Simpan Berita
|
<i class="fas fa-save"></i> Simpan Berita
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -264,7 +264,7 @@ function updateKelasCount() {
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.ql-toolbar { background-color: #f8f9fa; border-radius: 4px 4px 0 0; border-bottom: 2px solid #dee2e6; }
|
.ql-toolbar { background-color: #f8f9fa; border-radius: 4px 4px 0 0; border-bottom: 2px solid #dee2e6; }
|
||||||
.ql-container { font-size: 14px; font-family: Arial, sans-serif; min-height: 250px; }
|
.ql-container { font-size: 11px; font-family: Arial, sans-serif; min-height: 250px; }
|
||||||
.ql-editor { min-height: 250px; max-height: 500px; overflow-y: auto; }
|
.ql-editor { min-height: 250px; max-height: 500px; overflow-y: auto; }
|
||||||
.ql-editor h1 { font-size: 2em; color: #2c3e50; }
|
.ql-editor h1 { font-size: 2em; color: #2c3e50; }
|
||||||
.ql-editor h2 { font-size: 1.5em; color: #34495e; }
|
.ql-editor h2 { font-size: 1.5em; color: #34495e; }
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
@method('PUT')
|
@method('PUT')
|
||||||
|
|
||||||
<!-- ID Berita (Read-only) -->
|
<!-- ID Berita (Read-only) -->
|
||||||
<div style="background: var(--primary-light); padding: 15px; border-radius: var(--border-radius-sm); margin-bottom: 20px;">
|
<div style="background: var(--primary-light); padding: 15px; border-radius: var(--border-radius-sm); margin-bottom: 14px;">
|
||||||
<strong style="color: var(--primary-dark);">
|
<strong style="color: var(--primary-dark);">
|
||||||
<i class="fas fa-id-card"></i> ID Berita: {{ $berita->id_berita }}
|
<i class="fas fa-id-card"></i> ID Berita: {{ $berita->id_berita }}
|
||||||
</strong>
|
</strong>
|
||||||
|
|
@ -168,8 +168,8 @@ class="form-control @error('status') is-invalid @enderror"
|
||||||
<i class="fas fa-graduation-cap form-icon"></i>
|
<i class="fas fa-graduation-cap form-icon"></i>
|
||||||
Pilih Kelas yang Akan Menerima Berita <span style="color: var(--danger-color);">*</span>
|
Pilih Kelas yang Akan Menerima Berita <span style="color: var(--danger-color);">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div style="border: 2px solid var(--primary-light); border-radius: var(--border-radius-sm); padding: 20px; background-color: var(--primary-light);">
|
<div style="border: 2px solid var(--primary-light); border-radius: var(--border-radius-sm); padding: 14px; background-color: var(--primary-light);">
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px;">
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 11px;">
|
||||||
@foreach($kelasOptions as $kelas)
|
@foreach($kelasOptions as $kelas)
|
||||||
<div style="background: white; padding: 12px; border-radius: var(--border-radius-sm); box-shadow: var(--shadow-sm);">
|
<div style="background: white; padding: 12px; border-radius: var(--border-radius-sm); box-shadow: var(--shadow-sm);">
|
||||||
<label style="display: flex; align-items: center; margin: 0; cursor: pointer;">
|
<label style="display: flex; align-items: center; margin: 0; cursor: pointer;">
|
||||||
|
|
@ -195,7 +195,7 @@ class="kelas-checkbox"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Buttons -->
|
<!-- Submit Buttons -->
|
||||||
<div style="display: flex; gap: 10px; margin-top: 30px; padding-top: 20px; border-top: 2px solid var(--primary-light);">
|
<div style="display: flex; gap: 10px; margin-top: 22px; padding-top: 20px; border-top: 2px solid var(--primary-light);">
|
||||||
<button type="submit" class="btn btn-success">
|
<button type="submit" class="btn btn-success">
|
||||||
<i class="fas fa-save"></i> Update Berita
|
<i class="fas fa-save"></i> Update Berita
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -281,7 +281,7 @@ function updateKelasCount() {
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.ql-toolbar { background-color: #f8f9fa; border-radius: 4px 4px 0 0; border-bottom: 2px solid #dee2e6; }
|
.ql-toolbar { background-color: #f8f9fa; border-radius: 4px 4px 0 0; border-bottom: 2px solid #dee2e6; }
|
||||||
.ql-container { font-size: 14px; font-family: Arial, sans-serif; min-height: 250px; }
|
.ql-container { font-size: 11px; font-family: Arial, sans-serif; min-height: 250px; }
|
||||||
.ql-editor { min-height: 250px; max-height: 500px; overflow-y: auto; }
|
.ql-editor { min-height: 250px; max-height: 500px; overflow-y: auto; }
|
||||||
.ql-editor h1 { font-size: 2em; color: #2c3e50; }
|
.ql-editor h1 { font-size: 2em; color: #2c3e50; }
|
||||||
.ql-editor h2 { font-size: 1.5em; color: #34495e; }
|
.ql-editor h2 { font-size: 1.5em; color: #34495e; }
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Header Actions -->
|
<!-- Header Actions -->
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 11px;">
|
||||||
<!-- Search & Filter Form -->
|
<!-- Search & Filter Form -->
|
||||||
<form method="GET" action="{{ route('admin.berita.index') }}" style="display: flex; gap: 10px; flex-wrap: wrap; flex-grow: 1;">
|
<form method="GET" action="{{ route('admin.berita.index') }}" style="display: flex; gap: 10px; flex-wrap: wrap; flex-grow: 1;">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
|
|
@ -144,12 +144,12 @@ class="btn btn-warning btn-sm"
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div style="margin-top: 20px; display: flex; justify-content: center;">
|
<div style="margin-top: 14px; display: flex; justify-content: center;">
|
||||||
{{ $berita->appends(request()->query())->links() }}
|
{{ $berita->appends(request()->query())->links() }}
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div style="text-align: center; padding: 60px 20px;">
|
<div style="text-align: center; padding: 44px 14px;">
|
||||||
<i class="fas fa-newspaper" style="font-size: 4em; color: #ccc; margin-bottom: 20px;"></i>
|
<i class="fas fa-newspaper" style="font-size: 4em; color: #ccc; margin-bottom: 14px;"></i>
|
||||||
<h3 style="color: var(--text-light);">Belum Ada Berita</h3>
|
<h3 style="color: var(--text-light);">Belum Ada Berita</h3>
|
||||||
<p style="color: var(--text-light); margin-bottom: 25px;">
|
<p style="color: var(--text-light); margin-bottom: 25px;">
|
||||||
Mulai tambahkan berita pertama untuk santri pesantren.
|
Mulai tambahkan berita pertama untuk santri pesantren.
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Header Actions -->
|
<!-- Header Actions -->
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
||||||
<div>
|
<div>
|
||||||
<span class="badge {{ $berita->status_badge }}" style="font-size: 1em; padding: 8px 15px;">
|
<span class="badge {{ $berita->status_badge }}" style="font-size: 1em; padding: 6px 11px;">
|
||||||
@if($berita->status === 'published')
|
@if($berita->status === 'published')
|
||||||
<i class="fas fa-check-circle"></i> Published
|
<i class="fas fa-check-circle"></i> Published
|
||||||
@else
|
@else
|
||||||
|
|
@ -34,14 +34,14 @@
|
||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<div style="padding: 10px;">
|
<div style="padding: 10px;">
|
||||||
<!-- Header Berita -->
|
<!-- Header Berita -->
|
||||||
<div style="border-bottom: 3px solid var(--primary-color); padding-bottom: 25px; margin-bottom: 30px;">
|
<div style="border-bottom: 3px solid var(--primary-color); padding-bottom: 25px; margin-bottom: 22px;">
|
||||||
<div style="margin-bottom: 15px;">
|
<div style="margin-bottom: 15px;">
|
||||||
<span style="background: var(--primary-light); color: var(--primary-dark); padding: 6px 12px; border-radius: var(--border-radius-sm); font-weight: 600; font-size: 0.9em;">
|
<span style="background: var(--primary-light); color: var(--primary-dark); padding: 6px 12px; border-radius: var(--border-radius-sm); font-weight: 600; font-size: 0.9em;">
|
||||||
ID: {{ $berita->id_berita }}
|
ID: {{ $berita->id_berita }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 style="color: var(--primary-dark); margin-bottom: 20px; font-size: 2em; line-height: 1.3;">
|
<h1 style="color: var(--primary-dark); margin-bottom: 14px; font-size: 2em; line-height: 1.3;">
|
||||||
{{ $berita->judul }}
|
{{ $berita->judul }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
<!-- Konten Berita -->
|
<!-- Konten Berita -->
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4><i class="fas fa-align-left"></i> Konten Berita</h4>
|
<h4><i class="fas fa-align-left"></i> Konten Berita</h4>
|
||||||
<div style="line-height: 1.9; font-size: 1.05em; color: var(--text-color); background: var(--primary-light); padding: 25px; border-radius: var(--border-radius-sm); border-left: 4px solid var(--primary-color);">
|
<div style="line-height: 1.9; font-size: 1.05em; color: var(--text-color); background: var(--primary-light); padding: 18px; border-radius: var(--border-radius-sm); border-left: 4px solid var(--primary-color);">
|
||||||
{!! $berita->konten !!}
|
{!! $berita->konten !!}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
<i class="fas fa-graduation-cap"></i>
|
<i class="fas fa-graduation-cap"></i>
|
||||||
Target Kelas
|
Target Kelas
|
||||||
</h4>
|
</h4>
|
||||||
<div style="background: linear-gradient(135deg, #E3F2FD 0%, #D1E9F9 100%); padding: 20px; border-radius: var(--border-radius-sm); border-left: 4px solid var(--info-color);">
|
<div style="background: linear-gradient(135deg, #E3F2FD 0%, #D1E9F9 100%); padding: 14px; border-radius: var(--border-radius-sm); border-left: 4px solid var(--info-color);">
|
||||||
<p style="margin: 0; color: var(--text-color); font-size: 1em;">
|
<p style="margin: 0; color: var(--text-color); font-size: 1em;">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
Berita ini ditujukan untuk:
|
Berita ini ditujukan untuk:
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('title', 'Statistik Berita')
|
@section('title', 'Statistik Berita')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2><i class="fas fa-chart-bar"></i> Statistik Berita</h2>
|
<h2><i class="fas fa-chart-bar"></i> Statistik Berita</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Back Button -->
|
<!-- Back Button -->
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 14px;">
|
||||||
<a href="{{ route('admin.berita.index') }}" class="btn btn-secondary">
|
<a href="{{ route('admin.berita.index') }}" class="btn btn-secondary">
|
||||||
<i class="fas fa-arrow-left"></i> Kembali ke Daftar Berita
|
<i class="fas fa-arrow-left"></i> Kembali ke Daftar Berita
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dashboard Cards -->
|
<!-- Dashboard Cards -->
|
||||||
<div class="row-cards">
|
<div class="row-cards">
|
||||||
<div class="card card-info">
|
<div class="card card-info">
|
||||||
<h3>Total Berita</h3>
|
<h3>Total Berita</h3>
|
||||||
<div class="card-value">{{ $totalBerita }}</div>
|
<div class="card-value">{{ $totalBerita }}</div>
|
||||||
|
|
@ -45,10 +45,10 @@
|
||||||
<div class="card-value">{{ $beritaKelas }}</div>
|
<div class="card-value">{{ $beritaKelas }}</div>
|
||||||
<i class="fas fa-graduation-cap card-icon"></i>
|
<i class="fas fa-graduation-cap card-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grafik Distribusi -->
|
<!-- Grafik Distribusi -->
|
||||||
<div class="content-box" style="margin-top: 30px;">
|
<div class="content-box" style="margin-top: 22px;">
|
||||||
<h3 style="color: var(--primary-color); margin-bottom: 25px; display: flex; align-items: center;">
|
<h3 style="color: var(--primary-color); margin-bottom: 25px; display: flex; align-items: center;">
|
||||||
<i class="fas fa-chart-pie" style="margin-right: 10px;"></i>
|
<i class="fas fa-chart-pie" style="margin-right: 10px;"></i>
|
||||||
Distribusi Berita
|
Distribusi Berita
|
||||||
|
|
@ -56,8 +56,8 @@
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 30px;">
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 30px;">
|
||||||
<!-- Status Distribution -->
|
<!-- Status Distribution -->
|
||||||
<div style="background: linear-gradient(135deg, #F8FBF9 0%, #FFFFFF 100%); padding: 25px; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); border: 2px solid var(--primary-light);">
|
<div style="background: linear-gradient(135deg, #F8FBF9 0%, #FFFFFF 100%); padding: 18px; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); border: 2px solid var(--primary-light);">
|
||||||
<h4 style="margin-bottom: 20px; color: var(--primary-dark); display: flex; align-items: center;">
|
<h4 style="margin-bottom: 14px; color: var(--primary-dark); display: flex; align-items: center;">
|
||||||
<i class="fas fa-toggle-on" style="margin-right: 8px;"></i>
|
<i class="fas fa-toggle-on" style="margin-right: 8px;"></i>
|
||||||
Berdasarkan Status
|
Berdasarkan Status
|
||||||
</h4>
|
</h4>
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<!-- Published -->
|
<!-- Published -->
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 14px;">
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px; align-items: center;">
|
<div style="display: flex; justify-content: space-between; margin-bottom: 8px; align-items: center;">
|
||||||
<span style="font-weight: 600; color: var(--text-color);">
|
<span style="font-weight: 600; color: var(--text-color);">
|
||||||
<i class="fas fa-check-circle" style="color: var(--success-color);"></i> Published
|
<i class="fas fa-check-circle" style="color: var(--success-color);"></i> Published
|
||||||
|
|
@ -106,8 +106,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Target Distribution -->
|
<!-- Target Distribution -->
|
||||||
<div style="background: linear-gradient(135deg, #F8FBF9 0%, #FFFFFF 100%); padding: 25px; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); border: 2px solid var(--primary-light);">
|
<div style="background: linear-gradient(135deg, #F8FBF9 0%, #FFFFFF 100%); padding: 18px; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); border: 2px solid var(--primary-light);">
|
||||||
<h4 style="margin-bottom: 20px; color: var(--primary-dark); display: flex; align-items: center;">
|
<h4 style="margin-bottom: 14px; color: var(--primary-dark); display: flex; align-items: center;">
|
||||||
<i class="fas fa-bullseye" style="margin-right: 8px;"></i>
|
<i class="fas fa-bullseye" style="margin-right: 8px;"></i>
|
||||||
Berdasarkan Target
|
Berdasarkan Target
|
||||||
</h4>
|
</h4>
|
||||||
|
|
@ -118,7 +118,7 @@
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<!-- Semua Santri -->
|
<!-- Semua Santri -->
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 14px;">
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px; align-items: center;">
|
<div style="display: flex; justify-content: space-between; margin-bottom: 8px; align-items: center;">
|
||||||
<span style="font-weight: 600; color: var(--text-color);">
|
<span style="font-weight: 600; color: var(--text-color);">
|
||||||
<i class="fas fa-globe" style="color: var(--info-color);"></i> Semua Santri
|
<i class="fas fa-globe" style="color: var(--info-color);"></i> Semua Santri
|
||||||
|
|
@ -154,11 +154,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div class="content-box" style="margin-top: 30px;">
|
<div class="content-box" style="margin-top: 22px;">
|
||||||
<h3 style="color: var(--primary-color); margin-bottom: 20px; display: flex; align-items: center;">
|
<h3 style="color: var(--primary-color); margin-bottom: 14px; display: flex; align-items: center;">
|
||||||
<i class="fas fa-bolt" style="margin-right: 10px;"></i>
|
<i class="fas fa-bolt" style="margin-right: 10px;"></i>
|
||||||
Aksi Cepat
|
Aksi Cepat
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -180,19 +180,19 @@
|
||||||
<i class="fas fa-graduation-cap"></i> Berita Kelas Tertentu ({{ $beritaKelas }})
|
<i class="fas fa-graduation-cap"></i> Berita Kelas Tertentu ({{ $beritaKelas }})
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
@if($totalBerita == 0)
|
@if($totalBerita == 0)
|
||||||
<div class="content-box" style="margin-top: 30px; text-align: center; padding: 60px 20px;">
|
<div class="content-box" style="margin-top: 22px; text-align: center; padding: 44px 14px;">
|
||||||
<i class="fas fa-newspaper" style="font-size: 5em; color: #ccc; margin-bottom: 25px;"></i>
|
<i class="fas fa-newspaper" style="font-size: 5em; color: #ccc; margin-bottom: 25px;"></i>
|
||||||
<h3 style="color: var(--text-light); margin-bottom: 15px;">Belum Ada Berita</h3>
|
<h3 style="color: var(--text-light); margin-bottom: 15px;">Belum Ada Berita</h3>
|
||||||
<p style="color: var(--text-light); margin-bottom: 30px; max-width: 500px; margin-left: auto; margin-right: auto;">
|
<p style="color: var(--text-light); margin-bottom: 22px; max-width: 500px; margin-left: auto; margin-right: auto;">
|
||||||
Mulai dengan membuat berita pertama untuk pesantren Anda. Berita dapat dipublikasikan untuk semua santri atau target tertentu.
|
Mulai dengan membuat berita pertama untuk pesantren Anda. Berita dapat dipublikasikan untuk semua santri atau target tertentu.
|
||||||
</p>
|
</p>
|
||||||
<a href="{{ route('admin.berita.create') }}" class="btn btn-success btn-lg">
|
<a href="{{ route('admin.berita.create') }}" class="btn btn-success btn-lg">
|
||||||
<i class="fas fa-plus"></i> Buat Berita Pertama
|
<i class="fas fa-plus"></i> Buat Berita Pertama
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@endsection
|
@endsection
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
{{-- resources/views/admin/capaian/akses-santri.blade.php --}}
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<style>
|
||||||
|
.access-hero {
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 24px 28px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.access-hero.open { background: linear-gradient(135deg, #e8f5e9, #c8e6c9); border: 2px solid #66bb6a; }
|
||||||
|
.access-hero.closed { background: linear-gradient(135deg, #fff3e0, #ffe0b2); border: 2px solid #ffa726; }
|
||||||
|
.access-hero .ah-icon { font-size: 3.5rem; flex-shrink: 0; }
|
||||||
|
.access-hero h3 { margin: 0 0 5px; font-size: 1.15rem; }
|
||||||
|
.access-hero p { margin: 0; font-size: 0.85rem; color: #555; }
|
||||||
|
|
||||||
|
.status-badge-big {
|
||||||
|
display: inline-flex; align-items: center; gap: 8px;
|
||||||
|
padding: 8px 22px; border-radius: 25px; font-weight: 800;
|
||||||
|
font-size: 1rem; margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.status-badge-big.open { background: #2e7d32; color: #fff; }
|
||||||
|
.status-badge-big.closed { background: #e65100; color: #fff; }
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||||
|
border-left: 4px solid;
|
||||||
|
}
|
||||||
|
.info-box .ib-label { font-size: 0.72rem; color: #999; text-transform: uppercase; margin-bottom: 4px; }
|
||||||
|
.info-box .ib-val { font-size: 0.92rem; font-weight: 700; color: #333; }
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 22px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #e8f0ec;
|
||||||
|
}
|
||||||
|
.form-card h4 { margin: 0 0 16px; color: var(--primary-dark); font-size: 1rem; }
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 8px;
|
||||||
|
padding: 11px 28px; border-radius: 10px; border: none;
|
||||||
|
font-weight: 700; font-size: 0.9rem; cursor: pointer;
|
||||||
|
transition: all .2s;
|
||||||
|
}
|
||||||
|
.toggle-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,0.15); }
|
||||||
|
.toggle-btn.open-btn { background: linear-gradient(135deg, #43a047, #2e7d32); color: #fff; }
|
||||||
|
.toggle-btn.close-btn { background: linear-gradient(135deg, #ef5350, #c62828); color: #fff; }
|
||||||
|
|
||||||
|
.countdown-bar {
|
||||||
|
height: 10px; background: #f0f0f0;
|
||||||
|
border-radius: 20px; overflow: hidden; margin-top: 8px;
|
||||||
|
}
|
||||||
|
.countdown-fill {
|
||||||
|
height: 100%; border-radius: 20px;
|
||||||
|
background: linear-gradient(90deg, #66bb6a, #2e7d32);
|
||||||
|
transition: width .5s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="page-header" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
|
||||||
|
<h2><i class="fas fa-unlock-alt"></i> Kelola Akses Input Capaian Santri</h2>
|
||||||
|
<a href="{{ route('admin.capaian.index') }}" class="btn btn-secondary" style="padding:7px 16px;">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Alert --}}
|
||||||
|
@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-exclamation-circle"></i> {{ session('error') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- ===== STATUS HERO ===== --}}
|
||||||
|
<div class="access-hero {{ $isOpen ? 'open' : 'closed' }}">
|
||||||
|
<div class="ah-icon">{{ $isOpen ? '🔓' : '🔒' }}</div>
|
||||||
|
<div style="flex:1;">
|
||||||
|
<div class="status-badge-big {{ $isOpen ? 'open' : 'closed' }}">
|
||||||
|
<i class="fas fa-{{ $isOpen ? 'lock-open' : 'lock' }}"></i>
|
||||||
|
{{ $isOpen ? 'AKSES DIBUKA' : 'AKSES DITUTUP' }}
|
||||||
|
</div>
|
||||||
|
<h3>
|
||||||
|
@if($isOpen)
|
||||||
|
Santri sedang bisa menginputkan capaian mereka
|
||||||
|
@else
|
||||||
|
Santri belum bisa menginputkan capaian
|
||||||
|
@endif
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
@if($isOpen)
|
||||||
|
Dibuka oleh <strong>{{ $config['opened_by'] ?? '-' }}</strong>
|
||||||
|
pada {{ $config['opened_at'] ? \Carbon\Carbon::parse($config['opened_at'])->isoFormat('D MMM YYYY, HH:mm') : '-' }}
|
||||||
|
@if($config['id_semester'])
|
||||||
|
• Semester: <strong>{{ \App\Models\Semester::where('id_semester', $config['id_semester'])->value('nama_semester') ?? '-' }}</strong>
|
||||||
|
@else
|
||||||
|
• Semua semester diizinkan
|
||||||
|
@endif
|
||||||
|
@if($sisaWaktu)
|
||||||
|
• Sisa waktu: <strong>{{ $sisaWaktu }}</strong>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
@if($config['closed_at'])
|
||||||
|
Ditutup pada {{ \Carbon\Carbon::parse($config['closed_at'])->isoFormat('D MMM YYYY, HH:mm') }}
|
||||||
|
@else
|
||||||
|
Belum pernah dibuka
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
@if(!empty($config['catatan']))
|
||||||
|
<p style="margin-top:6px;font-style:italic;"><i class="fas fa-sticky-note"></i> "{{ $config['catatan'] }}"</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ===== INFO STATS ===== --}}
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-box" style="border-color:#66bb6a;">
|
||||||
|
<div class="ib-label">Status</div>
|
||||||
|
<div class="ib-val" style="color:{{ $isOpen ? '#2e7d32' : '#c62828' }};">
|
||||||
|
<i class="fas fa-{{ $isOpen ? 'check-circle' : 'times-circle' }}"></i>
|
||||||
|
{{ $isOpen ? 'Terbuka' : 'Tertutup' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-box" style="border-color:#81C6E8;">
|
||||||
|
<div class="ib-label">Dibuka Oleh</div>
|
||||||
|
<div class="ib-val">{{ $config['opened_by'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-box" style="border-color:#FFD56B;">
|
||||||
|
<div class="ib-label">Semester</div>
|
||||||
|
<div class="ib-val">
|
||||||
|
@if($config['id_semester'])
|
||||||
|
{{ \App\Models\Semester::where('id_semester', $config['id_semester'])->value('nama_semester') ?? '-' }}
|
||||||
|
@else
|
||||||
|
Semua Semester
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-box" style="border-color:#B39DDB;">
|
||||||
|
<div class="ib-label">Auto-Close</div>
|
||||||
|
<div class="ib-val">
|
||||||
|
@if($config['auto_close_at'])
|
||||||
|
{{ \Carbon\Carbon::parse($config['auto_close_at'])->isoFormat('D MMM HH:mm') }}
|
||||||
|
@else
|
||||||
|
Manual
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
|
||||||
|
|
||||||
|
{{-- ===== FORM BUKA AKSES ===== --}}
|
||||||
|
<div class="form-card">
|
||||||
|
<h4 style="color:#2e7d32;"><i class="fas fa-lock-open"></i> Buka Akses Input Capaian</h4>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.capaian.akses-santri.buka') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom:12px;">
|
||||||
|
<label style="font-size:.83rem;font-weight:600;color:#555;display:block;margin-bottom:4px;">
|
||||||
|
<i class="fas fa-calendar-alt"></i> Semester yang Dibuka
|
||||||
|
</label>
|
||||||
|
<select name="id_semester" class="form-control" style="font-size:.84rem;">
|
||||||
|
<option value="">-- Semua Semester --</option>
|
||||||
|
@foreach($semesters as $sem)
|
||||||
|
<option value="{{ $sem->id_semester }}"
|
||||||
|
{{ ($semesterAktif && $sem->id_semester == $semesterAktif->id_semester) ? 'selected' : '' }}>
|
||||||
|
{{ $sem->nama_semester }}{{ $sem->is_active ? ' ★ (Aktif)' : '' }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<small style="color:#999;font-size:.74rem;">Kosongkan = santri bisa input di semua semester</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom:12px;">
|
||||||
|
<label style="font-size:.83rem;font-weight:600;color:#555;display:block;margin-bottom:4px;">
|
||||||
|
<i class="fas fa-clock"></i> Durasi Otomatis Tutup (opsional)
|
||||||
|
</label>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;">
|
||||||
|
<input type="number" name="durasi_jam" min="1" max="720"
|
||||||
|
class="form-control" style="width:110px;font-size:.84rem;"
|
||||||
|
placeholder="cth: 24">
|
||||||
|
<span style="font-size:.84rem;color:#555;">jam</span>
|
||||||
|
</div>
|
||||||
|
<small style="color:#999;font-size:.74rem;">Kosongkan = harus ditutup manual oleh admin</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom:16px;">
|
||||||
|
<label style="font-size:.83rem;font-weight:600;color:#555;display:block;margin-bottom:4px;">
|
||||||
|
<i class="fas fa-sticky-note"></i> Catatan untuk Santri (opsional)
|
||||||
|
</label>
|
||||||
|
<input type="text" name="catatan" class="form-control" style="font-size:.84rem;"
|
||||||
|
placeholder="cth: Deadline input: Jumat 17.00 WIB" maxlength="255">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="toggle-btn open-btn">
|
||||||
|
<i class="fas fa-lock-open"></i> Buka Akses Sekarang
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ===== TUTUP AKSES ===== --}}
|
||||||
|
<div class="form-card">
|
||||||
|
<h4 style="color:#c62828;"><i class="fas fa-lock"></i> Tutup Akses Input Capaian</h4>
|
||||||
|
|
||||||
|
@if($isOpen)
|
||||||
|
<div style="background:#fbe9e7;border-radius:9px;padding:14px;margin-bottom:16px;">
|
||||||
|
<p style="margin:0;font-size:.84rem;color:#555;">
|
||||||
|
<i class="fas fa-info-circle" style="color:#ef5350;"></i>
|
||||||
|
Saat ini akses <strong>sedang dibuka</strong>. Klik tombol di bawah untuk menutup akses input capaian santri segera.
|
||||||
|
</p>
|
||||||
|
@if($sisaWaktu)
|
||||||
|
<p style="margin:8px 0 0;font-size:.8rem;color:#777;">
|
||||||
|
<i class="fas fa-hourglass-half"></i> Sisa waktu auto-close: <strong>{{ $sisaWaktu }}</strong>
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.capaian.akses-santri.tutup') }}"
|
||||||
|
onsubmit="return confirm('Yakin ingin menutup akses input capaian santri?')">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="toggle-btn close-btn">
|
||||||
|
<i class="fas fa-lock"></i> Tutup Akses Sekarang
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<div style="background:#f5f5f5;border-radius:9px;padding:14px;text-align:center;color:#aaa;">
|
||||||
|
<i class="fas fa-lock" style="font-size:2rem;display:block;margin-bottom:8px;"></i>
|
||||||
|
Akses saat ini sudah tertutup.<br>
|
||||||
|
<span style="font-size:.8rem;">Gunakan form di sebelah kiri untuk membuka akses.</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ===== PANDUAN ===== --}}
|
||||||
|
<div class="form-card" style="background:#f0f9ff;border:1px solid #b3e0ff;">
|
||||||
|
<h4 style="color:#0277bd;"><i class="fas fa-info-circle"></i> Alur Penggunaan</h4>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;font-size:.83rem;color:#555;">
|
||||||
|
<div style="display:flex;gap:10px;">
|
||||||
|
<div style="background:#1565c0;color:#fff;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;flex-shrink:0;">1</div>
|
||||||
|
<div><strong>Admin membuka akses</strong><br>Pilih semester & opsional durasi waktu lalu klik "Buka Akses".</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;">
|
||||||
|
<div style="background:#2e7d32;color:#fff;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;flex-shrink:0;">2</div>
|
||||||
|
<div><strong>Santri input capaian</strong><br>Santri login ke web-nya dan bisa input capaian sesuai materi kelasnya.</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;">
|
||||||
|
<div style="background:#e65100;color:#fff;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;flex-shrink:0;">3</div>
|
||||||
|
<div><strong>Data langsung masuk</strong><br>Data capaian santri langsung terlihat di dashboard admin & riwayat santri.</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;">
|
||||||
|
<div style="background:#6a1b9a;color:#fff;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;flex-shrink:0;">4</div>
|
||||||
|
<div><strong>Admin menutup akses</strong><br>Setelah selesai, tutup akses manual atau biarkan auto-close berjalan.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Materi Info Display --}}
|
{{-- Materi Info Display --}}
|
||||||
<div id="materiInfo" style="display: none; margin-bottom: 20px;">
|
<div id="materiInfo" style="display: none; margin-bottom: 14px;">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<i class="fas fa-book-open"></i>
|
<i class="fas fa-book-open"></i>
|
||||||
<strong>Detail Materi:</strong>
|
<strong>Detail Materi:</strong>
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
<h4><i class="fas fa-keyboard"></i> Input Halaman yang Sudah Selesai</h4>
|
<h4><i class="fas fa-keyboard"></i> Input Halaman yang Sudah Selesai</h4>
|
||||||
|
|
||||||
{{-- Tab Metode Input --}}
|
{{-- Tab Metode Input --}}
|
||||||
<div style="display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 2px solid var(--primary-light); padding-bottom: 10px;">
|
<div style="display: flex; gap: 10px; margin-bottom: 14px; border-bottom: 2px solid var(--primary-light); padding-bottom: 10px;">
|
||||||
<button type="button" class="btn btn-sm btn-primary" id="btnMetode1" onclick="switchMetode(1)">
|
<button type="button" class="btn btn-sm btn-primary" id="btnMetode1" onclick="switchMetode(1)">
|
||||||
<i class="fas fa-keyboard"></i> Metode 1: Input Range Text
|
<i class="fas fa-keyboard"></i> Metode 1: Input Range Text
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -154,7 +154,7 @@ class="form-control @error('halaman_selesai') is-invalid @enderror"
|
||||||
<div id="metode3" class="metode-input" style="display: none;">
|
<div id="metode3" class="metode-input" style="display: none;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label><i class="fas fa-sliders-h form-icon"></i> Pilih Halaman Sampai</label>
|
<label><i class="fas fa-sliders-h form-icon"></i> Pilih Halaman Sampai</label>
|
||||||
<div style="display: flex; align-items: center; gap: 15px;">
|
<div style="display: flex; align-items: center; gap: 11px;">
|
||||||
<span>Halaman 1 sampai</span>
|
<span>Halaman 1 sampai</span>
|
||||||
<input type="number" id="quickInputValue" class="form-control"
|
<input type="number" id="quickInputValue" class="form-control"
|
||||||
style="width: 150px;" min="1" placeholder="400">
|
style="width: 150px;" min="1" placeholder="400">
|
||||||
|
|
@ -176,12 +176,12 @@ class="form-control @error('halaman_selesai') is-invalid @enderror"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Preview Result --}}
|
{{-- Preview Result --}}
|
||||||
<div id="previewResult" style="display: none; margin-top: 20px;">
|
<div id="previewResult" style="display: none; margin-top: 14px;">
|
||||||
<div class="info-box" style="background: linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%);">
|
<div class="info-box" style="background: linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%);">
|
||||||
<h4 style="margin: 0 0 10px 0; color: var(--primary-dark);">
|
<h4 style="margin: 0 0 10px 0; color: var(--primary-dark);">
|
||||||
<i class="fas fa-chart-pie"></i> Preview Capaian
|
<i class="fas fa-chart-pie"></i> Preview Capaian
|
||||||
</h4>
|
</h4>
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-top: 15px;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 11px; margin-top: 15px;">
|
||||||
<div style="text-align: center; padding: 15px; background: white; border-radius: 8px;">
|
<div style="text-align: center; padding: 15px; background: white; border-radius: 8px;">
|
||||||
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Halaman Selesai</p>
|
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Halaman Selesai</p>
|
||||||
<h3 style="margin: 5px 0; color: var(--primary-color);" id="previewJumlah">0</h3>
|
<h3 style="margin: 5px 0; color: var(--primary-color);" id="previewJumlah">0</h3>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Materi Info Card --}}
|
{{-- Materi Info Card --}}
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
<div class="icon-wrapper icon-wrapper-lg">
|
<div class="icon-wrapper icon-wrapper-lg">
|
||||||
<i class="fas fa-book"></i>
|
<i class="fas fa-book"></i>
|
||||||
|
|
@ -28,13 +28,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Filter Semester --}}
|
{{-- Filter Semester --}}
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<form method="GET" action="{{ route('admin.capaian.detail-materi', $materi->id_materi) }}" class="filter-form-inline">
|
<form method="GET" action="{{ route('admin.capaian.detail-materi', $materi->id_materi) }}" class="filter-form-inline">
|
||||||
<select name="id_semester" class="form-control" style="width: 250px;">
|
<select name="id_semester" class="form-control" style="width: 250px;">
|
||||||
<option value="">Semua Semester</option>
|
<option value="">Semua Semester</option>
|
||||||
@foreach($semesters as $semester)
|
@foreach($semesters as $semester)
|
||||||
<option value="{{ $semester->id_semester }}" {{ $selectedSemester == $semester->id_semester ? 'selected' : '' }}>
|
<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>
|
</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Grafik Distribusi --}}
|
{{-- Grafik Distribusi --}}
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<h4 style="margin: 0 0 20px 0; color: var(--primary-dark);">
|
<h4 style="margin: 0 0 20px 0; color: var(--primary-dark);">
|
||||||
<i class="fas fa-chart-bar"></i> Distribusi Progress Santri
|
<i class="fas fa-chart-bar"></i> Distribusi Progress Santri
|
||||||
</h4>
|
</h4>
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
<h4><i class="fas fa-keyboard"></i> Update Halaman yang Sudah Selesai</h4>
|
<h4><i class="fas fa-keyboard"></i> Update Halaman yang Sudah Selesai</h4>
|
||||||
|
|
||||||
{{-- Tab Metode Input --}}
|
{{-- Tab Metode Input --}}
|
||||||
<div style="display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 2px solid var(--primary-light); padding-bottom: 10px;">
|
<div style="display: flex; gap: 10px; margin-bottom: 14px; border-bottom: 2px solid var(--primary-light); padding-bottom: 10px;">
|
||||||
<button type="button" class="btn btn-sm btn-primary" id="btnMetode1" onclick="switchMetode(1)">
|
<button type="button" class="btn btn-sm btn-primary" id="btnMetode1" onclick="switchMetode(1)">
|
||||||
<i class="fas fa-keyboard"></i> Metode 1: Input Range Text
|
<i class="fas fa-keyboard"></i> Metode 1: Input Range Text
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -116,7 +116,7 @@ class="form-control @error('halaman_selesai') is-invalid @enderror"
|
||||||
<div id="metode3" class="metode-input" style="display: none;">
|
<div id="metode3" class="metode-input" style="display: none;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label><i class="fas fa-sliders-h form-icon"></i> Pilih Halaman Sampai</label>
|
<label><i class="fas fa-sliders-h form-icon"></i> Pilih Halaman Sampai</label>
|
||||||
<div style="display: flex; align-items: center; gap: 15px;">
|
<div style="display: flex; align-items: center; gap: 11px;">
|
||||||
<span>Halaman {{ $capaian->materi->halaman_mulai }} sampai</span>
|
<span>Halaman {{ $capaian->materi->halaman_mulai }} sampai</span>
|
||||||
<input type="number" id="quickInputValue" class="form-control"
|
<input type="number" id="quickInputValue" class="form-control"
|
||||||
style="width: 150px;"
|
style="width: 150px;"
|
||||||
|
|
@ -141,12 +141,12 @@ class="form-control @error('halaman_selesai') is-invalid @enderror"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Preview Result --}}
|
{{-- Preview Result --}}
|
||||||
<div id="previewResult" style="margin-top: 20px;">
|
<div id="previewResult" style="margin-top: 14px;">
|
||||||
<div class="info-box" style="background: linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%);">
|
<div class="info-box" style="background: linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%);">
|
||||||
<h4 style="margin: 0 0 10px 0; color: var(--primary-dark);">
|
<h4 style="margin: 0 0 10px 0; color: var(--primary-dark);">
|
||||||
<i class="fas fa-chart-pie"></i> Preview Capaian
|
<i class="fas fa-chart-pie"></i> Preview Capaian
|
||||||
</h4>
|
</h4>
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-top: 15px;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 11px; margin-top: 15px;">
|
||||||
<div style="text-align: center; padding: 15px; background: white; border-radius: 8px;">
|
<div style="text-align: center; padding: 15px; background: white; border-radius: 8px;">
|
||||||
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Halaman Selesai</p>
|
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Halaman Selesai</p>
|
||||||
<h3 style="margin: 5px 0; color: var(--primary-color);" id="previewJumlah">{{ $capaian->jumlah_halaman_selesai }}</h3>
|
<h3 style="margin: 5px 0; color: var(--primary-color);" id="previewJumlah">{{ $capaian->jumlah_halaman_selesai }}</h3>
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,19 @@
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Action Button --}}
|
{{-- Action Button --}}
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<a href="{{ route('admin.capaian.create') }}" class="btn btn-success" style="padding: 12px 24px;">
|
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
||||||
|
<a href="{{ route('admin.capaian.create') }}" class="btn btn-success" style="padding: 9px 18px;">
|
||||||
<i class="fas fa-plus"></i> Input Capaian
|
<i class="fas fa-plus"></i> Input Capaian
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ route('admin.capaian.akses-santri') }}" class="btn btn-primary" style="padding: 9px 18px;">
|
||||||
|
<i class="fas fa-unlock-alt"></i> Kelola Akses Input Santri
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Filter Section --}}
|
{{-- Filter Section --}}
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<form method="GET" action="{{ route('admin.capaian.index') }}" class="filter-form-inline">
|
<form method="GET" action="{{ route('admin.capaian.index') }}" class="filter-form-inline">
|
||||||
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
||||||
{{-- Filter Kelas (Dropdown dynamic dari database) --}}
|
{{-- Filter Kelas (Dropdown dynamic dari database) --}}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Santri Info Card --}}
|
{{-- Santri Info Card --}}
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
<div class="icon-wrapper icon-wrapper-lg">
|
<div class="icon-wrapper icon-wrapper-lg">
|
||||||
<i class="fas fa-user-graduate"></i>
|
<i class="fas fa-user-graduate"></i>
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Filter Section --}}
|
{{-- Filter Section --}}
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<form method="GET" action="{{ route('admin.capaian.riwayat-santri', $santri->id_santri) }}" class="filter-form-inline">
|
<form method="GET" action="{{ route('admin.capaian.riwayat-santri', $santri->id_santri) }}" class="filter-form-inline">
|
||||||
<select name="id_semester" class="form-control" style="width: 250px;">
|
<select name="id_semester" class="form-control" style="width: 250px;">
|
||||||
<option value="">Semua Semester</option>
|
<option value="">Semua Semester</option>
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
|
|
||||||
@foreach(['Al-Qur\'an', 'Hadist', 'Materi Tambahan'] as $kategori)
|
@foreach(['Al-Qur\'an', 'Hadist', 'Materi Tambahan'] as $kategori)
|
||||||
@if(isset($groupedCapaians[$kategori]) && $groupedCapaians[$kategori]->count() > 0)
|
@if(isset($groupedCapaians[$kategori]) && $groupedCapaians[$kategori]->count() > 0)
|
||||||
<div style="margin-bottom: 30px;">
|
<div style="margin-bottom: 22px;">
|
||||||
<h4 style="color: var(--primary-dark); margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid var(--primary-light);">
|
<h4 style="color: var(--primary-dark); margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid var(--primary-light);">
|
||||||
<i class="fas fa-{{ $kategori == 'Al-Qur\'an' ? 'book-quran' : ($kategori == 'Hadist' ? 'scroll' : 'book') }}"></i>
|
<i class="fas fa-{{ $kategori == 'Al-Qur\'an' ? 'book-quran' : ($kategori == 'Hadist' ? 'scroll' : 'book') }}"></i>
|
||||||
Kategori: {{ $kategori }}
|
Kategori: {{ $kategori }}
|
||||||
|
|
@ -162,7 +162,7 @@ class="btn btn-sm btn-warning" title="Edit">
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
{{-- Pagination --}}
|
{{-- Pagination --}}
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 14px;">
|
||||||
{{ $capaians->links() }}
|
{{ $capaians->links() }}
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,22 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Progress Card --}}
|
{{-- Progress Card --}}
|
||||||
<div style="background: linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%); padding: 30px; border-radius: 12px; margin: 30px 0;">
|
<div style="background: linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%); padding: 22px; border-radius: 12px; margin: 22px 0;">
|
||||||
<h4 style="margin: 0 0 20px 0; color: var(--primary-dark);">
|
<h4 style="margin: 0 0 20px 0; color: var(--primary-dark);">
|
||||||
<i class="fas fa-chart-pie"></i> Progress Capaian
|
<i class="fas fa-chart-pie"></i> Progress Capaian
|
||||||
</h4>
|
</h4>
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px;">
|
||||||
<div style="text-align: center; padding: 20px; background: white; border-radius: 8px;">
|
<div style="text-align: center; padding: 14px; background: white; border-radius: 8px;">
|
||||||
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Halaman Selesai</p>
|
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Halaman Selesai</p>
|
||||||
<h2 style="margin: 10px 0; color: var(--primary-color);">{{ $capaian->jumlah_halaman_selesai }}</h2>
|
<h2 style="margin: 10px 0; color: var(--primary-color);">{{ $capaian->jumlah_halaman_selesai }}</h2>
|
||||||
<small class="text-muted">dari {{ $capaian->materi->total_halaman }} halaman</small>
|
<small class="text-muted">dari {{ $capaian->materi->total_halaman }} halaman</small>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center; padding: 20px; background: white; border-radius: 8px;">
|
<div style="text-align: center; padding: 14px; background: white; border-radius: 8px;">
|
||||||
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Persentase</p>
|
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Persentase</p>
|
||||||
<h2 style="margin: 10px 0; color: var(--success-color);">{{ number_format($capaian->persentase, 2) }}%</h2>
|
<h2 style="margin: 10px 0; color: var(--success-color);">{{ number_format($capaian->persentase, 2) }}%</h2>
|
||||||
<small class="text-muted">progress keseluruhan</small>
|
<small class="text-muted">progress keseluruhan</small>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center; padding: 20px; background: white; border-radius: 8px;">
|
<div style="text-align: center; padding: 14px; background: white; border-radius: 8px;">
|
||||||
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Status</p>
|
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">Status</p>
|
||||||
<div style="margin: 10px 0;">
|
<div style="margin: 10px 0;">
|
||||||
{!! $capaian->persentase_badge !!}
|
{!! $capaian->persentase_badge !!}
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 14px;">
|
||||||
<div class="progress-bar" style="height: 20px;">
|
<div class="progress-bar" style="height: 20px;">
|
||||||
<div class="progress-fill" style="width: {{ $capaian->persentase }}%; background: linear-gradient(90deg, var(--primary-color), var(--success-color)); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 0.85rem;">
|
<div class="progress-fill" style="width: {{ $capaian->persentase }}%; background: linear-gradient(90deg, var(--primary-color), var(--success-color)); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 0.85rem;">
|
||||||
{{ number_format($capaian->persentase, 1) }}%
|
{{ number_format($capaian->persentase, 1) }}%
|
||||||
|
|
@ -158,7 +158,7 @@
|
||||||
{{-- Visual Halaman (Grid Preview) --}}
|
{{-- Visual Halaman (Grid Preview) --}}
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4><i class="fas fa-th"></i> Visual Halaman yang Selesai</h4>
|
<h4><i class="fas fa-th"></i> Visual Halaman yang Selesai</h4>
|
||||||
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px;">
|
<div style="background: #f8f9fa; padding: 14px; border-radius: 8px;">
|
||||||
<div id="visualGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(45px, 1fr)); gap: 8px;">
|
<div id="visualGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(45px, 1fr)); gap: 8px;">
|
||||||
<!-- Will be generated by JavaScript -->
|
<!-- Will be generated by JavaScript -->
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -170,7 +170,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Action Buttons --}}
|
{{-- Action Buttons --}}
|
||||||
<div style="margin-top: 30px; display: flex; gap: 10px; justify-content: flex-end;">
|
<div style="margin-top: 22px; display: flex; gap: 10px; justify-content: flex-end;">
|
||||||
<a href="{{ route('admin.capaian.edit', $capaian) }}" class="btn btn-warning">
|
<a href="{{ route('admin.capaian.edit', $capaian) }}" class="btn btn-warning">
|
||||||
<i class="fas fa-edit"></i> Edit Capaian
|
<i class="fas fa-edit"></i> Edit Capaian
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,30 @@
|
||||||
{{-- Alert Panel --}}
|
{{-- resources/views/admin/dashboard/_alert-panel.blade.php --}}
|
||||||
@if($alerts['santriAlpaBeruntun']->isNotEmpty() || $alerts['sppJatuhTempo']->isNotEmpty() || $alerts['kepulanganPending']->isNotEmpty())
|
@if($alerts['santriAlpaBeruntun']->isNotEmpty() || $alerts['sppJatuhTempo']->isNotEmpty() || $alerts['kepulanganPending']->isNotEmpty())
|
||||||
<div class="content-section">
|
|
||||||
<h3><i class="fas fa-exclamation-circle"></i> Peringatan & Tindak Lanjut</h3>
|
<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,#FFD56B,#FFAB91);border-radius:6px;flex-shrink:0;">
|
||||||
|
<i class="fas fa-bell" style="font-size:.7rem;color:#fff;"></i>
|
||||||
|
</span>
|
||||||
|
Peringatan & Tindak Lanjut
|
||||||
|
</h4>
|
||||||
|
|
||||||
<div class="dash-alerts">
|
<div class="dash-alerts">
|
||||||
|
|
||||||
{{-- Santri Alpa Beruntun --}}
|
{{-- Santri Alpa Beruntun --}}
|
||||||
@if($alerts['santriAlpaBeruntun']->isNotEmpty())
|
@if($alerts['santriAlpaBeruntun']->isNotEmpty())
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<div class="alert-body">
|
<div class="alert-body">
|
||||||
<strong><i class="fas fa-user-times"></i> Santri Alpa Beruntun (7 Hari Terakhir)</strong>
|
<strong>
|
||||||
<ul class="alert-list">
|
<i class="fas fa-user-times"></i> Santri Alpa Beruntun (7 Hari Terakhir)
|
||||||
|
<span class="badge badge-danger badge-sm" style="margin-left:6px;">{{ $alerts['santriAlpaBeruntun']->count() }} santri</span>
|
||||||
|
</strong>
|
||||||
|
<ul class="alert-list" style="margin-top:6px;">
|
||||||
@foreach($alerts['santriAlpaBeruntun'] as $s)
|
@foreach($alerts['santriAlpaBeruntun'] as $s)
|
||||||
<li>{{ $s->nama }} <span class="badge badge-danger badge-sm">{{ $s->total_alpa }}x alpa</span></li>
|
<li style="display:flex;align-items:center;gap:8px;padding:3px 0;">
|
||||||
@endforeach
|
<i class="fas fa-circle" style="font-size:.4rem;color:var(--danger-color);flex-shrink:0;"></i>
|
||||||
</ul>
|
<span style="flex:1;">{{ $s->nama }}</span>
|
||||||
</div>
|
<span class="badge badge-danger badge-sm">{{ $s->total_alpa }}x alpa</span>
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- SPP Jatuh Tempo --}}
|
|
||||||
@if($alerts['sppJatuhTempo']->isNotEmpty())
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<div class="alert-body">
|
|
||||||
<strong><i class="fas fa-file-invoice-dollar"></i> SPP Jatuh Tempo</strong>
|
|
||||||
<ul class="alert-list">
|
|
||||||
@foreach($alerts['sppJatuhTempo'] as $s)
|
|
||||||
<li>
|
|
||||||
{{ $s->santri->nama_lengkap ?? '-' }}
|
|
||||||
— Bln {{ $s->bulan }}/{{ $s->tahun }}
|
|
||||||
<small>(jatuh tempo {{ $s->batas_bayar->translatedFormat('d M Y') }})</small>
|
|
||||||
</li>
|
</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -36,17 +32,42 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Pengajuan Kepulangan Pending --}}
|
{{-- SPP Jatuh Tempo --}}
|
||||||
|
@if(auth()->user()->isSuperAdmin() && $alerts['sppJatuhTempo']->isNotEmpty())
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<div class="alert-body">
|
||||||
|
<strong>
|
||||||
|
<i class="fas fa-file-invoice-dollar"></i> SPP Jatuh Tempo
|
||||||
|
<span class="badge badge-warning badge-sm" style="margin-left:6px;">{{ $alerts['sppJatuhTempo']->count() }} tagihan</span>
|
||||||
|
</strong>
|
||||||
|
<ul class="alert-list" style="margin-top:6px;">
|
||||||
|
@foreach($alerts['sppJatuhTempo'] as $s)
|
||||||
|
<li style="display:flex;align-items:center;gap:8px;padding:3px 0;flex-wrap:wrap;">
|
||||||
|
<i class="fas fa-circle" style="font-size:.4rem;color:#E6B85C;flex-shrink:0;"></i>
|
||||||
|
<span style="flex:1;">{{ $s->santri->nama_lengkap ?? '-' }}</span>
|
||||||
|
<span class="badge badge-warning badge-sm">Bln {{ $s->bulan }}/{{ $s->tahun }}</span>
|
||||||
|
<small style="color:var(--text-light);">jatuh tempo {{ $s->batas_bayar->translatedFormat('d M Y') }}</small>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Pengajuan Kepulangan --}}
|
||||||
@if($alerts['kepulanganPending']->isNotEmpty())
|
@if($alerts['kepulanganPending']->isNotEmpty())
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<div class="alert-body">
|
<div class="alert-body">
|
||||||
<strong><i class="fas fa-home"></i> Pengajuan Kepulangan Menunggu Review</strong>
|
<strong>
|
||||||
<ul class="alert-list">
|
<i class="fas fa-home"></i> Pengajuan Kepulangan Menunggu Review
|
||||||
|
<span class="badge badge-primary badge-sm" style="margin-left:6px;">{{ $alerts['kepulanganPending']->count() }} pengajuan</span>
|
||||||
|
</strong>
|
||||||
|
<ul class="alert-list" style="margin-top:6px;">
|
||||||
@foreach($alerts['kepulanganPending'] as $k)
|
@foreach($alerts['kepulanganPending'] as $k)
|
||||||
<li>
|
<li style="display:flex;align-items:center;gap:8px;padding:3px 0;flex-wrap:wrap;">
|
||||||
{{ $k->santri->nama_lengkap ?? '-' }}
|
<i class="fas fa-circle" style="font-size:.4rem;color:var(--info-color);flex-shrink:0;"></i>
|
||||||
— {{ $k->tanggal_pulang->translatedFormat('d M') }} s.d {{ $k->tanggal_kembali->translatedFormat('d M Y') }}
|
<span style="flex:1;">{{ $k->santri->nama_lengkap ?? '-' }}</span>
|
||||||
<small>({{ $k->alasan }})</small>
|
<span class="badge badge-info badge-sm">{{ $k->tanggal_pulang->translatedFormat('d M') }} – {{ $k->tanggal_kembali->translatedFormat('d M Y') }}</span>
|
||||||
</li>
|
</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -56,4 +77,5 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
{{-- Jadwal Kegiatan Hari Ini --}}
|
{{-- resources/views/admin/dashboard/_jadwal-kegiatan.blade.php --}}
|
||||||
<div class="content-section">
|
<div class="content-box" style="margin-bottom:16px;">
|
||||||
<h3><i class="fas fa-list-alt"></i> Jadwal Kegiatan — {{ $hari }}</h3>
|
<h4 style="margin:0 0 12px;font-size:.88rem;font-weight:700;color:var(--text-color);display:flex;align-items:center;gap:8px;">
|
||||||
<div class="content-box">
|
<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 }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
@if($kegiatan->isEmpty())
|
@if($kegiatan->isEmpty())
|
||||||
<p class="text-muted">Tidak ada kegiatan terjadwal hari ini.</p>
|
<div class="empty-state" style="padding:20px;">
|
||||||
|
<i class="fas fa-calendar-times"></i>
|
||||||
|
<p>Tidak ada kegiatan terjadwal hari ini.</p>
|
||||||
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="table-responsive">
|
<div class="table-responsive" style="overflow-x:auto;">
|
||||||
<table class="data-table">
|
<table class="data-table" style="margin-top:0;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Kegiatan</th>
|
<th>Kegiatan</th>
|
||||||
|
|
@ -20,32 +28,45 @@
|
||||||
@foreach($kegiatan as $k)
|
@foreach($kegiatan as $k)
|
||||||
<tr class="{{ $k->belum_input ? 'row-danger' : '' }}">
|
<tr class="{{ $k->belum_input ? 'row-danger' : '' }}">
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ $k->nama_kegiatan }}</strong>
|
<strong style="font-size:.82rem;">{{ $k->nama_kegiatan }}</strong>
|
||||||
@if($k->belum_input)
|
@if($k->belum_input)
|
||||||
<span class="badge badge-danger badge-sm">Belum input absensi!</span>
|
<span class="badge badge-danger badge-sm" style="display:inline-flex;margin-left:6px;animation:slideInDown .4s;">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> Belum input absensi!
|
||||||
|
</span>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td>{{ $k->kategori->nama_kategori ?? '-' }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
|
<span class="badge badge-info">{{ $k->kategori->nama_kategori ?? '-' }}</span>
|
||||||
|
</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') }}
|
{{ is_string($k->waktu_mulai) ? $k->waktu_mulai : $k->waktu_mulai->format('H:i') }}
|
||||||
—
|
<span style="color:var(--text-light);margin:0 2px;">–</span>
|
||||||
{{ is_string($k->waktu_selesai) ? $k->waktu_selesai : $k->waktu_selesai->format('H:i') }}
|
{{ is_string($k->waktu_selesai) ? $k->waktu_selesai : $k->waktu_selesai->format('H:i') }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if($k->status_kegiatan === 'berlangsung')
|
@if($k->status_kegiatan === 'berlangsung')
|
||||||
<span class="badge badge-info">Berlangsung</span>
|
<span class="badge badge-success" style="animation:slideInDown .5s;">
|
||||||
|
<i class="fas fa-circle" style="font-size:.45rem;"></i> Berlangsung
|
||||||
|
</span>
|
||||||
@elseif($k->status_kegiatan === 'selesai')
|
@elseif($k->status_kegiatan === 'selesai')
|
||||||
<span class="badge badge-success">Selesai</span>
|
<span class="badge badge-primary">
|
||||||
|
<i class="fas fa-check"></i> Selesai
|
||||||
|
</span>
|
||||||
@else
|
@else
|
||||||
<span class="badge badge-secondary">Belum Mulai</span>
|
<span class="badge badge-secondary">
|
||||||
|
<i class="fas fa-clock"></i> Belum Mulai
|
||||||
|
</span>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if($k->total_absensi > 0)
|
@if($k->total_absensi > 0)
|
||||||
<div class="progress-bar-wrap">
|
<div class="progress-bar-wrap">
|
||||||
<div class="progress-bar-fill" style="width: {{ $k->persen_kehadiran }}%"></div>
|
<div class="progress-bar-fill" style="width:{{ $k->persen_kehadiran }}%;"></div>
|
||||||
</div>
|
</div>
|
||||||
<small>{{ $k->persen_kehadiran }}% ({{ $k->total_absensi }} data)</small>
|
<small style="font-size:.68rem;color:var(--text-light);">
|
||||||
|
{{ $k->persen_kehadiran }}%
|
||||||
|
<span style="color:#bbb;">({{ $k->total_absensi }} data)</span>
|
||||||
|
</small>
|
||||||
@else
|
@else
|
||||||
<small class="text-muted">—</small>
|
<small class="text-muted">—</small>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -56,5 +77,4 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,36 +1,45 @@
|
||||||
{{-- KPI Cards --}}
|
{{-- resources/views/admin/dashboard/_kpi-cards.blade.php --}}
|
||||||
<div class="row-cards row-cards-5">
|
<div class="row-cards row-cards-5" style="margin-bottom:16px;">
|
||||||
|
|
||||||
<div class="card card-info">
|
<div class="card card-info">
|
||||||
<h3>Santri Aktif</h3>
|
<h3>Santri Aktif</h3>
|
||||||
<p class="card-value">{{ $kpi['totalSantriAktif'] }}</p>
|
<div class="card-value">{{ $kpi['totalSantriAktif'] }}</div>
|
||||||
|
<span class="card-sub">terdaftar & aktif</span>
|
||||||
<i class="fas fa-user-graduate card-icon"></i>
|
<i class="fas fa-user-graduate card-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card {{ $kpi['belumAbsensi'] > 0 ? 'card-warning' : 'card-success' }}">
|
<div class="card {{ $kpi['belumAbsensi'] > 0 ? 'card-warning' : 'card-success' }}">
|
||||||
<h3>Kegiatan Hari Ini</h3>
|
<h3>Kegiatan Hari Ini</h3>
|
||||||
<p class="card-value">{{ $kpi['totalKegiatan'] }}</p>
|
<div class="card-value">{{ $kpi['totalKegiatan'] }}</div>
|
||||||
<span class="card-sub">{{ $kpi['sudahAbsensi'] }} sudah absen · {{ $kpi['belumAbsensi'] }} belum</span>
|
<span class="card-sub">
|
||||||
|
<span style="color:#27ae60;font-weight:700;">{{ $kpi['sudahAbsensi'] }} absen</span>
|
||||||
|
·
|
||||||
|
<span style="{{ $kpi['belumAbsensi'] > 0 ? 'color:#e67e22;font-weight:700;' : '' }}">{{ $kpi['belumAbsensi'] }} belum</span>
|
||||||
|
</span>
|
||||||
<i class="fas fa-calendar-check card-icon"></i>
|
<i class="fas fa-calendar-check card-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card {{ $kpi['santriSakit'] > 0 ? 'card-danger' : 'card-success' }}">
|
<div class="card {{ $kpi['santriSakit'] > 0 ? 'card-danger' : 'card-success' }}">
|
||||||
<h3>Santri di UKP</h3>
|
<h3>Santri di UKP</h3>
|
||||||
<p class="card-value">{{ $kpi['santriSakit'] }}</p>
|
<div class="card-value">{{ $kpi['santriSakit'] }}</div>
|
||||||
<span class="card-sub">sedang dirawat</span>
|
<span class="card-sub">sedang dirawat</span>
|
||||||
<i class="fas fa-briefcase-medical card-icon"></i>
|
<i class="fas fa-briefcase-medical card-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card {{ $kpi['kepulanganMenunggu'] > 0 ? 'card-warning' : 'card-success' }}">
|
<div class="card {{ $kpi['kepulanganMenunggu'] > 0 ? 'card-warning' : 'card-success' }}">
|
||||||
<h3>Menunggu Approval</h3>
|
<h3>Menunggu Approval</h3>
|
||||||
<p class="card-value">{{ $kpi['kepulanganMenunggu'] }}</p>
|
<div class="card-value">{{ $kpi['kepulanganMenunggu'] }}</div>
|
||||||
<span class="card-sub">pengajuan kepulangan</span>
|
<span class="card-sub">pengajuan kepulangan</span>
|
||||||
<i class="fas fa-clock card-icon"></i>
|
<i class="fas fa-clock card-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(auth()->user()->isSuperAdmin())
|
||||||
<div class="card {{ $kpi['santriTanpaWali'] > 0 ? 'card-secondary' : 'card-success' }}">
|
<div class="card {{ $kpi['santriTanpaWali'] > 0 ? 'card-secondary' : 'card-success' }}">
|
||||||
<h3>Belum Ada Akun Wali</h3>
|
<h3>Belum Ada Akun Wali</h3>
|
||||||
<p class="card-value">{{ $kpi['santriTanpaWali'] }}</p>
|
<div class="card-value">{{ $kpi['santriTanpaWali'] }}</div>
|
||||||
<span class="card-sub">santri tanpa wali mobile</span>
|
<span class="card-sub">santri tanpa wali mobile</span>
|
||||||
<i class="fas fa-user-plus card-icon"></i>
|
<i class="fas fa-user-plus card-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,32 +1,254 @@
|
||||||
{{-- Ringkasan SPP Bulan Ini --}}
|
{{-- resources/views/admin/dashboard/_ringkasan-spp.blade.php --}}
|
||||||
<div class="content-box dash-chart-box">
|
@php
|
||||||
<h4><i class="fas fa-wallet"></i> SPP Bulan Ini</h4>
|
$total = ($spp['lunas'] ?? 0) + ($spp['belum'] ?? 0);
|
||||||
|
|
||||||
@php
|
|
||||||
$total = $spp['lunas'] + $spp['belum'];
|
|
||||||
$persenLunas = $total > 0 ? round(($spp['lunas'] / $total) * 100) : 0;
|
$persenLunas = $total > 0 ? round(($spp['lunas'] / $total) * 100) : 0;
|
||||||
@endphp
|
$terkumpul = (float) ($spp['terkumpul'] ?? 0);
|
||||||
|
$totalTagihan = (float) ($spp['totalTagihan'] ?? 0);
|
||||||
|
$persenNominal = $totalTagihan > 0 ? min(100, round($terkumpul / $totalTagihan * 100)) : 0;
|
||||||
|
|
||||||
<div class="chart-container chart-container-sm">
|
$pemasukanLain = (float) ($spp['pemasukanLain'] ?? 0);
|
||||||
<canvas id="sppDonutChart"></canvas>
|
$pengeluaran = (float) ($spp['pengeluaran'] ?? 0);
|
||||||
</div>
|
$totalPemasukan = $terkumpul + $pemasukanLain;
|
||||||
|
$sisaKas = $totalPemasukan - $pengeluaran;
|
||||||
|
|
||||||
<div class="spp-summary">
|
$kasMax = max($totalPemasukan, $pengeluaran, 1);
|
||||||
<div class="spp-stat">
|
$pBarMasuk = min(100, round($totalPemasukan / $kasMax * 100));
|
||||||
<span class="spp-label">Lunas</span>
|
$pBarKeluar = min(100, round($pengeluaran / $kasMax * 100));
|
||||||
<strong class="text-success">{{ $spp['lunas'] }} santri ({{ $persenLunas }}%)</strong>
|
@endphp
|
||||||
</div>
|
|
||||||
<div class="spp-stat">
|
{{-- Label section --}}
|
||||||
<span class="spp-label">Belum Lunas</span>
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
|
||||||
<strong class="text-danger">{{ $spp['belum'] }} santri</strong>
|
<span style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;
|
||||||
</div>
|
background:linear-gradient(135deg,var(--primary-color),var(--secondary-color));border-radius:6px;flex-shrink:0;">
|
||||||
<div class="spp-stat">
|
<i class="fas fa-wallet" style="font-size:.7rem;color:#fff;"></i>
|
||||||
<span class="spp-label">Terkumpul</span>
|
</span>
|
||||||
<strong>Rp {{ number_format($spp['terkumpul'], 0, ',', '.') }}</strong>
|
<span style="font-size:.88rem;font-weight:700;color:var(--text-color);">Keuangan Bulan Ini</span>
|
||||||
</div>
|
<a href="{{ route('admin.keuangan.laporan', ['bulan'=>date('n'),'tahun'=>date('Y')]) }}"
|
||||||
<div class="spp-stat">
|
style="margin-left:auto;font-size:.72rem;color:var(--primary-color);font-weight:600;text-decoration:none;display:flex;align-items:center;gap:4px;">
|
||||||
<span class="spp-label">Total Tagihan</span>
|
Lihat Neraca <i class="fas fa-arrow-right" style="font-size:.6rem;"></i>
|
||||||
<strong>Rp {{ number_format($spp['totalTagihan'], 0, ',', '.') }}</strong>
|
</a>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- 2 panel grid --}}
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:16px;">
|
||||||
|
|
||||||
|
{{-- ── Panel Kiri: Status SPP ── --}}
|
||||||
|
<div class="content-box" style="display:flex;flex-direction:column;gap:0;">
|
||||||
|
|
||||||
|
<h4 style="margin:0 0 14px;font-size:.8rem;font-weight:700;color:var(--primary-dark);
|
||||||
|
display:flex;align-items:center;gap:6px;">
|
||||||
|
<i class="fas fa-money-check-alt" style="color:var(--primary-color);"></i>
|
||||||
|
Status Pembayaran SPP
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{{-- Ring chart + legend --}}
|
||||||
|
<div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:14px;">
|
||||||
|
|
||||||
|
{{-- Canvas --}}
|
||||||
|
<div style="position:relative;width:110px;height:110px;flex-shrink:0;">
|
||||||
|
<canvas id="sppRingChart"></canvas>
|
||||||
|
<div style="position:absolute;inset:0;display:flex;flex-direction:column;
|
||||||
|
align-items:center;justify-content:center;pointer-events:none;">
|
||||||
|
<span style="font-size:1.5rem;font-weight:800;color:var(--text-color);line-height:1;">{{ $persenLunas }}%</span>
|
||||||
|
<span style="font-size:.6rem;color:var(--text-light);font-weight:500;">lunas</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Stats --}}
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px;flex:1;min-width:100px;">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;">
|
||||||
|
<span style="width:10px;height:10px;border-radius:50%;background:var(--primary-color);flex-shrink:0;display:inline-block;"></span>
|
||||||
|
<span style="font-size:.72rem;color:var(--text-light);flex:1;">Lunas</span>
|
||||||
|
<strong style="font-size:.82rem;color:var(--primary-color);">{{ $spp['lunas'] ?? 0 }}</strong>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;">
|
||||||
|
<span style="width:10px;height:10px;border-radius:50%;background:var(--danger-color);flex-shrink:0;display:inline-block;"></span>
|
||||||
|
<span style="font-size:.72rem;color:var(--text-light);flex:1;">Belum Lunas</span>
|
||||||
|
<strong style="font-size:.82rem;color:var(--danger-color);">{{ $spp['belum'] ?? 0 }}</strong>
|
||||||
|
</div>
|
||||||
|
<div style="height:1px;background:var(--primary-light);margin:2px 0;"></div>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<span style="font-size:.68rem;color:var(--text-light);">Terkumpul</span>
|
||||||
|
<span style="font-size:.72rem;font-weight:700;color:var(--text-color);">Rp {{ number_format($terkumpul/1000000,1) }}jt</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<span style="font-size:.68rem;color:var(--text-light);">Target</span>
|
||||||
|
<span style="font-size:.72rem;font-weight:600;color:var(--text-light);">Rp {{ number_format($totalTagihan/1000000,1) }}jt</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Progress bar nominal --}}
|
||||||
|
<div style="margin-bottom:14px;">
|
||||||
|
<div style="display:flex;justify-content:space-between;font-size:.68rem;color:var(--text-light);margin-bottom:4px;">
|
||||||
|
<span>Nominal terkumpul</span>
|
||||||
|
<span style="font-weight:700;color:var(--text-color);">{{ $persenNominal }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-wrap">
|
||||||
|
<div class="progress-bar-fill" style="width:{{ $persenNominal }}%;
|
||||||
|
background:linear-gradient(90deg,var(--primary-color),var(--primary-dark));"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Quick links --}}
|
||||||
|
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:auto;">
|
||||||
|
<a href="{{ route('admin.pembayaran-spp.index', ['tab'=>'belum-bayar','bulan'=>date('n'),'tahun'=>date('Y')]) }}"
|
||||||
|
class="btn btn-danger btn-sm" style="flex:1;justify-content:center;">
|
||||||
|
<i class="fas fa-exclamation-circle"></i> Belum ({{ $spp['belum'] ?? 0 }})
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.pembayaran-spp.generate') }}"
|
||||||
|
class="btn btn-warning btn-sm" style="flex:1;justify-content:center;">
|
||||||
|
<i class="fas fa-cogs"></i> Generate
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.pembayaran-spp.index', ['tab'=>'sudah-bayar','bulan'=>date('n'),'tahun'=>date('Y')]) }}"
|
||||||
|
class="btn btn-success btn-sm" style="flex:1;justify-content:center;">
|
||||||
|
<i class="fas fa-check-circle"></i> Lunas ({{ $spp['lunas'] ?? 0 }})
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ── Panel Kanan: Neraca Kas ── --}}
|
||||||
|
<div class="content-box" style="display:flex;flex-direction:column;gap:0;">
|
||||||
|
|
||||||
|
<h4 style="margin:0 0 14px;font-size:.8rem;font-weight:700;color:var(--primary-dark);
|
||||||
|
display:flex;align-items:center;gap:6px;">
|
||||||
|
<i class="fas fa-landmark" style="color:var(--info-color);"></i>
|
||||||
|
Neraca Kas Pondok
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{{-- Horizontal bars --}}
|
||||||
|
<div style="display:flex;flex-direction:column;gap:14px;margin-bottom:14px;">
|
||||||
|
|
||||||
|
{{-- Bar Pemasukan --}}
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;">
|
||||||
|
<span style="font-size:.72rem;color:var(--text-light);font-weight:600;display:flex;align-items:center;gap:6px;">
|
||||||
|
<span style="width:10px;height:10px;border-radius:50%;background:var(--success-color);display:inline-block;flex-shrink:0;"></span>
|
||||||
|
SPP + Pemasukan
|
||||||
|
</span>
|
||||||
|
<strong style="font-size:.78rem;color:var(--success-color);">
|
||||||
|
Rp {{ number_format($totalPemasukan,0,',','.') }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-wrap" style="height:10px;">
|
||||||
|
<div class="progress-bar-fill" style="width:{{ $pBarMasuk }}%;height:100%;
|
||||||
|
background:linear-gradient(90deg,var(--primary-color),#38ef7d);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Bar Pengeluaran --}}
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;">
|
||||||
|
<span style="font-size:.72rem;color:var(--text-light);font-weight:600;display:flex;align-items:center;gap:6px;">
|
||||||
|
<span style="width:10px;height:10px;border-radius:50%;background:var(--danger-color);display:inline-block;flex-shrink:0;"></span>
|
||||||
|
Pengeluaran
|
||||||
|
</span>
|
||||||
|
<strong style="font-size:.78rem;color:var(--danger-color);">
|
||||||
|
Rp {{ number_format($pengeluaran,0,',','.') }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-wrap" style="height:10px;">
|
||||||
|
<div class="progress-bar-fill" style="width:{{ $pBarKeluar }}%;height:100%;
|
||||||
|
background:linear-gradient(90deg,var(--danger-color),#FF6B7A);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($pemasukanLain > 0)
|
||||||
|
{{-- Bar SPP saja (breakdown) --}}
|
||||||
|
<div style="opacity:.8;">
|
||||||
|
@php $pBarSpp = min(100, round($terkumpul / $kasMax * 100)); @endphp
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;">
|
||||||
|
<span style="font-size:.68rem;color:var(--text-light);display:flex;align-items:center;gap:6px;">
|
||||||
|
<span style="width:8px;height:8px;border-radius:50%;background:var(--info-color);display:inline-block;flex-shrink:0;"></span>
|
||||||
|
└ dari SPP saja
|
||||||
|
</span>
|
||||||
|
<span style="font-size:.7rem;color:var(--info-color);font-weight:600;">
|
||||||
|
Rp {{ number_format($terkumpul,0,',','.') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-wrap" style="height:7px;">
|
||||||
|
<div class="progress-bar-fill" style="width:{{ $pBarSpp }}%;height:100%;
|
||||||
|
background:linear-gradient(90deg,var(--info-color),#5FAFE0);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Sisa Kas box --}}
|
||||||
|
<div style="padding:11px 14px;border-radius:var(--border-radius-sm);margin-bottom:14px;
|
||||||
|
background:{{ $sisaKas >= 0 ? 'linear-gradient(135deg,#E8F7F2,#D4F1E3)' : 'linear-gradient(135deg,#FFE8EA,#FFD5D8)' }};
|
||||||
|
border-left:4px solid {{ $sisaKas >= 0 ? 'var(--success-color)' : 'var(--danger-color)' }};
|
||||||
|
display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<span style="font-size:.75rem;font-weight:700;color:var(--text-color);display:flex;align-items:center;gap:6px;">
|
||||||
|
<i class="fas fa-{{ $sisaKas >= 0 ? 'piggy-bank' : 'exclamation-triangle' }}"
|
||||||
|
style="color:{{ $sisaKas >= 0 ? 'var(--success-color)' : 'var(--danger-color)' }};"></i>
|
||||||
|
Sisa Kas Bulan Ini
|
||||||
|
</span>
|
||||||
|
<strong style="font-size:1rem;font-weight:800;color:{{ $sisaKas >= 0 ? 'var(--success-color)' : 'var(--danger-color)' }};">
|
||||||
|
{{ $sisaKas >= 0 ? '+' : '' }}Rp {{ number_format($sisaKas,0,',','.') }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Quick links --}}
|
||||||
|
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:auto;">
|
||||||
|
<a href="{{ route('admin.keuangan.index') }}"
|
||||||
|
class="btn btn-info btn-sm" style="flex:1;justify-content:center;">
|
||||||
|
<i class="fas fa-book-open"></i> Buku Kas
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.keuangan.laporan', ['bulan'=>date('n'),'tahun'=>date('Y')]) }}"
|
||||||
|
class="btn btn-warning btn-sm" style="flex:1;justify-content:center;">
|
||||||
|
<i class="fas fa-chart-bar"></i> Laporan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Ring Chart Script --}}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var SPP_LUNAS = {{ (int)($spp['lunas'] ?? 0) }};
|
||||||
|
var SPP_BELUM = {{ (int)($spp['belum'] ?? 0) }};
|
||||||
|
|
||||||
|
function initRing() {
|
||||||
|
if (typeof Chart === 'undefined') { setTimeout(initRing, 50); return; }
|
||||||
|
var el = document.getElementById('sppRingChart');
|
||||||
|
if (!el) return;
|
||||||
|
if (el._ci) { el._ci.destroy(); }
|
||||||
|
var allZero = (SPP_LUNAS === 0 && SPP_BELUM === 0);
|
||||||
|
el._ci = new Chart(el, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels : allZero ? ['Belum ada data'] : ['Lunas', 'Belum Lunas'],
|
||||||
|
datasets: [{
|
||||||
|
data : allZero ? [1] : [SPP_LUNAS, SPP_BELUM],
|
||||||
|
backgroundColor: allZero ? ['#E8F7F2'] : ['#6FBA9D', '#FF8B94'],
|
||||||
|
borderWidth : allZero ? 0 : 3,
|
||||||
|
borderColor : '#fff',
|
||||||
|
hoverOffset : allZero ? 0 : 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true, maintainAspectRatio: false,
|
||||||
|
cutout: '70%',
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
enabled: !allZero,
|
||||||
|
callbacks: {
|
||||||
|
label: function (ctx) {
|
||||||
|
var total = ctx.dataset.data.reduce(function(a,b){return a+b;},0);
|
||||||
|
var pct = total > 0 ? Math.round(ctx.parsed/total*100) : 0;
|
||||||
|
return ' '+ctx.label+': '+ctx.formattedValue+' ('+pct+'%)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initRing);
|
||||||
|
} else { initRing(); }
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
{{-- Tren Kehadiran 4 Minggu Terakhir --}}
|
{{-- resources/views/admin/dashboard/_tren-kehadiran.blade.php --}}
|
||||||
<div class="content-box dash-chart-box">
|
<div class="content-box" style="margin-bottom:16px;">
|
||||||
<h4><i class="fas fa-chart-line"></i> Tren Kehadiran (4 Minggu)</h4>
|
<h4 style="margin:0 0 12px;font-size:.88rem;font-weight:700;color:var(--text-color);display:flex;align-items:center;gap:8px;">
|
||||||
<div class="chart-container">
|
<span style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;
|
||||||
|
background:linear-gradient(135deg,var(--info-color),#5FAFE0);border-radius:6px;flex-shrink:0;">
|
||||||
|
<i class="fas fa-chart-line" style="font-size:.7rem;color:#fff;"></i>
|
||||||
|
</span>
|
||||||
|
Tren Kehadiran — 4 Minggu Terakhir
|
||||||
|
</h4>
|
||||||
|
<div class="chart-container" style="height:220px;">
|
||||||
<canvas id="trenKehadiranChart"></canvas>
|
<canvas id="trenKehadiranChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,98 +1,159 @@
|
||||||
{{-- views/admin/dashboardAdmin.blade.php --}}
|
{{-- resources/views/admin/dashboardAdmin.blade.php --}}
|
||||||
@extends('layouts.app', ['isAdmin' => true])
|
@extends('layouts.app', ['isAdmin' => true])
|
||||||
|
|
||||||
@section('title', 'Dashboard Admin')
|
@section('title', 'Dashboard Admin')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|
||||||
|
{{-- ───────── PAGE HEADER ───────── --}}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Dashboard Admin</h2>
|
<div>
|
||||||
<p>{{ $hariIni }}, {{ $today->translatedFormat('d F Y') }}</p>
|
<h2>
|
||||||
|
@if(auth()->user()->role === 'super_admin')
|
||||||
|
<i class="fas fa-crown"></i> Super Admin
|
||||||
|
@elseif(auth()->user()->role === 'akademik')
|
||||||
|
<i class="fas fa-book-open"></i> Akademik
|
||||||
|
@else
|
||||||
|
<i class="fas fa-shield-alt"></i> Pamong
|
||||||
|
@endif
|
||||||
|
</h2>
|
||||||
|
<p style="margin:3px 0 0;font-size:.75rem;color:var(--text-light);">
|
||||||
|
{{ $hariIni }}, {{ $today->translatedFormat('d F Y') }}
|
||||||
|
·
|
||||||
|
<span style="display:inline-flex;align-items:center;gap:5px;">
|
||||||
|
<span style="width:7px;height:7px;border-radius:50%;background:#27ae60;
|
||||||
|
box-shadow:0 0 0 3px rgba(39,174,96,.2);display:inline-block;
|
||||||
|
animation:dashLivePulse 2s infinite;"></span>
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- 1. KPI Cards --}}
|
<style>
|
||||||
|
@keyframes dashLivePulse {
|
||||||
|
0%,100%{box-shadow:0 0 0 3px rgba(39,174,96,.2)}
|
||||||
|
50% {box-shadow:0 0 0 6px rgba(39,174,96,.05)}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2-col grid untuk SPP + Kas */
|
||||||
|
.dash-fin-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dash-fin-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.row-cards-5 { grid-template-columns: repeat(3,1fr); }
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.row-cards-5 { grid-template-columns: repeat(2,1fr); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{{-- ───────── 1. KPI CARDS ───────── --}}
|
||||||
@include('admin.dashboard._kpi-cards', ['kpi' => $kpiCards])
|
@include('admin.dashboard._kpi-cards', ['kpi' => $kpiCards])
|
||||||
|
|
||||||
{{-- 2. Jadwal Kegiatan Hari Ini --}}
|
{{-- ───────── 2. ALERTS ───────── --}}
|
||||||
@include('admin.dashboard._jadwal-kegiatan', ['kegiatan' => $kegiatanHariIni, 'hari' => $hariIni])
|
|
||||||
|
|
||||||
{{-- 3. Alert Panel --}}
|
|
||||||
@include('admin.dashboard._alert-panel', ['alerts' => $alerts])
|
@include('admin.dashboard._alert-panel', ['alerts' => $alerts])
|
||||||
|
|
||||||
{{-- Row: Grafik + SPP --}}
|
{{-- ───────── 3. JADWAL KEGIATAN ───────── --}}
|
||||||
<div class="dash-grid-2">
|
@include('admin.dashboard._jadwal-kegiatan', ['kegiatan' => $kegiatanHariIni, 'hari' => $hariIni])
|
||||||
{{-- 4. Grafik Tren Kehadiran --}}
|
|
||||||
@include('admin.dashboard._tren-kehadiran', ['trenKehadiran' => $trenKehadiran])
|
|
||||||
|
|
||||||
{{-- 5. Ringkasan SPP Bulan Ini --}}
|
{{-- ───────── 4. KEUANGAN (SPP + KAS) ───────── --}}
|
||||||
|
@if(auth()->user()->isSuperAdmin())
|
||||||
@include('admin.dashboard._ringkasan-spp', ['spp' => $sppBulanIni])
|
@include('admin.dashboard._ringkasan-spp', ['spp' => $sppBulanIni])
|
||||||
</div>
|
@endif
|
||||||
|
|
||||||
|
{{-- ───────── 5. TREN KEHADIRAN FULL WIDTH ───────── --}}
|
||||||
|
@include('admin.dashboard._tren-kehadiran', ['trenKehadiran' => $trenKehadiran])
|
||||||
|
|
||||||
{{-- 6. Feed Aktivitas Terbaru --}}
|
|
||||||
@include('admin.dashboard._feed-aktivitas', ['feed' => $feedAktivitas])
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('scripts')
|
@section('scripts')
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// ── Tren Kehadiran (Line Chart) ──
|
|
||||||
const trenCtx = document.getElementById('trenKehadiranChart');
|
// ── Tren Kehadiran Line Chart ─────────────────────────────────────────
|
||||||
if (trenCtx) {
|
var trenCtx = document.getElementById('trenKehadiranChart');
|
||||||
const trenData = @json($trenKehadiran);
|
if (!trenCtx) return;
|
||||||
const colors = ['#6FBA9D', '#FF8B94', '#81C6E8', '#FFD56B', '#B39DDB', '#FFAB91'];
|
|
||||||
const datasets = Object.keys(trenData.series).map((label, i) => ({
|
var trenData = @json($trenKehadiran);
|
||||||
label: label,
|
// Pakai palet warna yang sesuai theme (eucalyptus green + accents)
|
||||||
data: trenData.series[label],
|
var palette = ['#6FBA9D','#FF8B94','#81C6E8','#FFD56B','#B39DDB','#FFAB91'];
|
||||||
borderColor: colors[i % colors.length],
|
var datasets = [];
|
||||||
backgroundColor: colors[i % colors.length] + '20',
|
|
||||||
tension: 0.3,
|
Object.keys(trenData.series).forEach(function (key, i) {
|
||||||
fill: true,
|
var c = palette[i % palette.length];
|
||||||
pointRadius: 4,
|
datasets.push({
|
||||||
pointHoverRadius: 6,
|
label : key,
|
||||||
}));
|
data : trenData.series[key],
|
||||||
|
borderColor : c,
|
||||||
|
backgroundColor : c + '20',
|
||||||
|
borderWidth : 2.5,
|
||||||
|
tension : 0.4,
|
||||||
|
fill : true,
|
||||||
|
pointRadius : 5,
|
||||||
|
pointHoverRadius : 7,
|
||||||
|
pointBackgroundColor : c,
|
||||||
|
pointBorderColor : '#fff',
|
||||||
|
pointBorderWidth : 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
new Chart(trenCtx, {
|
new Chart(trenCtx, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: { labels: trenData.labels, datasets },
|
data: { labels: trenData.labels, datasets: datasets },
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive : true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
interaction : { mode: 'index', intersect: false },
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true } },
|
legend: {
|
||||||
tooltip: { callbacks: { label: ctx => ctx.dataset.label + ': ' + ctx.parsed.y + '%' } }
|
position: 'bottom',
|
||||||
|
labels : {
|
||||||
|
padding : 18,
|
||||||
|
usePointStyle: true,
|
||||||
|
font : { size: 11 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#2C3E50',
|
||||||
|
titleFont : { size: 12, weight: '700' },
|
||||||
|
bodyFont : { size: 11 },
|
||||||
|
padding : 10,
|
||||||
|
cornerRadius : 8,
|
||||||
|
callbacks : {
|
||||||
|
label: function (ctx) {
|
||||||
|
return ' ' + ctx.dataset.label + ': ' + ctx.parsed.y + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
y: { beginAtZero: true, max: 100, ticks: { callback: v => v + '%' } }
|
x: {
|
||||||
}
|
grid : { display: false },
|
||||||
}
|
ticks: { font: { size: 11 }, color: '#7F8C8D' }
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Ringkasan SPP (Donut Chart) ──
|
|
||||||
const sppCtx = document.getElementById('sppDonutChart');
|
|
||||||
if (sppCtx) {
|
|
||||||
const sppData = @json($sppBulanIni);
|
|
||||||
new Chart(sppCtx, {
|
|
||||||
type: 'doughnut',
|
|
||||||
data: {
|
|
||||||
labels: ['Lunas', 'Belum Lunas'],
|
|
||||||
datasets: [{
|
|
||||||
data: [sppData.lunas, sppData.belum],
|
|
||||||
backgroundColor: ['#6FBA9D', '#FF8B94'],
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: '#fff',
|
|
||||||
}]
|
|
||||||
},
|
},
|
||||||
options: {
|
y: {
|
||||||
responsive: true,
|
beginAtZero: true,
|
||||||
maintainAspectRatio: false,
|
max : 100,
|
||||||
cutout: '65%',
|
grid : { color: '#E8F7F2' },
|
||||||
plugins: {
|
ticks : {
|
||||||
legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true } },
|
callback: function (v) { return v + '%'; },
|
||||||
|
font : { size: 10 },
|
||||||
|
color : '#7F8C8D'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
// SPP ring chart diinisialisasi dari dalam _ringkasan-spp.blade.php
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
@section('title', 'Tambah Pelanggaran')
|
@section('title', 'Tambah Pelanggaran')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
{{-- Select2 CSS --}}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css">
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2><i class="fas fa-plus-circle"></i> Tambah Pelanggaran</h2>
|
<h2><i class="fas fa-plus-circle"></i> Tambah Pelanggaran</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -87,7 +90,7 @@ class="form-control @error('poin') is-invalid @enderror"
|
||||||
id="kafaroh"
|
id="kafaroh"
|
||||||
class="form-control @error('kafaroh') is-invalid @enderror"
|
class="form-control @error('kafaroh') is-invalid @enderror"
|
||||||
rows="6"
|
rows="6"
|
||||||
placeholder="Contoh: Membaca Al-Qur'an 1 juz, Sholat tahajud 2 rakaat, dll...">{{ old('kafaroh') }}</textarea>
|
placeholder="Contoh: Bangunan, Sholat tasbih, dll...">{{ old('kafaroh') }}</textarea>
|
||||||
@error('kafaroh')
|
@error('kafaroh')
|
||||||
<span class="invalid-feedback">{{ $message }}</span>
|
<span class="invalid-feedback">{{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
|
|
@ -111,7 +114,7 @@ class="form-control @error('kafaroh') is-invalid @enderror"
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group" style="margin-top: 30px;">
|
<div class="btn-group" style="margin-top: 22px;">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="fas fa-save"></i> Simpan
|
<i class="fas fa-save"></i> Simpan
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -121,4 +124,16 @@ class="form-control @error('kafaroh') is-invalid @enderror"
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#id_klasifikasi').select2({
|
||||||
|
placeholder: '-- Pilih Klasifikasi --',
|
||||||
|
allowClear: true,
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
@ -107,7 +107,7 @@ class="form-control @error('kafaroh') is-invalid @enderror"
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group" style="margin-top: 30px;">
|
<div class="btn-group" style="margin-top: 22px;">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="fas fa-save"></i> Update
|
<i class="fas fa-save"></i> Update
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2><i class="fas fa-list-ul"></i> Master Pelanggaran</h2>
|
<h2><i class="fas fa-list-ul"></i> Kategori Pelanggaran</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(session('success'))
|
@if(session('success'))
|
||||||
|
|
@ -20,9 +20,9 @@
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- Filter -->
|
<!-- Filter -->
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<form method="GET" action="{{ route('admin.kategori-pelanggaran.index') }}">
|
<form method="GET" action="{{ route('admin.kategori-pelanggaran.index') }}">
|
||||||
<div style="display: grid; grid-template-columns: 2fr 1fr auto; gap: 15px; align-items: end;">
|
<div style="display: grid; grid-template-columns: 2fr 1fr auto; gap: 11px; align-items: end;">
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
<label for="id_klasifikasi">
|
<label for="id_klasifikasi">
|
||||||
<i class="fas fa-filter form-icon"></i>
|
<i class="fas fa-filter form-icon"></i>
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
|
|
||||||
<!-- Tabel Data -->
|
<!-- Tabel Data -->
|
||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px;">
|
||||||
<h3 style="margin: 0; color: var(--primary-color);">
|
<h3 style="margin: 0; color: var(--primary-color);">
|
||||||
<i class="fas fa-table"></i> Daftar Pelanggaran
|
<i class="fas fa-table"></i> Daftar Pelanggaran
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -73,9 +73,6 @@
|
||||||
<a href="{{ route('admin.klasifikasi-pelanggaran.index') }}" class="btn btn-warning">
|
<a href="{{ route('admin.klasifikasi-pelanggaran.index') }}" class="btn btn-warning">
|
||||||
<i class="fas fa-tags"></i> Klasifikasi Pelanggaran
|
<i class="fas fa-tags"></i> Klasifikasi Pelanggaran
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('admin.pembinaan-sanksi.index') }}" class="btn btn-success">
|
|
||||||
<i class="fas fa-book-open"></i> Pembinaan & Sanksi
|
|
||||||
</a>
|
|
||||||
<a href="{{ route('admin.kategori-pelanggaran.create') }}" class="btn btn-primary">
|
<a href="{{ route('admin.kategori-pelanggaran.create') }}" class="btn btn-primary">
|
||||||
<i class="fas fa-plus-circle"></i> Tambah Pelanggaran
|
<i class="fas fa-plus-circle"></i> Tambah Pelanggaran
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<div style="margin-bottom: 30px;">
|
<div style="margin-bottom: 22px;">
|
||||||
<h3 style="color: var(--primary-color); margin-bottom: 15px;">Informasi Pelanggaran</h3>
|
<h3 style="color: var(--primary-color); margin-bottom: 15px;">Informasi Pelanggaran</h3>
|
||||||
|
|
||||||
<table style="width: 100%; margin-bottom: 20px;">
|
<table style="width: 100%; margin-bottom: 14px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width: 200px; padding: 10px 0; font-weight: 600;">ID Pelanggaran</td>
|
<td style="width: 200px; padding: 10px 0; font-weight: 600;">ID Pelanggaran</td>
|
||||||
<td style="padding: 10px 0;">
|
<td style="padding: 10px 0;">
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
</table>
|
</table>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="btn-group" style="margin-top: 30px;">
|
<div class="btn-group" style="margin-top: 22px;">
|
||||||
<a href="{{ route('admin.kategori-pelanggaran.edit', $kategori) }}" class="btn btn-warning">
|
<a href="{{ route('admin.kategori-pelanggaran.edit', $kategori) }}" class="btn btn-warning">
|
||||||
<i class="fas fa-edit"></i> Edit
|
<i class="fas fa-edit"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
{{-- views/admin/kegiatan/absensi/edit.blade.php --}}
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="page-header">
|
||||||
|
<h2><i class="fas fa-edit"></i> Edit Absensi</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-box" style="max-width: 600px;">
|
||||||
|
<div style="margin-bottom: 20px;">
|
||||||
|
<h3 style="margin: 0 0 8px 0; color: var(--primary-color);">{{ $absensi->kegiatan->nama_kegiatan }}</h3>
|
||||||
|
<p style="margin: 0; color: var(--text-light); font-size: 0.9rem;">
|
||||||
|
<i class="fas fa-tag"></i> {{ $absensi->kegiatan->kategori->nama_kategori ?? '-' }} |
|
||||||
|
<i class="fas fa-calendar"></i> {{ \Carbon\Carbon::parse($absensi->tanggal)->format('d M Y') }} |
|
||||||
|
<i class="fas fa-clock"></i> {{ $absensi->waktu_absen ? date('H:i', strtotime($absensi->waktu_absen)) : '-' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: var(--primary-light); padding: 14px; border-radius: 8px; margin-bottom: 20px;">
|
||||||
|
<table style="width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 120px; padding: 4px 0; color: var(--text-light);"><i class="fas fa-id-badge"></i> ID Santri</td>
|
||||||
|
<td style="padding: 4px 0;"><strong>{{ $absensi->santri->id_santri }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; color: var(--text-light);"><i class="fas fa-user"></i> Nama</td>
|
||||||
|
<td style="padding: 4px 0;"><strong>{{ $absensi->santri->nama_lengkap }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; color: var(--text-light);"><i class="fas fa-chalkboard"></i> Kelas</td>
|
||||||
|
<td style="padding: 4px 0;">{{ $absensi->santri->kelas_name ?? '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; color: var(--text-light);"><i class="fas fa-info-circle"></i> Status Saat Ini</td>
|
||||||
|
<td style="padding: 4px 0;">
|
||||||
|
{!! $absensi->status_badge !!}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ route('admin.absensi-kegiatan.update', $absensi->id) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 20px;">
|
||||||
|
<label style="font-weight: 600; margin-bottom: 10px; display: block;">
|
||||||
|
<i class="fas fa-exchange-alt"></i> Ubah Status Absensi:
|
||||||
|
</label>
|
||||||
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
|
@php
|
||||||
|
$statusOptions = [
|
||||||
|
'Hadir' => ['badge' => 'badge-success', 'icon' => 'fa-check'],
|
||||||
|
'Terlambat' => ['badge' => '', 'icon' => 'fa-clock', 'style' => 'background: #FF9800; color: white;'],
|
||||||
|
'Izin' => ['badge' => 'badge-warning', 'icon' => 'fa-envelope'],
|
||||||
|
'Sakit' => ['badge' => 'badge-info', 'icon' => 'fa-medkit'],
|
||||||
|
'Alpa' => ['badge' => 'badge-danger', 'icon' => 'fa-times'],
|
||||||
|
'Pulang' => ['badge' => '', 'icon' => 'fa-home', 'style' => 'background: #FFF3E0; color: #E65100;'],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
@foreach($statusOptions as $status => $opt)
|
||||||
|
<label style="cursor: pointer; margin: 0;">
|
||||||
|
<input type="radio" name="status" value="{{ $status }}" {{ $absensi->status == $status ? 'checked' : '' }} style="display: none;" class="status-radio">
|
||||||
|
<span class="badge {{ $opt['badge'] ?? '' }} status-option" style="{{ $opt['style'] ?? '' }} padding: 8px 16px; font-size: 0.9rem; border: 2px solid transparent; border-radius: 6px; transition: all 0.2s;">
|
||||||
|
<i class="fas {{ $opt['icon'] }}"></i> {{ $status }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@error('status')
|
||||||
|
<div class="text-danger" style="margin-top: 6px; font-size: 0.85rem;">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group" style="display: flex; gap: 10px;">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Simpan Perubahan
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('admin.absensi-kegiatan.rekap', $absensi->kegiatan_id) }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali ke Rekap
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.status-radio:checked + .status-option {
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(var(--primary-rgb, 76, 110, 245), 0.25);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.status-option:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
@extends('layouts.app')
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
<style>
|
|
||||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
|
||||||
.page-header h2 { margin: 0; font-size: 1.5rem; display: flex; align-items: center; gap: 10px; }
|
|
||||||
.btn-back { padding: 8px 16px; background: #6B7280; color: #fff; border: none; border-radius: 8px; font-size: 0.85rem; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; transition: background 0.2s; }
|
|
||||||
.btn-back:hover { background: #4B5563; color: #fff; }
|
|
||||||
.filter-box { background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); margin-bottom: 20px; }
|
|
||||||
.filter-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
|
|
||||||
.form-group { margin: 0; }
|
|
||||||
.form-group label { display: block; font-size: 0.85rem; margin-bottom: 5px; color: #64748b; font-weight: 500; }
|
|
||||||
.form-control { width: 100%; padding: 8px 12px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 0.85rem; }
|
|
||||||
.btn-filter { background: var(--primary-color); color: #fff; border: none; padding: 9px 16px; border-radius: 8px; cursor: pointer; font-size: 0.85rem; display: inline-flex; align-items: center; gap: 6px; transition: all 0.2s; }
|
|
||||||
.btn-filter:hover { background: #059669; }
|
|
||||||
.btn-reset { background: #6B7280; color: #fff; border: none; padding: 9px 12px; border-radius: 8px; font-size: 0.85rem; display: inline-flex; align-items: center; gap: 6px; text-decoration: none; }
|
|
||||||
.btn-reset:hover { background: #4B5563; color: #fff; }
|
|
||||||
.kelas-badge-container { position: relative; display: inline-block; }
|
|
||||||
.kelas-badge-more { cursor: pointer; position: relative; }
|
|
||||||
.kelas-tooltip { display: none; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 8px; background: #2c3e50; color: white; padding: 10px 12px; border-radius: 8px; font-size: 0.85rem; white-space: nowrap; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
|
||||||
.kelas-tooltip::after { content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 6px solid transparent; border-top-color: #2c3e50; }
|
|
||||||
.kelas-badge-more:hover .kelas-tooltip, .kelas-badge-more.active .kelas-tooltip { display: block; animation: fadeInUp 0.2s ease; }
|
|
||||||
@keyframes fadeInUp { from { opacity: 0; transform: translateX(-50%) translateY(5px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
|
|
||||||
.kelas-tooltip .badge { margin: 2px 3px; font-size: 0.8rem; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<h2><i class="fas fa-clipboard-check"></i> Absensi Kegiatan</h2>
|
|
||||||
<a href="{{ route('admin.kegiatan.index') }}" class="btn-back">
|
|
||||||
<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-exclamation-circle"></i> {{ session('error') }}
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<!-- Filter -->
|
|
||||||
<div class="filter-box">
|
|
||||||
<form method="GET">
|
|
||||||
<div class="filter-grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="search">Cari Kegiatan</label>
|
|
||||||
<input type="text" name="search" id="search" class="form-control" placeholder="Nama atau ID kegiatan..." value="{{ request('search') }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="hari">Hari</label>
|
|
||||||
<select name="hari" id="hari" class="form-control">
|
|
||||||
<option value="">-- Semua Hari --</option>
|
|
||||||
@foreach($hariList as $h)
|
|
||||||
<option value="{{ $h }}" {{ request('hari') == $h ? 'selected' : '' }}>{{ $h }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="kategori_id">Kategori</label>
|
|
||||||
<select name="kategori_id" id="kategori_id" class="form-control">
|
|
||||||
<option value="">-- Semua Kategori --</option>
|
|
||||||
@foreach($kategoris as $k)
|
|
||||||
<option value="{{ $k->kategori_id }}" {{ request('kategori_id') == $k->kategori_id ? 'selected' : '' }}>
|
|
||||||
{{ $k->nama_kategori }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="id_kelas">Kelas</label>
|
|
||||||
<select name="id_kelas" id="id_kelas" class="form-control">
|
|
||||||
<option value="">-- Semua Kelas --</option>
|
|
||||||
@foreach($kelasList->groupBy('kelompok.nama_kelompok') as $kelompokNama => $kelasList_group)
|
|
||||||
<optgroup label="{{ $kelompokNama }}">
|
|
||||||
@foreach($kelasList_group as $kelas)
|
|
||||||
<option value="{{ $kelas->id }}" {{ request('id_kelas') == $kelas->id ? 'selected' : '' }}>
|
|
||||||
{{ $kelas->nama_kelas }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
|
||||||
</optgroup>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: flex-end; gap: 10px;">
|
|
||||||
<button type="submit" class="btn-filter" style="flex: 1;">
|
|
||||||
<i class="fas fa-filter"></i> Filter
|
|
||||||
</button>
|
|
||||||
@if(request()->hasAny(['search', 'hari', 'kategori_id', 'id_kelas']))
|
|
||||||
<a href="{{ route('admin.absensi-kegiatan.index') }}" class="btn-reset">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-box">
|
|
||||||
|
|
||||||
@if($kegiatans->count() > 0)
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 50px;">No</th>
|
|
||||||
<th style="width: 100px;">Hari</th>
|
|
||||||
<th style="width: 120px;">Waktu</th>
|
|
||||||
<th>Nama Kegiatan</th>
|
|
||||||
<th style="width: 150px;">Kategori</th>
|
|
||||||
<th style="width: 150px;">Kelas</th>
|
|
||||||
<th style="width: 250px; text-align: center;">Aksi</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach($kegiatans as $index => $kegiatan)
|
|
||||||
<tr>
|
|
||||||
<td>{{ $kegiatans->firstItem() + $index }}</td>
|
|
||||||
<td><span class="badge badge-primary">{{ $kegiatan->hari }}</span></td>
|
|
||||||
<td>{{ date('H:i', strtotime($kegiatan->waktu_mulai)) }} - {{ date('H:i', strtotime($kegiatan->waktu_selesai)) }}</td>
|
|
||||||
<td><strong>{{ $kegiatan->nama_kegiatan }}</strong></td>
|
|
||||||
<td>{{ $kegiatan->kategori->nama_kategori }}</td>
|
|
||||||
<td>
|
|
||||||
@if($kegiatan->kelasKegiatan->isEmpty())
|
|
||||||
<span class="badge badge-info">Umum</span>
|
|
||||||
@else
|
|
||||||
@php
|
|
||||||
$firstKelas = $kegiatan->kelasKegiatan->first();
|
|
||||||
$remainingCount = $kegiatan->kelasKegiatan->count() - 1;
|
|
||||||
@endphp
|
|
||||||
<span class="badge badge-secondary">{{ $firstKelas->nama_kelas }}</span>
|
|
||||||
@if($remainingCount > 0)
|
|
||||||
<span class="kelas-badge-container">
|
|
||||||
<span class="badge badge-primary kelas-badge-more" onclick="this.classList.toggle('active')">
|
|
||||||
+{{ $remainingCount }} lainnya
|
|
||||||
<div class="kelas-tooltip">
|
|
||||||
<strong style="display: block; margin-bottom: 5px; border-bottom: 1px solid rgba(255,255,255,0.3); padding-bottom: 5px;">Semua Kelas:</strong>
|
|
||||||
@foreach($kegiatan->kelasKegiatan as $kelas)
|
|
||||||
<span class="badge badge-light">{{ $kelas->nama_kelas }}</span>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
@endif
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<a href="{{ route('admin.absensi-kegiatan.input', $kegiatan->kegiatan_id) }}" class="btn btn-sm btn-success" title="Input Absensi">
|
|
||||||
<i class="fas fa-clipboard-check"></i> Input
|
|
||||||
</a>
|
|
||||||
<a href="{{ route('admin.absensi-kegiatan.rekap', $kegiatan->kegiatan_id) }}" class="btn btn-sm btn-primary" title="Rekap Absensi">
|
|
||||||
<i class="fas fa-chart-bar"></i> Rekap
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@endforeach
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
{{ $kegiatans->links() }}
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-calendar-times"></i>
|
|
||||||
<h3>Belum Ada Kegiatan</h3>
|
|
||||||
<p>Silakan tambahkan kegiatan terlebih dahulu.</p>
|
|
||||||
<a href="{{ route('admin.kegiatan.create') }}" class="btn btn-success">
|
|
||||||
<i class="fas fa-plus"></i> Tambah Kegiatan
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Close tooltip when clicking outside
|
|
||||||
document.addEventListener('click', function(event) {
|
|
||||||
if (!event.target.closest('.kelas-badge-more')) {
|
|
||||||
document.querySelectorAll('.kelas-badge-more.active').forEach(badge => {
|
|
||||||
badge.classList.remove('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prevent table row click when clicking badge
|
|
||||||
document.querySelectorAll('.kelas-badge-more').forEach(badge => {
|
|
||||||
badge.addEventListener('click', function(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@endsection
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{-- views/admin/kegiatan/absensi/input.blade.php --}}
|
||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|
@ -5,8 +6,8 @@
|
||||||
<h2><i class="fas fa-clipboard-check"></i> Input Absensi: {{ $kegiatan->nama_kegiatan }}</h2>
|
<h2><i class="fas fa-clipboard-check"></i> Input Absensi: {{ $kegiatan->nama_kegiatan }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-box" style="margin-bottom: 20px;">
|
<div class="content-box" style="margin-bottom: 14px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 11px;">
|
||||||
<div>
|
<div>
|
||||||
<h3 style="margin: 0; color: var(--primary-color);">{{ $kegiatan->nama_kegiatan }}</h3>
|
<h3 style="margin: 0; color: var(--primary-color);">{{ $kegiatan->nama_kegiatan }}</h3>
|
||||||
<p style="margin: 5px 0 0 0; color: var(--text-light);">
|
<p style="margin: 5px 0 0 0; color: var(--text-light);">
|
||||||
|
|
@ -19,7 +20,7 @@
|
||||||
<button type="button" id="btnModeManual" class="btn btn-primary" onclick="setMode('manual')">
|
<button type="button" id="btnModeManual" class="btn btn-primary" onclick="setMode('manual')">
|
||||||
<i class="fas fa-hand-pointer"></i> Mode Manual
|
<i class="fas fa-hand-pointer"></i> Mode Manual
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="btnModeRfid" class="btn btn-success" onclick="setMode('rfid')">
|
<button type="button" id="btnModeRfid" class="btn btn-secondary" onclick="setMode('rfid')">
|
||||||
<i class="fas fa-id-card"></i> Mode RFID
|
<i class="fas fa-id-card"></i> Mode RFID
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -27,7 +28,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Info Kelas Kegiatan --}}
|
{{-- Info Kelas Kegiatan --}}
|
||||||
<div class="info-box" style="margin-bottom: 20px; border-left: 4px solid var(--primary-color);">
|
<div class="info-box" style="margin-bottom: 14px; border-left: 4px solid var(--primary-color);">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
@if($kegiatanInfo['is_umum'])
|
@if($kegiatanInfo['is_umum'])
|
||||||
<strong>Kegiatan Umum</strong> - Diikuti oleh semua santri aktif ({{ $santris->count() }} santri)
|
<strong>Kegiatan Umum</strong> - Diikuti oleh semua santri aktif ({{ $santris->count() }} santri)
|
||||||
|
|
@ -38,99 +39,183 @@
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Sudah ada data absensi? --}}
|
||||||
|
@php
|
||||||
|
$sudahAdaData = count($absensiData) > 0;
|
||||||
|
@endphp
|
||||||
|
@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).
|
||||||
|
Anda dapat mengubah status absensi lalu klik Simpan.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<!-- MODE MANUAL -->
|
<!-- MODE MANUAL -->
|
||||||
<div id="modeManual" class="content-box">
|
<div id="modeManual" class="content-box">
|
||||||
<form action="{{ route('admin.absensi-kegiatan.simpan') }}" method="POST">
|
<form action="{{ route('admin.absensi-kegiatan.simpan') }}" method="POST">
|
||||||
@csrf
|
@csrf
|
||||||
<input type="hidden" name="kegiatan_id" value="{{ $kegiatan->kegiatan_id }}">
|
<input type="hidden" name="kegiatan_id" value="{{ $kegiatan->kegiatan_id }}">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="filter-form-inline" style="margin-bottom: 14px; gap: 12px;">
|
||||||
<label for="tanggal">
|
<div class="filter-form-inline" style="gap: 8px;">
|
||||||
<i class="fas fa-calendar form-icon"></i>
|
<label style="font-weight: 600; white-space: nowrap; margin: 0;">
|
||||||
Tanggal Absensi
|
<i class="fas fa-calendar"></i> Tanggal:
|
||||||
</label>
|
</label>
|
||||||
<input type="date" name="tanggal" id="tanggal" class="form-control" value="{{ $tanggal }}" required>
|
<input type="date" name="tanggal" id="tanggal" class="form-control"
|
||||||
|
value="{{ $tanggal }}" required style="max-width: 170px;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="filter-form-inline" style="gap: 8px;">
|
||||||
<p><i class="fas fa-info-circle"></i> Pilih status absensi untuk setiap santri. Jika tidak dipilih, akan dianggap <strong>Alpa</strong>.</p>
|
<label style="font-weight: 600; white-space: nowrap; margin: 0;">
|
||||||
</div>
|
<i class="fas fa-school"></i> Pilih Kelas:
|
||||||
|
|
||||||
{{-- Filter Kelas (Manual Mode) --}}
|
|
||||||
@if(!$kegiatanInfo['is_umum'] && $kegiatanInfo['jumlah_kelas'] > 1)
|
|
||||||
<div class="form-group" style="max-width: 300px;">
|
|
||||||
<label for="filterKelas">
|
|
||||||
<i class="fas fa-filter form-icon"></i>
|
|
||||||
Filter Kelas
|
|
||||||
</label>
|
</label>
|
||||||
<select id="filterKelas" class="form-control">
|
<select id="kelasFilter" class="form-control" onchange="filterKelas(this.value)" style="max-width: 220px;">
|
||||||
<option value="">Semua Kelas</option>
|
<option value="semua">-- Tampilkan Semua Kelas --</option>
|
||||||
@foreach($kegiatan->kelasKegiatan as $kelas)
|
@foreach($santriGrouped as $kelasNama => $santriKelas)
|
||||||
<option value="{{ $kelas->nama_kelas }}">{{ $kelas->nama_kelas }}</option>
|
<option value="{{ $kelasNama }}">{{ $kelasNama }} ({{ $santriKelas->count() }} santri)</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
|
|
||||||
<table class="data-table">
|
<div style="margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
|
<button type="button" class="btn btn-sm btn-info" onclick="setAllStatus('Hadir')">
|
||||||
|
<i class="fas fa-check-double"></i> Semua Hadir
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm" style="background: #FF9800; color: white;" onclick="setAllStatus('Terlambat')">
|
||||||
|
<i class="fas fa-clock"></i> Semua Terlambat
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="setAllStatus('Alpa')">
|
||||||
|
<i class="fas fa-times"></i> Semua Alpa
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" onclick="clearAllStatus()">
|
||||||
|
<i class="fas fa-eraser"></i> Kosongkan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box" style="margin-bottom: 14px;">
|
||||||
|
<p style="margin: 0;"><i class="fas fa-info-circle"></i> Pilih kelas terlebih dahulu untuk menampilkan daftar santri. Santri tanpa pilihan status akan <strong>dilewati</strong>. Santri yang <strong>sedang pulang</strong> otomatis ditandai.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Group santri by kelas --}}
|
||||||
|
@foreach($santriGrouped as $kelasNama => $santriKelas)
|
||||||
|
@php
|
||||||
|
$hadirCount = 0;
|
||||||
|
$totalKelas = $santriKelas->count();
|
||||||
|
foreach ($santriKelas as $s) {
|
||||||
|
$st = $absensiData[$s->id_santri] ?? null;
|
||||||
|
if ($st === 'Hadir') $hadirCount++;
|
||||||
|
}
|
||||||
|
$sudahInputKelas = false;
|
||||||
|
foreach ($santriKelas as $s) {
|
||||||
|
if (isset($absensiData[$s->id_santri])) { $sudahInputKelas = true; break; }
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
<div class="kelas-group" data-kelas="{{ $kelasNama }}" style="margin-bottom: 20px; display: none;">
|
||||||
|
<div style="background: var(--primary-light); padding: 10px 14px; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h4 style="margin: 0; color: var(--primary-color); font-size: 0.95rem;">
|
||||||
|
<i class="fas fa-users"></i> {{ $kelasNama }}
|
||||||
|
</h4>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
@if($sudahInputKelas)
|
||||||
|
<span class="badge badge-info"><i class="fas fa-edit"></i> {{ $hadirCount }}/{{ $totalKelas }} hadir</span>
|
||||||
|
@endif
|
||||||
|
<span class="badge badge-primary">{{ $totalKelas }} santri</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="data-table" style="margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 50px;">No</th>
|
<th style="width: 50px;">No</th>
|
||||||
<th style="width: 100px;">ID Santri</th>
|
<th style="width: 100px;">ID Santri</th>
|
||||||
<th>Nama Santri</th>
|
<th>Nama Santri</th>
|
||||||
<th style="width: 100px;">Kelas</th>
|
<th style="width: 420px; text-align: center;">Status</th>
|
||||||
<th style="width: 300px; text-align: center;">Status</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach($santris as $index => $santri)
|
@foreach($santriKelas as $santri)
|
||||||
<tr>
|
|
||||||
<td>{{ $index + 1 }}</td>
|
|
||||||
<td><strong>{{ $santri->id_santri }}</strong></td>
|
|
||||||
<td>{{ $santri->nama_lengkap }}</td>
|
|
||||||
<td>
|
|
||||||
@php
|
@php
|
||||||
$kelasName = $santri->kelas_name ?? $santri->kelas ?? '-';
|
$isPulang = in_array($santri->id_santri, $santriSedangPulang ?? []);
|
||||||
|
$currentStatus = $absensiData[$santri->id_santri] ?? ($isPulang ? 'Pulang' : '');
|
||||||
@endphp
|
@endphp
|
||||||
<span class="badge badge-secondary">{{ $kelasName }}</span>
|
<tr @if($isPulang) style="background: #FFF8E1; opacity: 0.85;" @endif>
|
||||||
|
<td>{{ $loop->iteration }}</td>
|
||||||
|
<td><strong>{{ $santri->id_santri }}</strong></td>
|
||||||
|
<td>
|
||||||
|
{{ $santri->nama_lengkap }}
|
||||||
|
@if($isPulang)
|
||||||
|
<span class="badge" style="background: #FFF3E0; color: #E65100; font-size: 0.75rem; margin-left: 6px;">
|
||||||
|
<i class="fas fa-home"></i> Sedang Pulang
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if(isset($absensiData[$santri->id_santri]))
|
||||||
|
<span class="badge badge-secondary" style="font-size: 0.7rem; margin-left: 4px;">
|
||||||
|
<i class="fas fa-edit"></i> Sudah input
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@php
|
@if($isPulang)
|
||||||
$currentStatus = $absensiData[$santri->id_santri] ?? 'Alpa';
|
<input type="hidden" name="absensi[{{ $santri->id_santri }}]" value="Pulang" class="absensi-input">
|
||||||
@endphp
|
<span class="badge" style="background: #FFF3E0; color: #E65100; padding: 6px 14px; font-size: 0.85rem;">
|
||||||
<div style="display: flex; gap: 8px; justify-content: center;">
|
<i class="fas fa-home"></i> Pulang
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<div style="display: flex; gap: 5px; justify-content: center; flex-wrap: wrap;">
|
||||||
<label style="margin: 0; cursor: pointer;">
|
<label style="margin: 0; cursor: pointer;">
|
||||||
<input type="radio" name="absensi[{{ $santri->id_santri }}]" value="Hadir"
|
<input type="radio" name="absensi[{{ $santri->id_santri }}]" value="Hadir"
|
||||||
{{ $currentStatus == 'Hadir' ? 'checked' : '' }} required>
|
{{ $currentStatus == 'Hadir' ? 'checked' : '' }} class="absensi-radio absensi-input">
|
||||||
<span class="badge badge-success">Hadir</span>
|
<span class="badge badge-success">Hadir</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label style="margin: 0; cursor: pointer;">
|
||||||
|
<input type="radio" name="absensi[{{ $santri->id_santri }}]" value="Terlambat"
|
||||||
|
{{ $currentStatus == 'Terlambat' ? 'checked' : '' }} class="absensi-radio absensi-input">
|
||||||
|
<span class="badge" style="background: #FF9800; color: white;">Terlambat</span>
|
||||||
|
</label>
|
||||||
<label style="margin: 0; cursor: pointer;">
|
<label style="margin: 0; cursor: pointer;">
|
||||||
<input type="radio" name="absensi[{{ $santri->id_santri }}]" value="Izin"
|
<input type="radio" name="absensi[{{ $santri->id_santri }}]" value="Izin"
|
||||||
{{ $currentStatus == 'Izin' ? 'checked' : '' }}>
|
{{ $currentStatus == 'Izin' ? 'checked' : '' }} class="absensi-radio absensi-input">
|
||||||
<span class="badge badge-warning">Izin</span>
|
<span class="badge badge-warning">Izin</span>
|
||||||
</label>
|
</label>
|
||||||
<label style="margin: 0; cursor: pointer;">
|
<label style="margin: 0; cursor: pointer;">
|
||||||
<input type="radio" name="absensi[{{ $santri->id_santri }}]" value="Sakit"
|
<input type="radio" name="absensi[{{ $santri->id_santri }}]" value="Sakit"
|
||||||
{{ $currentStatus == 'Sakit' ? 'checked' : '' }}>
|
{{ $currentStatus == 'Sakit' ? 'checked' : '' }} class="absensi-radio absensi-input">
|
||||||
<span class="badge badge-info">Sakit</span>
|
<span class="badge badge-info">Sakit</span>
|
||||||
</label>
|
</label>
|
||||||
<label style="margin: 0; cursor: pointer;">
|
<label style="margin: 0; cursor: pointer;">
|
||||||
<input type="radio" name="absensi[{{ $santri->id_santri }}]" value="Alpa"
|
<input type="radio" name="absensi[{{ $santri->id_santri }}]" value="Alpa"
|
||||||
{{ $currentStatus == 'Alpa' ? 'checked' : '' }}>
|
{{ $currentStatus == 'Alpa' ? 'checked' : '' }} class="absensi-radio absensi-input">
|
||||||
<span class="badge badge-danger">Alpa</span>
|
<span class="badge badge-danger">Alpa</span>
|
||||||
</label>
|
</label>
|
||||||
|
@if($currentStatus)
|
||||||
|
<label style="margin: 0; cursor: pointer;" title="Hapus pilihan">
|
||||||
|
<button type="button" class="btn btn-sm" style="padding: 2px 8px; font-size: 0.75rem; background: #f1f1f1;" onclick="clearRadio('{{ $santri->id_santri }}')">
|
||||||
|
<i class="fas fa-undo"></i>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
<div class="btn-group" style="margin-top: 20px;">
|
<div id="noKelasSelected" class="empty-state" style="padding: 40px 20px;">
|
||||||
|
<i class="fas fa-hand-pointer"></i>
|
||||||
|
<h3>Pilih Kelas Terlebih Dahulu</h3>
|
||||||
|
<p>Silakan pilih kelas pada dropdown di atas untuk menampilkan daftar santri yang akan diabsen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group" style="margin-top: 14px;">
|
||||||
<button type="submit" class="btn btn-success">
|
<button type="submit" class="btn btn-success">
|
||||||
<i class="fas fa-save"></i> Simpan Absensi
|
<i class="fas fa-save"></i> {{ $sudahAdaData ? 'Update Absensi' : 'Simpan Absensi' }}
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ route('admin.absensi-kegiatan.index') }}" class="btn btn-secondary">
|
<a href="{{ route('admin.kegiatan.index') }}" class="btn btn-secondary">
|
||||||
<i class="fas fa-arrow-left"></i> Kembali
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -151,7 +236,7 @@
|
||||||
<p><i class="fas fa-id-card"></i> Tempelkan kartu RFID santri ke reader. Absensi akan otomatis tersimpan sebagai <strong>Hadir</strong>.</p>
|
<p><i class="fas fa-id-card"></i> Tempelkan kartu RFID santri ke reader. Absensi akan otomatis tersimpan sebagai <strong>Hadir</strong>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="background: linear-gradient(135deg, var(--primary-light) 0%, #D4F1E3 100%); padding: 30px; border-radius: var(--border-radius); text-align: center; margin-bottom: 20px;">
|
<div style="background: linear-gradient(135deg, var(--primary-light) 0%, #D4F1E3 100%); padding: 22px; border-radius: var(--border-radius); text-align: center; margin-bottom: 14px;">
|
||||||
<div id="rfidStatus" style="font-size: 1.5rem; font-weight: 600; color: var(--primary-color); margin-bottom: 15px;">
|
<div id="rfidStatus" style="font-size: 1.5rem; font-weight: 600; color: var(--primary-color); margin-bottom: 15px;">
|
||||||
<i class="fas fa-wifi"></i> Siap Scan RFID
|
<i class="fas fa-wifi"></i> Siap Scan RFID
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -167,47 +252,116 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group" style="margin-top: 20px;">
|
<div class="btn-group" style="margin-top: 14px;">
|
||||||
<button type="button" class="btn btn-warning" onclick="clearLog()">
|
<button type="button" class="btn btn-warning" onclick="clearLog()">
|
||||||
<i class="fas fa-trash"></i> Bersihkan Log
|
<i class="fas fa-trash"></i> Bersihkan Log
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ route('admin.absensi-kegiatan.index') }}" class="btn btn-secondary">
|
<a href="{{ route('admin.kegiatan.index') }}" class="btn btn-secondary">
|
||||||
<i class="fas fa-arrow-left"></i> Kembali
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentMode = 'manual';
|
// -- Kelas Filter --
|
||||||
const kegiatanId = '{{ $kegiatan->kegiatan_id }}';
|
function filterKelas(value) {
|
||||||
|
var groups = document.querySelectorAll('.kelas-group');
|
||||||
|
var emptyMsg = document.getElementById('noKelasSelected');
|
||||||
|
|
||||||
|
if (value === 'semua') {
|
||||||
|
for (var i = 0; i < groups.length; i++) {
|
||||||
|
groups[i].style.display = 'block';
|
||||||
|
toggleGroupInputs(groups[i], true);
|
||||||
|
}
|
||||||
|
emptyMsg.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
var found = false;
|
||||||
|
for (var i = 0; i < groups.length; i++) {
|
||||||
|
if (groups[i].getAttribute('data-kelas') === value) {
|
||||||
|
groups[i].style.display = 'block';
|
||||||
|
toggleGroupInputs(groups[i], true);
|
||||||
|
found = true;
|
||||||
|
} else {
|
||||||
|
groups[i].style.display = 'none';
|
||||||
|
toggleGroupInputs(groups[i], false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emptyMsg.style.display = found ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable/disable all inputs in a kelas group so hidden groups don't submit
|
||||||
|
function toggleGroupInputs(group, enabled) {
|
||||||
|
var inputs = group.querySelectorAll('.absensi-input');
|
||||||
|
for (var i = 0; i < inputs.length; i++) {
|
||||||
|
inputs[i].disabled = !enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Set All Status (for visible groups only) --
|
||||||
|
function setAllStatus(status) {
|
||||||
|
var groups = document.querySelectorAll('.kelas-group');
|
||||||
|
for (var i = 0; i < groups.length; i++) {
|
||||||
|
if (groups[i].style.display !== 'none') {
|
||||||
|
var radios = groups[i].querySelectorAll('input.absensi-radio[value="' + status + '"]');
|
||||||
|
for (var j = 0; j < radios.length; j++) {
|
||||||
|
radios[j].checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Clear radio selection for a specific santri --
|
||||||
|
function clearRadio(santriId) {
|
||||||
|
var radios = document.querySelectorAll('input[name="absensi[' + santriId + ']"]');
|
||||||
|
for (var i = 0; i < radios.length; i++) {
|
||||||
|
radios[i].checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Clear all selections in visible groups --
|
||||||
|
function clearAllStatus() {
|
||||||
|
var groups = document.querySelectorAll('.kelas-group');
|
||||||
|
for (var i = 0; i < groups.length; i++) {
|
||||||
|
if (groups[i].style.display !== 'none') {
|
||||||
|
var radios = groups[i].querySelectorAll('input.absensi-radio');
|
||||||
|
for (var j = 0; j < radios.length; j++) {
|
||||||
|
radios[j].checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Mode Switch --
|
||||||
|
var currentMode = 'manual';
|
||||||
|
var kegiatanId = '{{ $kegiatan->kegiatan_id }}';
|
||||||
|
|
||||||
function setMode(mode) {
|
function setMode(mode) {
|
||||||
currentMode = mode;
|
currentMode = mode;
|
||||||
|
var modeManualEl = document.getElementById('modeManual');
|
||||||
|
var modeRfidEl = document.getElementById('modeRfid');
|
||||||
|
var btnManual = document.getElementById('btnModeManual');
|
||||||
|
var btnRfid = document.getElementById('btnModeRfid');
|
||||||
|
|
||||||
if (mode === 'manual') {
|
if (mode === 'manual') {
|
||||||
document.getElementById('modeManual').style.display = 'block';
|
modeManualEl.style.display = 'block';
|
||||||
document.getElementById('modeRfid').style.display = 'none';
|
modeRfidEl.style.display = 'none';
|
||||||
document.getElementById('btnModeManual').classList.add('btn-primary');
|
btnManual.className = 'btn btn-primary';
|
||||||
document.getElementById('btnModeManual').classList.remove('btn-secondary');
|
btnRfid.className = 'btn btn-secondary';
|
||||||
document.getElementById('btnModeRfid').classList.remove('btn-success');
|
|
||||||
document.getElementById('btnModeRfid').classList.add('btn-secondary');
|
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('modeManual').style.display = 'none';
|
modeManualEl.style.display = 'none';
|
||||||
document.getElementById('modeRfid').style.display = 'block';
|
modeRfidEl.style.display = 'block';
|
||||||
document.getElementById('btnModeManual').classList.remove('btn-primary');
|
btnManual.className = 'btn btn-secondary';
|
||||||
document.getElementById('btnModeManual').classList.add('btn-secondary');
|
btnRfid.className = 'btn btn-success';
|
||||||
document.getElementById('btnModeRfid').classList.add('btn-success');
|
|
||||||
document.getElementById('btnModeRfid').classList.remove('btn-secondary');
|
|
||||||
document.getElementById('rfidInput').focus();
|
document.getElementById('rfidInput').focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFID Scanner Handler
|
// -- RFID Scanner --
|
||||||
document.getElementById('rfidInput').addEventListener('keypress', function(e) {
|
document.getElementById('rfidInput').addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const rfidUid = this.value.trim();
|
var rfidUid = this.value.trim();
|
||||||
|
|
||||||
if (rfidUid) {
|
if (rfidUid) {
|
||||||
scanRfid(rfidUid);
|
scanRfid(rfidUid);
|
||||||
this.value = '';
|
this.value = '';
|
||||||
|
|
@ -216,8 +370,8 @@ function setMode(mode) {
|
||||||
});
|
});
|
||||||
|
|
||||||
function scanRfid(rfidUid) {
|
function scanRfid(rfidUid) {
|
||||||
const tanggal = document.getElementById('tanggalRfid').value;
|
var tanggal = document.getElementById('tanggalRfid').value;
|
||||||
const statusEl = document.getElementById('rfidStatus');
|
var statusEl = document.getElementById('rfidStatus');
|
||||||
|
|
||||||
statusEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Memproses...';
|
statusEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Memproses...';
|
||||||
statusEl.style.color = 'var(--warning-color)';
|
statusEl.style.color = 'var(--warning-color)';
|
||||||
|
|
@ -234,30 +388,28 @@ function scanRfid(rfidUid) {
|
||||||
tanggal: tanggal
|
tanggal: tanggal
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(function(response) { return response.json(); })
|
||||||
.then(data => {
|
.then(function(data) {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
statusEl.innerHTML = '<i class="fas fa-check-circle"></i> ' + data.message;
|
statusEl.innerHTML = '<i class="fas fa-check-circle"></i> ' + data.message;
|
||||||
statusEl.style.color = 'var(--success-color)';
|
statusEl.style.color = 'var(--success-color)';
|
||||||
addLogEntry(data.data, 'success');
|
addLogEntry(data.data, 'success');
|
||||||
playSound('success');
|
|
||||||
} else {
|
} else {
|
||||||
statusEl.innerHTML = '<i class="fas fa-exclamation-circle"></i> ' + data.message;
|
statusEl.innerHTML = '<i class="fas fa-exclamation-circle"></i> ' + data.message;
|
||||||
statusEl.style.color = 'var(--danger-color)';
|
statusEl.style.color = 'var(--danger-color)';
|
||||||
playSound('error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(function() {
|
||||||
statusEl.innerHTML = '<i class="fas fa-wifi"></i> Siap Scan RFID';
|
statusEl.innerHTML = '<i class="fas fa-wifi"></i> Siap Scan RFID';
|
||||||
statusEl.style.color = 'var(--primary-color)';
|
statusEl.style.color = 'var(--primary-color)';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(function(error) {
|
||||||
statusEl.innerHTML = '<i class="fas fa-times-circle"></i> Koneksi error';
|
statusEl.innerHTML = '<i class="fas fa-times-circle"></i> Koneksi error';
|
||||||
statusEl.style.color = 'var(--danger-color)';
|
statusEl.style.color = 'var(--danger-color)';
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(function() {
|
||||||
statusEl.innerHTML = '<i class="fas fa-wifi"></i> Siap Scan RFID';
|
statusEl.innerHTML = '<i class="fas fa-wifi"></i> Siap Scan RFID';
|
||||||
statusEl.style.color = 'var(--primary-color)';
|
statusEl.style.color = 'var(--primary-color)';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
@ -265,28 +417,21 @@ function scanRfid(rfidUid) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLogEntry(data, type) {
|
function addLogEntry(data, type) {
|
||||||
const logContent = document.getElementById('rfidLogContent');
|
var logContent = document.getElementById('rfidLogContent');
|
||||||
|
|
||||||
if (logContent.querySelector('p')) {
|
if (logContent.querySelector('p')) {
|
||||||
logContent.innerHTML = '';
|
logContent.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = document.createElement('div');
|
var entry = document.createElement('div');
|
||||||
entry.style.cssText = 'padding: 12px; margin-bottom: 10px; border-radius: 8px; background: ' +
|
entry.style.cssText = 'padding: 12px; margin-bottom: 10px; border-radius: 8px; background: ' +
|
||||||
(type === 'success' ? 'linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%)' : 'linear-gradient(135deg, #FFE8EA 0%, #FFD5D8 100%)') +
|
(type === 'success' ? 'linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%)' : 'linear-gradient(135deg, #FFE8EA 0%, #FFD5D8 100%)') +
|
||||||
'; border-left: 4px solid ' + (type === 'success' ? 'var(--success-color)' : 'var(--danger-color)');
|
'; border-left: 4px solid ' + (type === 'success' ? 'var(--success-color)' : 'var(--danger-color)');
|
||||||
|
|
||||||
entry.innerHTML = `
|
entry.innerHTML = '<div style="display: flex; justify-content: space-between; align-items: center;">' +
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
'<div><strong>' + data.nama + '</strong> (' + data.id_santri + ')' +
|
||||||
<div>
|
'<div style="font-size: 0.85rem; color: var(--text-light); margin-top: 3px;">Kelas: ' + data.kelas + ' | Waktu: ' + data.waktu + '</div></div>' +
|
||||||
<strong>${data.nama}</strong> (${data.id_santri})
|
'<span class="badge badge-success"><i class="fas fa-check"></i> Hadir</span></div>';
|
||||||
<div style="font-size: 0.85rem; color: var(--text-light); margin-top: 3px;">
|
|
||||||
Kelas: ${data.kelas} | Waktu: ${data.waktu}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="badge badge-success"><i class="fas fa-check"></i> Hadir</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
logContent.insertBefore(entry, logContent.firstChild);
|
logContent.insertBefore(entry, logContent.firstChild);
|
||||||
}
|
}
|
||||||
|
|
@ -297,38 +442,14 @@ function clearLog() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function playSound(type) {
|
// -- Auto-focus RFID input --
|
||||||
// Bisa ditambahkan audio feedback
|
setInterval(function() {
|
||||||
const audio = new Audio(type === 'success' ? '/sounds/success.mp3' : '/sounds/error.mp3');
|
if (currentMode === 'rfid') {
|
||||||
audio.play().catch(() => {}); // Ignore errors
|
var rfidInput = document.getElementById('rfidInput');
|
||||||
}
|
if (document.activeElement !== rfidInput) {
|
||||||
|
rfidInput.focus();
|
||||||
// Auto-focus kembali ke input RFID jika kehilangan fokus
|
}
|
||||||
setInterval(() => {
|
|
||||||
if (currentMode === 'rfid' && document.activeElement !== document.getElementById('rfidInput')) {
|
|
||||||
document.getElementById('rfidInput').focus();
|
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Filter Kelas functionality (Manual Mode)
|
|
||||||
const filterKelasEl = document.getElementById('filterKelas');
|
|
||||||
if (filterKelasEl) {
|
|
||||||
filterKelasEl.addEventListener('change', function() {
|
|
||||||
const selectedKelas = this.value.toLowerCase();
|
|
||||||
const rows = document.querySelectorAll('#modeManual tbody tr');
|
|
||||||
|
|
||||||
rows.forEach(row => {
|
|
||||||
const kelasCell = row.querySelector('td:nth-child(4)'); // Kolom kelas
|
|
||||||
if (kelasCell) {
|
|
||||||
const kelasText = kelasCell.textContent.toLowerCase();
|
|
||||||
if (!selectedKelas || kelasText.includes(selectedKelas)) {
|
|
||||||
row.style.display = '';
|
|
||||||
} else {
|
|
||||||
row.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{-- views/admin/kegiatan/absensi/rekap.blade.php --}}
|
||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|
@ -11,6 +12,11 @@
|
||||||
<div class="card-value">{{ $stats['Hadir'] ?? 0 }}</div>
|
<div class="card-value">{{ $stats['Hadir'] ?? 0 }}</div>
|
||||||
<i class="fas fa-check-circle card-icon"></i>
|
<i class="fas fa-check-circle card-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card" style="border-top: 3px solid #FF9800;">
|
||||||
|
<h3>Terlambat</h3>
|
||||||
|
<div class="card-value">{{ $stats['Terlambat'] ?? 0 }}</div>
|
||||||
|
<i class="fas fa-clock card-icon" style="color: #FF9800;"></i>
|
||||||
|
</div>
|
||||||
<div class="card card-warning">
|
<div class="card card-warning">
|
||||||
<h3>Izin</h3>
|
<h3>Izin</h3>
|
||||||
<div class="card-value">{{ $stats['Izin'] ?? 0 }}</div>
|
<div class="card-value">{{ $stats['Izin'] ?? 0 }}</div>
|
||||||
|
|
@ -29,28 +35,46 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 14px;">
|
||||||
<form method="GET" class="filter-form-inline">
|
<form method="GET" class="filter-form-inline">
|
||||||
<input type="date" name="tanggal" class="form-control" value="{{ request('tanggal') }}">
|
<input type="date" name="tanggal" class="form-control" value="{{ request('tanggal') }}">
|
||||||
<input type="month" name="bulan" class="form-control" value="{{ request('bulan') }}" placeholder="Pilih Bulan">
|
<input type="month" name="bulan" class="form-control" value="{{ request('bulan') }}" placeholder="Pilih Bulan">
|
||||||
|
|
||||||
|
<select name="kelas_id" class="form-control" style="max-width: 200px;">
|
||||||
|
<option value="">Semua Kelas</option>
|
||||||
|
@foreach($kelasFilterList as $kelas)
|
||||||
|
<option value="{{ $kelas->id }}" {{ request('kelas_id') == $kelas->id ? 'selected' : '' }}>
|
||||||
|
{{ $kelas->nama_kelas }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="fas fa-filter"></i> Filter
|
<i class="fas fa-filter"></i> Filter
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if(request()->hasAny(['tanggal', 'bulan']))
|
@if(request()->hasAny(['tanggal', 'bulan', 'kelas_id']))
|
||||||
<a href="{{ route('admin.absensi-kegiatan.rekap', $kegiatan->kegiatan_id) }}" class="btn btn-secondary">
|
<a href="{{ route('admin.absensi-kegiatan.rekap', $kegiatan->kegiatan_id) }}" class="btn btn-secondary">
|
||||||
<i class="fas fa-times"></i> Reset
|
<i class="fas fa-times"></i> Reset
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<a href="{{ route('admin.absensi-kegiatan.index') }}" class="btn btn-secondary" style="margin-left: auto;">
|
<a href="{{ route('admin.kegiatan.index') }}" class="btn btn-secondary" style="margin-left: auto;">
|
||||||
<i class="fas fa-arrow-left"></i> Kembali
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
</a>
|
</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($absensis->count() > 0)
|
@if($absensis->count() > 0)
|
||||||
|
@foreach($absensiPerKelas as $namaKelas => $kelasAbsensis)
|
||||||
|
<div class="content-box" style="margin-bottom: 18px;">
|
||||||
|
<h4 style="margin: 0 0 12px; color: var(--primary-color);">
|
||||||
|
<i class="fas fa-school"></i> Kelas: {{ $namaKelas }}
|
||||||
|
<span class="badge badge-secondary" style="font-size: 0.8rem; margin-left: 6px;">
|
||||||
|
{{ $kelasAbsensis->count() }} data
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -58,25 +82,19 @@
|
||||||
<th style="width: 100px;">Tanggal</th>
|
<th style="width: 100px;">Tanggal</th>
|
||||||
<th style="width: 100px;">ID Santri</th>
|
<th style="width: 100px;">ID Santri</th>
|
||||||
<th>Nama Santri</th>
|
<th>Nama Santri</th>
|
||||||
<th style="width: 80px;">Kelas</th>
|
|
||||||
<th style="width: 120px; text-align: center;">Status</th>
|
<th style="width: 120px; text-align: center;">Status</th>
|
||||||
<th style="width: 100px;">Metode</th>
|
<th style="width: 100px;">Metode</th>
|
||||||
<th style="width: 100px;">Waktu</th>
|
<th style="width: 100px;">Waktu</th>
|
||||||
|
<th style="width: 120px; text-align: center;">Aksi</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach($absensis as $index => $absensi)
|
@foreach($kelasAbsensis as $index => $absensi)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $absensis->firstItem() + $index }}</td>
|
<td>{{ $index + 1 }}</td>
|
||||||
<td>{{ $absensi->tanggal->format('d/m/Y') }}</td>
|
<td>{{ $absensi->tanggal->format('d/m/Y') }}</td>
|
||||||
<td><strong>{{ $absensi->id_santri }}</strong></td>
|
<td><strong>{{ $absensi->id_santri }}</strong></td>
|
||||||
<td>{{ $absensi->santri->nama_lengkap }}</td>
|
<td>{{ $absensi->santri->nama_lengkap }}</td>
|
||||||
<td>
|
|
||||||
@php
|
|
||||||
$kelasName = $absensi->santri->kelas_name ?? $absensi->santri->kelas ?? '-';
|
|
||||||
@endphp
|
|
||||||
<span class="badge badge-secondary">{{ $kelasName }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">{!! $absensi->status_badge !!}</td>
|
<td class="text-center">{!! $absensi->status_badge !!}</td>
|
||||||
<td>
|
<td>
|
||||||
@if($absensi->metode_absen == 'RFID')
|
@if($absensi->metode_absen == 'RFID')
|
||||||
|
|
@ -86,14 +104,26 @@
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td>{{ $absensi->waktu_absen ? date('H:i', strtotime($absensi->waktu_absen)) : '-' }}</td>
|
<td>{{ $absensi->waktu_absen ? date('H:i', strtotime($absensi->waktu_absen)) : '-' }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div style="display: flex; gap: 4px; justify-content: center;">
|
||||||
|
<a href="{{ route('admin.absensi-kegiatan.edit', $absensi->id) }}" class="btn btn-sm btn-warning" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('admin.absensi-kegiatan.hapus', $absensi->id) }}" method="POST" style="display: inline;" onsubmit="return confirm('Yakin hapus absensi {{ $absensi->santri->nama_lengkap }}?');">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" title="Hapus">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
{{ $absensis->links() }}
|
|
||||||
</div>
|
</div>
|
||||||
|
@endforeach
|
||||||
@else
|
@else
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-clipboard"></i>
|
<i class="fas fa-clipboard"></i>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{-- views/admin/kegiatan/data/create.blade.php --}}
|
||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|
@ -10,10 +11,7 @@
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="kegiatan_id">
|
<label><i class="fas fa-hashtag form-icon"></i> ID Kegiatan (Otomatis)</label>
|
||||||
<i class="fas fa-hashtag form-icon"></i>
|
|
||||||
ID Kegiatan (Otomatis)
|
|
||||||
</label>
|
|
||||||
<input type="text" class="form-control" value="{{ $nextId }}" disabled>
|
<input type="text" class="form-control" value="{{ $nextId }}" disabled>
|
||||||
<small class="form-text">ID akan dibuat otomatis saat disimpan</small>
|
<small class="form-text">ID akan dibuat otomatis saat disimpan</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -31,9 +29,7 @@
|
||||||
</option>
|
</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('kategori_id')
|
@error('kategori_id') <span class="invalid-feedback">{{ $message }}</span> @enderror
|
||||||
<span class="invalid-feedback">{{ $message }}</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -41,96 +37,75 @@
|
||||||
<i class="fas fa-calendar-check form-icon"></i>
|
<i class="fas fa-calendar-check form-icon"></i>
|
||||||
Nama Kegiatan <span style="color: red;">*</span>
|
Nama Kegiatan <span style="color: red;">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text" name="nama_kegiatan" id="nama_kegiatan"
|
||||||
name="nama_kegiatan"
|
|
||||||
id="nama_kegiatan"
|
|
||||||
class="form-control @error('nama_kegiatan') is-invalid @enderror"
|
class="form-control @error('nama_kegiatan') is-invalid @enderror"
|
||||||
value="{{ old('nama_kegiatan') }}"
|
value="{{ old('nama_kegiatan') }}" placeholder="Nama Kegiatan" required>
|
||||||
placeholder="Contoh: Kajian Tafsir Al-Quran"
|
@error('nama_kegiatan') <span class="invalid-feedback">{{ $message }}</span> @enderror
|
||||||
required>
|
|
||||||
@error('nama_kegiatan')
|
|
||||||
<span class="invalid-feedback">{{ $message }}</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Hari — pill/tag horizontal --}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="hari">
|
<label>
|
||||||
<i class="fas fa-calendar-day form-icon"></i>
|
<i class="fas fa-calendar-day form-icon"></i>
|
||||||
Hari <span style="color: red;">*</span>
|
Hari Kegiatan <span style="color: red;">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select name="hari" id="hari" class="form-control @error('hari') is-invalid @enderror" required>
|
<small class="text-muted d-block mb-2" style="margin-top: -6px;">
|
||||||
<option value="">-- Pilih Hari --</option>
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Pilih satu atau lebih hari. Kegiatan dibuat otomatis untuk setiap hari yang dipilih.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<div class="hari-pills-wrap">
|
||||||
@foreach($hariList as $h)
|
@foreach($hariList as $h)
|
||||||
<option value="{{ $h }}" {{ old('hari') == $h ? 'selected' : '' }}>{{ $h }}</option>
|
@php $checked = in_array($h, old('hari', [])); @endphp
|
||||||
|
<label class="hari-pill {{ $checked ? 'active' : '' }}" for="hari{{ $loop->index }}">
|
||||||
|
<input type="checkbox" name="hari[]" value="{{ $h }}"
|
||||||
|
id="hari{{ $loop->index }}" {{ $checked ? 'checked' : '' }}>
|
||||||
|
{{ $h }}
|
||||||
|
</label>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</div>
|
||||||
@error('hari')
|
@error('hari') <span class="invalid-feedback d-block">{{ $message }}</span> @enderror
|
||||||
<span class="invalid-feedback">{{ $message }}</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 11px;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="waktu_mulai">
|
<label for="waktu_mulai">
|
||||||
<i class="fas fa-clock form-icon"></i>
|
<i class="fas fa-clock form-icon"></i> Waktu Mulai <span style="color: red;">*</span>
|
||||||
Waktu Mulai <span style="color: red;">*</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input type="time"
|
<input type="time" name="waktu_mulai" id="waktu_mulai"
|
||||||
name="waktu_mulai"
|
|
||||||
id="waktu_mulai"
|
|
||||||
class="form-control @error('waktu_mulai') is-invalid @enderror"
|
class="form-control @error('waktu_mulai') is-invalid @enderror"
|
||||||
value="{{ old('waktu_mulai') }}"
|
value="{{ old('waktu_mulai') }}" required>
|
||||||
required>
|
@error('waktu_mulai') <span class="invalid-feedback">{{ $message }}</span> @enderror
|
||||||
@error('waktu_mulai')
|
|
||||||
<span class="invalid-feedback">{{ $message }}</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="waktu_selesai">
|
<label for="waktu_selesai">
|
||||||
<i class="fas fa-clock form-icon"></i>
|
<i class="fas fa-clock form-icon"></i> Waktu Selesai <span style="color: red;">*</span>
|
||||||
Waktu Selesai <span style="color: red;">*</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input type="time"
|
<input type="time" name="waktu_selesai" id="waktu_selesai"
|
||||||
name="waktu_selesai"
|
|
||||||
id="waktu_selesai"
|
|
||||||
class="form-control @error('waktu_selesai') is-invalid @enderror"
|
class="form-control @error('waktu_selesai') is-invalid @enderror"
|
||||||
value="{{ old('waktu_selesai') }}"
|
value="{{ old('waktu_selesai') }}" required>
|
||||||
required>
|
@error('waktu_selesai') <span class="invalid-feedback">{{ $message }}</span> @enderror
|
||||||
@error('waktu_selesai')
|
|
||||||
<span class="invalid-feedback">{{ $message }}</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="materi">
|
<label for="materi">
|
||||||
<i class="fas fa-book form-icon"></i>
|
<i class="fas fa-book form-icon"></i> Materi/Topik
|
||||||
Materi/Topik
|
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text" name="materi" id="materi"
|
||||||
name="materi"
|
|
||||||
id="materi"
|
|
||||||
class="form-control @error('materi') is-invalid @enderror"
|
class="form-control @error('materi') is-invalid @enderror"
|
||||||
value="{{ old('materi') }}"
|
value="{{ old('materi') }}" placeholder="Contoh:Bacaan">
|
||||||
placeholder="Contoh: Surat Al-Baqarah Ayat 1-10">
|
@error('materi') <span class="invalid-feedback">{{ $message }}</span> @enderror
|
||||||
@error('materi')
|
|
||||||
<span class="invalid-feedback">{{ $message }}</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Pilih Kelas untuk Kegiatan --}}
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">
|
<label>
|
||||||
<i class="fas fa-layer-group form-icon"></i>
|
<i class="fas fa-layer-group form-icon"></i> Kelas yang Mengikuti Kegiatan
|
||||||
Kelas yang Mengikuti Kegiatan
|
|
||||||
</label>
|
</label>
|
||||||
<small class="text-muted d-block mb-3" style="margin-top: -8px;">
|
<small class="text-muted d-block mb-3" style="margin-top: -8px;">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
Kosongkan jika kegiatan untuk semua santri (umum).
|
Kosongkan jika kegiatan untuk semua santri (umum).
|
||||||
Pilih satu atau lebih kelas yang akan mengikuti kegiatan ini.
|
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
@foreach($kelompokKelas as $kelompok)
|
@foreach($kelompokKelas as $kelompok)
|
||||||
<div class="card mb-2" style="border: 1px solid #E8F7F2;">
|
<div class="card mb-2" style="border: 1px solid #E8F7F2;">
|
||||||
<div class="card-header py-2" style="background: linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%);">
|
<div class="card-header py-2" style="background: linear-gradient(135deg, #E8F7F2 0%, #D4F1E3 100%);">
|
||||||
|
|
@ -143,10 +118,8 @@ class="form-control @error('materi') is-invalid @enderror"
|
||||||
@forelse($kelompok->kelas as $kelas)
|
@forelse($kelompok->kelas as $kelas)
|
||||||
<div class="col-md-3 col-sm-4 col-6 mb-2">
|
<div class="col-md-3 col-sm-4 col-6 mb-2">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input"
|
<input class="form-check-input" type="checkbox"
|
||||||
type="checkbox"
|
name="kelas_ids[]" value="{{ $kelas->id }}"
|
||||||
name="kelas_ids[]"
|
|
||||||
value="{{ $kelas->id }}"
|
|
||||||
id="kelas{{ $kelas->id }}"
|
id="kelas{{ $kelas->id }}"
|
||||||
{{ in_array($kelas->id, old('kelas_ids', [])) ? 'checked' : '' }}>
|
{{ in_array($kelas->id, old('kelas_ids', [])) ? 'checked' : '' }}>
|
||||||
<label class="form-check-label" for="kelas{{ $kelas->id }}">
|
<label class="form-check-label" for="kelas{{ $kelas->id }}">
|
||||||
|
|
@ -155,9 +128,7 @@ class="form-control @error('materi') is-invalid @enderror"
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
<div class="col-12">
|
<div class="col-12"><small class="text-muted">Tidak ada kelas aktif</small></div>
|
||||||
<small class="text-muted">Tidak ada kelas aktif</small>
|
|
||||||
</div>
|
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -167,27 +138,51 @@ class="form-control @error('materi') is-invalid @enderror"
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="keterangan">
|
<label for="keterangan">
|
||||||
<i class="fas fa-align-left form-icon"></i>
|
<i class="fas fa-align-left form-icon"></i> Keterangan
|
||||||
Keterangan
|
|
||||||
</label>
|
</label>
|
||||||
<textarea name="keterangan"
|
<textarea name="keterangan" id="keterangan"
|
||||||
id="keterangan"
|
|
||||||
class="form-control @error('keterangan') is-invalid @enderror"
|
class="form-control @error('keterangan') is-invalid @enderror"
|
||||||
rows="4"
|
rows="4" placeholder="Catatan tambahan...">{{ old('keterangan') }}</textarea>
|
||||||
placeholder="Catatan tambahan tentang kegiatan ini...">{{ old('keterangan') }}</textarea>
|
@error('keterangan') <span class="invalid-feedback">{{ $message }}</span> @enderror
|
||||||
@error('keterangan')
|
|
||||||
<span class="invalid-feedback">{{ $message }}</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="submit" class="btn btn-success">
|
<button type="submit" class="btn btn-success">
|
||||||
<i class="fas fa-save"></i> Simpan
|
<i class="fas fa-save"></i> Simpan
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ route('admin.kegiatan.index') }}" class="btn btn-secondary">
|
<a href="{{ route('admin.kegiatan.jadwal') }}" class="btn btn-secondary">
|
||||||
<i class="fas fa-arrow-left"></i> Kembali
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hari-pills-wrap {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 8px;
|
||||||
|
padding: 12px 14px; border: 1px solid #E8F7F2;
|
||||||
|
border-radius: 10px; background: #FAFFFE;
|
||||||
|
}
|
||||||
|
.hari-pill {
|
||||||
|
display: inline-flex; align-items: center;
|
||||||
|
padding: 6px 16px; border-radius: 20px; cursor: pointer;
|
||||||
|
border: 1.5px solid #e2e8f0; background: #fff;
|
||||||
|
font-size: 0.85rem; font-weight: 500; color: #374151;
|
||||||
|
transition: all 0.18s; user-select: none; margin: 0;
|
||||||
|
}
|
||||||
|
.hari-pill input[type="checkbox"] { display: none; }
|
||||||
|
.hari-pill:hover { border-color: var(--primary-color); background: #f0fdf4; }
|
||||||
|
.hari-pill.active {
|
||||||
|
border-color: var(--primary-color); background: #ECFDF5;
|
||||||
|
color: var(--primary-dark); font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.hari-pill input[type="checkbox"]').forEach(function(cb) {
|
||||||
|
cb.addEventListener('change', function() {
|
||||||
|
var label = this.closest('.hari-pill');
|
||||||
|
label.classList.toggle('active', this.checked);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue