first commit
This commit is contained in:
commit
f27bf027de
|
|
@ -0,0 +1,18 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[docker-compose.yml]
|
||||||
|
indent_size = 4
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_TIMEZONE=UTC
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=laravel
|
||||||
|
# DB_USERNAME=root
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_ENCRYPTION=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/.phpunit.cache
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
auth.json
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
# SYSTEM CONTEXT - ROBOTKU V2
|
||||||
|
|
||||||
|
Dokumen ini berisi gambaran arsitektur sistem, skema basis data, dan aturan bisnis utama aplikasi **Robotku V2**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ringkasan Teknologi (Tech Stack)
|
||||||
|
|
||||||
|
- **Framework Backend**: Laravel 11 (PHP 8.2+)
|
||||||
|
- **Database**: MySQL / MariaDB
|
||||||
|
- **Frontend / Templating**: Laravel Blade, HTML5, Vanilla CSS, Bootstrap 4 / Stisla Admin Theme, jQuery, Selectric, List.js
|
||||||
|
- **Media & File Handling**: Intervention Image v3, Storage Public Disk, Laravel Excel (`Maatwebsite/Excel`)
|
||||||
|
- **Authentication & Roles**: Laravel Default Auth (`Users` & `Guru` Guard / Role Admin vs Guru)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Arsitektur Modul & Hak Akses
|
||||||
|
|
||||||
|
Aplikasi Robotku V2 terbagi menjadi 3 modul utama:
|
||||||
|
|
||||||
|
1. **Modul Admin (`/admin`)**:
|
||||||
|
- Pengelolaan Master Data: **Programs**, **Sekolah**, **Ruangan**, **Guru**, **Peserta/Siswa**, **Sesi**.
|
||||||
|
- Pengelolaan **Kelas**: Tambah/edit/hapus kelas, floating jadwal ekstrakurikuler vs reguler, penetapan koordinator & ruangan.
|
||||||
|
- Pengelolaan **Logbook Kelas**: Monitoring seluruh catatan logbook, foto kegiatan, pengajar pendamping, dan export/import Excel.
|
||||||
|
- Monitoring **Ketersediaan Mengajar Guru**: Melihat dan mengubah jadwal ketersediaan guru per hari & per sesi.
|
||||||
|
|
||||||
|
2. **Modul Guru (`/guru`)**:
|
||||||
|
- Dashboard Guru: Daftar kelas aktif yang diampu.
|
||||||
|
- Detail Kelas & Siswa: Informasi anggota kelas & riwayat logbook.
|
||||||
|
- Pengisian Logbook: Form pengisian kegiatan, foto kegiatan, materi, kendala & solusi dengan aturan deadline harian (maksimal jam 19:00 WIB pada hari yang sama).
|
||||||
|
- Pengaturan Ketersediaan Mengajar: Mengisi slot ketersediaan mengajar berdasarkan Master Sesi dengan bantuan shortcut "Centang Semua".
|
||||||
|
|
||||||
|
3. **Modul Guest / Orang Tua (`/angket/{kode_kelas}`)**:
|
||||||
|
- Formulir evaluasi/angket publik untuk orang tua siswa berdasarkan `kode_kelas`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Skema Database & Relasi Utamanya
|
||||||
|
|
||||||
|
### A. Master Data Core
|
||||||
|
- `programs` (`id`, `nama_program`, `level_number`, `created_at`, `updated_at`)
|
||||||
|
- `sekolah` (`id`, `nama_sekolah`, `is_active`, ...)
|
||||||
|
- `ruangan` (`id`, `nama_ruangan`, `lokasi_tipe`, `sekolah_id`, `status`, ...)
|
||||||
|
- `sesi` (`id`, `nama_sesi`, `jam`, `created_at`, `updated_at`)
|
||||||
|
- Contoh data: `Sesi 1` (`08:00-10:00`), `Sesi 2` (`10:00-12:00`), dst.
|
||||||
|
|
||||||
|
### B. User & Guru
|
||||||
|
- `users` (`id`, `name`, `email`, `role`, `password`, ...)
|
||||||
|
- `guru` (`id`, `user_id`, `nama`, `no_hp`, `status`, `level_maksimal`, ...)
|
||||||
|
- `ketersediaan_mengajars` (`id`, `guru_id`, `hari_id`, `sesi_id`, `status`, ...)
|
||||||
|
|
||||||
|
### C. Kelas & Detail Siswa
|
||||||
|
- `kelas` (`id`, `kode_kelas`, `nama_kelas`, `jenis_kelas`, `program_id`, `guru_id`, `harga_kelas`, `status`, `semester`, `tahun_ajaran`, `kategori_kelas`, `frekuensi_per_minggu`, `is_ekstrakurikuler`, `sekolah_id`, `hari_fix`, `sesi_fix`, `ruangan_fix_id`, `keterangan`, ...)
|
||||||
|
- *Catatan Schema*: Kolom `cabang` dan `total_pertemuan_program` telah dihapus. Field `jenis_kelas` kini digunakan untuk menyimpan **Total Pertemuan** (`12 Sesi` / `24 Sesi`).
|
||||||
|
- `pesertas` (`id`, `nama_anak`, `nama_orangtua`, `no_hp_orangtua`, `foto`, ...)
|
||||||
|
- `detail_kelas` (`id`, `kelas_id`, `peserta_id`, `status_siswa`, `harga_kelas`, `catatan`, ...)
|
||||||
|
- Relasi `Pesertas()` (`belongsTo(Pesertas::class, 'peserta_id')`). Filtered menggunakan `has('Pesertas')` untuk mencegah error null dereference.
|
||||||
|
|
||||||
|
### D. Logbook Kelas
|
||||||
|
- `logbook` (`id`, `kelas_id`, `sesi_id`, `pertemuan`, `kode_kelas`, `tanggal`, `start`, `end`, `materi`, `kegiatan_minggu_ini`, `perkembangan_siswa`, `kendala_dan_solusi`, `kebutuhan_minggu_depan`, `hal_baik`, `catatan`, `status`, `jenis_libur`, `jenis_libur_lainnya`, ...)
|
||||||
|
- Relasi `Sesi()` (`belongsTo(Sesi::class, 'sesi_id')`).
|
||||||
|
- `foto_logbook` (`id`, `logbook_id`, `foto`, `keterangan`, ...)
|
||||||
|
- `pengajar_logbook` (`id`, `logbook_id`, `guru_id`, ...)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Aturan Bisnis Utama (Business Rules)
|
||||||
|
|
||||||
|
1. **Total Pertemuan Kelas**:
|
||||||
|
- Pilihan Total Pertemuan dibatasi hanya **12 Sesi** dan **24 Sesi**.
|
||||||
|
2. **Fleksibilitas Logbook & Auto-Fill Sesi**:
|
||||||
|
- Setiap catatan logbook terhubung ke Master `Sesi` (`sesi_id`).
|
||||||
|
- Memilih Sesi pada modal Logbook secara otomatis mengisikan jam `start` dan `end`.
|
||||||
|
- Logbook lama yang dibuat sebelum fitur `sesi_id` di-mapping otomatis berdasarkan kesesuaian jam `start` & `end`.
|
||||||
|
3. **Deadline Pengisian Logbook Guru**:
|
||||||
|
- Pengisian/pengubahan logbook oleh Guru dibatasi hingga pukul **19:00 WIB** pada hari H kegiatan. Logbook tanggal lampau dikunci untuk mengedit.
|
||||||
|
4. **Ketersediaan Mengajar Guru**:
|
||||||
|
- Menggunakan tabel matriks Hari (Senin–Sabtu) dan Master Sesi. Dilengkapi opsi master "Centang Semua".
|
||||||
|
5. **Null-Safety Guarding**:
|
||||||
|
- Penanganan relasi `detail_kelas` ke `pesertas` diproteksi secara ketat agar jika terdapat data yatim (deleted/orphaned relation), sistem tidak mengalami crash (error 500).
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
# PRODUCT REQUIREMENT DOCUMENT (PRD) - ROBOTKU V2
|
||||||
|
|
||||||
|
Dokumen ini berisi spesifikasi kebutuhan produk, fitur-fitur utama, serta riwayat perubahan pada sistem manajemen bimbingan belajar robotika **Robotku V2**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visi & Tujuan Produk
|
||||||
|
|
||||||
|
**Robotku V2** bertujuan untuk menyediakan platform manajemen kelas, penjadwalan guru, serta dokumentasi kegiatan pembelajaran (logbook) yang terstruktur, efisien, dan ramah pengguna baik untuk pihak Administrasi (Admin) maupun Tenaga Pengajar (Guru).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Pengguna Sistem (User Personas)
|
||||||
|
|
||||||
|
1. **Administrator (Admin)**:
|
||||||
|
- Mengelola master data (Program, Sekolah, Ruangan, Guru, Siswa, Sesi).
|
||||||
|
- Membuka dan mengkonfigurasi kelas baru (Tipe Ekstrakurikuler / Reguler, Harga, Jadwal Fix/Floating).
|
||||||
|
- Memantau kelengkapan logbook pembelajaran dan foto kegiatan dari guru.
|
||||||
|
- Mengatur jadwal ketersediaan guru.
|
||||||
|
|
||||||
|
2. **Guru / Pengajar**:
|
||||||
|
- Mengisi logbook pembelajaran harian (Materi, Kegiatan, Perkembangan Siswa, Kendala/Solusi, dan Upload Foto).
|
||||||
|
- Melihat daftar siswa pada kelas yang diampu.
|
||||||
|
- Mengatur ketersediaan jam/sesi mengajar harian.
|
||||||
|
|
||||||
|
3. **Orang Tua / Wali Siswa**:
|
||||||
|
- Mengisi angket evaluasi pembelajaran secara online via link `kode_kelas`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Kebutuhan Fungsional (Functional Requirements)
|
||||||
|
|
||||||
|
### A. Fitur Manajemen Kelas & Penjadwalan
|
||||||
|
- **Master Sesi (`sesi`)**:
|
||||||
|
- Menyediakan master data Sesi (Nama Sesi & Rentang Jam).
|
||||||
|
- Terintegrasi dengan ketersediaan mengajar guru dan fitur logbook kelas.
|
||||||
|
- **Konfigurasi Kelas**:
|
||||||
|
- Pilihan **Total Pertemuan**: Dibatasi hanya `12 Sesi` dan `24 Sesi`.
|
||||||
|
- Tipe Kelas: **Reguler** atau **Ekstrakurikuler** (terikat ke Sekolah & Ruangan tertentu).
|
||||||
|
- Form layout yang simetris 3 kolom untuk memudahkan input data kelas.
|
||||||
|
|
||||||
|
### B. Fitur Logbook Kelas
|
||||||
|
- **Pencatatan Logbook**:
|
||||||
|
- Setiap logbook mencatat Pertemuan, Tanggal, Sesi Pertemuan (`sesi_id`), Jam Start/End, Materi, Catatan Kegiatan, Perkembangan Siswa, Kendala & Solusi, Kebutuhan Minggu Depan, Hal Baik, serta Foto Kegiatan.
|
||||||
|
- Fitur Tandai Libur (Libur Nasional, Ramadhan, atau Libur Lainnya).
|
||||||
|
- Duplikasi Logbook untuk mempercepat pembuatan catatan logbook pertemuan berikutnya.
|
||||||
|
- Export & Import Logbook via Excel.
|
||||||
|
- **Auto-Fill Sesi**:
|
||||||
|
- Memilih Sesi pada modal Logbook secara otomatis mengisi jam Start & End logbook.
|
||||||
|
- **Deadline Pengisian Guru**:
|
||||||
|
- Pengisian logbook oleh Guru dibatasi hingga **pukul 19:00 WIB** pada hari yang sama.
|
||||||
|
|
||||||
|
### C. Fitur Ketersediaan Mengajar Guru
|
||||||
|
- Pemetaan ketersediaan guru berdasarkan **Hari** dan **Master Sesi**.
|
||||||
|
- Shortcut **Centang Semua** global untuk memilih seluruh slot sesi dengan satu klik.
|
||||||
|
|
||||||
|
### D. Keamanan Data & Robustness
|
||||||
|
- **Null-Safety Guarding**: Mencegah error crash 500 (`Attempt to read property on null`) ketika membaca data relasi pivot yang yatim/invalid.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Kebutuhan Non-Fungsional (Non-Functional Requirements)
|
||||||
|
|
||||||
|
1. **Kemudahan Penggunaan (Usability)**:
|
||||||
|
- Interface modern dengan visual yang rapi, responsive, dan indikator status yang jelas.
|
||||||
|
2. **Kinerja & Responsivitas (Performance)**:
|
||||||
|
- Pencarian cepat dan paginasi interaktif berbasis List.js / DataTables.
|
||||||
|
- Kompresi foto otomatis (max 800px) menggunakan Intervention Image untuk efisiensi penyimpanan storage server.
|
||||||
|
3. **Keandalan (Reliability)**:
|
||||||
|
- Proteksi transaksi database dengan perlakuan cascade foreign key yang aman.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Riwayat Perubahan Terbaru (Changelog Update v2.x)
|
||||||
|
|
||||||
|
- **Master Data Sesi**: Penambahan tabel `sesi` (nama sesi dan jam) sebagai acuan tunggal untuk ketersediaan mengajar dan logbook.
|
||||||
|
- **Pembersihan Kolom `cabang`**: Menghapus kolom `cabang` dari tabel `kelas`, form input, serta filter query controller.
|
||||||
|
- **Pembersihan Field `total_pertemuan_program`**: Menghapus field `total_pertemuan_program` dan memperbarui label `Jenis Kelas` menjadi **Total Pertemuan** (`12 Sesi` / `24 Sesi`).
|
||||||
|
- **Penataan Form Edit Kelas**: Merapikan form edit kelas di detail kelas menjadi layout grid 3-kolom yang simetris dan sejajar.
|
||||||
|
- **Logbook Sesi (`sesi_id`)**: Menambahkan relasi `sesi_id` pada tabel `logbook` beserta auto-mapping logbook eksisting berdasarkan kecocokan jam.
|
||||||
|
- **Penanganan Null-Safety**: Menambahkan filter `has('Pesertas')` dan guard Blade `@if($item->Pesertas)` untuk mencegah crash 500 saat relasi siswa tidak terhubung.
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About Laravel
|
||||||
|
|
||||||
|
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||||
|
|
||||||
|
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||||
|
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||||
|
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||||
|
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||||
|
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||||
|
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||||
|
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||||
|
|
||||||
|
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||||
|
|
||||||
|
## Learning Laravel
|
||||||
|
|
||||||
|
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||||
|
|
||||||
|
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
||||||
|
|
||||||
|
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||||
|
|
||||||
|
## Laravel Sponsors
|
||||||
|
|
||||||
|
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||||
|
|
||||||
|
### Premium Partners
|
||||||
|
|
||||||
|
- **[Vehikl](https://vehikl.com/)**
|
||||||
|
- **[Tighten Co.](https://tighten.co)**
|
||||||
|
- **[WebReinvent](https://webreinvent.com/)**
|
||||||
|
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||||
|
- **[64 Robots](https://64robots.com)**
|
||||||
|
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
|
||||||
|
- **[Cyber-Duck](https://cyber-duck.co.uk)**
|
||||||
|
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||||
|
- **[Jump24](https://jump24.co.uk)**
|
||||||
|
- **[Redberry](https://redberry.international/laravel/)**
|
||||||
|
- **[Active Logic](https://activelogic.com)**
|
||||||
|
- **[byte5](https://byte5.de)**
|
||||||
|
- **[OP.GG](https://op.gg)**
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Services\Scheduling\DatasetLoader;
|
||||||
|
use App\Services\Scheduling\GeneticAlgorithmEngine;
|
||||||
|
|
||||||
|
class TestGaEngineCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'ga:test-parameters
|
||||||
|
{--semester= : Semester dataset}
|
||||||
|
{--tahun= : Tahun ajaran dataset}';
|
||||||
|
|
||||||
|
protected $description = 'Jalankan pengujian otomatis hyperparameter GA (Populasi, Crossover, Mutasi, Iterasi, & Pengujian Akhir) dan tampilkan tabel Bab 4';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info("================================──────────────────────────");
|
||||||
|
$this->info("🚀 MEMULAI OTOMASI PENGUJIAN ALGORITMA GENETIKA (BAB 4)");
|
||||||
|
$this->info("================================──────────────────────────");
|
||||||
|
|
||||||
|
$semester = $this->option('semester');
|
||||||
|
$tahun = $this->option('tahun');
|
||||||
|
|
||||||
|
$datasetLoader = new DatasetLoader();
|
||||||
|
$dataset = $datasetLoader->load($semester, $tahun);
|
||||||
|
|
||||||
|
$this->comment("Dataset berhasil dimuat: " . count($dataset->kelasList) . " Kelas, " . count($dataset->guruList) . " Guru, " . count($dataset->ruanganRobotkuIds) . " Ruangan.");
|
||||||
|
|
||||||
|
// 1. TAHAP 1: UJI POPULASI
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("📌 1. MENJALANKAN TABEL 4.1: PENGUJIAN JUMLAH POPULASI");
|
||||||
|
$popVariations = [10, 20, 30, 40, 50];
|
||||||
|
$tabel1 = [];
|
||||||
|
$bestPop = 20;
|
||||||
|
$bestPopFitness = -1;
|
||||||
|
|
||||||
|
foreach ($popVariations as $idx => $pop) {
|
||||||
|
$engine = new GeneticAlgorithmEngine(
|
||||||
|
populationSize: $pop,
|
||||||
|
crossoverRate: 0.5,
|
||||||
|
mutationRate: 0.5,
|
||||||
|
maxGenerations: 500,
|
||||||
|
tournamentSize: 3,
|
||||||
|
elitismCount: 2
|
||||||
|
);
|
||||||
|
$res = $engine->run($dataset);
|
||||||
|
$fitness = $res['bestChromosome']->fitness;
|
||||||
|
|
||||||
|
if ($fitness > $bestPopFitness) {
|
||||||
|
$bestPopFitness = $fitness;
|
||||||
|
$bestPop = $pop;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabel1[] = [
|
||||||
|
'Uji Ke-' => $idx + 1,
|
||||||
|
'Populasi' => $pop,
|
||||||
|
'Pc' => 0.5,
|
||||||
|
'Pm' => 0.5,
|
||||||
|
'Iterasi' => 500,
|
||||||
|
'Fitness Max' => round($fitness, 6),
|
||||||
|
'Total Penalti' => $res['bestChromosome']->totalPenalty,
|
||||||
|
'Waktu (s)' => round($res['executionTime'], 4),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$this->table(['Uji Ke-', 'Populasi', 'Pc', 'Pm', 'Iterasi', 'Fitness Max', 'Penalti', 'Waktu (s)'], $tabel1);
|
||||||
|
$this->info("✅ Populasi Terbaik Terpilih: {$bestPop} (Fitness: {$bestPopFitness})");
|
||||||
|
|
||||||
|
// 2. TAHAP 2: UJI CROSSOVER RATE
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("📌 2. MENJALANKAN TABEL 4.2: PENGUJIAN CROSSOVER RATE (Pc)");
|
||||||
|
$pcVariations = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
||||||
|
$tabel2 = [];
|
||||||
|
$bestPc = 0.6;
|
||||||
|
$bestPcFitness = -1;
|
||||||
|
|
||||||
|
foreach ($pcVariations as $idx => $pc) {
|
||||||
|
$engine = new GeneticAlgorithmEngine(
|
||||||
|
populationSize: $bestPop,
|
||||||
|
crossoverRate: $pc,
|
||||||
|
mutationRate: 0.5,
|
||||||
|
maxGenerations: 500,
|
||||||
|
tournamentSize: 3,
|
||||||
|
elitismCount: 2
|
||||||
|
);
|
||||||
|
$res = $engine->run($dataset);
|
||||||
|
$fitness = $res['bestChromosome']->fitness;
|
||||||
|
|
||||||
|
if ($fitness > $bestPcFitness) {
|
||||||
|
$bestPcFitness = $fitness;
|
||||||
|
$bestPc = $pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabel2[] = [
|
||||||
|
'Uji Ke-' => $idx + 1,
|
||||||
|
'Populasi' => $bestPop,
|
||||||
|
'Pc' => $pc,
|
||||||
|
'Pm' => 0.5,
|
||||||
|
'Iterasi' => 500,
|
||||||
|
'Fitness Max' => round($fitness, 6),
|
||||||
|
'Total Penalti' => $res['bestChromosome']->totalPenalty,
|
||||||
|
'Waktu (s)' => round($res['executionTime'], 4),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$this->table(['Uji Ke-', 'Populasi', 'Pc', 'Pm', 'Iterasi', 'Fitness Max', 'Penalti', 'Waktu (s)'], $tabel2);
|
||||||
|
$this->info("✅ Crossover Rate Terbaik Terpilih: {$bestPc} (Fitness: {$bestPcFitness})");
|
||||||
|
|
||||||
|
// 3. TAHAP 3: UJI MUTATION RATE
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("📌 3. MENJALANKAN TABEL 4.3: PENGUJIAN MUTATION RATE (Pm)");
|
||||||
|
$pmVariations = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
||||||
|
$tabel3 = [];
|
||||||
|
$bestPm = 0.3;
|
||||||
|
$bestPmFitness = -1;
|
||||||
|
|
||||||
|
foreach ($pmVariations as $idx => $pm) {
|
||||||
|
$engine = new GeneticAlgorithmEngine(
|
||||||
|
populationSize: $bestPop,
|
||||||
|
crossoverRate: $bestPc,
|
||||||
|
mutationRate: $pm,
|
||||||
|
maxGenerations: 500,
|
||||||
|
tournamentSize: 3,
|
||||||
|
elitismCount: 2
|
||||||
|
);
|
||||||
|
$res = $engine->run($dataset);
|
||||||
|
$fitness = $res['bestChromosome']->fitness;
|
||||||
|
|
||||||
|
if ($fitness > $bestPmFitness) {
|
||||||
|
$bestPmFitness = $fitness;
|
||||||
|
$bestPm = $pm;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabel3[] = [
|
||||||
|
'Uji Ke-' => $idx + 1,
|
||||||
|
'Populasi' => $bestPop,
|
||||||
|
'Pc' => $bestPc,
|
||||||
|
'Pm' => $pm,
|
||||||
|
'Iterasi' => 500,
|
||||||
|
'Fitness Max' => round($fitness, 6),
|
||||||
|
'Total Penalti' => $res['bestChromosome']->totalPenalty,
|
||||||
|
'Waktu (s)' => round($res['executionTime'], 4),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$this->table(['Uji Ke-', 'Populasi', 'Pc', 'Pm', 'Iterasi', 'Fitness Max', 'Penalti', 'Waktu (s)'], $tabel3);
|
||||||
|
$this->info("✅ Mutation Rate Terbaik Terpilih: {$bestPm} (Fitness: {$bestPmFitness})");
|
||||||
|
|
||||||
|
// 4. TAHAP 4: UJI ITERASI / GENERASI
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("📌 4. MENJALANKAN TABEL 4.4: PENGUJIAN JUMLAH ITERASI");
|
||||||
|
$iterVariations = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000];
|
||||||
|
$tabel4 = [];
|
||||||
|
$bestIter = 500;
|
||||||
|
$bestIterFitness = -1;
|
||||||
|
|
||||||
|
foreach ($iterVariations as $idx => $iter) {
|
||||||
|
$engine = new GeneticAlgorithmEngine(
|
||||||
|
populationSize: $bestPop,
|
||||||
|
crossoverRate: $bestPc,
|
||||||
|
mutationRate: $bestPm,
|
||||||
|
maxGenerations: $iter,
|
||||||
|
tournamentSize: 3,
|
||||||
|
elitismCount: 2
|
||||||
|
);
|
||||||
|
$res = $engine->run($dataset);
|
||||||
|
$fitness = $res['bestChromosome']->fitness;
|
||||||
|
|
||||||
|
if ($fitness > $bestIterFitness && $bestIterFitness < 1.0) {
|
||||||
|
$bestIterFitness = $fitness;
|
||||||
|
$bestIter = $iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabel4[] = [
|
||||||
|
'Uji Ke-' => $idx + 1,
|
||||||
|
'Populasi' => $bestPop,
|
||||||
|
'Pc' => $bestPc,
|
||||||
|
'Pm' => $bestPm,
|
||||||
|
'Iterasi' => $iter,
|
||||||
|
'Fitness Max' => round($fitness, 6),
|
||||||
|
'Total Penalti' => $res['bestChromosome']->totalPenalty,
|
||||||
|
'Waktu (s)' => round($res['executionTime'], 4),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$this->table(['Uji Ke-', 'Populasi', 'Pc', 'Pm', 'Iterasi', 'Fitness Max', 'Penalti', 'Waktu (s)'], $tabel4);
|
||||||
|
$this->info("✅ Iterasi Terbaik Terpilih: {$bestIter} (Fitness: {$bestIterFitness})");
|
||||||
|
|
||||||
|
// 5. TAHAP 5: PENGUJIAN AKHIR (RUN 10 KALI)
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("📌 5. MENJALANKAN TABEL 4.5: PENGUJIAN AKHIR (10 KALI EKSEKUSI)");
|
||||||
|
$tabel5 = [];
|
||||||
|
$totalFitness = 0;
|
||||||
|
$totalTime = 0;
|
||||||
|
|
||||||
|
for ($run = 1; $run <= 10; $run++) {
|
||||||
|
$engine = new GeneticAlgorithmEngine(
|
||||||
|
populationSize: $bestPop,
|
||||||
|
crossoverRate: $bestPc,
|
||||||
|
mutationRate: $bestPm,
|
||||||
|
maxGenerations: $bestIter,
|
||||||
|
tournamentSize: 3,
|
||||||
|
elitismCount: 2
|
||||||
|
);
|
||||||
|
$res = $engine->run($dataset);
|
||||||
|
$fitness = $res['bestChromosome']->fitness;
|
||||||
|
$time = $res['executionTime'];
|
||||||
|
|
||||||
|
$totalFitness += $fitness;
|
||||||
|
$totalTime += $time;
|
||||||
|
|
||||||
|
$tabel5[] = [
|
||||||
|
'Uji Ke-' => $run,
|
||||||
|
'Populasi' => $bestPop,
|
||||||
|
'Pc' => $bestPc,
|
||||||
|
'Pm' => $bestPm,
|
||||||
|
'Iterasi' => $bestIter,
|
||||||
|
'Fitness Terbaik' => round($fitness, 6),
|
||||||
|
'Total Penalti' => $res['bestChromosome']->totalPenalty,
|
||||||
|
'Run Time (s)' => round($time, 4),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$avgFitness = round($totalFitness / 10, 6);
|
||||||
|
$avgTime = round($totalTime / 10, 4);
|
||||||
|
|
||||||
|
$tabel5[] = [
|
||||||
|
'Uji Ke-' => 'Rata-rata',
|
||||||
|
'Populasi' => $bestPop,
|
||||||
|
'Pc' => $bestPc,
|
||||||
|
'Pm' => $bestPm,
|
||||||
|
'Iterasi' => $bestIter,
|
||||||
|
'Fitness Terbaik' => $avgFitness,
|
||||||
|
'Total Penalti' => 0,
|
||||||
|
'Run Time (s)' => $avgTime,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->table(['Uji Ke-', 'Populasi', 'Pc', 'Pm', 'Iterasi', 'Fitness Terbaik', 'Penalti', 'Run Time (s)'], $tabel5);
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("================================──────────────────────────");
|
||||||
|
$this->info("🎉 PENGUJIAN GA SELESAI DENGAN SUKSES!");
|
||||||
|
$this->info("⭐ KOMBINASI TERBAIK: Populasi = {$bestPop}, Pc = {$bestPc}, Pm = {$bestPm}, Iterasi = {$bestIter}");
|
||||||
|
$this->info("⭐ RATA-RATA FITNESS : {$avgFitness}");
|
||||||
|
$this->info("⭐ RATA-RATA WAKTU : {$avgTime} detik");
|
||||||
|
$this->info("================================──────────────────────────");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\Events;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||||
|
|
||||||
|
class AllEventsExport implements WithMultipleSheets
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Support\Collection
|
||||||
|
*/
|
||||||
|
protected $year;
|
||||||
|
|
||||||
|
public function __construct($year)
|
||||||
|
{
|
||||||
|
$this->year = $year;
|
||||||
|
}
|
||||||
|
public function sheets(): array
|
||||||
|
{
|
||||||
|
$sheets = [];
|
||||||
|
|
||||||
|
$query = Events::query();
|
||||||
|
|
||||||
|
if ($this->year != 'all'){
|
||||||
|
$query->whereYear('tanggal_event', $this->year);
|
||||||
|
}
|
||||||
|
$allEvents = $query->orderBy('created_at', 'desc')->get();
|
||||||
|
|
||||||
|
if($allEvents->count() <= 0){
|
||||||
|
$sheets[] = new EmptySheetExport();
|
||||||
|
return $sheets;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($allEvents as $event) {
|
||||||
|
$sheets[] = new PesertaEventExport($event->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sheets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||||
|
|
||||||
|
class AllKelasExport implements WithMultipleSheets
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Support\Collection
|
||||||
|
*/
|
||||||
|
protected $year;
|
||||||
|
|
||||||
|
public function __construct($year)
|
||||||
|
{
|
||||||
|
$this->year = $year;
|
||||||
|
}
|
||||||
|
public function sheets(): array
|
||||||
|
{
|
||||||
|
$sheets = [];
|
||||||
|
|
||||||
|
$query = Kelas::query();
|
||||||
|
|
||||||
|
if ($this->year != 'all'){
|
||||||
|
$query->whereYear('created_at', $this->year);
|
||||||
|
}
|
||||||
|
$allKelas = $query->orderBy('created_at', 'desc')->get();
|
||||||
|
|
||||||
|
if($allKelas->count() <= 0){
|
||||||
|
$sheets[] = new EmptySheetExport();
|
||||||
|
return $sheets;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($allKelas as $kelas) {
|
||||||
|
$sheets[] = new DetailKelasExport($kelas->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sheets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\DetailKelas;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithCustomStartCell;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Events\AfterSheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithCustomValueBinder;
|
||||||
|
|
||||||
|
class DetailKelasExport extends DefaultValueBinder implements FromCollection, WithHeadings, WithEvents, WithMapping, WithCustomStartCell, WithColumnFormatting, WithTitle, WithCustomValueBinder
|
||||||
|
{
|
||||||
|
protected $kelasId;
|
||||||
|
|
||||||
|
public function __construct($kelasId)
|
||||||
|
{
|
||||||
|
$this->kelasId = $kelasId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return DetailKelas::with('siswa.Sekolah')->where('kelas_id', $this->kelasId)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ["No", "Nama Anak", "Orang Tua", "No HP Orangtua", "Kelas", "Sekolah", "Status", "Tgl. Daftar"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping supaya nomor urut otomatis
|
||||||
|
public function map($detailKelas): array
|
||||||
|
{
|
||||||
|
static $no = 1;
|
||||||
|
$noHp = $detailKelas->siswa?->no_hp_orangtua;
|
||||||
|
return [
|
||||||
|
$no++, // No
|
||||||
|
Str::title($detailKelas->siswa?->nama_anak ?? '-'),
|
||||||
|
Str::title($detailKelas->siswa?->nama_orangtua ?? '-'),
|
||||||
|
$noHp ? "+62" . $noHp : 'No Hp Belum Diisi',
|
||||||
|
$detailKelas->siswa?->kelas ?? '-',
|
||||||
|
$detailKelas->siswa?->Sekolah?->nama_sekolah ?? ($detailKelas->siswa?->asal_sekolah ?? '-'),
|
||||||
|
$detailKelas->status_siswa ?? '-',
|
||||||
|
Carbon::parse($detailKelas->created_at)->locale('id')->translatedFormat('d M Y'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function columnFormats(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'D' => NumberFormat::FORMAT_TEXT,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bindValue(Cell $cell, $value)
|
||||||
|
{
|
||||||
|
if (is_string($value) && str_starts_with($value, '+')) {
|
||||||
|
$cell->setValueExplicit($value, DataType::TYPE_STRING);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::bindValue($cell, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startCell(): string
|
||||||
|
{
|
||||||
|
return 'A6';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerEvents(): array
|
||||||
|
{
|
||||||
|
$kelasData = Kelas::with('guru')->find($this->kelasId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
AfterSheet::class => function(AfterSheet $event) use ($kelasData) {
|
||||||
|
$sheet = $event->sheet;
|
||||||
|
|
||||||
|
// Header utama
|
||||||
|
$sheet->mergeCells('A1:H1');
|
||||||
|
$sheet->mergeCells('A2:H2');
|
||||||
|
$sheet->mergeCells('A3:H3');
|
||||||
|
$sheet->mergeCells('A4:H4');
|
||||||
|
$sheet->mergeCells('A5:H5');
|
||||||
|
$sheet->setCellValue(
|
||||||
|
'A2',
|
||||||
|
strtoupper(($kelasData ? $kelasData->nama_kelas : ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($kelasData) {
|
||||||
|
$sheet->setCellValue(
|
||||||
|
'A3',
|
||||||
|
"{$kelasData->jenis_kelas} | {$kelasData->status}"
|
||||||
|
);
|
||||||
|
|
||||||
|
$sheet->setCellValue(
|
||||||
|
'A4',
|
||||||
|
$kelasData->Guru->nama ? "Kordinator Kelas " . $kelasData->Guru->nama : '-'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->getStyle('A2')->applyFromArray([
|
||||||
|
'font' => ['bold' => true, 'size' => 16],
|
||||||
|
'alignment' => ['horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sheet->getStyle('A3:A4')->applyFromArray([
|
||||||
|
'font' => ['italic' => true, 'size' => 11],
|
||||||
|
'alignment' => ['horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sheet->getStyle('A6:H6')->applyFromArray([
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'EFEFEF'],
|
||||||
|
],
|
||||||
|
'alignment' => ['horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$lastRow = $sheet->getHighestRow();
|
||||||
|
$sheet->getStyle("A6:H{$lastRow}")->applyFromArray([
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// $sheet->mergeCells("A" . ($lastRow+1) . ":C" . ($lastRow+1));
|
||||||
|
$sheet->setCellValue("G" . ($lastRow+1), "Total");
|
||||||
|
$sheet->setCellValue("H" . ($lastRow+1), $lastRow - 6);
|
||||||
|
|
||||||
|
$sheet->getStyle("G" . ($lastRow+1) . ":H" . ($lastRow+1))->applyFromArray([
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'alignment' => ['horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_RIGHT],
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$sheet->getStyle("G" . ($lastRow+1))->applyFromArray([
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'EFEFEF'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$lastRow = $sheet->getHighestRow();
|
||||||
|
$sheet->getStyle("A7:A{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("D7:D{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("E7:E{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("F7:F{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("G7:G{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("H7:H{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
|
||||||
|
$sheet->getColumnDimension('A')->setWidth(5);
|
||||||
|
$sheet->getColumnDimension('B')->setWidth(32);
|
||||||
|
$sheet->getColumnDimension('C')->setWidth(32);
|
||||||
|
$sheet->getColumnDimension('D')->setWidth(25);
|
||||||
|
$sheet->getColumnDimension('E')->setWidth(8);
|
||||||
|
$sheet->getColumnDimension('F')->setWidth(32);
|
||||||
|
$sheet->getColumnDimension('G')->setWidth(19);
|
||||||
|
$sheet->getColumnDimension('H')->setWidth(19);
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
$kelas = Kelas::find($this->kelasId);
|
||||||
|
return $kelas ? $kelas->nama_kelas : 'Kelas';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
|
||||||
|
class EmptySheetExport implements FromCollection, WithTitle
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Support\Collection
|
||||||
|
*/
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return collect([
|
||||||
|
["Data tidak ditemukan"]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function title(): string{
|
||||||
|
return "Kosong Guys!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class GaGenerationSheet implements FromCollection, WithTitle, WithHeadings, WithStyles, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected array $gaResult;
|
||||||
|
protected mixed $targetGen;
|
||||||
|
|
||||||
|
public function __construct(array $gaResult, mixed $targetGen = 0)
|
||||||
|
{
|
||||||
|
$this->gaResult = $gaResult;
|
||||||
|
$this->targetGen = $targetGen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return $this->targetGen === 'final' ? '3. Matriks Generasi Final' : '2. Matriks Gen 0 (Awal)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
$genTitle = $this->targetGen === 'final' ? 'GENERASI FINAL (HASIL CONVERGENCE)' : 'GENERASI KE-0 (INISIALISASI POPULASI ACAK)';
|
||||||
|
return [
|
||||||
|
["TABEL POPULASI & BREAKDOWN FITNESS - {$genTitle}"],
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
'No Individu',
|
||||||
|
'Nilai Fitness',
|
||||||
|
'Formula Excel Fitness',
|
||||||
|
'Total Penalti',
|
||||||
|
'B1 (Guru)',
|
||||||
|
'B2 (Ruang)',
|
||||||
|
'B3 (Tersedia)',
|
||||||
|
'B4 (Kelas)',
|
||||||
|
'B5 (Level)',
|
||||||
|
'B6 (Sebaran)',
|
||||||
|
'B7 (Konsisten)',
|
||||||
|
'Kromosom Vector Gen [G, R, S, Hari_P]'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection(): Collection
|
||||||
|
{
|
||||||
|
$genLogs = $this->gaResult['generationLogs'] ?? [];
|
||||||
|
$targetLog = null;
|
||||||
|
|
||||||
|
if ($this->targetGen === 'final') {
|
||||||
|
$targetLog = end($genLogs);
|
||||||
|
} else {
|
||||||
|
foreach ($genLogs as $log) {
|
||||||
|
if ($log['generation'] == $this->targetGen) {
|
||||||
|
$targetLog = $log;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$targetLog) {
|
||||||
|
return new Collection([['Data generasi tidak ditemukan']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($targetLog['individus'] as $idx => $ind) {
|
||||||
|
$rowNum = $idx + 4; // Dimulai dari baris ke-4 Excel
|
||||||
|
$genesText = [];
|
||||||
|
if (!empty($ind['genesFormatted'])) {
|
||||||
|
foreach ($ind['genesFormatted'] as $gf) {
|
||||||
|
$genesText[] = $gf['text'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$geneStr = implode(' ', $genesText);
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'Individu[' . $ind['index'] . ']',
|
||||||
|
$ind['fitness'],
|
||||||
|
"=1/(1+D{$rowNum})", // Formula Excel Asli
|
||||||
|
$ind['penalty'],
|
||||||
|
'-', // Breakdown detail di baris Excel
|
||||||
|
'-',
|
||||||
|
'-',
|
||||||
|
'-',
|
||||||
|
'-',
|
||||||
|
'-',
|
||||||
|
'-',
|
||||||
|
$geneStr
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Collection($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
$sheet->mergeCells('A1:L1');
|
||||||
|
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(12);
|
||||||
|
$sheet->getStyle('A3:L3')->getFont()->setBold(true);
|
||||||
|
$sheet->getStyle('A3:L3')->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)->getStartColor()->setARGB('E2E8F0');
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
|
||||||
|
class GaPresentationCrossoverSheet implements FromView, WithTitle, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected array $simData;
|
||||||
|
|
||||||
|
public function __construct(array $simData)
|
||||||
|
{
|
||||||
|
$this->simData = $simData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '4. Operator Crossover';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
return view('admin.exports.presentation.sheet4-crossover', [
|
||||||
|
'sim' => $this->simData
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
|
||||||
|
class GaPresentationDatasetSheet implements FromView, WithTitle, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected array $simData;
|
||||||
|
|
||||||
|
public function __construct(array $simData)
|
||||||
|
{
|
||||||
|
$this->simData = $simData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '1. Dataset Sampel & Parameter';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
return view('admin.exports.presentation.sheet1-dataset', [
|
||||||
|
'sim' => $this->simData
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||||
|
|
||||||
|
class GaPresentationExport implements WithMultipleSheets
|
||||||
|
{
|
||||||
|
protected array $simData;
|
||||||
|
|
||||||
|
public function __construct(array $simData)
|
||||||
|
{
|
||||||
|
$this->simData = $simData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sheets(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new GaPresentationDatasetSheet($this->simData),
|
||||||
|
new GaPresentationFitnessSheet($this->simData),
|
||||||
|
new GaPresentationSelectionSheet($this->simData),
|
||||||
|
new GaPresentationCrossoverSheet($this->simData),
|
||||||
|
new GaPresentationMutationSheet($this->simData),
|
||||||
|
new GaPresentationNewGenSheet($this->simData),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
|
||||||
|
class GaPresentationFitnessSheet implements FromView, WithTitle, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected array $simData;
|
||||||
|
|
||||||
|
public function __construct(array $simData)
|
||||||
|
{
|
||||||
|
$this->simData = $simData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '2. Inisialisasi & Fitness (Gen 0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
return view('admin.exports.presentation.sheet2-fitness', [
|
||||||
|
'sim' => $this->simData
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
|
||||||
|
class GaPresentationMutationSheet implements FromView, WithTitle, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected array $simData;
|
||||||
|
|
||||||
|
public function __construct(array $simData)
|
||||||
|
{
|
||||||
|
$this->simData = $simData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '5. Operator Mutasi & Repair';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
return view('admin.exports.presentation.sheet5-mutation', [
|
||||||
|
'sim' => $this->simData
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
|
||||||
|
class GaPresentationNewGenSheet implements FromView, WithTitle, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected array $simData;
|
||||||
|
|
||||||
|
public function __construct(array $simData)
|
||||||
|
{
|
||||||
|
$this->simData = $simData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '6. Generasi Baru & Perbandingan';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
return view('admin.exports.presentation.sheet6-newgen', [
|
||||||
|
'sim' => $this->simData
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
|
||||||
|
class GaPresentationSelectionSheet implements FromView, WithTitle, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected array $simData;
|
||||||
|
|
||||||
|
public function __construct(array $simData)
|
||||||
|
{
|
||||||
|
$this->simData = $simData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '3. Operator Seleksi (Tournament)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
return view('admin.exports.presentation.sheet3-selection', [
|
||||||
|
'sim' => $this->simData
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||||
|
|
||||||
|
class GaSimulationExport implements WithMultipleSheets
|
||||||
|
{
|
||||||
|
protected array $gaResult;
|
||||||
|
|
||||||
|
public function __construct(array $gaResult)
|
||||||
|
{
|
||||||
|
$this->gaResult = $gaResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sheets(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'1. Ringkasan & Parameter' => new GaSummarySheet($this->gaResult),
|
||||||
|
'2. Matriks Generasi awal (Gen 0)' => new GaGenerationSheet($this->gaResult, 0),
|
||||||
|
'3. Matriks Generasi Final' => new GaGenerationSheet($this->gaResult, 'final'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class GaSummarySheet implements FromCollection, WithTitle, WithHeadings, WithStyles, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected array $gaResult;
|
||||||
|
|
||||||
|
public function __construct(array $gaResult)
|
||||||
|
{
|
||||||
|
$this->gaResult = $gaResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '1. Ringkasan & Parameter';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['SIMULASI & METRIK EVALUASI ALGORITMA GENETIKA (GA)'],
|
||||||
|
['LKP ROBOTKU - SISTEM PENJADWALAN OTOMATIS'],
|
||||||
|
[],
|
||||||
|
['Parameter / Metrik Engine', 'Nilai Evaluasi', 'Keterangan'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection(): Collection
|
||||||
|
{
|
||||||
|
$best = $this->gaResult['bestChromosome'] ?? null;
|
||||||
|
$fitness = $best ? $best->fitness : 0;
|
||||||
|
$penalty = $best ? $best->totalPenalty : 0;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
['Status Optimal', $fitness == 1.0 ? 'SUKSES (FITNESS OPTIMAL 1.0)' : 'TERCAPAI BATAS MAX GENERASI', 'Tidak ada bentrok jadwal (Total Penalti = 0)'],
|
||||||
|
['Nilai Fitness Terbaik', $fitness, 'Dihitung dengan rumus: 1 / (1 + Total Penalti)'],
|
||||||
|
['Total Penalti Aturan Batasan (Constraints)', $penalty, 'Jumlah total pelanggaran aturan B1 s.d B7'],
|
||||||
|
['Jumlah Generasi Ditempuh', $this->gaResult['generations'] ?? 0, 'Iterasi siklus seleksi, crossover, & mutasi'],
|
||||||
|
['Waktu Eksekusi (Execution Time)', ($this->gaResult['executionTime'] ?? 0) . ' Detik', 'Waktu komputasi engine GA'],
|
||||||
|
['Penggunaan Memori (Memory Usage)', ($this->gaResult['memoryUsage'] ?? 0) . ' KB', 'Penggunaan RAM server'],
|
||||||
|
[],
|
||||||
|
['RINCIAN BREAKDOWN PENALTI TERDETEKSI (BEST SOLUTION)', '', ''],
|
||||||
|
['B1 : Bentrok Guru', $best->penaltyBreakdown['B1'] ?? 0, 'Guru mengajar >1 kelas di (Hari, Sesi) sama'],
|
||||||
|
['B2 : Bentrok Ruangan LKP', $best->penaltyBreakdown['B2'] ?? 0, 'Ruangan dipakai >1 kelas di (Hari, Sesi) sama'],
|
||||||
|
['B3 : Ketersediaan Guru', $best->penaltyBreakdown['B3'] ?? 0, 'Guru diplot di jam ketidaktersediaannya'],
|
||||||
|
['B4 : Bentrok Kelompok Kelas', $best->penaltyBreakdown['B4'] ?? 0, '1 Kelas diplot di 2 tempat di jam sama'],
|
||||||
|
['B5 : Kualifikasi Level Guru', $best->penaltyBreakdown['B5'] ?? 0, 'Level maks guru < level kelas'],
|
||||||
|
['B6 : Sebaran Hari (2x/minggu)', $best->penaltyBreakdown['B6'] ?? 0, 'Hari sama atau selisih hari < 2 hari'],
|
||||||
|
['B7 : Konsistensi Guru (2x/minggu)', $best->penaltyBreakdown['B7'] ?? 0, 'Guru pertemuan 1 & 2 berbeda'],
|
||||||
|
];
|
||||||
|
|
||||||
|
return new Collection($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
$sheet->mergeCells('A1:C1');
|
||||||
|
$sheet->mergeCells('A2:C2');
|
||||||
|
$sheet->getStyle('A1:A2')->getFont()->setBold(true)->setSize(12);
|
||||||
|
$sheet->getStyle('A4:C4')->getFont()->setBold(true);
|
||||||
|
$sheet->getStyle('A4:C4')->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)->getStartColor()->setARGB('E2E8F0');
|
||||||
|
$sheet->getStyle('A11:C11')->getFont()->setBold(true);
|
||||||
|
$sheet->getStyle('A11:C11')->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)->getStartColor()->setARGB('CBD5E1');
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
|
||||||
|
class JadwalDetailSheet implements FromView, WithTitle, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected $jadwalMaster;
|
||||||
|
|
||||||
|
public function __construct($jadwalMaster)
|
||||||
|
{
|
||||||
|
$this->jadwalMaster = $jadwalMaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '2. Daftar Detail Jadwal';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
return view('admin.exports.excel-detail', [
|
||||||
|
'jadwalMaster' => $this->jadwalMaster
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
|
||||||
|
class JadwalMingguanExport implements FromView, WithTitle, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected $jadwalMaster;
|
||||||
|
|
||||||
|
public function __construct($jadwalMaster)
|
||||||
|
{
|
||||||
|
$this->jadwalMaster = $jadwalMaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return 'Master Jadwal Mingguan Robotku';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
return view('admin.exports.excel-detail', [
|
||||||
|
'jadwalMaster' => $this->jadwalMaster
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
|
||||||
|
class JadwalTimetableSheet implements FromView, WithTitle, ShouldAutoSize
|
||||||
|
{
|
||||||
|
protected $jadwalMaster;
|
||||||
|
protected $sesiList;
|
||||||
|
|
||||||
|
public function __construct($jadwalMaster, $sesiList)
|
||||||
|
{
|
||||||
|
$this->jadwalMaster = $jadwalMaster;
|
||||||
|
$this->sesiList = $sesiList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '1. Timetable Grid Mingguan';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
$days = [
|
||||||
|
1 => 'Senin',
|
||||||
|
2 => 'Selasa',
|
||||||
|
3 => 'Rabu',
|
||||||
|
4 => 'Kamis',
|
||||||
|
5 => 'Jumat',
|
||||||
|
6 => 'Sabtu',
|
||||||
|
7 => 'Minggu'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Grouping data jadwal berdasarkan "hari-sesi_id"
|
||||||
|
$jadwalGrouped = $this->jadwalMaster->groupBy(function ($item) {
|
||||||
|
return "{$item->hari}-{$item->sesi_id}";
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('admin.exports.excel-timetable', [
|
||||||
|
'jadwalMaster' => $this->jadwalMaster,
|
||||||
|
'sesiList' => $this->sesiList,
|
||||||
|
'days' => $days,
|
||||||
|
'jadwalGrouped' => $jadwalGrouped
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||||
|
use Maatwebsite\Excel\Events\AfterSheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
|
class LogbookKelasMultiExport implements WithMultipleSheets
|
||||||
|
{
|
||||||
|
protected $request;
|
||||||
|
|
||||||
|
public function __construct(Request $request)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sheets(): array
|
||||||
|
{
|
||||||
|
$sheets = [];
|
||||||
|
|
||||||
|
$query = Kelas::query()
|
||||||
|
->when($this->request->filled('semester'), function ($q) {
|
||||||
|
$q->where('semester', $this->request->semester);
|
||||||
|
})
|
||||||
|
->when($this->request->filled('tahun'), function ($q) {
|
||||||
|
$q->where('tahun_ajaran', $this->request->tahun);
|
||||||
|
})
|
||||||
|
->when($this->request->filled('is_ekstrakurikuler'), function ($q) {
|
||||||
|
$q->where('is_ekstrakurikuler', $this->request->is_ekstrakurikuler);
|
||||||
|
})
|
||||||
|
->when($this->request->filled('program_id'), function ($q) {
|
||||||
|
$q->where('program_id', $this->request->program_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
$allKelas = $query->orderBy('id', 'asc')->get();
|
||||||
|
|
||||||
|
if ($allKelas->count() <= 0) {
|
||||||
|
$sheets[] = new EmptySheetExport();
|
||||||
|
return $sheets;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($allKelas as $kelas) {
|
||||||
|
$sheets[] = new LogbookKelasSheetExport($kelas->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sheets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogbookKelasSheetExport implements FromCollection, WithHeadings, WithTitle, ShouldAutoSize, WithStyles, WithEvents
|
||||||
|
{
|
||||||
|
protected $kelasId;
|
||||||
|
protected $kelas;
|
||||||
|
|
||||||
|
public function __construct($kelasId)
|
||||||
|
{
|
||||||
|
$this->kelasId = $kelasId;
|
||||||
|
$this->kelas = Kelas::with(['programs', 'sekolah'])->find($kelasId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
if (!$this->kelas) {
|
||||||
|
return 'Kelas ' . $this->kelasId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat nama sheet max 31 karakter
|
||||||
|
$title = $this->kelas->nama_kelas;
|
||||||
|
$title = preg_replace('/[\\\\*?:\[\]\/]/', '', $title);
|
||||||
|
if (mb_strlen($title) > 31) {
|
||||||
|
$title = mb_substr($title, 0, 28) . '...';
|
||||||
|
}
|
||||||
|
return $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'No',
|
||||||
|
'Pertemuan / Sesi',
|
||||||
|
'Tanggal',
|
||||||
|
'Jam',
|
||||||
|
'Materi',
|
||||||
|
'Pengajar',
|
||||||
|
'Kegiatan',
|
||||||
|
'Perkembangan Siswa',
|
||||||
|
'Kendala & Solusi',
|
||||||
|
'Kebutuhan',
|
||||||
|
'Hal Baik',
|
||||||
|
'Catatan',
|
||||||
|
'Status',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
$logbooks = Logbooks::with(['Guru', 'Sesi'])
|
||||||
|
->where('kelas_id', $this->kelasId)
|
||||||
|
->orderBy('id', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$rows = collect();
|
||||||
|
$no = 1;
|
||||||
|
|
||||||
|
foreach ($logbooks as $log) {
|
||||||
|
$pengajar = $log->Guru ? $log->Guru->nama : '-';
|
||||||
|
|
||||||
|
$jam = '-';
|
||||||
|
if ($log->start && $log->end) {
|
||||||
|
$jam = date('H:i', strtotime($log->start)) . ' - ' . date('H:i', strtotime($log->end));
|
||||||
|
} elseif ($log->Sesi) {
|
||||||
|
$jam = $log->Sesi->jam;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tgl = '-';
|
||||||
|
if ($log->tanggal) {
|
||||||
|
$tgl = date('d M Y', strtotime($log->tanggal));
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows->push([
|
||||||
|
'no' => $no++,
|
||||||
|
'pertemuan' => $log->pertemuan ?? '-',
|
||||||
|
'tanggal' => $tgl,
|
||||||
|
'jam' => $jam,
|
||||||
|
'materi' => $log->materi ?? '-',
|
||||||
|
'pengajar' => $pengajar,
|
||||||
|
'kegiatan' => $log->kegiatan_minggu_ini ?? '-',
|
||||||
|
'perkembangan' => $log->perkembangan_siswa ?? '-',
|
||||||
|
'kendala' => $log->kendala_dan_solusi ?? '-',
|
||||||
|
'kebutuhan' => $log->kebutuhan_minggu_depan ?? '-',
|
||||||
|
'hal_baik' => $log->hal_baik ?? '-',
|
||||||
|
'catatan' => $log->catatan ?? '-',
|
||||||
|
'status' => $log->status ?? '-',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($logbooks->count() == 0) {
|
||||||
|
$rows->push([
|
||||||
|
'no' => 1,
|
||||||
|
'pertemuan' => '-',
|
||||||
|
'tanggal' => '-',
|
||||||
|
'jam' => '-',
|
||||||
|
'materi' => 'Belum ada logbook',
|
||||||
|
'pengajar' => '-',
|
||||||
|
'kegiatan' => '-',
|
||||||
|
'perkembangan' => '-',
|
||||||
|
'kendala' => '-',
|
||||||
|
'kebutuhan' => '-',
|
||||||
|
'hal_baik' => '-',
|
||||||
|
'catatan' => '-',
|
||||||
|
'status' => '-',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
6 => ['font' => ['bold' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
AfterSheet::class => function (AfterSheet $event) {
|
||||||
|
$sheet = $event->sheet->getDelegate();
|
||||||
|
|
||||||
|
$namaKelas = strtoupper($this->kelas ? $this->kelas->nama_kelas : 'LOGBOOK KELAS');
|
||||||
|
|
||||||
|
// Insert 5 baris di paling atas untuk Header Dokumen LKP Robotku
|
||||||
|
$sheet->insertNewRowBefore(1, 5);
|
||||||
|
|
||||||
|
// Judul Utama (Row 2)
|
||||||
|
$sheet->mergeCells('A2:M2');
|
||||||
|
$sheet->setCellValue('A2', 'LOGBOOK ' . $namaKelas);
|
||||||
|
$sheet->getStyle('A2')->getFont()->setBold(true)->setSize(14);
|
||||||
|
$sheet->getStyle('A2')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
|
||||||
|
// Subtitle 1 (Row 3)
|
||||||
|
$sheet->mergeCells('A3:M3');
|
||||||
|
$sheet->setCellValue('A3', 'LKP Robotku');
|
||||||
|
$sheet->getStyle('A3')->getFont()->setBold(true)->setSize(11);
|
||||||
|
$sheet->getStyle('A3')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
|
||||||
|
// Subtitle 2 (Row 4)
|
||||||
|
$sheet->mergeCells('A4:M4');
|
||||||
|
$sheet->setCellValue('A4', 'Kota Probolinggo');
|
||||||
|
$sheet->getStyle('A4')->getFont()->setBold(true)->setSize(10);
|
||||||
|
$sheet->getStyle('A4')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
|
||||||
|
// Style Header Tabel (Row 6)
|
||||||
|
$sheet->getStyle('A6:M6')->getFill()
|
||||||
|
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
|
||||||
|
->getStartColor()->setARGB('FFF2F2F2');
|
||||||
|
|
||||||
|
$sheet->getStyle('A6:M6')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle('A6:M6')->getAlignment()->setVertical(\PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER);
|
||||||
|
|
||||||
|
// Border Seluruh Tabel
|
||||||
|
$highestRow = $sheet->getHighestRow();
|
||||||
|
if ($highestRow >= 6) {
|
||||||
|
$sheet->getStyle("A6:M{$highestRow}")->getBorders()->getAllBorders()
|
||||||
|
->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
|
class MasterDataExport implements WithMultipleSheets
|
||||||
|
{
|
||||||
|
public function sheets(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new MasterGuruSheet(),
|
||||||
|
new MasterKelasSheet(),
|
||||||
|
new MasterRuanganSheet(),
|
||||||
|
new MasterLevelSheet(),
|
||||||
|
new MasterSesiSheet(),
|
||||||
|
new MasterHariSheet(),
|
||||||
|
new MasterSekolahSheet(),
|
||||||
|
new MasterKetersediaanGuruSheet(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MasterGuruSheet implements FromCollection, WithHeadings, WithTitle, ShouldAutoSize, WithStyles
|
||||||
|
{
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return 'Master Guru';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ['ID Guru', 'Nama Guru', 'Status', 'Level Maksimal Mengajar', 'Divisi Guru'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return DB::table('guru')
|
||||||
|
->select(
|
||||||
|
'guru.id',
|
||||||
|
'guru.nama',
|
||||||
|
'guru.status',
|
||||||
|
'guru.level_maksimal',
|
||||||
|
'guru.divisi'
|
||||||
|
)
|
||||||
|
->orderBy('guru.id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MasterKelasSheet implements FromCollection, WithHeadings, WithTitle, ShouldAutoSize, WithStyles
|
||||||
|
{
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return 'Master Kelas';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ['ID Kelas', 'Nama Kelas', 'Level', 'Level Number', 'Tipe Kelas', 'Frekuensi/Minggu', 'Sekolah', 'Hari Fix', 'Sesi Fix', 'Ruangan Fix'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
$hariMap = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
|
||||||
|
|
||||||
|
$rows = DB::table('kelas')
|
||||||
|
->leftJoin('programs', 'kelas.program_id', '=', 'programs.id')
|
||||||
|
->leftJoin('sekolah', 'kelas.sekolah_id', '=', 'sekolah.id')
|
||||||
|
->leftJoin('ruangan', 'kelas.ruangan_fix_id', '=', 'ruangan.id')
|
||||||
|
->leftJoin('sesi', 'kelas.sesi_fix', '=', 'sesi.id')
|
||||||
|
->select(
|
||||||
|
'kelas.id',
|
||||||
|
'kelas.nama_kelas',
|
||||||
|
'programs.level',
|
||||||
|
'programs.kelas as program_kelas',
|
||||||
|
'programs.level_number',
|
||||||
|
'kelas.is_ekstrakurikuler',
|
||||||
|
'kelas.frekuensi_per_minggu',
|
||||||
|
'sekolah.nama_sekolah',
|
||||||
|
'kelas.hari_fix',
|
||||||
|
'sesi.nama_sesi',
|
||||||
|
'ruangan.nama_ruangan'
|
||||||
|
)
|
||||||
|
->where('kelas.status', 'Aktif')
|
||||||
|
->orderBy('kelas.id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $rows->map(function ($r) use ($hariMap) {
|
||||||
|
$namaLevel = $r->level ? ('Level ' . $r->level . ($r->program_kelas ? ' (' . $r->program_kelas . ')' : '')) : '-';
|
||||||
|
return [
|
||||||
|
'id' => $r->id,
|
||||||
|
'nama_kelas' => $r->nama_kelas,
|
||||||
|
'nama_level' => $namaLevel,
|
||||||
|
'level_number' => $r->level_number ?? '-',
|
||||||
|
'tipe_kelas' => $r->is_ekstrakurikuler ? 'Ekstrakurikuler' : 'Reguler',
|
||||||
|
'frekuensi_per_minggu' => $r->frekuensi_per_minggu . 'x',
|
||||||
|
'nama_sekolah' => $r->nama_sekolah ?? 'LKP Robotku',
|
||||||
|
'hari_fix' => $r->hari_fix ? ($hariMap[$r->hari_fix] ?? $r->hari_fix) : '-',
|
||||||
|
'sesi_fix' => $r->nama_sesi ?? '-',
|
||||||
|
'ruangan_fix' => $r->nama_ruangan ?? '-',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MasterRuanganSheet implements FromCollection, WithHeadings, WithTitle, ShouldAutoSize, WithStyles
|
||||||
|
{
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return 'Master Ruangan';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ['ID Ruangan', 'Nama Ruangan', 'Tipe Lokasi', 'Status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return DB::table('ruangan')
|
||||||
|
->select('id', 'nama_ruangan', 'lokasi_tipe', 'status')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MasterLevelSheet implements FromCollection, WithHeadings, WithTitle, ShouldAutoSize, WithStyles
|
||||||
|
{
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return 'Master Level';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ['ID Level', 'Level', 'Kategori Rombel', 'Level Number', 'Goal', 'Penjelasan'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return DB::table('programs')
|
||||||
|
->select('id', 'level', 'kelas', 'level_number', 'goal', 'penjelasan')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MasterSesiSheet implements FromCollection, WithHeadings, WithTitle, ShouldAutoSize, WithStyles
|
||||||
|
{
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return 'Master Sesi';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ['ID Sesi', 'Nama Sesi', 'Jam Operasional'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return DB::table('sesi')
|
||||||
|
->select('id', 'nama_sesi', 'jam')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MasterHariSheet implements FromCollection, WithHeadings, WithTitle, ShouldAutoSize, WithStyles
|
||||||
|
{
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return 'Master Hari';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ['ID Hari', 'Nama Hari'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return DB::table('hari')
|
||||||
|
->select('id', 'nama_hari')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MasterSekolahSheet implements FromCollection, WithHeadings, WithTitle, ShouldAutoSize, WithStyles
|
||||||
|
{
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return 'Master Sekolah';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ['ID Sekolah', 'Nama Sekolah', 'Alamat'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return DB::table('sekolah')
|
||||||
|
->select('id', 'nama_sekolah', 'alamat_sekolah')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MasterKetersediaanGuruSheet implements FromCollection, WithHeadings, WithTitle, ShouldAutoSize, WithStyles
|
||||||
|
{
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return 'Ketersediaan Mengajar Guru';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ['Guru ID', 'Nama Guru', 'Hari', 'Sesi ID', 'Nama Sesi', 'Jam'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
$hariMap = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
|
||||||
|
|
||||||
|
$rows = DB::table('ketersediaan_mengajars')
|
||||||
|
->join('guru', 'ketersediaan_mengajars.guru_id', '=', 'guru.id')
|
||||||
|
->join('sesi', 'ketersediaan_mengajars.sesi_id', '=', 'sesi.id')
|
||||||
|
->select(
|
||||||
|
'guru.id as guru_id',
|
||||||
|
'guru.nama as nama_guru',
|
||||||
|
'ketersediaan_mengajars.hari',
|
||||||
|
'sesi.id as sesi_id',
|
||||||
|
'sesi.nama_sesi',
|
||||||
|
'sesi.jam'
|
||||||
|
)
|
||||||
|
->orderBy('guru.id')
|
||||||
|
->orderBy('ketersediaan_mengajars.hari')
|
||||||
|
->orderBy('sesi.id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $rows->map(function ($r) use ($hariMap) {
|
||||||
|
return [
|
||||||
|
'guru_id' => $r->guru_id,
|
||||||
|
'nama_guru' => $r->nama_guru,
|
||||||
|
'hari' => $hariMap[$r->hari] ?? $r->hari,
|
||||||
|
'sesi_id' => $r->sesi_id,
|
||||||
|
'nama_sesi' => $r->nama_sesi,
|
||||||
|
'jam' => $r->jam,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\Events;
|
||||||
|
use App\Models\PesertaEvent;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithCustomStartCell;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Events\AfterSheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithCustomValueBinder;
|
||||||
|
|
||||||
|
class PesertaEventExport extends DefaultValueBinder implements FromCollection, WithHeadings, WithEvents, WithMapping, WithCustomStartCell, WithColumnFormatting, WithTitle, WithCustomValueBinder
|
||||||
|
{
|
||||||
|
protected $eventId;
|
||||||
|
|
||||||
|
public function __construct($eventId)
|
||||||
|
{
|
||||||
|
$this->eventId = $eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return PesertaEvent::with('Peserta.Sekolah')->where('event_id', $this->eventId)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return ["No", "Nama Anak", "Orang Tua", "No HP Orangtua", "Kelas", "Sekolah", "Status", "Tgl. Daftar"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping supaya nomor urut otomatis
|
||||||
|
public function map($peserta): array
|
||||||
|
{
|
||||||
|
static $no = 1;
|
||||||
|
return [
|
||||||
|
$no++, // No
|
||||||
|
Str::title($peserta->Peserta->nama_anak ?? '-'),
|
||||||
|
Str::title($peserta->Peserta->nama_orangtua ?? '-'),
|
||||||
|
$peserta->Peserta->no_hp_orangtua
|
||||||
|
? "+62" . $peserta->Peserta->no_hp_orangtua
|
||||||
|
: 'No Hp Belum Diisi',
|
||||||
|
$peserta->Peserta->kelas ?? '-',
|
||||||
|
$peserta->Peserta?->Sekolah?->nama_sekolah ?? ($peserta->Peserta?->asal_sekolah ?? '-'),
|
||||||
|
$peserta->status ?? '-',
|
||||||
|
Carbon::parse($peserta->created_at)->locale('id')->translatedFormat('d M Y'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function columnFormats(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'D' => NumberFormat::FORMAT_TEXT,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bindValue(Cell $cell, $value)
|
||||||
|
{
|
||||||
|
if (is_string($value) && str_starts_with($value, '+')) {
|
||||||
|
$cell->setValueExplicit($value, DataType::TYPE_STRING);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::bindValue($cell, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startCell(): string
|
||||||
|
{
|
||||||
|
return 'A6';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerEvents(): array
|
||||||
|
{
|
||||||
|
$eventData = Events::find($this->eventId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
AfterSheet::class => function(AfterSheet $event) use ($eventData) {
|
||||||
|
$sheet = $event->sheet;
|
||||||
|
|
||||||
|
// Header utama
|
||||||
|
$sheet->mergeCells('A1:H1');
|
||||||
|
$sheet->mergeCells('A2:H2');
|
||||||
|
$sheet->mergeCells('A3:H3');
|
||||||
|
$sheet->mergeCells('A4:H4');
|
||||||
|
$sheet->mergeCells('A5:H5');
|
||||||
|
$sheet->setCellValue(
|
||||||
|
'A2',
|
||||||
|
strtoupper(($eventData ? $eventData->nama_event : ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($eventData) {
|
||||||
|
$sheet->setCellValue(
|
||||||
|
'A3',
|
||||||
|
"{$eventData->jenis_event} | {$eventData->lokasi_event} | " .
|
||||||
|
\Carbon\Carbon::parse($eventData->tanggal_event)->translatedFormat('d M Y')
|
||||||
|
);
|
||||||
|
|
||||||
|
$sheet->setCellValue(
|
||||||
|
'A4',
|
||||||
|
$eventData->waktu_mulai . " - " . $eventData->waktu_selesai . " WIB"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->getStyle('A2')->applyFromArray([
|
||||||
|
'font' => ['bold' => true, 'size' => 16],
|
||||||
|
'alignment' => ['horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sheet->getStyle('A3:A4')->applyFromArray([
|
||||||
|
'font' => ['italic' => true, 'size' => 11],
|
||||||
|
'alignment' => ['horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sheet->getStyle('A6:H6')->applyFromArray([
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'EFEFEF'],
|
||||||
|
],
|
||||||
|
'alignment' => ['horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$lastRow = $sheet->getHighestRow();
|
||||||
|
$sheet->getStyle("A6:H{$lastRow}")->applyFromArray([
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// $sheet->mergeCells("A" . ($lastRow+1) . ":C" . ($lastRow+1));
|
||||||
|
$sheet->setCellValue("G" . ($lastRow+1), "Total");
|
||||||
|
$sheet->setCellValue("H" . ($lastRow+1), $lastRow - 6);
|
||||||
|
|
||||||
|
$sheet->getStyle("G" . ($lastRow+1) . ":H" . ($lastRow+1))->applyFromArray([
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'alignment' => ['horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_RIGHT],
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$sheet->getStyle("G" . ($lastRow+1))->applyFromArray([
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'EFEFEF'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$lastRow = $sheet->getHighestRow();
|
||||||
|
$sheet->getStyle("A7:A{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("D7:D{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("E7:E{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("F7:F{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("G7:G{$lastRow}")
|
||||||
|
->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
|
||||||
|
|
||||||
|
$sheet->getColumnDimension('A')->setWidth(5);
|
||||||
|
$sheet->getColumnDimension('B')->setWidth(32);
|
||||||
|
$sheet->getColumnDimension('C')->setWidth(32);
|
||||||
|
$sheet->getColumnDimension('D')->setWidth(25);
|
||||||
|
$sheet->getColumnDimension('E')->setWidth(8);
|
||||||
|
$sheet->getColumnDimension('F')->setWidth(32);
|
||||||
|
$sheet->getColumnDimension('G')->setWidth(19);
|
||||||
|
$sheet->getColumnDimension('H')->setWidth(19);
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
$event = Events::find($this->eventId);
|
||||||
|
return $event ? $event->nama_event : 'Event';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,274 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
use App\Models\HistoriSiswa;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use App\Models\Siswa;
|
||||||
|
use App\Models\Programs;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
use App\Models\Guru;
|
||||||
|
use App\Models\Ruangan;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Sesi;
|
||||||
|
use App\Models\JadwalKelas;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function insertHistori(){
|
||||||
|
$data = Siswa::all();
|
||||||
|
$successCount = 0;
|
||||||
|
$failedEntries = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($data); $i++) {
|
||||||
|
try {
|
||||||
|
$result = HistoriSiswa::create([
|
||||||
|
'siswa_id' => $data[$i]->id,
|
||||||
|
'status' => $data[$i]->status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$successCount++;
|
||||||
|
} else {
|
||||||
|
$failedEntries[] = [
|
||||||
|
'siswa_id' => $data[$i]->id,
|
||||||
|
'reason' => 'Create method returned false'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$failedEntries[] = [
|
||||||
|
'siswa_id' => $data[$i]->id,
|
||||||
|
'reason' => $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalRecords = count($data);
|
||||||
|
$failedCount = count($failedEntries);
|
||||||
|
|
||||||
|
if ($failedCount > 0) {
|
||||||
|
// Log atau tampilkan error
|
||||||
|
|
||||||
|
// Opsional: kembalikan informasi kegagalan jika diperlukan
|
||||||
|
return [
|
||||||
|
'status' => 'partial',
|
||||||
|
'message' => "{$successCount} data berhasil disimpan, {$failedCount} data gagal",
|
||||||
|
'success_count' => $successCount,
|
||||||
|
'failed_count' => $failedCount,
|
||||||
|
'failed_entries' => $failedEntries
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Semua berhasil
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => "Semua {$totalRecords} data berhasil disimpan"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$data = Siswa::with('Programs')->get();
|
||||||
|
$programs = Programs::all();
|
||||||
|
$programsCount = $programs->count();
|
||||||
|
$groupPrograms = DB::table('programs')
|
||||||
|
->leftJoin('siswa', 'programs.id', '=', 'siswa.program_id')
|
||||||
|
->selectRaw('programs.level, programs.kelas, COUNT(siswa.id) AS jumlah_peserta_program, (SELECT COUNT(*) FROM siswa) AS total_peserta')
|
||||||
|
->groupBy('programs.id', 'programs.level', 'programs.kelas')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$totalGuru = Guru::count();
|
||||||
|
$totalRuangan = Ruangan::count();
|
||||||
|
$totalKelas = Kelas::count();
|
||||||
|
$totalSesi = Sesi::count();
|
||||||
|
$totalJadwal = JadwalKelas::count();
|
||||||
|
$totalSiswaAktif = $data->whereIn('status', ['Aktif 24 Sesi', 'Aktif 12 Sesi', 'Aktif Ekstra'])->count();
|
||||||
|
|
||||||
|
$gaResult = session('ga_log_result');
|
||||||
|
|
||||||
|
return view('admin.pages.dashboard', compact(
|
||||||
|
'data',
|
||||||
|
'groupPrograms',
|
||||||
|
'programsCount',
|
||||||
|
'totalGuru',
|
||||||
|
'totalRuangan',
|
||||||
|
'totalKelas',
|
||||||
|
'totalSesi',
|
||||||
|
'totalJadwal',
|
||||||
|
'totalSiswaAktif',
|
||||||
|
'gaResult'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dashboardData(){
|
||||||
|
Carbon::setLocale('id');
|
||||||
|
|
||||||
|
$now = Carbon::now();
|
||||||
|
$tanggalMulai = $now->copy()->subMonths(5)->startOfMonth();
|
||||||
|
$tanggalAkhir = $now->copy()->endOfMonth();
|
||||||
|
|
||||||
|
// --- Data reguler per bulan ---
|
||||||
|
$siswaPerBulanReguler = HistoriSiswa::whereBetween('created_at', [$tanggalMulai, $tanggalAkhir])
|
||||||
|
->where(function($q) {
|
||||||
|
$q->where('status', 'Aktif 24 Sesi')
|
||||||
|
->orWhere('status', 'Aktif 12 Sesi');
|
||||||
|
})
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as bulan_id, COUNT(id) as jumlah_siswa")
|
||||||
|
->groupBy('bulan_id')
|
||||||
|
->pluck('jumlah_siswa', 'bulan_id'); // hasil key => value
|
||||||
|
|
||||||
|
// --- Data ekstra per bulan ---
|
||||||
|
$siswaPerBulanEkstra = HistoriSiswa::whereBetween('created_at', [$tanggalMulai, $tanggalAkhir])
|
||||||
|
->where('status', 'Aktif Ekstra')
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as bulan_id, COUNT(id) as jumlah_siswa")
|
||||||
|
->groupBy('bulan_id')
|
||||||
|
->pluck('jumlah_siswa', 'bulan_id');
|
||||||
|
|
||||||
|
// --- Siapkan array per bulan (6 bulan terakhir) ---
|
||||||
|
$dataLengkap = [];
|
||||||
|
$dataLengkapReguler = [];
|
||||||
|
$dataLengkapEkstra = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < 6; $i++) {
|
||||||
|
$bulan = $now->copy()->subMonths(5 - $i);
|
||||||
|
$bulanId = $bulan->format('Y-m');
|
||||||
|
$bulanTampil = $bulan->translatedFormat('M Y');
|
||||||
|
|
||||||
|
$jumlahReguler = $siswaPerBulanReguler[$bulanId] ?? 0;
|
||||||
|
$jumlahEkstra = $siswaPerBulanEkstra[$bulanId] ?? 0;
|
||||||
|
|
||||||
|
$dataLengkap[] = [
|
||||||
|
'bulan' => $bulanTampil,
|
||||||
|
'jumlah_siswa' => $jumlahReguler + $jumlahEkstra,
|
||||||
|
];
|
||||||
|
|
||||||
|
$dataLengkapReguler[] = [
|
||||||
|
'bulan' => $bulanTampil,
|
||||||
|
'jumlah_siswa' => $jumlahReguler,
|
||||||
|
];
|
||||||
|
|
||||||
|
$dataLengkapEkstra[] = [
|
||||||
|
'bulan' => $bulanTampil,
|
||||||
|
'jumlah_siswa' => $jumlahEkstra,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Total siswa (berdasarkan status terakhir di tabel Siswa) ---
|
||||||
|
$data = Siswa::get();
|
||||||
|
$total_siswa_reguler = $data->where('status', 'Aktif 24 Sesi')->count()
|
||||||
|
+ $data->where('status', 'Aktif 12 Sesi')->count();
|
||||||
|
$total_siswa_ekstra = $data->where('status', 'Aktif Ekstra')->count();
|
||||||
|
$total_siswa_aktif = $total_siswa_reguler + $total_siswa_ekstra;
|
||||||
|
|
||||||
|
// --- Event dan sesi ---
|
||||||
|
$total_sesi = Logbooks::count();
|
||||||
|
$total_kelas = Kelas::count();
|
||||||
|
$total_guru = Guru::count();
|
||||||
|
$total_ruangan = Ruangan::count();
|
||||||
|
|
||||||
|
// --- Data Notifikasi GA Terakhir ---
|
||||||
|
$gaResult = session('ga_log_result');
|
||||||
|
$gaDataFormatted = null;
|
||||||
|
if ($gaResult) {
|
||||||
|
$gaDataFormatted = [
|
||||||
|
'semester' => $gaResult['semester'] ?? 'Genap',
|
||||||
|
'tahunAjaran' => $gaResult['tahunAjaran'] ?? '2025/2026',
|
||||||
|
'executionTime' => $gaResult['executionTime'] ?? '0.00',
|
||||||
|
'generations' => $gaResult['generations'] ?? 0,
|
||||||
|
'fitness' => isset($gaResult['bestChromosome']) ? number_format($gaResult['bestChromosome']->fitness, 4) : '1.0000',
|
||||||
|
'isOptimal' => $gaResult['isOptimal'] ?? true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Kelas hari ini ---
|
||||||
|
$jharini = Logbooks::whereDate('tanggal', Carbon::today())->count();
|
||||||
|
$jcompleted = Logbooks::whereDate('tanggal', Carbon::today())
|
||||||
|
->whereIn('status', ['completed', 'libur'])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
// --- Izin Pending ---
|
||||||
|
$izinpending = [];
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'total_siswa' => $total_siswa_reguler,
|
||||||
|
'total_siswa_ekstra' => $total_siswa_ekstra,
|
||||||
|
'total_siswa_aktif' => $total_siswa_aktif,
|
||||||
|
'data_perbulan' => $dataLengkap,
|
||||||
|
'data_perbulanreguler' => $dataLengkapReguler,
|
||||||
|
'data_perbulanekstra' => $dataLengkapEkstra,
|
||||||
|
'total_sesi' => $total_sesi,
|
||||||
|
'total_kelas' => $total_kelas,
|
||||||
|
'total_guru' => $total_guru,
|
||||||
|
'total_ruangan' => $total_ruangan,
|
||||||
|
'ga_result' => $gaDataFormatted,
|
||||||
|
'jharini' => $jharini,
|
||||||
|
'jcompleted' => $jcompleted,
|
||||||
|
'izinpending' => $izinpending,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dashboardChartData(Request $request)
|
||||||
|
{
|
||||||
|
Carbon::setLocale('id');
|
||||||
|
|
||||||
|
$monthCount = min((int) $request->get('months', 5), 24);
|
||||||
|
if ($monthCount < 1) $monthCount = 1;
|
||||||
|
|
||||||
|
$now = Carbon::now();
|
||||||
|
$tanggalMulai = $now->copy()->subMonths($monthCount - 1)->startOfMonth();
|
||||||
|
$tanggalAkhir = $now->copy()->endOfMonth();
|
||||||
|
|
||||||
|
// --- Data reguler per bulan ---
|
||||||
|
$siswaPerBulanReguler = HistoriSiswa::whereBetween('created_at', [$tanggalMulai, $tanggalAkhir])
|
||||||
|
->where(function($q) {
|
||||||
|
$q->where('status', 'Aktif 24 Sesi')
|
||||||
|
->orWhere('status', 'Aktif 12 Sesi');
|
||||||
|
})
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as bulan_id, COUNT(id) as jumlah_siswa")
|
||||||
|
->groupBy('bulan_id')
|
||||||
|
->pluck('jumlah_siswa', 'bulan_id');
|
||||||
|
|
||||||
|
// --- Data ekstra per bulan ---
|
||||||
|
$siswaPerBulanEkstra = HistoriSiswa::whereBetween('created_at', [$tanggalMulai, $tanggalAkhir])
|
||||||
|
->where('status', 'Aktif Ekstra')
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as bulan_id, COUNT(id) as jumlah_siswa")
|
||||||
|
->groupBy('bulan_id')
|
||||||
|
->pluck('jumlah_siswa', 'bulan_id');
|
||||||
|
|
||||||
|
$labels = [];
|
||||||
|
$dataTotal = [];
|
||||||
|
$dataReguler = [];
|
||||||
|
$dataEkstra = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $monthCount; $i++) {
|
||||||
|
$bulan = $now->copy()->subMonths(($monthCount - 1) - $i);
|
||||||
|
$bulanId = $bulan->format('Y-m');
|
||||||
|
|
||||||
|
$jumlahReguler = $siswaPerBulanReguler[$bulanId] ?? 0;
|
||||||
|
$jumlahEkstra = $siswaPerBulanEkstra[$bulanId] ?? 0;
|
||||||
|
|
||||||
|
$labels[] = $bulan->translatedFormat('M Y');
|
||||||
|
$dataTotal[] = $jumlahReguler + $jumlahEkstra;
|
||||||
|
$dataReguler[] = $jumlahReguler;
|
||||||
|
$dataEkstra[] = $jumlahEkstra;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'labels' => $labels,
|
||||||
|
'data_total' => $dataTotal,
|
||||||
|
'data_reguler' => $dataReguler,
|
||||||
|
'data_ekstra' => $dataEkstra,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,495 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\DivisiGuru;
|
||||||
|
use App\Models\Guru;
|
||||||
|
use App\Models\JadwalKelas;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use App\Models\PengajarLogbook;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class GuruController extends Controller
|
||||||
|
{
|
||||||
|
public function index(){
|
||||||
|
$divisi_guru = DivisiGuru::all();
|
||||||
|
$data = Guru::orderByRaw("FIELD(status, 'Aktif', 'Non Aktif')")->orderBy('start_join', 'asc')->get();
|
||||||
|
return view('admin.pages.guru', compact('data', 'divisi_guru'));
|
||||||
|
}
|
||||||
|
public function generateNID($date)
|
||||||
|
{
|
||||||
|
$tahunMasuk = date('Y', strtotime($date));
|
||||||
|
$lastPegawai = Guru::whereYear('created_at', $tahunMasuk)->latest('id')->first();
|
||||||
|
|
||||||
|
if ($lastPegawai) {
|
||||||
|
$lastNumber = (int) substr($lastPegawai->nid, -4);
|
||||||
|
$newNumber = str_pad($lastNumber + 1, 4, '0', STR_PAD_LEFT);
|
||||||
|
} else {
|
||||||
|
$newNumber = '0001';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tahunMasuk . $newNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDataGuru(){
|
||||||
|
|
||||||
|
$data = Guru::all();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'Semua' => $data->count(),
|
||||||
|
'Aktif' => $data->where('status', 'Aktif')->count(),
|
||||||
|
'Non-Aktif' => $data->where('status', 'Non Aktif')->count(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dateFormat($date){
|
||||||
|
$tanggalparse = $date
|
||||||
|
? Carbon::parse($date)->locale('id')->translatedFormat('d F Y')
|
||||||
|
: null;
|
||||||
|
return $tanggalparse;
|
||||||
|
}
|
||||||
|
public function show($id, Request $request){
|
||||||
|
$divisi_guru = DivisiGuru::all();
|
||||||
|
$guru = Guru::findOrFail($id);
|
||||||
|
|
||||||
|
// ====== Alltime Stats ======
|
||||||
|
// Sesi/Kelas dari JadwalKelas dan Logbook
|
||||||
|
$kelasIdsJadwal = JadwalKelas::where('guru_id', $id)->pluck('kelas_id');
|
||||||
|
$kelasIdsLogbook = Logbooks::where('guru_id', $id)->pluck('kelas_id');
|
||||||
|
$kelasIds = $kelasIdsJadwal->concat($kelasIdsLogbook)->unique()->filter();
|
||||||
|
|
||||||
|
$kelasPic = Kelas::whereIn('id', $kelasIds)->get();
|
||||||
|
$sesiPic = Logbooks::whereIn('kelas_id', $kelasIds)->count();
|
||||||
|
$sesiCompleted = Logbooks::whereIn('kelas_id', $kelasIds)->where('status', 'completed')->count();
|
||||||
|
$jumlahKelas = $kelasPic->count();
|
||||||
|
|
||||||
|
// Sesi sebagai pengajar (via logbook.guru_id)
|
||||||
|
$sesiPengajar = Logbooks::where('guru_id', $id)->count();
|
||||||
|
|
||||||
|
// ====== Rekap per Semester ======
|
||||||
|
$semestersPic = Kelas::whereIn('id', $kelasIds)
|
||||||
|
->whereNotNull('semester')
|
||||||
|
->whereNotNull('tahun_ajaran')
|
||||||
|
->select('semester', 'tahun_ajaran')
|
||||||
|
->distinct()
|
||||||
|
->get()
|
||||||
|
->toBase()
|
||||||
|
->map(fn($k) => ['semester' => $k->semester, 'tahun_ajaran' => $k->tahun_ajaran]);
|
||||||
|
|
||||||
|
$semestersPengajar = Logbooks::where('guru_id', $id)
|
||||||
|
->join('kelas', 'logbook.kelas_id', '=', 'kelas.id')
|
||||||
|
->whereNotNull('kelas.semester')
|
||||||
|
->whereNotNull('kelas.tahun_ajaran')
|
||||||
|
->select('kelas.semester', 'kelas.tahun_ajaran')
|
||||||
|
->distinct()
|
||||||
|
->get()
|
||||||
|
->toBase()
|
||||||
|
->map(fn($k) => ['semester' => $k->semester, 'tahun_ajaran' => $k->tahun_ajaran]);
|
||||||
|
|
||||||
|
// Gabungkan dan deduplikasi semester
|
||||||
|
$allSemesters = $semestersPic->concat($semestersPengajar)
|
||||||
|
->filter(fn($s) => !empty($s['semester']) && !empty($s['tahun_ajaran']))
|
||||||
|
->unique(fn($s) => $s['semester'] . '-' . $s['tahun_ajaran'])
|
||||||
|
->sortByDesc(fn($s) => $s['tahun_ajaran'] . '-' . $s['semester'])
|
||||||
|
->values();
|
||||||
|
|
||||||
|
// Hitung jumlah logbook + kelas per semester
|
||||||
|
$rekap = $allSemesters->map(function ($sem) use ($id, $kelasIds) {
|
||||||
|
$kelas = Kelas::whereIn('id', $kelasIds)
|
||||||
|
->where('semester', $sem['semester'])
|
||||||
|
->where('tahun_ajaran', $sem['tahun_ajaran'])
|
||||||
|
->get();
|
||||||
|
$semKelasIds = $kelas->pluck('id');
|
||||||
|
|
||||||
|
$logbookPengajar = Logbooks::where('guru_id', $id)
|
||||||
|
->whereIn('kelas_id',
|
||||||
|
Kelas::where('semester', $sem['semester'])
|
||||||
|
->where('tahun_ajaran', $sem['tahun_ajaran'])
|
||||||
|
->pluck('id')
|
||||||
|
)->count();
|
||||||
|
|
||||||
|
$logbookPic = Logbooks::whereIn('kelas_id', $semKelasIds)->count();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'semester' => $sem['semester'],
|
||||||
|
'tahun_ajaran' => $sem['tahun_ajaran'],
|
||||||
|
'jumlah_kelas' => $kelas->count(),
|
||||||
|
'sesi_pic' => $logbookPic,
|
||||||
|
'sesi_pengajar' => $logbookPengajar,
|
||||||
|
'total_sesi' => max($logbookPic, $logbookPengajar),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$data = $guru;
|
||||||
|
|
||||||
|
// ====== Ketersediaan Mengajar ======
|
||||||
|
$sesiList = \App\Models\Sesi::orderBy('id', 'asc')->get();
|
||||||
|
$ketersediaan = $guru->ketersediaanMengajar->groupBy('hari');
|
||||||
|
$days = [1, 2, 3, 4, 5, 6];
|
||||||
|
$ketersediaanData = [];
|
||||||
|
foreach ($days as $day) {
|
||||||
|
$ketersediaanData[$day] = $ketersediaan->has($day)
|
||||||
|
? $ketersediaan->get($day)->pluck('sesi_id')->toArray()
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.pages.guru_detail', compact(
|
||||||
|
'data', 'sesiPic', 'sesiPengajar', 'sesiCompleted', 'jumlahKelas', 'rekap', 'divisi_guru', 'sesiList', 'ketersediaanData'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function raportDetail($id, $semester, $tahun_ajaran){
|
||||||
|
$guru = Guru::findOrFail($id);
|
||||||
|
|
||||||
|
$kelasIdsJadwal = JadwalKelas::where('guru_id', $id)->pluck('kelas_id');
|
||||||
|
$kelasIdsLogbook = Logbooks::where('guru_id', $id)->pluck('kelas_id');
|
||||||
|
$allTeacherKelasIds = $kelasIdsJadwal->concat($kelasIdsLogbook)->unique()->filter();
|
||||||
|
|
||||||
|
// Kelas sebagai pengajar di semester ini
|
||||||
|
$kelasPic = Kelas::whereIn('id', $allTeacherKelasIds)
|
||||||
|
->where('semester', $semester)
|
||||||
|
->where('tahun_ajaran', $tahun_ajaran)
|
||||||
|
->with('Programs')
|
||||||
|
->get();
|
||||||
|
$kelasIdsPic = $kelasPic->pluck('id');
|
||||||
|
|
||||||
|
// Logbook di semester ini
|
||||||
|
$logbookPic = Logbooks::whereIn('kelas_id', $kelasIdsPic)
|
||||||
|
->with('Kelas.Programs', 'FotoLogbook', 'Guru')
|
||||||
|
->orderBy('tanggal', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Logbook sebagai pengajar di semester ini
|
||||||
|
$logbookPengajar = Logbooks::where('guru_id', $id)
|
||||||
|
->whereIn('kelas_id',
|
||||||
|
Kelas::where('semester', $semester)
|
||||||
|
->where('tahun_ajaran', $tahun_ajaran)
|
||||||
|
->pluck('id')
|
||||||
|
)
|
||||||
|
->with('Kelas.Programs', 'FotoLogbook', 'Guru')
|
||||||
|
->orderBy('tanggal', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.pages.guru_raport_detail', compact(
|
||||||
|
'guru', 'semester', 'tahun_ajaran',
|
||||||
|
'kelasPic', 'logbookPic', 'logbookPengajar'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getData($id){
|
||||||
|
$data = Guru::where('id', $id)->first();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'id' => $data->id,
|
||||||
|
'nama' => $data->nama ?? null,
|
||||||
|
'no_hp' => $data->no_hp ?? null,
|
||||||
|
'tempat_lahir' => $data->tempat_lahir ?? null,
|
||||||
|
'tanggal_lahir' => $data->tanggal_lahir ?? null,
|
||||||
|
'tanggal_lahir_parse' => $this->dateFormat($data->tanggal_lahir),
|
||||||
|
'jenis_kelamin' => $data->jenis_kelamin ?? null,
|
||||||
|
'email' => $data->email ?? null,
|
||||||
|
'alamat' => $data->alamat ?? null,
|
||||||
|
'pendidikan_terakhir' => $data->pendidikan_terakhir ?? null,
|
||||||
|
'instansi_pendidikan' => $data->instansi_pendidikan ?? null,
|
||||||
|
'jurusan' => $data->jurusan ?? null,
|
||||||
|
'divisi' => $data->divisi ?? null,
|
||||||
|
'jabatan' => $data->jabatan ?? null,
|
||||||
|
'level_maksimal' => $data->level_maksimal ?? null,
|
||||||
|
'start_join' => $data->start_join ?? null,
|
||||||
|
'start_join_parse' => $this->dateFormat($data->start_join),
|
||||||
|
'pekerja' => $data->pekerja ?? null,
|
||||||
|
'status' => $data->status ?? null,
|
||||||
|
'no_rekening' => $data->no_rekening ?? null,
|
||||||
|
'bank_keterangan' => $data->bank_keterangan ?? null,
|
||||||
|
'talent_mapping' => $data->talent_mapping ?? null,
|
||||||
|
'foto' => $data->foto ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function store(Request $request){
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama' => 'required|string|max:255',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama.required' => 'Nama anak wajib diisi',
|
||||||
|
'nama.max' => 'Nama anak terlalu panjang',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($request->no_hp !== null || $request->no_hp !== ''){
|
||||||
|
$str = ltrim($request->no_hp, '0');
|
||||||
|
$validated['no_hp'] = $str;
|
||||||
|
}
|
||||||
|
if ($request->tempat_lahir !== null || $request->tempat_lahir !== ''){
|
||||||
|
$validated['tempat_lahir'] = $request['tempat_lahir'];
|
||||||
|
}
|
||||||
|
if ($request->tanggal_lahir !== null || $request->tanggal_lahir !== ''){
|
||||||
|
$validated['tanggal_lahir'] = $request['tanggal_lahir'];
|
||||||
|
}
|
||||||
|
if ($request->jenis_kelamin !== null || $request->jenis_kelamin !== ''){
|
||||||
|
$validated['jenis_kelamin'] = $request['jenis_kelamin'];
|
||||||
|
}else{
|
||||||
|
$validated['jenis_kelamin'] = 'L';
|
||||||
|
}
|
||||||
|
if ($request->email !== null || $request->email !== ''){
|
||||||
|
$validated['email'] = $request['email'];
|
||||||
|
}
|
||||||
|
if ($request->alamat !== null || $request->alamat !== ''){
|
||||||
|
$validated['alamat'] = $request['alamat'];
|
||||||
|
}
|
||||||
|
if ($request->pendidikan_terakhir !== null || $request->pendidikan_terakhir !== ''){
|
||||||
|
$validated['pendidikan_terakhir'] = $request['pendidikan_terakhir'];
|
||||||
|
}
|
||||||
|
if ($request->instansi_pendidikan !== null || $request->instansi_pendidikan !== ''){
|
||||||
|
$validated['instansi_pendidikan'] = $request['instansi_pendidikan'];
|
||||||
|
}
|
||||||
|
if ($request->jurusan !== null || $request->jurusan !== ''){
|
||||||
|
$validated['jurusan'] = $request['jurusan'];
|
||||||
|
}
|
||||||
|
if ($request->divisi !== null || $request->divisi !== ''){
|
||||||
|
$validated['divisi'] = $request['divisi'];
|
||||||
|
}
|
||||||
|
if ($request->jabatan !== null || $request->jabatan !== ''){
|
||||||
|
$validated['jabatan'] = $request['jabatan'];
|
||||||
|
}
|
||||||
|
if ($request->filled('level_maksimal') || $request->level_maksimal !== null){
|
||||||
|
$validated['level_maksimal'] = $request['level_maksimal'] ?: null;
|
||||||
|
}
|
||||||
|
if ($request->start_join !== null || $request->start_join !== ''){
|
||||||
|
$validated['start_join'] = $request['start_join'];
|
||||||
|
$validated['nid'] = $this->generateNID($request->start_join);
|
||||||
|
}
|
||||||
|
if ($request->pekerja !== null || $request->pekerja !== ''){
|
||||||
|
$validated['pekerja'] = $request['pekerja'];
|
||||||
|
}
|
||||||
|
if ($request->status !== null || $request->status !== ''){
|
||||||
|
$validated['status'] = $request['status'];
|
||||||
|
}
|
||||||
|
if ($request->no_rekening !== null || $request->no_rekening !== ''){
|
||||||
|
$validated['no_rekening'] = $request['no_rekening'];
|
||||||
|
}
|
||||||
|
if ($request->bank_keterangan !== null || $request->bank_keterangan !== ''){
|
||||||
|
$validated['bank_keterangan'] = $request['bank_keterangan'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('talent_mapping')) {
|
||||||
|
$file = $request->file('talent_mapping');
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
$file->storeAs('talent-mapping', $hashedName, 'public');
|
||||||
|
$validated['talent_mapping'] = 'talent-mapping/' . $hashedName;
|
||||||
|
}
|
||||||
|
if ($request->hasFile('foto')) {
|
||||||
|
$file = $request->file('foto');
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
$file->storeAs('anak', $hashedName, 'public');
|
||||||
|
$validated['foto'] = 'anak/' . $hashedName;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $validated['nama'],
|
||||||
|
'username' => strtolower($validated['nama']),
|
||||||
|
'email' => $request->email,
|
||||||
|
'no_hp_orangtua' => $request->no_hp,
|
||||||
|
'password' => bcrypt('12345'),
|
||||||
|
'role' => 'guru',
|
||||||
|
]);
|
||||||
|
$validated['user_id'] = $user->id;
|
||||||
|
Guru::create($validated);
|
||||||
|
|
||||||
|
return redirect('/admin/guru')->with('success', 'Data Berhasil Disimpan');
|
||||||
|
}catch (QueryException $e){
|
||||||
|
return redirect('/admin/guru')->with('error', 'Data Gagal Disimpan');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public function update(Request $request, $id){
|
||||||
|
$data = Guru::where('id', $id)->first();
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama' => 'required|string|max:255',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama.required' => 'Nama anak wajib diisi',
|
||||||
|
'nama.max' => 'Nama anak terlalu panjang',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($request->no_hp !== null || $request->no_hp !== ''){
|
||||||
|
$str = ltrim($request->no_hp, '0');
|
||||||
|
$validated['no_hp'] = $str;
|
||||||
|
}
|
||||||
|
if ($request->tempat_lahir !== null || $request->tempat_lahir !== ''){
|
||||||
|
$validated['tempat_lahir'] = $request['tempat_lahir'];
|
||||||
|
}
|
||||||
|
if ($request->tanggal_lahir !== null || $request->tanggal_lahir !== ''){
|
||||||
|
$validated['tanggal_lahir'] = $request['tanggal_lahir'];
|
||||||
|
}
|
||||||
|
if ($request->jenis_kelamin !== null || $request->jenis_kelamin !== ''){
|
||||||
|
$validated['jenis_kelamin'] = $request['jenis_kelamin'];
|
||||||
|
}else{
|
||||||
|
$validated['jenis_kelamin'] = 'L';
|
||||||
|
}
|
||||||
|
if ($request->email !== null || $request->email !== ''){
|
||||||
|
$validated['email'] = $request['email'];
|
||||||
|
}
|
||||||
|
if ($request->alamat !== null || $request->alamat !== ''){
|
||||||
|
$validated['alamat'] = $request['alamat'];
|
||||||
|
}
|
||||||
|
if ($request->pendidikan_terakhir !== null || $request->pendidikan_terakhir !== ''){
|
||||||
|
$validated['pendidikan_terakhir'] = $request['pendidikan_terakhir'];
|
||||||
|
}
|
||||||
|
if ($request->instansi_pendidikan !== null || $request->instansi_pendidikan !== ''){
|
||||||
|
$validated['instansi_pendidikan'] = $request['instansi_pendidikan'];
|
||||||
|
}
|
||||||
|
if ($request->jurusan !== null || $request->jurusan !== ''){
|
||||||
|
$validated['jurusan'] = $request['jurusan'];
|
||||||
|
}
|
||||||
|
if ($request->divisi !== null || $request->divisi !== ''){
|
||||||
|
$validated['divisi'] = $request['divisi'];
|
||||||
|
}
|
||||||
|
if ($request->jabatan !== null || $request->jabatan !== ''){
|
||||||
|
$validated['jabatan'] = $request['jabatan'];
|
||||||
|
}
|
||||||
|
if ($request->has('level_maksimal')){
|
||||||
|
$validated['level_maksimal'] = $request['level_maksimal'] ?: null;
|
||||||
|
}
|
||||||
|
if ($request->start_join !== null || $request->start_join !== ''){
|
||||||
|
$validated['start_join'] = $request['start_join'];
|
||||||
|
// $validated['nid'] = $this->generateNID($request->start_join);
|
||||||
|
}
|
||||||
|
if ($request->pekerja !== null || $request->pekerja !== ''){
|
||||||
|
$validated['pekerja'] = $request['pekerja'];
|
||||||
|
}
|
||||||
|
if ($request->status !== null || $request->status !== ''){
|
||||||
|
$validated['status'] = $request['status'];
|
||||||
|
}
|
||||||
|
if ($request->no_rekening !== null || $request->no_rekening !== ''){
|
||||||
|
$validated['no_rekening'] = $request['no_rekening'];
|
||||||
|
}
|
||||||
|
if ($request->bank_keterangan !== null || $request->bank_keterangan !== ''){
|
||||||
|
$validated['bank_keterangan'] = $request['bank_keterangan'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('talent_mapping')) {
|
||||||
|
if ($data->talent_mapping) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->talent_mapping);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('talent_mapping');
|
||||||
|
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$file->storeAs('talent-mapping', $hashedName, 'public');
|
||||||
|
|
||||||
|
$validated['talent_mapping'] = 'talent-mapping/' . $hashedName;
|
||||||
|
}
|
||||||
|
if ($request->hasFile('foto')) {
|
||||||
|
if ($data->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('foto');
|
||||||
|
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$file->storeAs('guru', $hashedName, 'public');
|
||||||
|
|
||||||
|
$validated['foto'] = 'guru/' . $hashedName;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$data->update($validated);
|
||||||
|
$firstWord = explode(' ', trim($data->nama))[0];
|
||||||
|
|
||||||
|
$user = User::where('id', $data->user_id)->first();
|
||||||
|
|
||||||
|
if($user === null){
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $data->nama,
|
||||||
|
'username' => strtolower($firstWord),
|
||||||
|
'email' => $data->email,
|
||||||
|
'no_hp_orangtua' => $data->no_hp,
|
||||||
|
'password' => bcrypt('12345'),
|
||||||
|
'role' => 'guru',
|
||||||
|
]);
|
||||||
|
$data->update(['user_id' => $user->id]);
|
||||||
|
}else{
|
||||||
|
$user->update([
|
||||||
|
'name' => $data->nama,
|
||||||
|
'username' => strtolower($firstWord),
|
||||||
|
'no_hp_orangtua' => $data->no_hp,
|
||||||
|
'email' => $data->email,
|
||||||
|
'role' => 'guru',
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
return redirect('/admin/guru')->with('success', 'Data Berhasil Diubah');
|
||||||
|
}catch (QueryException $e){
|
||||||
|
// dd($e->getMessage());
|
||||||
|
return redirect('/admin/guru')->with('error', 'Data Gagal Disimpan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(string $id)
|
||||||
|
{
|
||||||
|
$data = Guru::findOrFail($id);
|
||||||
|
if ($data->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::where('id', $data->user_id)->first();
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
$user->delete();
|
||||||
|
return redirect('/admin/guru')->with('success', 'Data Berhasil Dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function updateKetersediaan(Request $request, $id)
|
||||||
|
{
|
||||||
|
$guru = Guru::findOrFail($id);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'ketersediaan' => 'nullable|array',
|
||||||
|
'ketersediaan.*' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $request->input('ketersediaan', []);
|
||||||
|
|
||||||
|
foreach ([1, 2, 3, 4, 5, 6] as $day) {
|
||||||
|
$daySesiIds = $data[$day] ?? [];
|
||||||
|
|
||||||
|
\App\Models\KetersediaanMengajar::where('guru_id', $id)
|
||||||
|
->where('hari', $day)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
foreach ($daySesiIds as $sesiId) {
|
||||||
|
\App\Models\KetersediaanMengajar::create([
|
||||||
|
'guru_id' => $id,
|
||||||
|
'hari' => $day,
|
||||||
|
'sesi_id' => $sesiId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.guru.detail', ['id' => $id, 'tab' => 'ketersediaan'])
|
||||||
|
->with('success', 'Ketersediaan mengajar guru berhasil diperbarui');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,332 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Guru;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class JadwalController extends Controller
|
||||||
|
{
|
||||||
|
public function getDataHariIni(){
|
||||||
|
$jharini = Logbooks::whereDate('tanggal', Carbon::today())
|
||||||
|
->orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->get();
|
||||||
|
return $jharini->toJson();
|
||||||
|
}
|
||||||
|
public function index(){
|
||||||
|
$data = [];
|
||||||
|
$peserta = [];
|
||||||
|
$guru = Guru::where('status', 'Aktif')->select('id', 'nama')->get();
|
||||||
|
|
||||||
|
return view('admin.pages.jadwal', compact('data', 'peserta', 'guru'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calendarData(){
|
||||||
|
$jharini = Logbooks::orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->with('Kelas.Guru', 'FotoLogbook')
|
||||||
|
->get();
|
||||||
|
return json_encode($jharini);
|
||||||
|
}
|
||||||
|
public function calendarCountData($id){
|
||||||
|
|
||||||
|
if ($id != 0){
|
||||||
|
$jharini = Logbooks::whereHas('kelas', function($query) use ($id) {
|
||||||
|
$query->where('guru_id', $id);
|
||||||
|
})
|
||||||
|
->orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->with('Kelas', 'FotoLogbook')
|
||||||
|
->get();
|
||||||
|
}else{
|
||||||
|
$jharini = Logbooks::orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->with('Kelas', 'FotoLogbook')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = $jharini->count();
|
||||||
|
$completed = $jharini->where('status', 'completed')->count();
|
||||||
|
$kelas = $jharini->pluck('kelas_id')->unique()->count();
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'kelas' => $kelas,
|
||||||
|
'total' => $total,
|
||||||
|
'completed' => $completed,
|
||||||
|
// 'data' => $jharini
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($response);
|
||||||
|
}
|
||||||
|
public function calendar(){
|
||||||
|
$guru = Guru::where('status', 'Aktif')->get();
|
||||||
|
return view('admin.pages.jadwal-calendar', compact('guru'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// AJAX Endpoints untuk menampilkan data tanpa timeout
|
||||||
|
public function getJadwalHariIni(){
|
||||||
|
$jharini = Logbooks::whereDate('tanggal', Carbon::today())
|
||||||
|
->orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->with(['Kelas.guru', 'Guru', 'FotoLogbook'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $jharini,
|
||||||
|
'count' => count($jharini)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJadwalBesok(){
|
||||||
|
$jbesok = Logbooks::whereDate('tanggal', Carbon::tomorrow())
|
||||||
|
->orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->with(['Kelas.guru', 'Guru', 'FotoLogbook'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $jbesok,
|
||||||
|
'count' => count($jbesok)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLogbookData(){
|
||||||
|
Carbon::setLocale('id');
|
||||||
|
|
||||||
|
$groupedLogbooks = Logbooks::orderBy('tanggal', 'asc')
|
||||||
|
->with(['Kelas.guru', 'Guru', 'FotoLogbook'])
|
||||||
|
->get()
|
||||||
|
->groupBy(function($item) {
|
||||||
|
return \Carbon\Carbon::parse($item->tanggal)->translatedFormat('F Y');
|
||||||
|
});
|
||||||
|
|
||||||
|
$now = \Carbon\Carbon::now();
|
||||||
|
|
||||||
|
$sortedLogbooks = collect([
|
||||||
|
'upcoming' => collect(),
|
||||||
|
'past' => collect(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$logbookSummary = collect();
|
||||||
|
|
||||||
|
foreach ($groupedLogbooks as $bulanTahun => $items) {
|
||||||
|
$firstItemDate = \Carbon\Carbon::parse($items->first()->tanggal);
|
||||||
|
$category = $firstItemDate->greaterThanOrEqualTo($now->startOfMonth()) ? 'upcoming' : 'past';
|
||||||
|
$sortedLogbooks[$category][$bulanTahun] = $items;
|
||||||
|
|
||||||
|
// Menyimpan jumlah logbook tiap bulan
|
||||||
|
$logbookSummary[$bulanTahun] = [
|
||||||
|
'jumlah_sesi' => $items->count(),
|
||||||
|
'jumlah_sesi_completed' => $items->where('status', 'completed')->count(),
|
||||||
|
'jumlah_siswa' => $items->pluck('Kelas')->filter()->flatMap->siswa->unique('id')->count(),
|
||||||
|
'jumlah_kelas' => $items->pluck('kelas_id')->unique()->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$logbooksUpcoming = $sortedLogbooks['upcoming'];
|
||||||
|
$logbooksPast = $sortedLogbooks['past']->sortByDesc(function($items, $bulanTahun) {
|
||||||
|
return \Carbon\Carbon::parse($items->first()->tanggal);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'upcoming' => $logbooksUpcoming,
|
||||||
|
'past' => $logbooksPast,
|
||||||
|
'summary' => $logbookSummary
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateGAIndex()
|
||||||
|
{
|
||||||
|
$semesters = \App\Models\Kelas::whereNotNull('semester')->distinct()->pluck('semester');
|
||||||
|
$years = \App\Models\Kelas::whereNotNull('tahun_ajaran')->distinct()->pluck('tahun_ajaran');
|
||||||
|
|
||||||
|
return view('admin.pages.generate-ga', compact('semesters', 'years'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasilGAIndex()
|
||||||
|
{
|
||||||
|
return view('admin.pages.hasil-ga');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jadwalMingguan()
|
||||||
|
{
|
||||||
|
$sesiList = \App\Models\Sesi::orderBy('id', 'asc')->get();
|
||||||
|
return view('admin.pages.jadwal-mingguan', compact('sesiList'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jadwalKromosom()
|
||||||
|
{
|
||||||
|
$jadwalMaster = \App\Models\JadwalKelas::with(['kelas', 'guru', 'ruangan', 'sesi'])
|
||||||
|
->orderBy('hari', 'asc')
|
||||||
|
->orderBy('sesi_id', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.pages.jadwal-kromosom', compact('jadwalMaster'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJadwalMingguanData(Request $request)
|
||||||
|
{
|
||||||
|
$masterJadwal = \App\Models\JadwalKelas::with(['kelas', 'guru', 'ruangan', 'sesi'])->get();
|
||||||
|
$data = $masterJadwal->map(function ($j) {
|
||||||
|
return [
|
||||||
|
'id' => $j->id,
|
||||||
|
'hari' => $j->hari,
|
||||||
|
'sesi_id' => $j->sesi_id,
|
||||||
|
'kelas' => $j->kelas,
|
||||||
|
'guru' => $j->guru,
|
||||||
|
'ruangan' => $j->ruangan,
|
||||||
|
'sesi' => $j->sesi,
|
||||||
|
'is_master' => true
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'is_master' => true,
|
||||||
|
'data' => $data
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateGA(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'semester' => 'nullable|string',
|
||||||
|
'tahun_ajaran' => 'nullable|string',
|
||||||
|
'population_size' => 'nullable|numeric|min:10|max:200',
|
||||||
|
'crossover_rate' => 'nullable|numeric|min:0.1|max:1.0',
|
||||||
|
'mutation_rate' => 'nullable|numeric|min:0.01|max:1.0',
|
||||||
|
'max_generations' => 'nullable|numeric|min:10|max:2000',
|
||||||
|
'random_seed' => 'nullable|numeric',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$semester = $request->input('semester');
|
||||||
|
$tahunAjaran = $request->input('tahun_ajaran');
|
||||||
|
$popSize = (int) $request->input('population_size', 50);
|
||||||
|
$crossRate = (float) $request->input('crossover_rate', 0.8);
|
||||||
|
$mutRate = (float) $request->input('mutation_rate', 0.1);
|
||||||
|
$maxGen = (int) $request->input('max_generations', 500);
|
||||||
|
|
||||||
|
// Solusi A: Pool 10 Controlled Seeds Teruji (Bervariasi Terkontrol & Reproducible)
|
||||||
|
$presetSeeds = [202, 101, 303, 404, 505, 606, 707, 808, 909, 1001];
|
||||||
|
|
||||||
|
$paramSignature = "ga_{$semester}_{$tahunAjaran}_{$popSize}_{$crossRate}_{$mutRate}";
|
||||||
|
$lastSignature = session('ga_last_param_signature');
|
||||||
|
$attemptIndex = session()->has('ga_attempt_index') ? (int) session('ga_attempt_index') : -1;
|
||||||
|
|
||||||
|
if ($lastSignature !== $paramSignature || $attemptIndex < 0) {
|
||||||
|
// Reset ke percobaan ke-1 (index 0) jika parameter diubah atau session baru
|
||||||
|
$attemptIndex = 0;
|
||||||
|
} else {
|
||||||
|
// Lanjutkan ke percobaan berikutnya (berputar di 0..9)
|
||||||
|
$attemptIndex = ($attemptIndex + 1) % count($presetSeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->put('ga_last_param_signature', $paramSignature);
|
||||||
|
session()->put('ga_attempt_index', $attemptIndex);
|
||||||
|
|
||||||
|
$seed = $request->filled('random_seed')
|
||||||
|
? (int) $request->input('random_seed')
|
||||||
|
: $presetSeeds[$attemptIndex];
|
||||||
|
|
||||||
|
$service = new \App\Services\Scheduling\ScheduleGeneratorService();
|
||||||
|
$result = $service->generate(
|
||||||
|
semester: $semester,
|
||||||
|
tahunAjaran: $tahunAjaran,
|
||||||
|
populationSize: $popSize,
|
||||||
|
crossoverRate: $crossRate,
|
||||||
|
mutationRate: $mutRate,
|
||||||
|
maxGenerations: $maxGen,
|
||||||
|
seed: $seed
|
||||||
|
);
|
||||||
|
|
||||||
|
$result['attemptNumber'] = $attemptIndex + 1;
|
||||||
|
$result['seedUsed'] = $seed;
|
||||||
|
$result['semester'] = $semester ?? 'Genap';
|
||||||
|
$result['tahunAjaran'] = $tahunAjaran ?? '2025/2026';
|
||||||
|
$result['populationSize'] = $popSize;
|
||||||
|
$result['crossoverRate'] = $crossRate;
|
||||||
|
$result['mutationRate'] = $mutRate;
|
||||||
|
$result['maxGenerations'] = $maxGen;
|
||||||
|
|
||||||
|
$statusMsg = $result['isOptimal']
|
||||||
|
? "Penjadwalan GA Berhasil Optimal (Fitness: 1.00 pada Generasi ke-{$result['generations']}, Waktu: {$result['executionTime']}s)"
|
||||||
|
: "Penjadwalan GA Selesai pada Generasi ke-{$result['generations']} (Fitness: " . round($result['bestChromosome']->fitness, 4) . ", Waktu: {$result['executionTime']}s)";
|
||||||
|
|
||||||
|
session()->put('ga_log_result', $result);
|
||||||
|
|
||||||
|
return redirect('/admin/hasil-ga')->with('success', $statusMsg);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return redirect('/admin/jadwal-mingguan')->with('error', 'Gagal memproses GA: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportExcelGA()
|
||||||
|
{
|
||||||
|
$result = session('ga_log_result');
|
||||||
|
if (!$result) {
|
||||||
|
return redirect('/admin/generate-ga')->with('error', 'Belum ada hasil eksekusi GA. Silakan jalankan eksekusi terlebih dahulu.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = 'Simulasi_Algoritma_Genetika_Robotku_' . date('Ymd_His') . '.xlsx';
|
||||||
|
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\GaSimulationExport($result), $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportExcelMingguan()
|
||||||
|
{
|
||||||
|
$jadwalMaster = \App\Models\JadwalKelas::with(['kelas', 'guru', 'ruangan', 'sesi'])
|
||||||
|
->orderBy('hari', 'asc')
|
||||||
|
->orderBy('sesi_id', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($jadwalMaster->isEmpty()) {
|
||||||
|
return redirect()->back()->with('error', 'Belum ada data jadwal kelas untuk di-export.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = 'Master_Jadwal_Kelas_Mingguan_' . date('Ymd_His') . '.xlsx';
|
||||||
|
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\JadwalMingguanExport($jadwalMaster), $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportExcelGaPresentation()
|
||||||
|
{
|
||||||
|
$service = new \App\Services\Scheduling\GaPresentationSimulationService();
|
||||||
|
$simData = $service->generateSimulationData();
|
||||||
|
|
||||||
|
$fileName = 'Simulasi_GA_Presentasi_Robotku_' . date('Ymd_His') . '.xlsx';
|
||||||
|
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\GaPresentationExport($simData), $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateLogbookFromJadwal(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'tanggal_mulai' => 'required|date',
|
||||||
|
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
|
||||||
|
], [
|
||||||
|
'tanggal_mulai.required' => 'Tanggal mulai harus diisi.',
|
||||||
|
'tanggal_selesai.required' => 'Tanggal selesai harus diisi.',
|
||||||
|
'tanggal_selesai.after_or_equal' => 'Tanggal selesai harus sama atau setelah tanggal mulai.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$service = new \App\Services\LogbookGeneratorService();
|
||||||
|
$result = $service->generateFromJadwal(
|
||||||
|
startDate: $request->input('tanggal_mulai'),
|
||||||
|
endDate: $request->input('tanggal_selesai')
|
||||||
|
);
|
||||||
|
|
||||||
|
$msg = "Berhasil memproses logbook dari Master Jadwal: {$result['created']} entri baru dibuat";
|
||||||
|
if ($result['updated'] > 0) {
|
||||||
|
$msg .= ", {$result['updated']} logbook pending diperbarui";
|
||||||
|
}
|
||||||
|
if ($result['skipped'] > 0) {
|
||||||
|
$msg .= ", {$result['skipped']} logbook completed dilewati";
|
||||||
|
}
|
||||||
|
$msg .= ".";
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', $msg);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return redirect()->back()->with('error', 'Gagal generate logbook: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,492 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Exports\AllKelasExport;
|
||||||
|
use App\Exports\DetailKelasExport;
|
||||||
|
use App\Exports\PesertaEventExport;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\DetailKelas;
|
||||||
|
use App\Models\Events;
|
||||||
|
use App\Models\FotoLogbooks;
|
||||||
|
use App\Models\Guru;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use App\Models\Siswa;
|
||||||
|
use App\Models\Programs;
|
||||||
|
use App\Models\Ruangan;
|
||||||
|
use App\Models\Sekolah;
|
||||||
|
use App\Models\Sesi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
|
class KelasController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request){
|
||||||
|
$data = Kelas::withCount('siswa')
|
||||||
|
->with(['sekolah', 'ruanganFix', 'Programs'])
|
||||||
|
->when($request->filled('semester'), function ($q) use ($request) {
|
||||||
|
$q->where('semester', $request->semester);
|
||||||
|
})
|
||||||
|
->when($request->filled('tahun'), function ($q) use ($request) {
|
||||||
|
$q->where('tahun_ajaran', $request->tahun);
|
||||||
|
})
|
||||||
|
->when($request->filled('is_ekstrakurikuler'), function ($q) use ($request) {
|
||||||
|
$q->where('is_ekstrakurikuler', $request->is_ekstrakurikuler);
|
||||||
|
})
|
||||||
|
->when($request->filled('program_id'), function ($q) use ($request) {
|
||||||
|
$q->where('program_id', $request->program_id);
|
||||||
|
})
|
||||||
|
->orderByRaw("CASE
|
||||||
|
WHEN status = 'Aktif' THEN 1
|
||||||
|
WHEN status = 'Tidak Aktif' THEN 2
|
||||||
|
ELSE 3 END")
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$semesters = Kelas::whereNotNull('semester')->distinct()->pluck('semester');
|
||||||
|
$years = Kelas::whereNotNull('tahun_ajaran')->distinct()->pluck('tahun_ajaran');
|
||||||
|
$programs = Programs::all();
|
||||||
|
|
||||||
|
return view('admin.pages.kelas', compact('data', 'semesters', 'years', 'programs'));
|
||||||
|
}
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$programs = Programs::all();
|
||||||
|
$guru = Guru::where('status', 'Aktif')->select('id', 'nama')->get();
|
||||||
|
$sekolahList = Sekolah::where('is_active', 1)->orderBy('nama_sekolah', 'asc')->get();
|
||||||
|
$ruanganList = Ruangan::where('status', 'aktif')->orderBy('nama_ruangan', 'asc')->get();
|
||||||
|
return view ('admin.pages.kelas-tambah', compact('programs', 'guru', 'sekolahList', 'ruanganList'));
|
||||||
|
}
|
||||||
|
public function checkScheduleConflict(Request $request)
|
||||||
|
{
|
||||||
|
if ($request->is_ekstrakurikuler != 1) {
|
||||||
|
return response()->json(['conflict' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ruanganId = $request->ruangan_fix_id;
|
||||||
|
$hari = $request->hari_fix;
|
||||||
|
$sesi = $request->sesi_fix;
|
||||||
|
$semester = trim($request->semester ?? '');
|
||||||
|
$tahunAjaran = trim($request->tahun_ajaran ?? '');
|
||||||
|
$excludeId = $request->kelas_id;
|
||||||
|
|
||||||
|
if (!$ruanganId || !$hari || !$sesi) {
|
||||||
|
return response()->json(['conflict' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = Kelas::where('is_ekstrakurikuler', 1)
|
||||||
|
->where('ruangan_fix_id', $ruanganId)
|
||||||
|
->where('hari_fix', $hari)
|
||||||
|
->where('sesi_fix', $sesi)
|
||||||
|
->where('status', 'Aktif');
|
||||||
|
|
||||||
|
if (!empty($semester)) {
|
||||||
|
$query->where('semester', $semester);
|
||||||
|
}
|
||||||
|
if (!empty($tahunAjaran)) {
|
||||||
|
$query->where('tahun_ajaran', $tahunAjaran);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($excludeId) {
|
||||||
|
$query->where('id', '!=', $excludeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conflicting = $query->first();
|
||||||
|
|
||||||
|
if ($conflicting) {
|
||||||
|
return response()->json([
|
||||||
|
'conflict' => true,
|
||||||
|
'nama_kelas' => $conflicting->nama_kelas,
|
||||||
|
'message' => "Ruangan, Hari, dan Sesi telah digunakan oleh kelas ekstrakurikuler <b>{$conflicting->nama_kelas}</b>. Silahkan pilih ruangan, hari, atau sesi yang berbeda"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['conflict' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request){
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_kelas' => 'required|string|max:255',
|
||||||
|
'jenis_kelas' => 'required',
|
||||||
|
'program_id' => 'required',
|
||||||
|
'harga_kelas' => 'required',
|
||||||
|
'status' => 'required',
|
||||||
|
'semester' => 'nullable',
|
||||||
|
'tahun_ajaran' => 'nullable',
|
||||||
|
'frekuensi_per_minggu' => 'required|integer|in:1,2,3',
|
||||||
|
'is_ekstrakurikuler' => 'required|in:0,1',
|
||||||
|
'sekolah_id' => 'required_if:is_ekstrakurikuler,1|nullable|exists:sekolah,id',
|
||||||
|
'hari_fix' => 'required_if:is_ekstrakurikuler,1|nullable|integer|between:1,6',
|
||||||
|
'sesi_fix' => 'required_if:is_ekstrakurikuler,1|nullable|integer|between:1,5',
|
||||||
|
'ruangan_fix_id' => 'required_if:is_ekstrakurikuler,1|nullable|exists:ruangan,id',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_kelas.required' => 'Nama kelas wajib diisi',
|
||||||
|
'nama_kelas.max' => 'Nama kelas terlalu panjang',
|
||||||
|
'jenis_kelas.required' => 'Total pertemuan wajib diisi',
|
||||||
|
'program_id.required' => 'Program wajib diisi',
|
||||||
|
'harga_kelas.required' => 'Harga kelas wajib diisi',
|
||||||
|
'status.required' => 'Status kelas wajib diisi',
|
||||||
|
'frekuensi_per_minggu.required' => 'Frekuensi per minggu wajib diisi',
|
||||||
|
'is_ekstrakurikuler.required' => 'Tipe kelas wajib dipilih',
|
||||||
|
'sekolah_id.required_if' => 'Sekolah wajib dipilih untuk kelas ekstrakurikuler',
|
||||||
|
'hari_fix.required_if' => 'Hari fix wajib dipilih untuk kelas ekstrakurikuler',
|
||||||
|
'sesi_fix.required_if' => 'Sesi fix wajib dipilih untuk kelas ekstrakurikuler',
|
||||||
|
'ruangan_fix_id.required_if' => 'Ruangan fix wajib dipilih untuk kelas ekstrakurikuler',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($validated['is_ekstrakurikuler'] == 0) {
|
||||||
|
$validated['sekolah_id'] = null;
|
||||||
|
$validated['hari_fix'] = null;
|
||||||
|
$validated['sesi_fix'] = null;
|
||||||
|
$validated['ruangan_fix_id'] = null;
|
||||||
|
} else if ($validated['status'] === 'Aktif') {
|
||||||
|
$conflictQuery = Kelas::where('is_ekstrakurikuler', 1)
|
||||||
|
->where('ruangan_fix_id', $validated['ruangan_fix_id'])
|
||||||
|
->where('hari_fix', $validated['hari_fix'])
|
||||||
|
->where('sesi_fix', $validated['sesi_fix'])
|
||||||
|
->where('status', 'Aktif');
|
||||||
|
|
||||||
|
if (!empty($validated['semester'])) {
|
||||||
|
$conflictQuery->where('semester', $validated['semester']);
|
||||||
|
}
|
||||||
|
if (!empty($validated['tahun_ajaran'])) {
|
||||||
|
$conflictQuery->where('tahun_ajaran', $validated['tahun_ajaran']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conflicting = $conflictQuery->first();
|
||||||
|
if ($conflicting) {
|
||||||
|
return redirect()->back()
|
||||||
|
->withErrors(['ruangan_fix_id' => "Ruangan, Hari, dan Sesi Fix telah digunakan oleh kelas ekstrakurikuler '{$conflicting->nama_kelas}'."])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['harga_kelas'] = (int) str_replace('.', '', $validated['harga_kelas']);
|
||||||
|
$validated['kode_kelas'] = strtoupper(Str::random(6));
|
||||||
|
|
||||||
|
if ($request->keterangan){ $validated['keterangan'] = $request->keterangan; }
|
||||||
|
|
||||||
|
Kelas::create($validated);
|
||||||
|
return redirect('/admin/kelas')->with('success', 'Data Berhasil Disimpan');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id){
|
||||||
|
$data = Kelas::with(['sekolah', 'ruanganFix'])->where('id', $id)->first();
|
||||||
|
$programs = Programs::all();
|
||||||
|
$guru = Guru::where('status', 'Aktif')->select('id', 'nama')->get();
|
||||||
|
$sekolahList = Sekolah::where('is_active', 1)->orderBy('nama_sekolah', 'asc')->get();
|
||||||
|
$ruanganList = Ruangan::where('status', 'aktif')->orderBy('nama_ruangan', 'asc')->get();
|
||||||
|
|
||||||
|
if ($data == null){
|
||||||
|
return redirect('/admin/kelas')->with('error', 'Data kelas tidak ditemukan');
|
||||||
|
}
|
||||||
|
$peserta = DetailKelas::has('siswa')->where('kelas_id', $id)->get();
|
||||||
|
$logbook = Logbooks::with('Sesi')->where('kelas_id', $id)->orderBy('tanggal', 'asc')->get();
|
||||||
|
$sesiList = Sesi::orderBy('id', 'asc')->get();
|
||||||
|
|
||||||
|
return view('admin.pages.kelas-detail', compact('data', 'peserta', 'programs', 'logbook', 'guru', 'sekolahList', 'ruanganList', 'sesiList'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id){
|
||||||
|
$data = Kelas::findOrFail($id);
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_kelas' => 'required|string|max:255',
|
||||||
|
'jenis_kelas' => 'required',
|
||||||
|
'program_id' => 'required',
|
||||||
|
'harga_kelas' => 'required',
|
||||||
|
'status' => 'required',
|
||||||
|
'semester' => 'nullable',
|
||||||
|
'tahun_ajaran' => 'nullable',
|
||||||
|
'frekuensi_per_minggu' => 'required|integer|in:1,2,3',
|
||||||
|
'is_ekstrakurikuler' => 'required|in:0,1',
|
||||||
|
'sekolah_id' => 'required_if:is_ekstrakurikuler,1|nullable|exists:sekolah,id',
|
||||||
|
'hari_fix' => 'required_if:is_ekstrakurikuler,1|nullable|integer|between:1,6',
|
||||||
|
'sesi_fix' => 'required_if:is_ekstrakurikuler,1|nullable|integer|between:1,5',
|
||||||
|
'ruangan_fix_id' => 'required_if:is_ekstrakurikuler,1|nullable|exists:ruangan,id',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_kelas.required' => 'Nama kelas wajib diisi',
|
||||||
|
'nama_kelas.max' => 'Nama kelas terlalu panjang',
|
||||||
|
'jenis_kelas.required' => 'Total pertemuan wajib diisi',
|
||||||
|
'program_id.required' => 'Program wajib diisi',
|
||||||
|
'harga_kelas.required' => 'Harga kelas wajib diisi',
|
||||||
|
'status.required' => 'Status kelas wajib diisi',
|
||||||
|
'frekuensi_per_minggu.required' => 'Frekuensi per minggu wajib diisi',
|
||||||
|
'is_ekstrakurikuler.required' => 'Tipe kelas wajib dipilih',
|
||||||
|
'sekolah_id.required_if' => 'Sekolah wajib dipilih untuk kelas ekstrakurikuler',
|
||||||
|
'hari_fix.required_if' => 'Hari fix wajib dipilih untuk kelas ekstrakurikuler',
|
||||||
|
'sesi_fix.required_if' => 'Sesi fix wajib dipilih untuk kelas ekstrakurikuler',
|
||||||
|
'ruangan_fix_id.required_if' => 'Ruangan fix wajib dipilih untuk kelas ekstrakurikuler',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($validated['is_ekstrakurikuler'] == 0) {
|
||||||
|
$validated['sekolah_id'] = null;
|
||||||
|
$validated['hari_fix'] = null;
|
||||||
|
$validated['sesi_fix'] = null;
|
||||||
|
$validated['ruangan_fix_id'] = null;
|
||||||
|
} else if ($validated['status'] === 'Aktif') {
|
||||||
|
$conflictQuery = Kelas::where('is_ekstrakurikuler', 1)
|
||||||
|
->where('ruangan_fix_id', $validated['ruangan_fix_id'])
|
||||||
|
->where('hari_fix', $validated['hari_fix'])
|
||||||
|
->where('sesi_fix', $validated['sesi_fix'])
|
||||||
|
->where('status', 'Aktif')
|
||||||
|
->where('id', '!=', $id);
|
||||||
|
|
||||||
|
if (!empty($validated['semester'])) {
|
||||||
|
$conflictQuery->where('semester', $validated['semester']);
|
||||||
|
}
|
||||||
|
if (!empty($validated['tahun_ajaran'])) {
|
||||||
|
$conflictQuery->where('tahun_ajaran', $validated['tahun_ajaran']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conflicting = $conflictQuery->first();
|
||||||
|
if ($conflicting) {
|
||||||
|
return redirect()->back()
|
||||||
|
->withErrors(['ruangan_fix_id' => "Ruangan, Hari, dan Sesi Fix telah digunakan oleh kelas ekstrakurikuler '{$conflicting->nama_kelas}'."])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->status_selesai){
|
||||||
|
$validated['status_selesai'] = 'Selesai';
|
||||||
|
}else{
|
||||||
|
$validated['status_selesai'] = 'Belum';
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['harga_kelas'] = (int) str_replace('.', '', $validated['harga_kelas']);
|
||||||
|
|
||||||
|
if ($request->keterangan){ $validated['keterangan'] = $request->keterangan; }
|
||||||
|
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' .$id;
|
||||||
|
return redirect($direct)->with('success', 'Data Kelas Berhasil Diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSiswasKelas($kelas_id){
|
||||||
|
$pesertaTerdaftar = DetailKelas::where('kelas_id', $kelas_id)->pluck('siswa_id')->toArray();
|
||||||
|
|
||||||
|
$siswa = Siswa::whereNotIn('id', $pesertaTerdaftar)->get();
|
||||||
|
|
||||||
|
return response()->json([$siswa]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tambahSiswas(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'kelas_id' => 'required',
|
||||||
|
'id' => 'required',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kelas_id.required' => 'Event tidak ditemukan',
|
||||||
|
'id.required' => 'Silahkan pilih setidaknya satu siswa',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
$kelas = Kelas::where('id', $request->kelas_id)->select('harga_kelas')->first();
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($request->id); $i++) {
|
||||||
|
$siswa = Siswa::where('id', $request->id[$i])->select('id', 'nama_anak')->first();
|
||||||
|
$cekPeserta = DetailKelas::where('siswa_id', $request->id[$i])->where('kelas_id', $request->kelas_id)->count();
|
||||||
|
if ($cekPeserta == 0) {
|
||||||
|
if ($siswa != null) {
|
||||||
|
DetailKelas::create([
|
||||||
|
'kelas_id' => $request->kelas_id,
|
||||||
|
'siswa_id' => $request->id[$i],
|
||||||
|
'nama_anak' => $siswa->nama_anak,
|
||||||
|
'status_siswa' => "Aktif",
|
||||||
|
'harga_kelas' => $kelas->harga_kelas,
|
||||||
|
]);
|
||||||
|
}else{
|
||||||
|
$direct = '/admin/kelas/' . $request->kelas_id;
|
||||||
|
$message = "Gagal menyimpan siswa, id " . $request->id[$i] . " tidak ditemukan";
|
||||||
|
return redirect($direct)->with('error', $message);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
$direct = '/admin/kelas/' . $request->kelas_id;
|
||||||
|
$message = "Anak dengan nama " . $siswa->nama_anak . " telah terdaftar di kelas ini";
|
||||||
|
return redirect($direct)->with('error', $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' . $request->kelas_id;
|
||||||
|
$message = "Siswa berhasil ditambahkan pada kelas";
|
||||||
|
return redirect($direct)->with('success', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPesertaKelas($id){
|
||||||
|
$data = DetailKelas::where('id', $id)->first();
|
||||||
|
$peserta = Siswa::where('id', $data->siswa_id)->first();
|
||||||
|
// $user = User::where('id', $data->user_id)->select('id', 'email')->first();
|
||||||
|
|
||||||
|
$created_at = Carbon::parse($data->created_at)->locale('id')->translatedFormat('l, d F Y - H:i') . ' WIB';
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'id' => $data->id,
|
||||||
|
'nama_anak' => $peserta->nama_anak,
|
||||||
|
'nama_orangtua' => $peserta->nama_orangtua,
|
||||||
|
'no_hp_orangtua' => $peserta->no_hp_orangtua,
|
||||||
|
'usia' => $peserta->usia ?? null,
|
||||||
|
'jenis_kelamin' => $peserta->jenis_kelamin ?? null,
|
||||||
|
'email' => $user->email ?? null,
|
||||||
|
'asal_sekolah' => $peserta->asal_sekolah ?? null,
|
||||||
|
'program_id'=> $peserta->program_id ?? null,
|
||||||
|
'status' => $data->status_siswa ?? null,
|
||||||
|
'alamat' => $peserta->alamat ?? null,
|
||||||
|
'bukti_pembayaran' => $data->bukti_pembayaran ?? null,
|
||||||
|
'harga_kelas' => $data->harga_kelas,
|
||||||
|
'catatan' => $data->catatan,
|
||||||
|
'tanggal_daftar' => $created_at ?? null,
|
||||||
|
|
||||||
|
'foto' => $peserta->foto ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editSiswa(Request $request, $id){
|
||||||
|
|
||||||
|
$data = DetailKelas::where('id', $id)->first();
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_anak' => 'required',
|
||||||
|
'status_siswa' => 'required',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_anak.required' => 'Nama anak tidak ditemukan',
|
||||||
|
'status_siswa.required' => 'Harap pilih status siswa terlebih dahulu',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($request->catatan !== null || $request->catatan !== ''){ $validated['catatan'] = $request->catatan; }
|
||||||
|
|
||||||
|
if ($request->hasFile('bukti_pembayaran')) {
|
||||||
|
if ($data->bukti_pembayaran) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->bukti_pembayaran);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('bukti_pembayaran');
|
||||||
|
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$file->storeAs('bukti-pembayaran', $hashedName, 'public');
|
||||||
|
|
||||||
|
$validated['bukti_pembayaran'] = 'bukti-pembayaran/' . $hashedName;
|
||||||
|
|
||||||
|
DetailKelas::where('id', $id)->update([
|
||||||
|
'nama_anak' => $validated['nama_anak'],
|
||||||
|
'status_siswa' => $validated['status_siswa'],
|
||||||
|
'bukti_pembayaran' => $validated['bukti_pembayaran'],
|
||||||
|
'catatan' => $validated['catatan'],
|
||||||
|
]);
|
||||||
|
}else{
|
||||||
|
DetailKelas::where('id', $id)->update([
|
||||||
|
'nama_anak' => $validated['nama_anak'],
|
||||||
|
'status_siswa' => $validated['status_siswa'],
|
||||||
|
'catatan' => $validated['catatan'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' . $request->kelas_id;
|
||||||
|
$message = "Data siswa dikelas berhasil diubah";
|
||||||
|
return redirect($direct)->with('success', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyerSiswaFromKelas($id){
|
||||||
|
$data = DetailKelas::findOrFail($id);
|
||||||
|
$kelas_id = $data->kelas_id;
|
||||||
|
|
||||||
|
if ($data->bukti_pembayaran) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->bukti_pembayaran);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
$direct = '/admin/kelas/' . $kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Data siswa berhasil dihapus');
|
||||||
|
}
|
||||||
|
public function destroy($id){
|
||||||
|
$data = Kelas::findOrFail($id);
|
||||||
|
|
||||||
|
$detailkelas = DetailKelas::where('kelas_id', $id)->get();
|
||||||
|
|
||||||
|
if($detailkelas->count() > 0){
|
||||||
|
for($i = 0; $i < count($detailkelas); $i++){
|
||||||
|
$detailkelas[$i]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$logbooks = Logbooks::where('kelas_id', $id)->get();
|
||||||
|
|
||||||
|
if($logbooks->count() > 0){
|
||||||
|
for($i = 0; $i < count($logbooks); $i++){
|
||||||
|
$logbooks_id = $logbooks[$i]->id;
|
||||||
|
|
||||||
|
$fotoLogbook = FotoLogbooks::where('logbook_id', $logbooks_id)->get();
|
||||||
|
if($fotoLogbook->count() > 0){
|
||||||
|
for($b = 0; $b < count($fotoLogbook); $b++){
|
||||||
|
if ($fotoLogbook[$b]->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $fotoLogbook[$b]->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fotoLogbook[$b]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$logbooks[$i]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
return redirect('/admin/kelas')->with('success', 'Data kelas berhasil dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportExcel($id)
|
||||||
|
{
|
||||||
|
$namaKelas = Kelas::select('nama_kelas')->where('id', $id)->first();
|
||||||
|
|
||||||
|
$safeNamaKelas = preg_replace('/[\/\\\\]/', '-', $namaKelas->nama_kelas);
|
||||||
|
|
||||||
|
$fileName = 'Siswa ' . $safeNamaKelas . '.xlsx';
|
||||||
|
|
||||||
|
return Excel::download(new DetailKelasExport($id), $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportExcelAll($year){
|
||||||
|
$fileName = "Kelas Robotku" . ($year !== "all" ? " $year" : "") . ".xlsx";
|
||||||
|
|
||||||
|
return Excel::download(new AllKelasExport($year), $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportExcelFilter(Request $request)
|
||||||
|
{
|
||||||
|
$fileName = "Logbook Kelas Robotku " . date('Y-m-d') . ".xlsx";
|
||||||
|
return Excel::download(new \App\Exports\LogbookKelasMultiExport($request), $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportLogbookFilter(Request $request)
|
||||||
|
{
|
||||||
|
$fileName = "Logbook Kelas Robotku " . date('Y-m-d') . ".xlsx";
|
||||||
|
return Excel::download(new \App\Exports\LogbookKelasMultiExport($request), $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportSiswaFilter(Request $request)
|
||||||
|
{
|
||||||
|
$fileName = "Data Kelas & Siswa Robotku " . date('Y-m-d') . ".xlsx";
|
||||||
|
return Excel::download(new \App\Exports\FilteredKelasExport($request), $fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,534 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Imports\LogbookImport;
|
||||||
|
use App\Models\FotoLogbooks;
|
||||||
|
use App\Models\Guru;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use App\Models\PengajarLogbook;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use Intervention\Image\Laravel\Facades\Image;
|
||||||
|
|
||||||
|
class LogbookController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function getLogbook($id){
|
||||||
|
$data = Logbooks::where('id', $id)->first();
|
||||||
|
|
||||||
|
$logbook = FotoLogbooks::where('logbook_id', $data->id)->get()->map(function ($logbook) {
|
||||||
|
return [
|
||||||
|
'id' => $logbook->id,
|
||||||
|
'foto' => $logbook->foto,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$tanggalparse = $data->tanggal
|
||||||
|
? Carbon::parse($data->tanggal)->locale('id')->translatedFormat('l, d F Y')
|
||||||
|
: null;
|
||||||
|
$startparse = $data->start
|
||||||
|
? Carbon::parse($data->start)->locale('id')->translatedFormat('H:i')
|
||||||
|
: null;
|
||||||
|
$endparse = $data->end
|
||||||
|
? Carbon::parse($data->end)->locale('id')->translatedFormat('H:i')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$guruPengajar = $data->guru_id ? Guru::select('id', 'nama', 'no_hp')->where('id', $data->guru_id)->first() : ($data->Kelas->guru ?? null);
|
||||||
|
$guruId = $data->guru_id ?? ($data->Kelas->guru_id ?? null);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'id' => $data->id,
|
||||||
|
'tanggal' => $data->tanggal ?? null,
|
||||||
|
'start' => $data->start ?? null,
|
||||||
|
'start_parse' => $startparse ?? null,
|
||||||
|
'end' => $data->end ?? null,
|
||||||
|
'end_parse' => $endparse ?? null,
|
||||||
|
'tanggalparse' => $tanggalparse ?? null,
|
||||||
|
'materi' => $data->materi ?? null,
|
||||||
|
'kegiatan_minggu_ini' => $data->kegiatan_minggu_ini ?? null,
|
||||||
|
'perkembangan_siswa' => $data->perkembangan_siswa ?? null,
|
||||||
|
'kendala_dan_solusi' => $data->kendala_dan_solusi ?? null,
|
||||||
|
'kebutuhan_minggu_depan' => $data->kebutuhan_minggu_depan ?? null,
|
||||||
|
'hal_baik' => $data->hal_baik ?? null,
|
||||||
|
'catatan' => $data->catatan ?? null,
|
||||||
|
'pertemuan' => $data->pertemuan ?? null,
|
||||||
|
'guru' => $guruPengajar->nama ?? null,
|
||||||
|
'guru_id' => $guruId,
|
||||||
|
'nama_guru' => $guruPengajar->nama ?? null,
|
||||||
|
'no_hp_guru' => $guruPengajar->no_hp ?? null,
|
||||||
|
'ruangan_id' => $data->ruangan_id ?? null,
|
||||||
|
'logbooks' => $logbook,
|
||||||
|
'kelas_id' => $data->kelas_id ?? null,
|
||||||
|
'nama_kelas' => $data->Kelas->nama_kelas ?? null,
|
||||||
|
'status' => $data->status ?? null,
|
||||||
|
'sesi_id' => $data->sesi_id ?? null,
|
||||||
|
'nama_sesi' => $data->Sesi->nama_sesi ?? null,
|
||||||
|
'jam_sesi' => $data->Sesi->jam ?? null,
|
||||||
|
'jenis_libur' => $data->jenis_libur ?? null,
|
||||||
|
'jenis_libur_lainnya' => $data->jenis_libur_lainnya ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function duplicate($id){
|
||||||
|
$logbook = Logbooks::findOrFail($id);
|
||||||
|
|
||||||
|
$newLogbook = $logbook->replicate([
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
'status'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newLogbook->status = 'draft';
|
||||||
|
|
||||||
|
if($logbook->status == 'libur'){
|
||||||
|
$newLogbook->status = 'libur';
|
||||||
|
}
|
||||||
|
|
||||||
|
$newLogbook->save();
|
||||||
|
|
||||||
|
$pengajarLama = PengajarLogbook::where('logbook_id', $logbook->id)->get();
|
||||||
|
|
||||||
|
foreach ($pengajarLama as $pengajar) {
|
||||||
|
PengajarLogbook::create([
|
||||||
|
'logbook_id' => $newLogbook->id,
|
||||||
|
'guru_id' => $pengajar->guru_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' .$logbook->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil ditambah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request){
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'kelas_id' => 'required',
|
||||||
|
'kode_kelas' => 'required',
|
||||||
|
'guru_id' => 'nullable|exists:guru,id',
|
||||||
|
'sesi_id' => 'nullable|exists:sesi,id',
|
||||||
|
'pertemuan' => 'nullable',
|
||||||
|
'tanggal' => 'nullable',
|
||||||
|
'start' => 'nullable',
|
||||||
|
'end' => 'nullable',
|
||||||
|
'materi' => 'nullable',
|
||||||
|
'catatan' => 'nullable',
|
||||||
|
'kegiatan_minggu_ini' => 'nullable',
|
||||||
|
'perkembangan_siswa' => 'nullable',
|
||||||
|
'kendala_dan_solusi' => 'nullable',
|
||||||
|
'kebutuhan_minggu_depan' => 'nullable',
|
||||||
|
'hal_baik' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kelas_id.required' => 'ID kelas tidak ditemukan',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kondisi jika libur
|
||||||
|
if ($request->status_libur && $request->status_libur == "on"){
|
||||||
|
|
||||||
|
$validated['status'] = "libur";
|
||||||
|
$validated['jenis_libur'] = $request->jenis_libur ?? null;
|
||||||
|
$validated['jenis_libur_lainnya'] = $request->jenis_libur_lainnya ?? null;
|
||||||
|
$logbook = Logbooks::create($validated);
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil ditambah');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$validated['status'] = "Draft";
|
||||||
|
$validated['jenis_libur'] = null;
|
||||||
|
$validated['jenis_libur_lainnya'] = null;
|
||||||
|
|
||||||
|
$logbook = Logbooks::create($validated);
|
||||||
|
$logbook_id = $logbook->id;
|
||||||
|
|
||||||
|
$uploadedFiles = [];
|
||||||
|
if ($request->hasFile('fotos')) {
|
||||||
|
|
||||||
|
foreach ($request->file('fotos') as $file) {
|
||||||
|
if ($file->isValid()) {
|
||||||
|
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$path = $file->storeAs('foto-logbook', $hashedName, 'public');
|
||||||
|
|
||||||
|
$fullPath = Storage::disk('public')->path($path);
|
||||||
|
|
||||||
|
// Resize gambar menggunakan Intervention Image
|
||||||
|
Image::read($fullPath)
|
||||||
|
->orient()
|
||||||
|
->scaleDown(width: 800)
|
||||||
|
->save($fullPath, quality: 85);
|
||||||
|
|
||||||
|
FotoLogbooks::create([
|
||||||
|
'logbook_id' => $logbook_id,
|
||||||
|
'keterangan' => '',
|
||||||
|
'foto' => 'foto-logbook/' . $hashedName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$uploadedFiles[] = 'foto-logbook/' . $hashedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
Logbooks::where('id', $logbook_id)->update(['status' => 'completed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('guru_id')) {
|
||||||
|
$logbook->update(['guru_id' => $request->guru_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil ditambah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addrow(Request $request)
|
||||||
|
{
|
||||||
|
$validate = $request->validate([
|
||||||
|
'tanggal' => 'nullable|date',
|
||||||
|
'start' => 'nullable|date_format:H:i',
|
||||||
|
'end' => 'nullable|date_format:H:i',
|
||||||
|
'pertemuan' => 'nullable|numeric',
|
||||||
|
'materi' => 'nullable|string',
|
||||||
|
'catatan' => 'nullable|string',
|
||||||
|
'kelas_id' => 'required|exists:kelas,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kelas = Kelas::find($validate['kelas_id']);
|
||||||
|
if (!$kelas) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Kelas tidak ditemukan',
|
||||||
|
'data' => null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validate['kode_kelas'] = $kelas->kode_kelas;
|
||||||
|
|
||||||
|
if (!empty($validate['tanggal'])) {
|
||||||
|
$tanggal = Carbon::parse($validate['tanggal']);
|
||||||
|
|
||||||
|
if (!empty($validate['end'])) {
|
||||||
|
$endTime = Carbon::createFromFormat('H:i', $validate['end']);
|
||||||
|
$combinedEnd = $tanggal->copy()->setTimeFrom($endTime);
|
||||||
|
|
||||||
|
$validate['status'] = $combinedEnd->isPast() ? 'perlu diisi' : 'Draft';
|
||||||
|
} else {
|
||||||
|
$validate['status'] = $tanggal->lessThanOrEqualTo(Carbon::today()) ? 'perlu diisi' : 'Draft';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$validate['status'] = 'Draft';
|
||||||
|
}
|
||||||
|
|
||||||
|
$logbook = Logbooks::create($validate);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Sesi berhasil ditambahkan',
|
||||||
|
'data' => $logbook
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id){
|
||||||
|
$data = Logbooks::where('id', $id)->first();
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'kelas_id' => 'required',
|
||||||
|
'guru_id' => 'nullable|exists:guru,id',
|
||||||
|
'sesi_id' => 'nullable|exists:sesi,id',
|
||||||
|
'pertemuan' => 'nullable',
|
||||||
|
'tanggal' => 'nullable',
|
||||||
|
'start' => 'nullable',
|
||||||
|
'end' => 'nullable',
|
||||||
|
'materi' => 'nullable',
|
||||||
|
'catatan' => 'nullable',
|
||||||
|
'kegiatan_minggu_ini' => 'nullable',
|
||||||
|
'perkembangan_siswa' => 'nullable',
|
||||||
|
'kendala_dan_solusi' => 'nullable',
|
||||||
|
'kebutuhan_minggu_depan' => 'nullable',
|
||||||
|
'hal_baik' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kelas_id.required' => 'ID kelas tidak ditemukan',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kondisi jika libur
|
||||||
|
if ($request->status_libur && $request->status_libur == "on"){
|
||||||
|
|
||||||
|
$validated['status'] = "libur";
|
||||||
|
$validated['jenis_libur'] = $request->jenis_libur ?? null;
|
||||||
|
$validated['jenis_libur_lainnya'] = $request->jenis_libur_lainnya ?? null;
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$validated['status'] = "Draft";
|
||||||
|
|
||||||
|
$logbook_id = $data->id;
|
||||||
|
|
||||||
|
$uploadedFiles = [];
|
||||||
|
if ($request->hasFile('fotos')) {
|
||||||
|
foreach ($request->file('fotos') as $file) {
|
||||||
|
if ($file->isValid()) {
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$path = $file->storeAs('foto-logbook', $hashedName, 'public');
|
||||||
|
|
||||||
|
$fullPath = Storage::disk('public')->path($path);
|
||||||
|
|
||||||
|
Image::read($fullPath)
|
||||||
|
->orient()
|
||||||
|
->scaleDown(width: 800)
|
||||||
|
->save($fullPath, quality: 85);
|
||||||
|
|
||||||
|
// Simpan ke database
|
||||||
|
FotoLogbooks::create([
|
||||||
|
'logbook_id' => $logbook_id,
|
||||||
|
'keterangan' => '',
|
||||||
|
'foto' => 'foto-logbook/' . $hashedName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$uploadedFiles[] = 'foto-logbook/' . $hashedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
$validated['status'] = "completed";
|
||||||
|
}
|
||||||
|
$validated['jenis_libur'] = null;
|
||||||
|
$validated['jenis_libur_lainnya'] = null;
|
||||||
|
|
||||||
|
if ($request->has('guru_id')) {
|
||||||
|
$validated['guru_id'] = $request->guru_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id){
|
||||||
|
$data = Logbooks::findOrFail($id);
|
||||||
|
$kelas_id = $data->kelas_id;
|
||||||
|
|
||||||
|
$fotoLogbook = FotoLogbooks::where('logbook_id', $id)->get();
|
||||||
|
if($fotoLogbook->count() > 0){
|
||||||
|
for($b = 0; $b < count($fotoLogbook); $b++){
|
||||||
|
if ($fotoLogbook[$b]->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $fotoLogbook[$b]->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fotoLogbook[$b]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' . $kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Data logbook berhasil dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroySelected(Request $request){
|
||||||
|
|
||||||
|
$logbook_id = $request->id;
|
||||||
|
$kelas_id = Logbooks::where('id', $logbook_id[0])->first();
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($logbook_id); $i++){
|
||||||
|
$logbook = Logbooks::where('id', $logbook_id[$i])->first();
|
||||||
|
|
||||||
|
$fotoLogbook = FotoLogbooks::where('logbook_id', $logbook->id)->get();
|
||||||
|
if($fotoLogbook->count() > 0){
|
||||||
|
for($b = 0; $b < count($fotoLogbook); $b++){
|
||||||
|
if ($fotoLogbook[$b]->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $fotoLogbook[$b]->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fotoLogbook[$b]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$logbook->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' . $kelas_id->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Data logbook berhasil dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyFoto($id){
|
||||||
|
$dataFoto = FotoLogbooks::findOrFail($id);
|
||||||
|
|
||||||
|
if ($dataFoto){
|
||||||
|
$logbook = Logbooks::where('id', $dataFoto->logbook_id)->select('id', 'kelas_id')->first();
|
||||||
|
$filePath = 'storage/' . $dataFoto->foto;
|
||||||
|
if ($filePath) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataFoto->delete();
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $dataFoto->logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
Logbooks::where('id', $logbook->id)->update(['status' => 'completed']);
|
||||||
|
}else{
|
||||||
|
Logbooks::where('id', $logbook->id)->update(['status' => 'draft']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' . $logbook->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Data logbook berhasil dihapus');
|
||||||
|
|
||||||
|
}else{
|
||||||
|
$direct = '/admin/kelas';
|
||||||
|
return redirect($direct)->with('error', 'Data tidak ditemukan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function import(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate(
|
||||||
|
[
|
||||||
|
'file' => 'required|mimes:csv,xls,xlsx'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'file.required' => 'File tidak boleh kosong',
|
||||||
|
'file.mimes' => 'File harus berbentuk csv',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
Excel::import(new LogbookImport, $request->file('file'));
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Import logbook telah berhasil');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// edit and delete logbook from jadwal
|
||||||
|
public function updateLog(Request $request, $id){
|
||||||
|
$data = Logbooks::where('id', $id)->first();
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'kelas_id' => 'required',
|
||||||
|
'guru_id' => 'nullable|exists:guru,id',
|
||||||
|
'kegiatan_minggu_ini' => 'nullable',
|
||||||
|
'perkembangan_siswa' => 'nullable',
|
||||||
|
'kendala_dan_solusi' => 'nullable',
|
||||||
|
'kebutuhan_minggu_depan' => 'nullable',
|
||||||
|
'hal_baik' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kelas_id.required' => 'ID kelas tidak ditemukan',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kondisi jika libur
|
||||||
|
if ($request->status_libur && $request->status_libur == "on"){
|
||||||
|
|
||||||
|
$validated['status'] = "libur";
|
||||||
|
$validated['jenis_libur'] = $request->jenis_libur ?? null;
|
||||||
|
$validated['jenis_libur_lainnya'] = $request->jenis_libur_lainnya ?? null;
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/admin/jadwal/';
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->catatan !== null || $request->catatan !== ''){ $validated['catatan'] = $request->catatan; }
|
||||||
|
if ($request->materi !== null || $request->materi !== ''){ $validated['materi'] = $request->materi; }
|
||||||
|
|
||||||
|
$validated['status'] = "Draft";
|
||||||
|
|
||||||
|
$logbook_id = $data->id;
|
||||||
|
|
||||||
|
$uploadedFiles = [];
|
||||||
|
if ($request->hasFile('fotos')) {
|
||||||
|
foreach ($request->file('fotos') as $file) {
|
||||||
|
if ($file->isValid()) {
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$path = $file->storeAs('foto-logbook', $hashedName, 'public');
|
||||||
|
|
||||||
|
$fullPath = Storage::disk('public')->path($path);
|
||||||
|
|
||||||
|
Image::read($fullPath)
|
||||||
|
->orient()
|
||||||
|
->scaleDown(width: 800)
|
||||||
|
->save($fullPath, quality: 85);
|
||||||
|
|
||||||
|
// Simpan ke database
|
||||||
|
FotoLogbooks::create([
|
||||||
|
'logbook_id' => $logbook_id,
|
||||||
|
'keterangan' => '',
|
||||||
|
'foto' => 'foto-logbook/' . $hashedName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$uploadedFiles[] = 'foto-logbook/' . $hashedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
$validated['status'] = "completed";
|
||||||
|
}
|
||||||
|
$validated['jenis_libur'] = null;
|
||||||
|
$validated['jenis_libur_lainnya'] = null;
|
||||||
|
|
||||||
|
if ($request->has('guru_id')) {
|
||||||
|
$validated['guru_id'] = $request->guru_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/admin/jadwal/';
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyFotoLog($id){
|
||||||
|
$dataFoto = FotoLogbooks::findOrFail($id);
|
||||||
|
|
||||||
|
if ($dataFoto){
|
||||||
|
$logbook = Logbooks::where('id', $dataFoto->logbook_id)->select('id', 'kelas_id')->first();
|
||||||
|
$filePath = 'storage/' . $dataFoto->foto;
|
||||||
|
if ($filePath) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataFoto->delete();
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $dataFoto->logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
Logbooks::where('id', $logbook->id)->update(['status' => 'completed']);
|
||||||
|
}else{
|
||||||
|
Logbooks::where('id', $logbook->id)->update(['status' => 'draft']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/admin/jadwal/';
|
||||||
|
return redirect($direct)->with('success', 'Data logbook berhasil dihapus');
|
||||||
|
|
||||||
|
}else{
|
||||||
|
$direct = '/admin/jadwal/';
|
||||||
|
return redirect($direct)->with('error', 'Data tidak ditemukan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Siswa;
|
||||||
|
use App\Models\Programs;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class ProgramController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
// public function resizeGambar($requestFile, $tempatSimpan, $simpan, $index = 0)
|
||||||
|
// {
|
||||||
|
// $file = $requestFile;
|
||||||
|
//
|
||||||
|
// $filename = time() . '_' . $index . '.' . $file->getClientOriginalExtension();
|
||||||
|
//
|
||||||
|
// $resizedImage = Image::make($file);
|
||||||
|
// $resizedImage->orientate();
|
||||||
|
// $resizedImage->resize(500, 500, function ($constraint) {
|
||||||
|
// $constraint->aspectRatio();
|
||||||
|
// $constraint->upsize();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// $imageEncoded = (string) $resizedImage->encode();
|
||||||
|
//
|
||||||
|
// $path = $tempatSimpan . $filename;
|
||||||
|
// Storage::put($path, $imageEncoded);
|
||||||
|
//
|
||||||
|
// $gambar = $simpan . $filename;
|
||||||
|
// return $gambar;
|
||||||
|
// }
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$data = Programs::all();
|
||||||
|
return view('admin.pages.program', compact('data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function data($id){
|
||||||
|
|
||||||
|
$data = Programs::findOrFail($id);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'id' => $data->id,
|
||||||
|
'level' => $data->level,
|
||||||
|
'level_number' => $data->level_number,
|
||||||
|
'kelas' => $data->kelas,
|
||||||
|
'goal' => $data->goal ?? '',
|
||||||
|
'penjelasan' => $data->penjelasan ?? '',
|
||||||
|
'capaian' => $data->capaian ?? '',
|
||||||
|
'penjelasan_tambahan' => $data->penjelasan_tambahan ?? '',
|
||||||
|
'gambar_load' => $data->gambar_load ?? '',
|
||||||
|
'gambar' => $data->gambar ?? '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'level' => 'required',
|
||||||
|
'level_number' => 'nullable|integer|in:1,2,3,4',
|
||||||
|
'kelas' => 'required',
|
||||||
|
'goal' => 'required',
|
||||||
|
'penjelasan' => 'required',
|
||||||
|
'capaian' => 'required',
|
||||||
|
'gambar' => 'required|image|mimes:jpeg,png,jpg,gif,svg',
|
||||||
|
], [
|
||||||
|
'level.required' => 'Level wajib diisi!',
|
||||||
|
'level_number.integer' => 'Level Number harus berupa angka!',
|
||||||
|
'level_number.in' => 'Level Number tidak valid!',
|
||||||
|
'kelas.required' => 'Kelas wajib diisi!',
|
||||||
|
'goal.required' => 'Goal wajib diisi!',
|
||||||
|
'penjelasan.required' => 'Penjelasan wajib diisi!',
|
||||||
|
'capaian.required' => 'Capaian wajib diisi!',
|
||||||
|
'gambar.required' => 'Gambar wajib diisi!',
|
||||||
|
'gambar.image' => 'Gambar harus berupa gambar!',
|
||||||
|
'gambar.mimes' => 'Format gambar yang diperbolehkan adalah jpeg, png, jpg, gif, svg',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->back()->withErrors($validator)->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $validator->validated();
|
||||||
|
|
||||||
|
if ($request->hasFile('gambar')) {
|
||||||
|
$file = $request->file('gambar');
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
$file->storeAs('program', $hashedName, 'public');
|
||||||
|
$validated['gambar'] = 'program/' . $hashedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazy load jika nanti dibutuhkan
|
||||||
|
// if ($request->hasFile('gambar_load')) {
|
||||||
|
// $file = $request->file('gambar_load');
|
||||||
|
//
|
||||||
|
// $hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
//
|
||||||
|
// $file->storeAs('program', $hashedName, 'public');
|
||||||
|
//
|
||||||
|
// $validated['gambar_load'] = 'program/' . $hashedName;
|
||||||
|
|
||||||
|
$validated['gambar_load'] = '';
|
||||||
|
|
||||||
|
Programs::create($validated);
|
||||||
|
|
||||||
|
return redirect('/admin/program')->with('success', 'Data Program berhasil disimpan');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(string $id)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(string $id)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id)
|
||||||
|
{
|
||||||
|
$data = Programs::find($id);
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'level' => 'required',
|
||||||
|
'level_number' => 'nullable|integer|in:1,2,3,4',
|
||||||
|
'kelas' => 'required',
|
||||||
|
'goal' => 'required',
|
||||||
|
'penjelasan' => 'required',
|
||||||
|
'capaian' => 'required',
|
||||||
|
], [
|
||||||
|
'level.required' => 'Level wajib diisi!',
|
||||||
|
'level_number.integer' => 'Level Number harus berupa angka!',
|
||||||
|
'level_number.in' => 'Level Number tidak valid!',
|
||||||
|
'kelas.required' => 'Kelas wajib diisi!',
|
||||||
|
'goal.required' => 'Goal wajib diisi!',
|
||||||
|
'penjelasan.required' => 'Penjelasan wajib diisi!',
|
||||||
|
'capaian.required' => 'Capaian wajib diisi!',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validated = $validator->validated();
|
||||||
|
|
||||||
|
if ($request->hasFile('gambar')) {
|
||||||
|
if ($data->gambar) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->gambar);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('gambar');
|
||||||
|
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$file->storeAs('program', $hashedName, 'public');
|
||||||
|
|
||||||
|
$validated['gambar'] = 'program/' . $hashedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if ($request->hasFile('gambar_load')) {
|
||||||
|
// if ($data->gambar_load) {
|
||||||
|
// $imagePath = storage_path('app/public/' . $data->gambar_load);
|
||||||
|
// if (file_exists($imagePath)) {
|
||||||
|
// unlink($imagePath);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $file = $request->file('gambar_load');
|
||||||
|
//
|
||||||
|
// $hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
//
|
||||||
|
// $file->storeAs('program', $hashedName, 'public');
|
||||||
|
//
|
||||||
|
// $validated['gambar_load'] = 'program/' . $hashedName;
|
||||||
|
// }
|
||||||
|
// if ($request->file('gambar')) {
|
||||||
|
// $validatedDetail['gambar_load'] = $this->resizeGambar($request->file('gambar'), 'public/program/', 'program/', '0');
|
||||||
|
// if ($data->gambar_load != null || $data->gambar_load != "") {
|
||||||
|
// $filePath = 'storage/' . $data->gambar_load;
|
||||||
|
// if (file_exists($filePath)) {
|
||||||
|
// unlink($filePath);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
$validated['gambar_load'] = ''; // kosongi dulu
|
||||||
|
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
return redirect('/admin/program')->with('success', 'Data Program berhasil diedit');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(string $id)
|
||||||
|
{
|
||||||
|
$data = Programs::findOrFail($id);
|
||||||
|
|
||||||
|
if ($data->gambar) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->gambar);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($data->gambar_load != null || $data->gambar_load != '') {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->gambar_load);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
return redirect('/admin/program')->with('success', 'Data Program berhasil dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Ruangan;
|
||||||
|
use App\Models\Sekolah;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class RuanganController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$data = Ruangan::with('sekolah')->orderBy('created_at', 'desc')->get();
|
||||||
|
$sekolahList = Sekolah::whereNotIn('id', [172, 173, 63])->orderBy('nama_sekolah', 'asc')->get();
|
||||||
|
return view('admin.pages.ruangan', compact('data', 'sekolahList'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single data for ajax editing.
|
||||||
|
*/
|
||||||
|
public function data($id)
|
||||||
|
{
|
||||||
|
$data = Ruangan::with('sekolah')->findOrFail($id);
|
||||||
|
return response()->json($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'nama_ruangan' => 'required|string|max:255',
|
||||||
|
'kapasitas' => 'nullable|integer|min:0',
|
||||||
|
'status' => 'required|in:aktif,nonaktif',
|
||||||
|
'lokasi_tipe' => 'required|in:robotku,sekolah',
|
||||||
|
'sekolah_id' => 'required_if:lokasi_tipe,sekolah|nullable|exists:sekolah,id',
|
||||||
|
], [
|
||||||
|
'nama_ruangan.required' => 'Nama ruangan wajib diisi!',
|
||||||
|
'nama_ruangan.string' => 'Nama ruangan harus berupa teks!',
|
||||||
|
'nama_ruangan.max' => 'Nama ruangan maksimal 255 karakter!',
|
||||||
|
'kapasitas.integer' => 'Kapasitas harus berupa angka!',
|
||||||
|
'kapasitas.min' => 'Kapasitas tidak boleh kurang dari 0!',
|
||||||
|
'status.required' => 'Status wajib dipilih!',
|
||||||
|
'status.in' => 'Status tidak valid!',
|
||||||
|
'lokasi_tipe.required' => 'Lokasi tipe wajib dipilih!',
|
||||||
|
'lokasi_tipe.in' => 'Lokasi tipe tidak valid!',
|
||||||
|
'sekolah_id.required_if' => 'Sekolah wajib dipilih jika lokasi tipe adalah Sekolah!',
|
||||||
|
'sekolah_id.exists' => 'Sekolah tidak valid!',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->back()->withErrors($validator)->withInput()->with('error', 'Gagal menyimpan data. Periksa inputan Anda.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $validator->validated();
|
||||||
|
if ($validated['lokasi_tipe'] === 'robotku') {
|
||||||
|
$validated['sekolah_id'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ruangan::create($validated);
|
||||||
|
|
||||||
|
return redirect('/admin/ruangan')->with('success', 'Data Ruangan berhasil disimpan');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id)
|
||||||
|
{
|
||||||
|
$ruangan = Ruangan::findOrFail($id);
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'nama_ruangan' => 'required|string|max:255',
|
||||||
|
'kapasitas' => 'nullable|integer|min:0',
|
||||||
|
'status' => 'required|in:aktif,nonaktif',
|
||||||
|
'lokasi_tipe' => 'required|in:robotku,sekolah',
|
||||||
|
'sekolah_id' => 'required_if:lokasi_tipe,sekolah|nullable|exists:sekolah,id',
|
||||||
|
], [
|
||||||
|
'nama_ruangan.required' => 'Nama ruangan wajib diisi!',
|
||||||
|
'nama_ruangan.string' => 'Nama ruangan harus berupa teks!',
|
||||||
|
'nama_ruangan.max' => 'Nama ruangan maksimal 255 karakter!',
|
||||||
|
'kapasitas.integer' => 'Kapasitas harus berupa angka!',
|
||||||
|
'kapasitas.min' => 'Kapasitas tidak boleh kurang dari 0!',
|
||||||
|
'status.required' => 'Status wajib dipilih!',
|
||||||
|
'status.in' => 'Status tidak valid!',
|
||||||
|
'lokasi_tipe.required' => 'Lokasi tipe wajib dipilih!',
|
||||||
|
'lokasi_tipe.in' => 'Lokasi tipe tidak valid!',
|
||||||
|
'sekolah_id.required_if' => 'Sekolah wajib dipilih jika lokasi tipe adalah Sekolah!',
|
||||||
|
'sekolah_id.exists' => 'Sekolah tidak valid!',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->back()->withErrors($validator)->withInput()->with('error', 'Gagal memperbarui data. Periksa inputan Anda.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $validator->validated();
|
||||||
|
if ($validated['lokasi_tipe'] === 'robotku') {
|
||||||
|
$validated['sekolah_id'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ruangan->update($validated);
|
||||||
|
|
||||||
|
return redirect('/admin/ruangan')->with('success', 'Data Ruangan berhasil diperbarui');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(string $id)
|
||||||
|
{
|
||||||
|
$ruangan = Ruangan::findOrFail($id);
|
||||||
|
$ruangan->delete();
|
||||||
|
|
||||||
|
return redirect('/admin/ruangan')->with('success', 'Data Ruangan berhasil dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Sekolah;
|
||||||
|
use App\Models\Siswa;
|
||||||
|
use App\Models\DetailKelas;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class SekolahController extends Controller
|
||||||
|
{
|
||||||
|
public function inputData(){
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pesertas = Siswa::whereNull('sekolah_id')
|
||||||
|
->whereNotNull('asal_sekolah')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($pesertas as $peserta) {
|
||||||
|
$asal = trim($peserta->asal_sekolah);
|
||||||
|
|
||||||
|
// Cek apakah sekolah sudah ada
|
||||||
|
$sekolah = Sekolah::where('nama_sekolah', $asal)->first();
|
||||||
|
|
||||||
|
if (!$sekolah) {
|
||||||
|
// Kalau belum ada, buat baru
|
||||||
|
$sekolah = Sekolah::create([
|
||||||
|
'nama_sekolah' => $asal,
|
||||||
|
'status_kerja_sama' => 'Tidak Ada',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update peserta dengan sekolah_id
|
||||||
|
$peserta->update([
|
||||||
|
'sekolah_id' => $sekolah->id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
return response()->json(['message' => 'Data sekolah berhasil dihubungkan dengan peserta.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return response()->json(['error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$data = Sekolah::withCount(['siswa as total_siswa'])
|
||||||
|
->where('id', '!=', 0)
|
||||||
|
->get()
|
||||||
|
->map(function ($item) {
|
||||||
|
$siswaIds = Siswa::where('sekolah_id', $item->id)->pluck('id');
|
||||||
|
|
||||||
|
// Hitung jumlah siswa yang ikut kelas
|
||||||
|
$siswaKelas = DetailKelas::whereIn('siswa_id', $siswaIds)
|
||||||
|
->distinct('siswa_id')
|
||||||
|
->count('siswa_id');
|
||||||
|
|
||||||
|
$item->siswa_kelas = $siswaKelas;
|
||||||
|
return $item;
|
||||||
|
})
|
||||||
|
->sortByDesc('total_siswa')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return view('admin.pages.sekolah', compact('data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDataSekolahCount(){
|
||||||
|
$data = Sekolah::all();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'Semua' => $data->count(),
|
||||||
|
'Aktif' => $data->where('status_kerja_sama', 'Aktif')->count(),
|
||||||
|
'Dalam Pengajuan' => $data->where('status_kerja_sama', 'Dalam Pengajuan')->count(),
|
||||||
|
'Tidak Ada' => $data->where('status_kerja_sama', 'Tidak Ada')->count(),
|
||||||
|
'Berakhir' => $data->where('status_kerja_sama', 'Berakhir')->count(),
|
||||||
|
'Tidak Tertarik' => $data->where('status_kerja_sama', 'Tidak Tertarik')->count(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDataSekolah($sekolah_id){
|
||||||
|
$sekolah = Sekolah::withCount(['siswa as total_siswa'])
|
||||||
|
->where('id', '!=', 0)
|
||||||
|
->get()
|
||||||
|
->map(function ($item) {
|
||||||
|
$siswaIds = Siswa::where('sekolah_id', $item->id)->pluck('id');
|
||||||
|
|
||||||
|
$siswaKelas = DetailKelas::whereIn('siswa_id', $siswaIds)
|
||||||
|
->distinct('siswa_id')
|
||||||
|
->count('siswa_id');
|
||||||
|
|
||||||
|
$item->siswa_kelas = $siswaKelas;
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
})
|
||||||
|
->where('id', '!=', $sekolah_id)
|
||||||
|
->sortByDesc('total_siswa')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return response()->json([$sekolah]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pindahSekolah(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'siswa_ids' => 'required|array',
|
||||||
|
'sekolah_id' => 'required|integer|exists:sekolah,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Siswa::whereIn('id', $request->siswa_ids)
|
||||||
|
->update(['sekolah_id' => $request->sekolah_id]);
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(){
|
||||||
|
return view('admin.pages.create-sekolah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request){
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_sekolah' => 'required',
|
||||||
|
'jenis_lembaga' => 'required',
|
||||||
|
'email' => 'nullable',
|
||||||
|
'alamat_sekolah' => 'nullable',
|
||||||
|
'provinsi' => 'nullable',
|
||||||
|
'kota' => 'nullable',
|
||||||
|
'status_kerja_sama' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_sekolah.required' => 'Nama sekolah wajib diisi',
|
||||||
|
'jenis_lembaga.required' => 'jenis lembaga wajib dipilih',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if ($request->no_telp !== null || $request->no_telp !== ''){
|
||||||
|
$str = ltrim($request->no_telp, '0');
|
||||||
|
$validated['no_telp'] = $str;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('logo')) {
|
||||||
|
$file = $request->file('logo');
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
$file->storeAs('sekolah', $hashedName, 'public');
|
||||||
|
$validated['logo'] = 'sekolah/' . $hashedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['latitude'] = 0;
|
||||||
|
$validated['longitude'] = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Sekolah::create($validated);
|
||||||
|
return redirect('/admin/sekolah')->with('success', 'Data Berhasil Disimpan');
|
||||||
|
}catch (QueryException $e){
|
||||||
|
return redirect('/admin/sekolah')->with('error', 'Data Gagal Disimpan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id){
|
||||||
|
$data = Sekolah::findOrFail($id);
|
||||||
|
|
||||||
|
$siswa = Siswa::where('sekolah_id', $id)->get();
|
||||||
|
|
||||||
|
$siswaIds = $siswa->pluck('id');
|
||||||
|
|
||||||
|
$kelasResume = DB::table('detail_kelas')
|
||||||
|
->join('kelas', 'detail_kelas.kelas_id', '=', 'kelas.id')
|
||||||
|
->whereIn('detail_kelas.siswa_id', $siswaIds)
|
||||||
|
->select(
|
||||||
|
'kelas.id',
|
||||||
|
'kelas.nama_kelas',
|
||||||
|
DB::raw('COUNT(DISTINCT detail_kelas.siswa_id) as total_siswa')
|
||||||
|
)
|
||||||
|
->groupBy('kelas.id', 'kelas.nama_kelas')
|
||||||
|
->orderByDesc('total_siswa')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.pages.edit-sekolah', compact('data', 'siswa', 'kelasResume'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id){
|
||||||
|
$data = Sekolah::findOrFail($id);
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_sekolah' => 'required',
|
||||||
|
'jenis_lembaga' => 'required',
|
||||||
|
'email' => 'nullable',
|
||||||
|
'alamat_sekolah' => 'nullable',
|
||||||
|
'provinsi' => 'nullable',
|
||||||
|
'kota' => 'nullable',
|
||||||
|
'status_kerja_sama' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_sekolah.required' => 'Nama sekolah wajib diisi',
|
||||||
|
'jenis_lembaga.required' => 'jenis lembaga wajib dipilih',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if ($request->no_telp !== null || $request->no_telp !== ''){
|
||||||
|
$str = ltrim($request->no_telp, '0');
|
||||||
|
$validated['no_telp'] = $str;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('logo')) {
|
||||||
|
if ($data->logo) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->logo);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('logo');
|
||||||
|
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$file->storeAs('sekolah', $hashedName, 'public');
|
||||||
|
|
||||||
|
$validated['logo'] = 'sekolah/' . $hashedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data->update($validated);
|
||||||
|
return redirect('/admin/sekolah')->with('success', 'Data Berhasil Diedit');
|
||||||
|
}catch (QueryException $e){
|
||||||
|
return redirect('/admin/sekolah')->with('error', 'Data Gagal Disimpan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id){
|
||||||
|
$data = Sekolah::findOrFail($id);
|
||||||
|
if ($data->logo) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->logo);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
return redirect('/admin/sekolah')->with('success', 'Data Berhasil Dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\DivisiGuru;
|
||||||
|
use App\Models\Guru;
|
||||||
|
use App\Models\Hari;
|
||||||
|
use App\Models\Sesi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SettingController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.pages.settings.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function masterDataIndex()
|
||||||
|
{
|
||||||
|
return view('admin.pages.master-data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divisi Guru
|
||||||
|
public function divisiGuruIndex(){
|
||||||
|
$data = DivisiGuru::all();
|
||||||
|
return view('admin.pages.settings.divisi-guru', compact('data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function divisiGuruStore(Request $request){
|
||||||
|
$validate = $request->validate(
|
||||||
|
['nama_divisi' => 'required|string'],
|
||||||
|
['nama_divisi.required' => 'Nama divisi harus diisi.']
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = DivisiGuru::create($validate);
|
||||||
|
|
||||||
|
return redirect('/admin/settings/divisi-guru')->with('success', 'Divisi guru berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function divisiGuruUpdate(Request $request, $id){
|
||||||
|
$data = DivisiGuru::where('id', $id)->first();
|
||||||
|
|
||||||
|
$validate = $request->validate(
|
||||||
|
['nama_divisi' => 'required|string'],
|
||||||
|
['nama_divisi.required' => 'Nama divisi harus diisi.']
|
||||||
|
);
|
||||||
|
|
||||||
|
$data->update($validate);
|
||||||
|
|
||||||
|
return redirect('/admin/settings/divisi-guru')->with('success', 'Divisi guru berhasil diedit.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function divisiGuruDestroy($id){
|
||||||
|
$data = DivisiGuru::where('id', $id)->first();
|
||||||
|
$data->delete();
|
||||||
|
|
||||||
|
return redirect('/admin/settings/divisi-guru')->with('success', 'Divisi guru berhasil dihapus.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master Sesi
|
||||||
|
public function masterSesiIndex(){
|
||||||
|
$data = Sesi::orderBy('id', 'asc')->get();
|
||||||
|
return view('admin.pages.settings.master-sesi', compact('data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function masterSesiStore(Request $request){
|
||||||
|
$validate = $request->validate([
|
||||||
|
'nama_sesi' => 'required|string|max:255',
|
||||||
|
'jam' => 'required|string|max:255',
|
||||||
|
], [
|
||||||
|
'nama_sesi.required' => 'Nama sesi harus diisi.',
|
||||||
|
'jam.required' => 'Jam sesi harus diisi.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sesi::create($validate);
|
||||||
|
|
||||||
|
return redirect('/admin/settings/master-sesi')->with('success', 'Master sesi berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function masterSesiUpdate(Request $request, $id){
|
||||||
|
$sesi = Sesi::findOrFail($id);
|
||||||
|
|
||||||
|
$validate = $request->validate([
|
||||||
|
'nama_sesi' => 'required|string|max:255',
|
||||||
|
'jam' => 'required|string|max:255',
|
||||||
|
], [
|
||||||
|
'nama_sesi.required' => 'Nama sesi harus diisi.',
|
||||||
|
'jam.required' => 'Jam sesi harus diisi.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sesi->update($validate);
|
||||||
|
|
||||||
|
return redirect('/admin/settings/master-sesi')->with('success', 'Master sesi berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function masterSesiDestroy($id){
|
||||||
|
$sesi = Sesi::findOrFail($id);
|
||||||
|
$sesi->delete();
|
||||||
|
|
||||||
|
return redirect('/admin/settings/master-sesi')->with('success', 'Master sesi berhasil dihapus.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master Hari
|
||||||
|
public function masterHariIndex(){
|
||||||
|
$data = Hari::orderBy('id', 'asc')->get();
|
||||||
|
return view('admin.pages.settings.master-hari', compact('data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function masterHariStore(Request $request){
|
||||||
|
$validate = $request->validate([
|
||||||
|
'nama_hari' => 'required|string|max:255',
|
||||||
|
], [
|
||||||
|
'nama_hari.required' => 'Nama hari harus diisi.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Hari::create($validate);
|
||||||
|
|
||||||
|
return redirect('/admin/settings/master-hari')->with('success', 'Master hari berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function masterHariUpdate(Request $request, $id){
|
||||||
|
$hari = Hari::findOrFail($id);
|
||||||
|
|
||||||
|
$validate = $request->validate([
|
||||||
|
'nama_hari' => 'required|string|max:255',
|
||||||
|
], [
|
||||||
|
'nama_hari.required' => 'Nama hari harus diisi.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$hari->update($validate);
|
||||||
|
|
||||||
|
return redirect('/admin/settings/master-hari')->with('success', 'Master hari berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function masterHariDestroy($id){
|
||||||
|
$hari = Hari::findOrFail($id);
|
||||||
|
$hari->delete();
|
||||||
|
|
||||||
|
return redirect('/admin/settings/master-hari')->with('success', 'Master hari berhasil dihapus.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ketersediaanIndex()
|
||||||
|
{
|
||||||
|
$setting = \App\Models\KetersediaanSetting::getRecord();
|
||||||
|
return view('admin.pages.settings.ketersediaan', compact('setting'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ketersediaanUpdate(Request $request)
|
||||||
|
{
|
||||||
|
$setting = \App\Models\KetersediaanSetting::getRecord();
|
||||||
|
$setting->update([
|
||||||
|
'lock_edit' => $request->has('lock_edit') ? true : false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Pengaturan ketersediaan mengajar berhasil disimpan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportMasterData()
|
||||||
|
{
|
||||||
|
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\MasterDataExport, 'master_data_robotku_' . date('Y-m-d') . '.xlsx');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,573 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Guests\PesertaController;
|
||||||
|
use App\Models\DetailKelas;
|
||||||
|
use App\Models\HistoriSiswa;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Siswa;
|
||||||
|
use App\Models\Programs;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Sekolah;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class SiswaController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$data = Siswa::with('Programs')
|
||||||
|
->where('is_active', true)
|
||||||
|
->get()
|
||||||
|
->sortByDesc('created_at')
|
||||||
|
->map(function ($item) {
|
||||||
|
$nama = trim($item->nama_anak);
|
||||||
|
|
||||||
|
if ($item->double_data_checked) {
|
||||||
|
$item->status_duplikat = 'clear';
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sudahTerdaftar = Siswa::where('is_active', 1)
|
||||||
|
->where('nama_anak', 'LIKE', '%' . $nama . '%')
|
||||||
|
->where('id', '!=', $item->id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
$item->status_duplikat = $sudahTerdaftar ? 'sudah_terdaftar' : 'clear';
|
||||||
|
return $item;
|
||||||
|
});
|
||||||
|
|
||||||
|
$sekolah = Sekolah::where('id', '!=', 0)
|
||||||
|
->orderBy('nama_sekolah', 'asc')->get();
|
||||||
|
|
||||||
|
return view('admin.pages.siswa', compact('data', 'sekolah'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function getApprovedListCount(){
|
||||||
|
$count = Siswa::where('is_active', false)->count();
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'count' => $count
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aprovedList(){
|
||||||
|
$data = Siswa::with('Programs')
|
||||||
|
->where('is_active', 0)
|
||||||
|
->get()
|
||||||
|
->sortByDesc('created_at')
|
||||||
|
->map(function ($item) {
|
||||||
|
$nama = trim($item->nama_anak);
|
||||||
|
|
||||||
|
$sudahTerdaftar = Siswa::where('is_active', 1)
|
||||||
|
->where(function ($q) use ($nama) {
|
||||||
|
$q->where('nama_anak', 'LIKE', '%' . $nama . '%')
|
||||||
|
->orWhereRaw('? LIKE CONCAT("%", nama_anak, "%")', [$nama]);
|
||||||
|
})
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
$item->status_duplikat = $sudahTerdaftar ? 'sudah_terdaftar' : 'clear';
|
||||||
|
return $item;
|
||||||
|
});
|
||||||
|
|
||||||
|
$sekolah = Sekolah::orderByDesc('id')->get();
|
||||||
|
|
||||||
|
return view('admin.pages.approval-list-siswa', compact('data', 'sekolah'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compareSiswa(Request $request, $id)
|
||||||
|
{
|
||||||
|
$baru = Siswa::with('Programs', 'sekolah')->find($id);
|
||||||
|
|
||||||
|
if (!$baru) {
|
||||||
|
if ($request->ajax()) {
|
||||||
|
return response()->json(['error' => 'Data siswa baru tidak ditemukan'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(404, 'Data siswa baru tidak ditemukan');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nama = trim($baru->nama_anak);
|
||||||
|
|
||||||
|
// Cari data aktif yang mirip dua arah dengan nama baru
|
||||||
|
$mirip = Siswa::where('id', '!=', $baru->id)
|
||||||
|
->where('is_active', 1)
|
||||||
|
->where(function ($q) use ($nama) {
|
||||||
|
$q->where('nama_anak', 'LIKE', '%' . $nama . '%')
|
||||||
|
->orWhereRaw('? LIKE CONCAT("%", nama_anak, "%")', [$nama]);
|
||||||
|
})
|
||||||
|
->with('Programs', 'sekolah')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// helper untuk membangun history kelas
|
||||||
|
$buildHistory = function ($peserta) {
|
||||||
|
$dk = DetailKelas::where('siswa_id', $peserta->id)->get();
|
||||||
|
$kelasIds = $dk->pluck('kelas_id');
|
||||||
|
$kelasArr = Kelas::whereIn('id', $kelasIds)->get()->map(function ($kelas) use ($dk) {
|
||||||
|
$k = $dk->where('kelas_id', $kelas->id)->first();
|
||||||
|
return [
|
||||||
|
'nama_kelas' => $kelas->nama_kelas,
|
||||||
|
'jenis_kelas' => $kelas->jenis_kelas ?? '-',
|
||||||
|
'tanggal_bergabung' => Carbon::parse($k->created_at)->locale('id')->translatedFormat('d M Y'),
|
||||||
|
'status' => $k->status_siswa ?? '-',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return ['events' => collect([]), 'kelas' => $kelasArr];
|
||||||
|
};
|
||||||
|
|
||||||
|
// jika request ajax, kirim JSON seperti sebelumnya
|
||||||
|
if ($request->ajax()) {
|
||||||
|
$baruHist = $buildHistory($baru);
|
||||||
|
return response()->json([
|
||||||
|
'baru' => array_merge([
|
||||||
|
'id' => $baru->id,
|
||||||
|
'nama_anak' => $baru->nama_anak ?? 'Belum diisi',
|
||||||
|
'nama_orangtua' => $baru->nama_orangtua ?? 'Belum diisi',
|
||||||
|
'no_hp_orangtua' => $baru->no_hp_orangtua ?? 'Belum diisi',
|
||||||
|
'asal_sekolah' => $baru->asal_sekolah ?? optional($baru->sekolah)->nama_sekolah ?? 'Belum diisi',
|
||||||
|
'kelas' => $baru->kelas ?? 'Belum diisi',
|
||||||
|
'tanggal_daftar' => optional($baru->created_at)->translatedFormat('d M Y - H:i') ?? 'Belum diisi',
|
||||||
|
'foto' => $baru->foto
|
||||||
|
? asset('storage/' . $baru->foto)
|
||||||
|
: ($baru->jenis_kelamin == 'P'
|
||||||
|
? asset('img/kid-woman.png')
|
||||||
|
: asset('img/kid-man.png')),
|
||||||
|
], $baruHist),
|
||||||
|
|
||||||
|
'mirip' => $mirip->map(function ($s) use ($buildHistory) {
|
||||||
|
$hist = $buildHistory($s);
|
||||||
|
return array_merge([
|
||||||
|
'id' => $s->id,
|
||||||
|
'nama_anak' => $s->nama_anak ?? 'Belum diisi',
|
||||||
|
'nama_orangtua' => $s->nama_orangtua ?? 'Belum diisi',
|
||||||
|
'no_hp_orangtua' => $s->no_hp_orangtua ?? 'Belum diisi',
|
||||||
|
'asal_sekolah' => $s->asal_sekolah ?? optional($s->sekolah)->nama_sekolah ?? 'Belum diisi',
|
||||||
|
'kelas' => $s->kelas ?? 'Belum diisi',
|
||||||
|
'tanggal_daftar' => optional($s->created_at)->translatedFormat('d M Y - H:i') ?? 'Belum diisi',
|
||||||
|
'foto' => $s->foto
|
||||||
|
? asset('storage/' . $s->foto)
|
||||||
|
: ($s->jenis_kelamin == 'P'
|
||||||
|
? asset('img/kid-woman.png')
|
||||||
|
: asset('img/kid-man.png')),
|
||||||
|
], $hist);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tambahkan histori ke objek sebelum kirim ke view
|
||||||
|
$baruHist = $buildHistory($baru);
|
||||||
|
$baru->events = $baruHist['events'];
|
||||||
|
$baru->kelasHistory = $baruHist['kelas'];
|
||||||
|
|
||||||
|
$mirip->transform(function ($s) use ($buildHistory) {
|
||||||
|
$h = $buildHistory($s);
|
||||||
|
$s->events = $h['events'];
|
||||||
|
$s->kelasHistory = $h['kelas'];
|
||||||
|
return $s;
|
||||||
|
});
|
||||||
|
|
||||||
|
// render halaman comparasion bila bukan ajax
|
||||||
|
return view('admin.pages.compare-siswa', compact('baru', 'mirip'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approveSiswa(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'duplicate_id' => 'required|integer',
|
||||||
|
'siswa_baru_id' => 'required|integer',
|
||||||
|
'merge_fields' => 'sometimes|array',
|
||||||
|
'merge_fields.*' => 'string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicateId = $request->duplicate_id; // ID siswa lama/utama
|
||||||
|
$siswaBaruId = $request->siswa_baru_id; // ID siswa baru (hasil input)
|
||||||
|
$mergeFields = $request->input('merge_fields', []);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$siswaBaru = Siswa::findOrFail($siswaBaruId);
|
||||||
|
|
||||||
|
if ($siswaBaruId == $duplicateId) {
|
||||||
|
// treat as new student approval
|
||||||
|
$sekolah_id = $siswaBaru->sekolah_id;
|
||||||
|
|
||||||
|
if (empty($sekolah_id)) {
|
||||||
|
$sekolah = Sekolah::create([
|
||||||
|
'nama_sekolah' => $siswaBaru->asal_sekolah
|
||||||
|
]);
|
||||||
|
$sekolah_id = $sekolah->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$siswaBaru->update([
|
||||||
|
'is_active' => 1,
|
||||||
|
'sekolah_id' => $sekolah_id,
|
||||||
|
'asal_sekolah' => null,
|
||||||
|
'double_data_checked' => 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
$msg = "Siswa berhasil disetujui";
|
||||||
|
} else {
|
||||||
|
// update duplicate record with selected fields from new data before merging
|
||||||
|
if (!empty($mergeFields)) {
|
||||||
|
$allowed = [
|
||||||
|
'nama_orangtua', 'no_hp_orangtua', 'asal_sekolah',
|
||||||
|
'kelas', 'tempat_lahir', 'tanggal_lahir', 'jenis_kelamin',
|
||||||
|
'email', 'program_id', 'status', 'sekolah_id'
|
||||||
|
];
|
||||||
|
$updateData = [];
|
||||||
|
foreach ($mergeFields as $field) {
|
||||||
|
if (in_array($field, $allowed) && isset($siswaBaru->$field)) {
|
||||||
|
$updateData[$field] = $siswaBaru->$field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($updateData)) {
|
||||||
|
Siswa::where('id', $duplicateId)->update($updateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$kelasBaru = DetailKelas::where('siswa_id', $siswaBaruId)->get();
|
||||||
|
|
||||||
|
foreach ($kelasBaru as $kelas) {
|
||||||
|
$sudahAda = DetailKelas::where('kelas_id', $kelas->kelas_id)
|
||||||
|
->where('siswa_id', $duplicateId)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($sudahAda) {
|
||||||
|
$kelas->delete();
|
||||||
|
} else {
|
||||||
|
$kelas->update(['siswa_id' => $duplicateId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Siswa::where('id', $duplicateId)->update(['is_active' => 1]);
|
||||||
|
$siswaBaru->delete();
|
||||||
|
|
||||||
|
$msg = "Data siswa berhasil digabungkan";
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => $msg,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_anak' => 'required|string|max:255|unique:siswa',
|
||||||
|
'kelas' => 'required',
|
||||||
|
'nama_orangtua' => 'required',
|
||||||
|
'program_id'=> 'required',
|
||||||
|
'status' => 'required',
|
||||||
|
'jenis_kelamin' => 'required',
|
||||||
|
'sekolah_id' => 'nullable',
|
||||||
|
'asal_sekolah' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_anak.required' => 'Nama anak wajib diisi',
|
||||||
|
'nama_anak.max' => 'Nama anak terlalu panjang',
|
||||||
|
'nama_anak.unique' => 'Nama anak tersebut sudah ada',
|
||||||
|
'kelas.required' => 'Kelas wajib diisi',
|
||||||
|
'nama_orangtua.required' => 'Nama orangtua wajib diisi',
|
||||||
|
'program_id.required' => 'Program wajib dipilih',
|
||||||
|
'status.required' => 'Status wajib diisi',
|
||||||
|
'jenis_kelamin.required' => 'Jenis kelamin anak wajib diisi',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($request->no_hp_orangtua !== null || $request->no_hp_orangtua !== ''){
|
||||||
|
$str = ltrim($request->no_hp_orangtua, '0');
|
||||||
|
$validated['no_hp_orangtua'] = $str;
|
||||||
|
}
|
||||||
|
if ($request->keterangan !== null || $request->keterangan !== ''){
|
||||||
|
$validated['keterangan'] = $request->keterangan;
|
||||||
|
}else{
|
||||||
|
$validated['keterangan'] = null;
|
||||||
|
}
|
||||||
|
if ($request->tempat_lahir !== null || $request->tempat_lahir !== ''){
|
||||||
|
$validated['tempat_lahir'] = $request->tempat_lahir;
|
||||||
|
}
|
||||||
|
if (!empty($request->tanggal_lahir)) {
|
||||||
|
$validated['tanggal_lahir'] = $request->tanggal_lahir;
|
||||||
|
$validated['usia'] = Carbon::parse($request->tanggal_lahir)->age;
|
||||||
|
} else {
|
||||||
|
$validated['usia'] = $request->usia ?? null;
|
||||||
|
}
|
||||||
|
if ($request->hasFile('foto')) {
|
||||||
|
$file = $request->file('foto');
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
$file->storeAs('anak', $hashedName, 'public');
|
||||||
|
$validated['foto'] = 'anak/' . $hashedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['register_by'] = 1;
|
||||||
|
$validated['is_active'] = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$siswa = Siswa::create($validated);
|
||||||
|
|
||||||
|
$namaProgram = Programs::select('level', 'kelas')->where('id', $request->program_id)->first();
|
||||||
|
|
||||||
|
$namaProgram = 'Level '. $namaProgram->level . ' ' . $namaProgram->kelas;
|
||||||
|
|
||||||
|
// add data to histori siswa
|
||||||
|
HistoriSiswa::create([
|
||||||
|
'siswa_id' => $siswa->id,
|
||||||
|
'status' => $siswa->status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect('/admin/siswa')->with('success', 'Data Berhasil Disimpan');
|
||||||
|
}catch (QueryException $e){
|
||||||
|
return redirect('/admin/siswa')->with('error', 'Data anak telah ada');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getDataSiswa(string $id)
|
||||||
|
{
|
||||||
|
$data = Siswa::with('Programs')->where('id', $id)->first();
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
return response()->json(['error' => 'Peserta tidak ditemukan'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$events = collect([]);
|
||||||
|
|
||||||
|
$detailKelas = DetailKelas::where('siswa_id', $data->id)->get();
|
||||||
|
$kelasIds = $detailKelas->pluck('kelas_id');
|
||||||
|
|
||||||
|
$kelas = Kelas::whereIn('id', $kelasIds)->get()->map(function ($kelas) use ($detailKelas) {
|
||||||
|
$detailKelasSingle = $detailKelas->where('kelas_id', $kelas->id)->first();
|
||||||
|
return [
|
||||||
|
'nama_kelas' => $kelas->nama_kelas,
|
||||||
|
'jenis_kelas' => $kelas->jenis_kelas ?? '-',
|
||||||
|
'tanggal_bergabung' => Carbon::parse($detailKelasSingle->created_at)->locale('id')->translatedFormat('d M Y'),
|
||||||
|
'status' => $detailKelasSingle->status_siswa ?? '-',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$created_at = Carbon::parse($data->created_at)->locale('id')->translatedFormat('l, d F Y - H:i') . ' WIB';
|
||||||
|
$register_by = match ($data->register_by) {
|
||||||
|
1 => 'Admin',
|
||||||
|
0 => 'Self (Freetrial)',
|
||||||
|
default => 'Undifined',
|
||||||
|
};
|
||||||
|
$usia = 0;
|
||||||
|
if ($data->tanggal_lahir == null || $data->tanggal_lahir === '' || $data->tanggal_lahir == '0000-00-00') {
|
||||||
|
$usia = $data->usia ?? null;
|
||||||
|
}else{
|
||||||
|
$usia = Carbon::parse($data->tanggal_lahir)->age;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'id' => $data->id,
|
||||||
|
'nama_anak' => $data->nama_anak,
|
||||||
|
'nama_orangtua' => $data->nama_orangtua,
|
||||||
|
'no_hp_orangtua' => $data->no_hp_orangtua,
|
||||||
|
'usia' => $usia ?? null,
|
||||||
|
'kelas' => $data->kelas,
|
||||||
|
'tempat_lahir' => $data->tempat_lahir ?? null,
|
||||||
|
'tanggal_lahir' => $data->tanggal_lahir ?? null,
|
||||||
|
'jenis_kelamin' => $data->jenis_kelamin ?? null,
|
||||||
|
'email' => $data->email ?? null,
|
||||||
|
'program_id'=> $data->program_id ?? null,
|
||||||
|
'program_kelas' => $data->Programs->kelas ?? null,
|
||||||
|
'level' => $data->Programs->level ?? null,
|
||||||
|
'program' => optional($data->Programs)->level ? 'Level ' . optional($data->Programs)->level . ' (' . optional($data->Programs)->kelas . ')' : null,
|
||||||
|
'status' => $data->status ?? null,
|
||||||
|
'sekolah_id' => $data->sekolah_id ?? null,
|
||||||
|
'asal_sekolah' => optional($data->sekolah)->nama_sekolah
|
||||||
|
?? $data->asal_sekolah
|
||||||
|
?? null,
|
||||||
|
'keterangan' => $data->keterangan ?? null,
|
||||||
|
'foto' => $data->foto ?? null,
|
||||||
|
'tanggal_daftar' => $created_at ?? null,
|
||||||
|
'register_by' => $register_by ?? null,
|
||||||
|
'events' => $events,
|
||||||
|
'histori' => $kelas
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(string $id)
|
||||||
|
{
|
||||||
|
$data = Siswa::with(['Programs', 'sekolah'])->findOrFail($id);
|
||||||
|
|
||||||
|
$detailKelas = DetailKelas::where('siswa_id', $data->id)->get();
|
||||||
|
$kelasIds = $detailKelas->pluck('kelas_id');
|
||||||
|
|
||||||
|
$kelasHistory = Kelas::whereIn('id', $kelasIds)->get()->map(function ($kelas) use ($detailKelas) {
|
||||||
|
$detailKelasSingle = $detailKelas->where('kelas_id', $kelas->id)->first();
|
||||||
|
return [
|
||||||
|
'nama_kelas' => $kelas->nama_kelas,
|
||||||
|
'jenis_kelas' => $kelas->jenis_kelas ?? '-',
|
||||||
|
'tanggal_bergabung' => ($detailKelasSingle && $detailKelasSingle->created_at)
|
||||||
|
? Carbon::parse($detailKelasSingle->created_at)->locale('id')->translatedFormat('d M Y')
|
||||||
|
: '-',
|
||||||
|
'status' => $detailKelasSingle->status_siswa ?? '-',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$tanggal_daftar = Carbon::parse($data->created_at)->locale('id')->translatedFormat('d F Y - H:i') . ' WIB';
|
||||||
|
$register_by = match ($data->register_by) {
|
||||||
|
1 => 'Admin',
|
||||||
|
0 => 'Self (Freetrial)',
|
||||||
|
default => 'Undefined',
|
||||||
|
};
|
||||||
|
|
||||||
|
$usia = 0;
|
||||||
|
if (empty($data->tanggal_lahir) || $data->tanggal_lahir == '0000-00-00') {
|
||||||
|
$usia = $data->usia ?? null;
|
||||||
|
} else {
|
||||||
|
$usia = Carbon::parse($data->tanggal_lahir)->age;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sekolah = Sekolah::where('id', '!=', 0)
|
||||||
|
->orderBy('nama_sekolah', 'asc')->get();
|
||||||
|
|
||||||
|
return view('admin.pages.siswa_detail', compact('data', 'kelasHistory', 'tanggal_daftar', 'register_by', 'usia', 'sekolah'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getSiswaCount()
|
||||||
|
{
|
||||||
|
$data = Siswa::get();
|
||||||
|
return response()->json([
|
||||||
|
'Semua' => $data->count() ?? null,
|
||||||
|
'Contacted' => $data->where('status', 'Contacted')->count() ?? null,
|
||||||
|
'Freetrial' => $data->where('status', 'Freetrial')->count() ?? null,
|
||||||
|
'Booking' => $data->where('status', 'Booking')->count() ?? null,
|
||||||
|
'Terdaftar-24-Sesi' => $data->where('status', 'Terdaftar 24 Sesi')->count() ?? null,
|
||||||
|
'Terdaftar-12-Sesi' => $data->where('status', 'Terdaftar 12 Sesi')->count() ?? null,
|
||||||
|
'Aktif-24-Sesi' => $data->where('status', 'Aktif 24 Sesi')->count() ?? null,
|
||||||
|
'Aktif-12-Sesi' => $data->where('status', 'Aktif 12 Sesi')->count() ?? null,
|
||||||
|
'Aktif-Ekstra' => $data->where('status', 'Aktif Ekstra')->count() ?? null,
|
||||||
|
'Tidak-Aktif' => $data->where('status', 'Tidak Aktif')->count() ?? null,
|
||||||
|
'Berhenti' => $data->where('status', 'Berhenti')->count() ?? null,
|
||||||
|
'Tidak-Tertarik' => $data->where('status', 'Tidak Tertarik')->count() ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, string $id)
|
||||||
|
{
|
||||||
|
$data = Siswa::with('Programs')->findOrFail($id);
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_anak' => 'required|string|max:255',
|
||||||
|
'kelas' => 'required',
|
||||||
|
'nama_orangtua' => 'required',
|
||||||
|
'program_id'=> 'required',
|
||||||
|
'status'=> 'required',
|
||||||
|
'jenis_kelamin' => 'required',
|
||||||
|
'sekolah_id' => 'nullable',
|
||||||
|
'asal_sekolah' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_anak.required' => 'Nama anak wajib diisi',
|
||||||
|
'nama_anak.max' => 'Nama Anak Terlalu Panjang',
|
||||||
|
'kelas.required' => 'Kelas wajib diisi',
|
||||||
|
'nama_orangtua.required' => 'Nama orangtua wajib diisi',
|
||||||
|
'program_id.required' => 'Program wajib dipilih',
|
||||||
|
'status.required' => 'Status wajib diisi',
|
||||||
|
'jenis_kelamin.required' => 'Jenis kelamin anak wajib diisi',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if ($request->no_hp_orangtua !== null || $request->no_hp_orangtua !== ''){
|
||||||
|
$str = ltrim($request->no_hp_orangtua, '0');
|
||||||
|
$validated['no_hp_orangtua'] = $str;
|
||||||
|
}
|
||||||
|
if ($request->keterangan !== null || $request->keterangan !== ''){
|
||||||
|
$validated['keterangan'] = $request->keterangan;
|
||||||
|
}else{
|
||||||
|
$validated['keterangan'] = null;
|
||||||
|
}
|
||||||
|
if ($request->tempat_lahir !== null || $request->tempat_lahir !== ''){
|
||||||
|
$validated['tempat_lahir'] = $request->tempat_lahir;
|
||||||
|
}
|
||||||
|
if (!empty($request->tanggal_lahir)) {
|
||||||
|
$validated['tanggal_lahir'] = $request->tanggal_lahir;
|
||||||
|
$validated['usia'] = Carbon::parse($request->tanggal_lahir)->age;
|
||||||
|
} else {
|
||||||
|
$validated['usia'] = $request->usia ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('foto')) {
|
||||||
|
if ($data->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('foto');
|
||||||
|
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$file->storeAs('anak', $hashedName, 'public');
|
||||||
|
|
||||||
|
$validated['foto'] = 'anak/' . $hashedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
// histori siswa check
|
||||||
|
$historiSiswa = HistoriSiswa::where('siswa_id', $id)
|
||||||
|
->whereMonth('created_at', Carbon::now()->month)
|
||||||
|
->whereYear('created_at', Carbon::now()->year)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if(!empty($historiSiswa)){
|
||||||
|
$historiSiswa->update([
|
||||||
|
'status' => $data->status,
|
||||||
|
]);
|
||||||
|
}else{
|
||||||
|
HistoriSiswa::create([
|
||||||
|
'siswa_id' => $id,
|
||||||
|
'status' => $data->status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect('/admin/siswa')->with('success', 'Data Berhasil Diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(string $id)
|
||||||
|
{
|
||||||
|
$data = Siswa::with('Programs')->findOrFail($id);
|
||||||
|
if ($data->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
|
||||||
|
return redirect('/admin/siswa')->with('success', 'Data Berhasil Dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class UserAdminController extends Controller
|
||||||
|
{
|
||||||
|
public function index(){
|
||||||
|
$data = User::where('role', 'admin')->get();
|
||||||
|
return view('admin.pages.admin', compact('data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request){
|
||||||
|
$validate = $request->validate([
|
||||||
|
'name' => 'required',
|
||||||
|
'email' => 'required',
|
||||||
|
'username' => 'required',
|
||||||
|
]);
|
||||||
|
if($request->password == null || $request->password == ''){
|
||||||
|
$validate['password'] = 123456;
|
||||||
|
}else{
|
||||||
|
$validate['password'] = $request->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validate['role'] = 'admin';
|
||||||
|
|
||||||
|
User::create($validate);
|
||||||
|
|
||||||
|
return redirect('/admin/user-admin')->with('success', 'Data Berhasil Disimpan');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id){
|
||||||
|
$data = User::find($id);
|
||||||
|
$validate = $request->validate([
|
||||||
|
'name' => 'required',
|
||||||
|
'email' => 'required',
|
||||||
|
'username' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->password){
|
||||||
|
$validate['password'] = $request->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->update($validate);
|
||||||
|
|
||||||
|
return redirect('/admin/user-admin')->with('success', 'Data Berhasil Diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id){
|
||||||
|
$data = User::findOrFail($id);
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
|
||||||
|
return redirect('/admin/user-admin')->with('success', 'Data Berhasil Dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class AuthenticationController extends Controller
|
||||||
|
{
|
||||||
|
public function login()
|
||||||
|
{
|
||||||
|
return view('login');
|
||||||
|
}
|
||||||
|
protected function redirectTo()
|
||||||
|
{
|
||||||
|
switch (Auth::user()->role) {
|
||||||
|
case 'siswa':
|
||||||
|
return route('siswa.dashboard');
|
||||||
|
case 'admin':
|
||||||
|
return route('admin.dashboard');
|
||||||
|
case 'guru':
|
||||||
|
return route('guru.dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhoneToDatabase($phone)
|
||||||
|
{
|
||||||
|
$phone = preg_replace('/\D/', '', $phone);
|
||||||
|
|
||||||
|
if (substr($phone, 0, 1) === '0') {
|
||||||
|
$phone = substr($phone, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (substr($phone, 0, 2) === '62') {
|
||||||
|
$phone = substr($phone, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (substr($phone, 0, 2) === '62') {
|
||||||
|
$phone = substr($phone, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $phone;
|
||||||
|
}
|
||||||
|
public function auth(Request $request){
|
||||||
|
// Validasi input
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'username' => 'required',
|
||||||
|
'password' => 'required'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'username.required' => 'Masukkan username Anda.',
|
||||||
|
'password.required' => 'Masukkan password Anda.'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$remember = $request->filled('remember');
|
||||||
|
|
||||||
|
$user = User::where('username', $request->username)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($user && Auth::attempt(['username' => $user->username, 'password' => $request->password], $remember)) {
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
return redirect($this->redirectTo())->with('success', '🤖 Selamat datang Kembali');
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('statusError', 'Akun anda tidak ditemukan, Periksa kembali username dan password anda!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(Request $request): RedirectResponse{
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/login')->with('statusSuccess', 'Logout berhasil');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Guru;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Events;
|
||||||
|
use App\Models\HistoriSiswa;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use App\Models\Siswa;
|
||||||
|
use App\Models\EvaluationAssignment;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class DashboardGuruController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function dashboardData()
|
||||||
|
{
|
||||||
|
Carbon::setLocale('id');
|
||||||
|
|
||||||
|
$guru_id = auth()->user()->Guru->id;
|
||||||
|
|
||||||
|
$total_sesi_hari_ini = Logbooks::whereHas('kelas', function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id);
|
||||||
|
})->whereDate('tanggal', Carbon::today())->count();
|
||||||
|
|
||||||
|
$total_sesi_seminggu = Logbooks::whereHas('kelas', function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id);
|
||||||
|
})->whereBetween('tanggal', [
|
||||||
|
Carbon::now()->startOfWeek(),
|
||||||
|
Carbon::now()->endOfWeek(),
|
||||||
|
])->count();
|
||||||
|
|
||||||
|
$total_sesi = Logbooks::whereHas('kelas', function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id);
|
||||||
|
})->count();
|
||||||
|
|
||||||
|
$total_kelas = Kelas::where('guru_id', $guru_id)->count();
|
||||||
|
|
||||||
|
$jharini = Logbooks::whereHas('kelas', function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id);
|
||||||
|
})->whereDate('tanggal', Carbon::today())->count();
|
||||||
|
|
||||||
|
$jcompleted = Logbooks::whereHas('kelas', function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id);
|
||||||
|
})
|
||||||
|
->whereDate('tanggal', Carbon::today())
|
||||||
|
->whereIn('status', ['completed', 'libur'])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$pending_p2p = EvaluationAssignment::where('evaluator_id', $guru_id)
|
||||||
|
->where('status', 'pending')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'total_sesi_hari_ini' => $total_sesi_hari_ini,
|
||||||
|
'total_sesi_seminggu' => $total_sesi_seminggu,
|
||||||
|
'total_sesi' => $total_sesi,
|
||||||
|
'total_kelas' => $total_kelas,
|
||||||
|
'jharini' => $jharini,
|
||||||
|
'jcompleted' => $jcompleted,
|
||||||
|
'pending_p2p' => $pending_p2p,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChartData(Request $request)
|
||||||
|
{
|
||||||
|
Carbon::setLocale('id');
|
||||||
|
$guru_id = auth()->user()->Guru->id;
|
||||||
|
|
||||||
|
// Jumlah bulan yang ditampilkan (default 5, max 24)
|
||||||
|
$monthCount = min((int) $request->get('months', 5), 24);
|
||||||
|
if ($monthCount < 1) $monthCount = 1;
|
||||||
|
|
||||||
|
$months = collect();
|
||||||
|
for ($i = $monthCount - 1; $i >= 0; $i--) {
|
||||||
|
$months->push(Carbon::now()->subMonths($i)->startOfMonth());
|
||||||
|
}
|
||||||
|
|
||||||
|
$labels = [];
|
||||||
|
$totalSesi = [];
|
||||||
|
$totalCompleted = [];
|
||||||
|
|
||||||
|
foreach ($months as $month) {
|
||||||
|
$start = $month->copy()->startOfMonth();
|
||||||
|
$end = $month->copy()->endOfMonth();
|
||||||
|
|
||||||
|
$total = Logbooks::whereHas('kelas', function($q) use ($guru_id) {
|
||||||
|
$q->where('guru_id', $guru_id);
|
||||||
|
})
|
||||||
|
->whereBetween('tanggal', [$start->toDateString(), $end->toDateString()])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$completed = Logbooks::whereHas('kelas', function($q) use ($guru_id) {
|
||||||
|
$q->where('guru_id', $guru_id);
|
||||||
|
})
|
||||||
|
->whereBetween('tanggal', [$start->toDateString(), $end->toDateString()])
|
||||||
|
->where('status', 'completed')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$labels[] = $month->translatedFormat('M Y');
|
||||||
|
$totalSesi[] = $total;
|
||||||
|
$totalCompleted[] = $completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'labels' => $labels,
|
||||||
|
'total_sesi' => $totalSesi,
|
||||||
|
'total_completed' => $totalCompleted,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('guru.pages.dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Guru;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\FotoLogbooks;
|
||||||
|
use App\Models\Programs;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class GaleriGuruController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$guru = auth()->user()->Guru;
|
||||||
|
if (!$guru) {
|
||||||
|
abort(403, 'Akses ditolak. Anda bukan terdaftar sebagai Guru.');
|
||||||
|
}
|
||||||
|
$guru_id = $guru->id;
|
||||||
|
|
||||||
|
// Query classes where the teacher is the PIC or where they taught a session (logbook)
|
||||||
|
$kelas = Kelas::where(function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id)
|
||||||
|
->orWhereHas('logbooks', function($q) use ($guru_id) {
|
||||||
|
$q->where('guru_id', $guru_id);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($request->filled('semester'), function ($q) use ($request) {
|
||||||
|
$q->where('semester', $request->semester);
|
||||||
|
})
|
||||||
|
->when($request->filled('tahun'), function ($q) use ($request) {
|
||||||
|
$q->where('tahun_ajaran', $request->tahun);
|
||||||
|
})
|
||||||
|
->when($request->filled('is_ekstrakurikuler'), function ($q) use ($request) {
|
||||||
|
$q->where('is_ekstrakurikuler', $request->is_ekstrakurikuler);
|
||||||
|
})
|
||||||
|
->when($request->filled('program_id'), function ($q) use ($request) {
|
||||||
|
$q->where('program_id', $request->program_id);
|
||||||
|
})
|
||||||
|
->with(['logbooks' => function($query) {
|
||||||
|
$query->has('FotoLogbook')
|
||||||
|
->with('FotoLogbook')
|
||||||
|
->orderBy('tanggal', 'desc');
|
||||||
|
}])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$semesters = Kelas::whereNotNull('semester')->distinct()->pluck('semester');
|
||||||
|
$years = Kelas::whereNotNull('tahun_ajaran')->distinct()->pluck('tahun_ajaran');
|
||||||
|
$programs = Programs::all();
|
||||||
|
|
||||||
|
return view('guru.pages.galeri', compact('kelas', 'semesters', 'years', 'programs'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download($id)
|
||||||
|
{
|
||||||
|
$foto = FotoLogbooks::findOrFail($id);
|
||||||
|
$guru = auth()->user()->Guru;
|
||||||
|
if (!$guru) {
|
||||||
|
abort(403, 'Akses ditolak. Anda bukan terdaftar sebagai Guru.');
|
||||||
|
}
|
||||||
|
$guru_id = $guru->id;
|
||||||
|
|
||||||
|
// Security check: Verify if the teacher has access to this logbook's class
|
||||||
|
$logbook = $foto->Logbooks;
|
||||||
|
if ($logbook) {
|
||||||
|
$kelas = $logbook->Kelas;
|
||||||
|
if ($kelas) {
|
||||||
|
$isPIC = $kelas->guru_id == $guru_id;
|
||||||
|
$hasTaught = $logbook->guru_id == $guru_id;
|
||||||
|
|
||||||
|
if ($isPIC || $hasTaught) {
|
||||||
|
$path = storage_path('app/public/' . $foto->foto);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return response()->download($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(404, 'File tidak ditemukan atau Anda tidak memiliki akses.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Guru;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Guru;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class JadwalGuruController extends Controller
|
||||||
|
{
|
||||||
|
public function getDataHariIni(){
|
||||||
|
$jharini = Logbooks::whereDate('tanggal', Carbon::today())
|
||||||
|
->orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->get();
|
||||||
|
return $jharini->toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(){
|
||||||
|
$data = [];
|
||||||
|
$peserta = [];
|
||||||
|
$guru = Guru::where('status', 'Aktif')->select('id', 'nama')->get();
|
||||||
|
|
||||||
|
// Batas waktu pengisian logbook hari ini adalah jam 19:00
|
||||||
|
$deadline = Carbon::today()->setTime(19, 0, 0);
|
||||||
|
$now = Carbon::now();
|
||||||
|
$isDeadlinePassed = $now->greaterThan($deadline);
|
||||||
|
$remainingSeconds = $isDeadlinePassed ? 0 : (int) $now->diffInSeconds($deadline);
|
||||||
|
|
||||||
|
return view('guru.pages.jadwal', compact('data', 'peserta', 'guru', 'deadline', 'isDeadlinePassed', 'remainingSeconds'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calendarData(){
|
||||||
|
$guru_id = auth()->user()->Guru->id;
|
||||||
|
|
||||||
|
$jharini = Logbooks::whereHas('kelas', function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id);
|
||||||
|
})
|
||||||
|
->orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->with('Kelas', 'FotoLogbook')
|
||||||
|
->get();
|
||||||
|
return json_encode($jharini);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calendar(){
|
||||||
|
return view('guru.pages.jadwal-calendar');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// AJAX Endpoints untuk menampilkan data tanpa timeout
|
||||||
|
// ============================================
|
||||||
|
public function getJadwalHariIni(){
|
||||||
|
$guru_id = auth()->user()->Guru->id;
|
||||||
|
|
||||||
|
$jharini = Logbooks::where(function($q) use ($guru_id) {
|
||||||
|
$q->where('guru_id', $guru_id)
|
||||||
|
->orWhereHas('kelas', function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->whereDate('tanggal', Carbon::today())
|
||||||
|
->orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->with(['Kelas.guru', 'Guru', 'FotoLogbook'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Batas waktu pengisian logbook hari ini adalah jam 19:00
|
||||||
|
$deadline = Carbon::today()->setTime(19, 0, 0);
|
||||||
|
$now = Carbon::now();
|
||||||
|
$isDeadlinePassed = $now->greaterThan($deadline);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $jharini,
|
||||||
|
'count' => count($jharini),
|
||||||
|
'is_deadline_passed' => $isDeadlinePassed,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJadwalBesok(){
|
||||||
|
$guru_id = auth()->user()->Guru->id;
|
||||||
|
|
||||||
|
$jbesok = Logbooks::where(function($q) use ($guru_id) {
|
||||||
|
$q->where('guru_id', $guru_id)
|
||||||
|
->orWhereHas('kelas', function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->whereDate('tanggal', Carbon::tomorrow())
|
||||||
|
->orderByRaw("CASE WHEN start IS NULL OR start = '' THEN 1 ELSE 0 END, start ASC")
|
||||||
|
->with(['Kelas.guru', 'Guru', 'FotoLogbook'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $jbesok,
|
||||||
|
'count' => count($jbesok)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLogbookData(){
|
||||||
|
Carbon::setLocale('id');
|
||||||
|
$guru_id = auth()->user()->Guru->id;
|
||||||
|
|
||||||
|
$groupedLogbooks = Logbooks::whereHas('kelas', function($query) use ($guru_id) {
|
||||||
|
$query->where('guru_id', $guru_id);
|
||||||
|
})
|
||||||
|
->orderBy('tanggal', 'asc')
|
||||||
|
->with('Kelas.Guru', 'Kelas.siswa', 'FotoLogbook')
|
||||||
|
->get()
|
||||||
|
->groupBy(function($item) {
|
||||||
|
return \Carbon\Carbon::parse($item->tanggal)->translatedFormat('F Y');
|
||||||
|
});
|
||||||
|
|
||||||
|
$now = \Carbon\Carbon::now();
|
||||||
|
|
||||||
|
$sortedLogbooks = collect([
|
||||||
|
'upcoming' => collect(),
|
||||||
|
'past' => collect(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$logbookSummary = collect();
|
||||||
|
|
||||||
|
foreach ($groupedLogbooks as $bulanTahun => $items) {
|
||||||
|
$firstItemDate = \Carbon\Carbon::parse($items->first()->tanggal);
|
||||||
|
$category = $firstItemDate->greaterThanOrEqualTo($now->copy()->startOfMonth()) ? 'upcoming' : 'past';
|
||||||
|
$sortedLogbooks[$category][$bulanTahun] = $items;
|
||||||
|
|
||||||
|
// Menyimpan jumlah logbook tiap bulan
|
||||||
|
$logbookSummary[$bulanTahun] = [
|
||||||
|
'jumlah_sesi' => $items->count(),
|
||||||
|
'jumlah_sesi_completed' => $items->where('status', 'completed')->count(),
|
||||||
|
'jumlah_siswa' => $items->pluck('Kelas')->filter()->flatMap->siswa->unique('id')->count(),
|
||||||
|
'jumlah_kelas' => $items->pluck('kelas_id')->unique()->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$logbooksUpcoming = $sortedLogbooks['upcoming'];
|
||||||
|
$logbooksPast = $sortedLogbooks['past']->sortByDesc(function($items, $bulanTahun) {
|
||||||
|
return \Carbon\Carbon::parse($items->first()->tanggal);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'upcoming' => $logbooksUpcoming,
|
||||||
|
'past' => $logbooksPast,
|
||||||
|
'summary' => $logbookSummary
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,305 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Guru;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\DetailKelas;
|
||||||
|
use App\Models\FotoLogbooks;
|
||||||
|
use App\Models\Guru;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use App\Models\Siswa;
|
||||||
|
use App\Models\Programs;
|
||||||
|
use App\Models\Sesi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class KelasGuruController extends Controller
|
||||||
|
{
|
||||||
|
public function index(){
|
||||||
|
$guru_id = auth()->user()->Guru->id;
|
||||||
|
|
||||||
|
$data = Kelas::withCount('siswa')->withCount('logbooks')
|
||||||
|
->where('guru_id', $guru_id)
|
||||||
|
->orderByRaw("CASE
|
||||||
|
WHEN status = 'Aktif' THEN 1
|
||||||
|
WHEN status = 'Tidak Aktif' THEN 2
|
||||||
|
ELSE 3 END")
|
||||||
|
->get();
|
||||||
|
return view('guru.pages.kelas', compact('data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$programs = Programs::all();
|
||||||
|
$guru = Guru::where('status', 'Aktif')->select('id', 'nama')->get();
|
||||||
|
return view ('guru.pages.kelas-tambah', compact('programs', 'guru'));
|
||||||
|
}
|
||||||
|
public function store(Request $request){
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_kelas' => 'required|string|max:255',
|
||||||
|
'jenis_kelas' => 'required',
|
||||||
|
'program_id' => 'required',
|
||||||
|
'guru_id' => 'required',
|
||||||
|
'harga_kelas' => 'required',
|
||||||
|
'status' => 'required',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_kelas.required' => 'Nama kelas wajib diisi',
|
||||||
|
'nama_kelas.max' => 'Nama event terlalu panjang',
|
||||||
|
'jenis_kelas.required' => 'Jenis kelas wajib diisi',
|
||||||
|
'program_id.required' => 'Program wajib diisi',
|
||||||
|
'guru_id.required' => 'Guru wajib diisi',
|
||||||
|
'harga_kelas.required' => 'Harga kelas wajib diisi',
|
||||||
|
'status.required' => 'Status kelas wajib diisi',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$validated['harga_kelas'] = (int) str_replace('.', '', $validated['harga_kelas']);
|
||||||
|
$validated['kode_kelas'] = strtoupper(Str::random(6));
|
||||||
|
|
||||||
|
if ($request->keterangan){ $validated['keterangan'] = $request->keterangan; }
|
||||||
|
|
||||||
|
Kelas::create($validated);
|
||||||
|
return redirect('/guru/kelas')->with('success', 'Data Berhasil Disimpan');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id){
|
||||||
|
$data = Kelas::where('id', $id)->first();
|
||||||
|
$programs = Programs::all();
|
||||||
|
$guru = Guru::where('status', 'Aktif')->select('id', 'nama')->get();
|
||||||
|
|
||||||
|
if ($data == null){
|
||||||
|
return redirect('/guru/kelas')->with('error', 'Data kelas tidak ditemukan');
|
||||||
|
}
|
||||||
|
$peserta = DetailKelas::has('siswa')->where('kelas_id', $id)->get();
|
||||||
|
$logbook = Logbooks::with('Sesi')->where('kelas_id', $id)->orderBy('tanggal', 'asc')->get();
|
||||||
|
$sesiList = Sesi::orderBy('id', 'asc')->get();
|
||||||
|
|
||||||
|
// Batas waktu pengisian logbook hari ini adalah jam 19:00
|
||||||
|
$deadline = Carbon::today()->setTime(19, 0, 0);
|
||||||
|
$now = Carbon::now();
|
||||||
|
$isDeadlinePassed = $now->greaterThan($deadline);
|
||||||
|
$remainingSeconds = $isDeadlinePassed ? 0 : (int) $now->diffInSeconds($deadline);
|
||||||
|
|
||||||
|
return view('guru.pages.kelas-detail', compact('data', 'peserta', 'programs', 'logbook', 'guru', 'isDeadlinePassed', 'remainingSeconds', 'sesiList'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id){
|
||||||
|
$data = Kelas::findOrFail($id);
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_kelas' => 'required|string|max:255',
|
||||||
|
'jenis_kelas' => 'required',
|
||||||
|
'program_id' => 'required',
|
||||||
|
'guru_id' => 'required',
|
||||||
|
'harga_kelas' => 'required',
|
||||||
|
'status' => 'required',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_kelas.required' => 'Nama kelas wajib diisi',
|
||||||
|
'nama_kelas.max' => 'Nama event terlalu panjang',
|
||||||
|
'jenis_kelas.required' => 'Jenis kelas wajib diisi',
|
||||||
|
'program_id.required' => 'Program wajib diisi',
|
||||||
|
'guru_id.required' => 'Guru wajib diisi',
|
||||||
|
'harga_kelas.required' => 'Harga kelas wajib diisi',
|
||||||
|
'status.required' => 'Status kelas wajib diisi',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$validated['harga_kelas'] = (int) str_replace('.', '', $validated['harga_kelas']);
|
||||||
|
|
||||||
|
if ($request->keterangan){ $validated['keterangan'] = $request->keterangan; }
|
||||||
|
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' .$id;
|
||||||
|
return redirect($direct)->with('success', 'Data Kelas Berhasil Diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSiswasKelas($kelas_id){
|
||||||
|
$pesertaTerdaftar = DetailKelas::where('kelas_id', $kelas_id)->pluck('siswa_id')->toArray();
|
||||||
|
|
||||||
|
$siswa = Siswa::whereNotIn('id', $pesertaTerdaftar)->get();
|
||||||
|
|
||||||
|
return response()->json([$siswa]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tambahSiswas(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'kelas_id' => 'required',
|
||||||
|
'id' => 'required',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kelas_id.required' => 'Event tidak ditemukan',
|
||||||
|
'id.required' => 'Silahkan pilih setidaknya satu siswa',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
$kelas = Kelas::where('id', $request->kelas_id)->select('harga_kelas')->first();
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($request->id); $i++) {
|
||||||
|
$siswa = Siswa::where('id', $request->id[$i])->select('id', 'nama_anak')->first();
|
||||||
|
$cekPeserta = DetailKelas::where('siswa_id', $request->id[$i])->where('kelas_id', $request->kelas_id)->count();
|
||||||
|
if ($cekPeserta == 0) {
|
||||||
|
if ($siswa != null) {
|
||||||
|
DetailKelas::create([
|
||||||
|
'kelas_id' => $request->kelas_id,
|
||||||
|
'siswa_id' => $request->id[$i],
|
||||||
|
'nama_anak' => $siswa->nama_anak,
|
||||||
|
'status_siswa' => "Aktif",
|
||||||
|
'harga_kelas' => $kelas->harga_kelas,
|
||||||
|
]);
|
||||||
|
}else{
|
||||||
|
$direct = '/admin/kelas/' . $request->kelas_id;
|
||||||
|
$message = "Gagal menyimpan siswa, id " . $request->id[$i] . " tidak ditemukan";
|
||||||
|
return redirect($direct)->with('error', $message);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
$direct = '/admin/kelas/' . $request->kelas_id;
|
||||||
|
$message = "Anak dengan nama " . $siswa->nama_anak . " telah terdaftar di kelas ini";
|
||||||
|
return redirect($direct)->with('error', $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' . $request->kelas_id;
|
||||||
|
$message = "Siswa berhasil ditambahkan pada kelas";
|
||||||
|
return redirect($direct)->with('success', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPesertaKelas($id){
|
||||||
|
$data = DetailKelas::where('id', $id)->first();
|
||||||
|
$peserta = Siswa::where('id', $data->siswa_id)->first();
|
||||||
|
// $user = User::where('id', $data->user_id)->select('id', 'email')->first();
|
||||||
|
|
||||||
|
$created_at = Carbon::parse($data->created_at)->locale('id')->translatedFormat('l, d F Y - H:i') . ' WIB';
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'id' => $data->id,
|
||||||
|
'nama_anak' => $peserta->nama_anak,
|
||||||
|
'nama_orangtua' => $peserta->nama_orangtua,
|
||||||
|
'no_hp_orangtua' => $peserta->no_hp_orangtua,
|
||||||
|
'usia' => $peserta->usia ?? null,
|
||||||
|
'jenis_kelamin' => $peserta->jenis_kelamin ?? null,
|
||||||
|
'email' => $user->email ?? null,
|
||||||
|
'asal_sekolah' => $peserta->asal_sekolah ?? null,
|
||||||
|
'program_id'=> $peserta->program_id ?? null,
|
||||||
|
'status' => $data->status_siswa ?? null,
|
||||||
|
'alamat' => $peserta->alamat ?? null,
|
||||||
|
'bukti_pembayaran' => $data->bukti_pembayaran ?? null,
|
||||||
|
'harga_kelas' => $data->harga_kelas,
|
||||||
|
'catatan' => $data->catatan,
|
||||||
|
'tanggal_daftar' => $created_at ?? null,
|
||||||
|
|
||||||
|
'foto' => $peserta->foto ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editSiswa(Request $request, $id){
|
||||||
|
|
||||||
|
$data = DetailKelas::where('id', $id)->first();
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'nama_anak' => 'required',
|
||||||
|
'status_siswa' => 'required',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_anak.required' => 'Nama anak tidak ditemukan',
|
||||||
|
'status_siswa.required' => 'Harap pilih status siswa terlebih dahulu',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($request->catatan !== null || $request->catatan !== ''){ $validated['catatan'] = $request->catatan; }
|
||||||
|
|
||||||
|
if ($request->hasFile('bukti_pembayaran')) {
|
||||||
|
if ($data->bukti_pembayaran) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->bukti_pembayaran);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('bukti_pembayaran');
|
||||||
|
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$file->storeAs('bukti-pembayaran', $hashedName, 'public');
|
||||||
|
|
||||||
|
$validated['bukti_pembayaran'] = 'bukti-pembayaran/' . $hashedName;
|
||||||
|
|
||||||
|
DetailKelas::where('id', $id)->update([
|
||||||
|
'nama_anak' => $validated['nama_anak'],
|
||||||
|
'status_siswa' => $validated['status_siswa'],
|
||||||
|
'bukti_pembayaran' => $validated['bukti_pembayaran'],
|
||||||
|
'catatan' => $validated['catatan'],
|
||||||
|
]);
|
||||||
|
}else{
|
||||||
|
DetailKelas::where('id', $id)->update([
|
||||||
|
'nama_anak' => $validated['nama_anak'],
|
||||||
|
'status_siswa' => $validated['status_siswa'],
|
||||||
|
'catatan' => $validated['catatan'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/admin/kelas/' . $request->kelas_id;
|
||||||
|
$message = "Data siswa dikelas berhasil diubah";
|
||||||
|
return redirect($direct)->with('success', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyerSiswaFromKelas($id){
|
||||||
|
$data = DetailKelas::findOrFail($id);
|
||||||
|
$kelas_id = $data->kelas_id;
|
||||||
|
|
||||||
|
if ($data->bukti_pembayaran) {
|
||||||
|
$imagePath = storage_path('app/public/' . $data->bukti_pembayaran);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
$direct = '/admin/kelas/' . $kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Data siswa berhasil dihapus');
|
||||||
|
}
|
||||||
|
public function destroy($id){
|
||||||
|
$data = Kelas::findOrFail($id);
|
||||||
|
|
||||||
|
$detailkelas = DetailKelas::where('kelas_id', $id)->get();
|
||||||
|
|
||||||
|
if($detailkelas->count() > 0){
|
||||||
|
for($i = 0; $i < count($detailkelas); $i++){
|
||||||
|
$detailkelas[$i]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$logbooks = Logbooks::where('kelas_id', $id)->get();
|
||||||
|
|
||||||
|
if($logbooks->count() > 0){
|
||||||
|
for($i = 0; $i < count($logbooks); $i++){
|
||||||
|
$logbooks_id = $logbooks[$i]->id;
|
||||||
|
|
||||||
|
$fotoLogbook = FotoLogbooks::where('logbook_id', $logbooks_id)->get();
|
||||||
|
if($fotoLogbook->count() > 0){
|
||||||
|
for($b = 0; $b < count($fotoLogbook); $b++){
|
||||||
|
if ($fotoLogbook[$b]->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $fotoLogbook[$b]->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fotoLogbook[$b]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$logbooks[$i]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
return redirect('/admin/kelas')->with('success', 'Data kelas berhasil dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,523 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Guru;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Imports\LogbookImport;
|
||||||
|
use App\Models\FotoLogbooks;
|
||||||
|
use App\Models\Kelas;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use App\Models\PengajarLogbook;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Intervention\Image\Laravel\Facades\Image;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
|
class LogbookGuruController extends Controller
|
||||||
|
{
|
||||||
|
public function getLogbook($id){
|
||||||
|
$data = Logbooks::where('id', $id)->first();
|
||||||
|
|
||||||
|
$logbook = FotoLogbooks::where('logbook_id', $data->id)->get()->map(function ($logbook) {
|
||||||
|
return [
|
||||||
|
'id' => $logbook->id,
|
||||||
|
'foto' => $logbook->foto,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$tanggalparse = $data->tanggal
|
||||||
|
? Carbon::parse($data->tanggal)->locale('id')->translatedFormat('l, d F Y')
|
||||||
|
: null;
|
||||||
|
$startparse = $data->start
|
||||||
|
? Carbon::parse($data->start)->locale('id')->translatedFormat('H:i')
|
||||||
|
: null;
|
||||||
|
$endparse = $data->end
|
||||||
|
? Carbon::parse($data->end)->locale('id')->translatedFormat('H:i')
|
||||||
|
: null;
|
||||||
|
return response()->json([
|
||||||
|
'id' => $data->id,
|
||||||
|
'tanggal' => $data->tanggal ?? null,
|
||||||
|
'start' => $data->start ?? null,
|
||||||
|
'start_parse' => $startparse ?? null,
|
||||||
|
'end' => $data->end ?? null,
|
||||||
|
'end_parse' => $endparse ?? null,
|
||||||
|
'tanggalparse' => $tanggalparse ?? null,
|
||||||
|
'materi' => $data->materi ?? null,
|
||||||
|
'catatan' => $data->catatan ?? null,
|
||||||
|
'pertemuan' => $data->pertemuan ?? null,
|
||||||
|
'logbooks' => $logbook,
|
||||||
|
'kelas_id' => $data->kelas_id ?? null,
|
||||||
|
'sesi_id' => $data->sesi_id ?? null,
|
||||||
|
'nama_sesi' => $data->Sesi->nama_sesi ?? null,
|
||||||
|
'jam_sesi' => $data->Sesi->jam ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function store(Request $request){
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'kelas_id' => 'required',
|
||||||
|
'kode_kelas' => 'required',
|
||||||
|
'sesi_id' => 'nullable|exists:sesi,id',
|
||||||
|
'pertemuan' => 'nullable',
|
||||||
|
'tanggal' => 'nullable',
|
||||||
|
'start' => 'nullable',
|
||||||
|
'end' => 'nullable',
|
||||||
|
'materi' => 'nullable',
|
||||||
|
'catatan' => 'nullable',
|
||||||
|
'kegiatan_minggu_ini' => 'nullable',
|
||||||
|
'perkembangan_siswa' => 'nullable',
|
||||||
|
'kendala_dan_solusi' => 'nullable',
|
||||||
|
'kebutuhan_minggu_depan' => 'nullable',
|
||||||
|
'hal_baik' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kelas_id.required' => 'ID kelas tidak ditemukan',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Validasi Deadline Pengisian Logbook ─────────────────────────────
|
||||||
|
$tanggalInput = $request->tanggal;
|
||||||
|
if ($tanggalInput) {
|
||||||
|
$parsedTanggal = Carbon::parse($tanggalInput);
|
||||||
|
if ($parsedTanggal->lt(Carbon::today())) {
|
||||||
|
$direct = '/guru/kelas/' . $request->kelas_id;
|
||||||
|
return redirect($direct)->with('error', 'Logbook dengan tanggal lampau tidak dapat diisi. Pengisian hanya bisa dilakukan pada hari yang sama.');
|
||||||
|
}
|
||||||
|
if ($parsedTanggal->isToday() && Carbon::now()->greaterThan(Carbon::today()->setTime(19, 0, 0))) {
|
||||||
|
$direct = '/guru/kelas/' . $request->kelas_id;
|
||||||
|
return redirect($direct)->with('error', 'Batas waktu pengisian logbook hari ini telah berakhir (19:00 WIB). Silakan hubungi koordinator jika ada kendala.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Kondisi jika libur
|
||||||
|
if ($request->status_libur && $request->status_libur == "on"){
|
||||||
|
|
||||||
|
$validated['status'] = "libur";
|
||||||
|
$validated['jenis_libur'] = $request->jenis_libur ?? null;
|
||||||
|
$validated['jenis_libur_lainnya'] = $request->jenis_libur_lainnya ?? null;
|
||||||
|
$logbook = Logbooks::create($validated);
|
||||||
|
|
||||||
|
$direct = '/guru/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil ditambah');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$validated['status'] = "Draft";
|
||||||
|
$validated['jenis_libur'] = null;
|
||||||
|
$validated['jenis_libur_lainnya'] = null;
|
||||||
|
|
||||||
|
$logbook = Logbooks::create($validated);
|
||||||
|
$logbook_id = $logbook->id;
|
||||||
|
|
||||||
|
$uploadedFiles = [];
|
||||||
|
if ($request->hasFile('fotos')) {
|
||||||
|
|
||||||
|
foreach ($request->file('fotos') as $file) {
|
||||||
|
if ($file->isValid()) {
|
||||||
|
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$path = $file->storeAs('foto-logbook', $hashedName, 'public');
|
||||||
|
|
||||||
|
$fullPath = Storage::disk('public')->path($path);
|
||||||
|
|
||||||
|
// Resize gambar menggunakan Intervention Image
|
||||||
|
Image::read($fullPath)
|
||||||
|
->orient()
|
||||||
|
->scaleDown(width: 800)
|
||||||
|
->save($fullPath, quality: 85);
|
||||||
|
|
||||||
|
FotoLogbooks::create([
|
||||||
|
'logbook_id' => $logbook_id,
|
||||||
|
'keterangan' => '',
|
||||||
|
'foto' => 'foto-logbook/' . $hashedName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$uploadedFiles[] = 'foto-logbook/' . $hashedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
Logbooks::where('id', $logbook_id)->update(['status' => 'completed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('guru_id')) {
|
||||||
|
$logbook->update(['guru_id' => $request->guru_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/guru/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil ditambah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id){
|
||||||
|
$data = Logbooks::where('id', $id)->first();
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'kelas_id' => 'required',
|
||||||
|
'sesi_id' => 'nullable|exists:sesi,id',
|
||||||
|
'pertemuan' => 'nullable',
|
||||||
|
'tanggal' => 'nullable',
|
||||||
|
'start' => 'nullable',
|
||||||
|
'end' => 'nullable',
|
||||||
|
'materi' => 'nullable',
|
||||||
|
'catatan' => 'nullable',
|
||||||
|
'kegiatan_minggu_ini' => 'nullable',
|
||||||
|
'perkembangan_siswa' => 'nullable',
|
||||||
|
'kendala_dan_solusi' => 'nullable',
|
||||||
|
'kebutuhan_minggu_depan' => 'nullable',
|
||||||
|
'hal_baik' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kelas_id.required' => 'ID kelas tidak ditemukan',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Validasi Deadline Pengisian Logbook ─────────────────────────────
|
||||||
|
$tanggalLogbook = $request->tanggal ?? $data->tanggal;
|
||||||
|
if ($tanggalLogbook) {
|
||||||
|
$parsedTanggal = Carbon::parse($tanggalLogbook);
|
||||||
|
if ($parsedTanggal->lt(Carbon::today())) {
|
||||||
|
$direct = '/guru/kelas/' . $request->kelas_id;
|
||||||
|
return redirect($direct)->with('error', 'Logbook dengan tanggal lampau tidak dapat diubah. Pengisian hanya bisa dilakukan pada hari yang sama.');
|
||||||
|
}
|
||||||
|
if ($parsedTanggal->isToday() && Carbon::now()->greaterThan(Carbon::today()->setTime(19, 0, 0))) {
|
||||||
|
$direct = '/guru/kelas/' . $request->kelas_id;
|
||||||
|
return redirect($direct)->with('error', 'Batas waktu pengisian logbook hari ini telah berakhir (19:00 WIB). Silakan hubungi koordinator jika ada kendala.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Kondisi jika libur
|
||||||
|
if ($request->status_libur && $request->status_libur == "on"){
|
||||||
|
|
||||||
|
$validated['status'] = "libur";
|
||||||
|
$validated['jenis_libur'] = $request->jenis_libur ?? null;
|
||||||
|
$validated['jenis_libur_lainnya'] = $request->jenis_libur_lainnya ?? null;
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/guru/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['status'] = "Draft";
|
||||||
|
|
||||||
|
$logbook_id = $data->id;
|
||||||
|
|
||||||
|
$uploadedFiles = [];
|
||||||
|
if ($request->hasFile('fotos')) {
|
||||||
|
foreach ($request->file('fotos') as $file) {
|
||||||
|
if ($file->isValid()) {
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$path = $file->storeAs('foto-logbook', $hashedName, 'public');
|
||||||
|
|
||||||
|
$fullPath = Storage::disk('public')->path($path);
|
||||||
|
|
||||||
|
Image::read($fullPath)
|
||||||
|
->orient()
|
||||||
|
->scaleDown(width: 800)
|
||||||
|
->save($fullPath, quality: 85);
|
||||||
|
|
||||||
|
// Simpan ke database
|
||||||
|
FotoLogbooks::create([
|
||||||
|
'logbook_id' => $logbook_id,
|
||||||
|
'keterangan' => '',
|
||||||
|
'foto' => 'foto-logbook/' . $hashedName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$uploadedFiles[] = 'foto-logbook/' . $hashedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
$validated['status'] = "completed";
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['jenis_libur'] = null;
|
||||||
|
if ($request->has('guru_id')) {
|
||||||
|
$validated['guru_id'] = $request->guru_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/guru/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id){
|
||||||
|
$data = Logbooks::findOrFail($id);
|
||||||
|
$kelas_id = $data->kelas_id;
|
||||||
|
|
||||||
|
$fotoLogbook = FotoLogbooks::where('logbook_id', $id)->get();
|
||||||
|
if($fotoLogbook->count() > 0){
|
||||||
|
for($b = 0; $b < count($fotoLogbook); $b++){
|
||||||
|
if ($fotoLogbook[$b]->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $fotoLogbook[$b]->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fotoLogbook[$b]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->delete();
|
||||||
|
|
||||||
|
$direct = '/guru/kelas/' . $kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Data logbook berhasil dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroySelected(Request $request){
|
||||||
|
|
||||||
|
$logbook_id = $request->id;
|
||||||
|
$kelas_id = Logbooks::where('id', $logbook_id[0])->first();
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($logbook_id); $i++){
|
||||||
|
$logbook = Logbooks::where('id', $logbook_id[$i])->first();
|
||||||
|
|
||||||
|
$fotoLogbook = FotoLogbooks::where('logbook_id', $logbook->id)->get();
|
||||||
|
if($fotoLogbook->count() > 0){
|
||||||
|
for($b = 0; $b < count($fotoLogbook); $b++){
|
||||||
|
if ($fotoLogbook[$b]->foto) {
|
||||||
|
$imagePath = storage_path('app/public/' . $fotoLogbook[$b]->foto);
|
||||||
|
if (file_exists($imagePath)) {
|
||||||
|
unlink($imagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fotoLogbook[$b]->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$logbook->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/guru/kelas/' . $kelas_id->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Data logbook berhasil dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyFoto($id){
|
||||||
|
$dataFoto = FotoLogbooks::findOrFail($id);
|
||||||
|
|
||||||
|
if ($dataFoto){
|
||||||
|
$logbook = Logbooks::where('id', $dataFoto->logbook_id)->select('id', 'kelas_id')->first();
|
||||||
|
$filePath = 'storage/' . $dataFoto->foto;
|
||||||
|
if ($filePath) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataFoto->delete();
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $dataFoto->logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
Logbooks::where('id', $logbook->id)->update(['status' => 'completed']);
|
||||||
|
}else{
|
||||||
|
Logbooks::where('id', $logbook->id)->update(['status' => 'draft']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/guru/kelas/' . $logbook->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Data logbook berhasil dihapus');
|
||||||
|
|
||||||
|
}else{
|
||||||
|
$direct = '/guru/kelas';
|
||||||
|
return redirect($direct)->with('error', 'Data tidak ditemukan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function import(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate(
|
||||||
|
[
|
||||||
|
'file' => 'required|mimes:csv,xls,xlsx'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'file.required' => 'File tidak boleh kosong',
|
||||||
|
'file.mimes' => 'File harus berbentuk csv',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
Excel::import(new LogbookImport, $request->file('file'));
|
||||||
|
|
||||||
|
$direct = '/guru/kelas/' .$request->kelas_id;
|
||||||
|
return redirect($direct)->with('success', 'Import logbook telah berhasil');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// edit and delete logbook from jadwal
|
||||||
|
public function updateLog(Request $request, $id){
|
||||||
|
$data = Logbooks::where('id', $id)->first();
|
||||||
|
$validated = $request->validate(
|
||||||
|
[
|
||||||
|
'kelas_id' => 'required',
|
||||||
|
'kegiatan_minggu_ini' => 'nullable',
|
||||||
|
'perkembangan_siswa' => 'nullable',
|
||||||
|
'kendala_dan_solusi' => 'nullable',
|
||||||
|
'kebutuhan_minggu_depan' => 'nullable',
|
||||||
|
'hal_baik' => 'nullable',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kelas_id.required' => 'ID kelas tidak ditemukan',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Validasi Deadline Pengisian Logbook ─────────────────────────────
|
||||||
|
$tanggalLogbook = $data->tanggal;
|
||||||
|
if ($tanggalLogbook) {
|
||||||
|
$parsedTanggal = Carbon::parse($tanggalLogbook);
|
||||||
|
if ($parsedTanggal->lt(Carbon::today())) {
|
||||||
|
return redirect('/guru/jadwal')->with('error', 'Logbook dengan tanggal lampau tidak dapat diubah. Pengisian hanya bisa dilakukan pada hari yang sama.');
|
||||||
|
}
|
||||||
|
if ($parsedTanggal->isToday() && Carbon::now()->greaterThan(Carbon::today()->setTime(19, 0, 0))) {
|
||||||
|
return redirect('/guru/jadwal')->with('error', 'Batas waktu pengisian logbook hari ini telah berakhir (19:00 WIB). Silakan hubungi koordinator jika ada kendala.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Kondisi jika libur
|
||||||
|
if ($request->status_libur && $request->status_libur == "on"){
|
||||||
|
|
||||||
|
$validated['status'] = "libur";
|
||||||
|
$validated['jenis_libur'] = $request->jenis_libur ?? null;
|
||||||
|
$validated['jenis_libur_lainnya'] = $request->jenis_libur_lainnya ?? null;
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/admin/jadwal/';
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->catatan !== null || $request->catatan !== ''){ $validated['catatan'] = $request->catatan; }
|
||||||
|
if ($request->materi !== null || $request->materi !== ''){ $validated['materi'] = $request->materi; }
|
||||||
|
|
||||||
|
$validated['status'] = "Draft";
|
||||||
|
|
||||||
|
$logbook_id = $data->id;
|
||||||
|
|
||||||
|
$uploadedFiles = [];
|
||||||
|
if ($request->hasFile('fotos')) {
|
||||||
|
foreach ($request->file('fotos') as $file) {
|
||||||
|
if ($file->isValid()) {
|
||||||
|
$hashedName = md5(time() . $file->getClientOriginalName()) . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
$path = $file->storeAs('foto-logbook', $hashedName, 'public');
|
||||||
|
|
||||||
|
$fullPath = Storage::disk('public')->path($path);
|
||||||
|
|
||||||
|
Image::read($fullPath)
|
||||||
|
->orient()
|
||||||
|
->scaleDown(width: 800)
|
||||||
|
->save($fullPath, quality: 85);
|
||||||
|
|
||||||
|
// Simpan ke database
|
||||||
|
FotoLogbooks::create([
|
||||||
|
'logbook_id' => $logbook_id,
|
||||||
|
'keterangan' => '',
|
||||||
|
'foto' => 'foto-logbook/' . $hashedName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$uploadedFiles[] = 'foto-logbook/' . $hashedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
$validated['status'] = "completed";
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated['jenis_libur'] = null;
|
||||||
|
$validated['jenis_libur_lainnya'] = null;
|
||||||
|
|
||||||
|
if ($request->has('guru_id')) {
|
||||||
|
$validated['guru_id'] = $request->guru_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->update($validated);
|
||||||
|
|
||||||
|
$direct = '/guru/jadwal/';
|
||||||
|
return redirect($direct)->with('success', 'Logbook berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyFotoLog($id){
|
||||||
|
$dataFoto = FotoLogbooks::findOrFail($id);
|
||||||
|
|
||||||
|
if ($dataFoto){
|
||||||
|
$logbook = Logbooks::where('id', $dataFoto->logbook_id)->select('id', 'kelas_id')->first();
|
||||||
|
$filePath = 'storage/' . $dataFoto->foto;
|
||||||
|
if ($filePath) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataFoto->delete();
|
||||||
|
|
||||||
|
$checkUploadFile = FotoLogbooks::where('logbook_id', $dataFoto->logbook_id)->count();
|
||||||
|
|
||||||
|
if ($checkUploadFile > 0) {
|
||||||
|
Logbooks::where('id', $logbook->id)->update(['status' => 'completed']);
|
||||||
|
}else{
|
||||||
|
Logbooks::where('id', $logbook->id)->update(['status' => 'draft']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = '/guru/jadwal/';
|
||||||
|
return redirect($direct)->with('success', 'Data logbook berhasil dihapus');
|
||||||
|
|
||||||
|
}else{
|
||||||
|
$direct = '/guru/jadwal/';
|
||||||
|
return redirect($direct)->with('error', 'Data tidak ditemukan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function addrow(Request $request)
|
||||||
|
{
|
||||||
|
$validate = $request->validate([
|
||||||
|
'tanggal' => 'nullable|date',
|
||||||
|
'start' => 'nullable|date_format:H:i',
|
||||||
|
'end' => 'nullable|date_format:H:i',
|
||||||
|
'pertemuan' => 'nullable|numeric',
|
||||||
|
'materi' => 'nullable|string',
|
||||||
|
'catatan' => 'nullable|string',
|
||||||
|
'kelas_id' => 'required|exists:kelas,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kelas = Kelas::find($validate['kelas_id']);
|
||||||
|
if (!$kelas) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Kelas tidak ditemukan',
|
||||||
|
'data' => null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validate['kode_kelas'] = $kelas->kode_kelas;
|
||||||
|
|
||||||
|
if (!empty($validate['tanggal'])) {
|
||||||
|
$tanggal = Carbon::parse($validate['tanggal']);
|
||||||
|
|
||||||
|
if (!empty($validate['end'])) {
|
||||||
|
$endTime = Carbon::createFromFormat('H:i', $validate['end']);
|
||||||
|
$combinedEnd = $tanggal->copy()->setTimeFrom($endTime);
|
||||||
|
|
||||||
|
$validate['status'] = $combinedEnd->isPast() ? 'perlu diisi' : 'Draft';
|
||||||
|
} else {
|
||||||
|
$validate['status'] = $tanggal->lessThanOrEqualTo(Carbon::today()) ? 'perlu diisi' : 'Draft';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$validate['status'] = 'Draft';
|
||||||
|
}
|
||||||
|
|
||||||
|
$logbook = Logbooks::create($validate);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Sesi berhasil ditambahkan',
|
||||||
|
'data' => $logbook
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Guru;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Guru;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class ProfileGuruController extends Controller
|
||||||
|
{
|
||||||
|
public function index(){
|
||||||
|
return view('guru.pages.profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dateFormat($date){
|
||||||
|
$tanggalparse = $date
|
||||||
|
? Carbon::parse($date)->locale('id')->translatedFormat('d F Y')
|
||||||
|
: null;
|
||||||
|
return $tanggalparse;
|
||||||
|
}
|
||||||
|
public function getDataProfile(){
|
||||||
|
$guru_id = auth()->user()->Guru->id;
|
||||||
|
$data = Guru::where('id', $guru_id)->first();
|
||||||
|
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'id' => $data->id,
|
||||||
|
'nama' => $data->nama ?? null,
|
||||||
|
'no_hp' => $data->no_hp ?? null,
|
||||||
|
'tempat_lahir' => $data->tempat_lahir ?? null,
|
||||||
|
'tanggal_lahir' => $data->tanggal_lahir ?? null,
|
||||||
|
'tanggal_lahir_parse' => $this->dateFormat($data->tanggal_lahir),
|
||||||
|
'jenis_kelamin' => $data->jenis_kelamin ?? null,
|
||||||
|
'email' => $data->email ?? null,
|
||||||
|
'alamat' => $data->alamat ?? null,
|
||||||
|
'pendidikan_terakhir' => $data->pendidikan_terakhir ?? null,
|
||||||
|
'instansi_pendidikan' => $data->instansi_pendidikan ?? null,
|
||||||
|
'jurusan' => $data->jurusan ?? null,
|
||||||
|
'divisi' => $data->divisi ?? null,
|
||||||
|
'jabatan' => $data->jabatan ?? null,
|
||||||
|
'start_join' => $data->start_join ?? null,
|
||||||
|
'start_join_parse' => $this->dateFormat($data->start_join),
|
||||||
|
'pekerja' => $data->pekerja ?? null,
|
||||||
|
'status' => $data->status ?? null,
|
||||||
|
'no_rekening' => $data->no_rekening ?? null,
|
||||||
|
'bank_keterangan' => $data->bank_keterangan ?? null,
|
||||||
|
'talent_mapping' => $data->talent_mapping ?? null,
|
||||||
|
'foto' => $data->foto ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveDataProfile(Request $request)
|
||||||
|
{
|
||||||
|
$guru = auth()->user()->Guru;
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'nama' => 'required|string|max:255',
|
||||||
|
], [
|
||||||
|
'nama.required' => 'Nama anak wajib diisi',
|
||||||
|
'nama.max' => 'Nama anak terlalu panjang',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Ambil semua field opsional
|
||||||
|
$fields = [
|
||||||
|
'no_hp', 'tempat_lahir', 'tanggal_lahir', 'jenis_kelamin', 'email',
|
||||||
|
'alamat', 'pendidikan_terakhir', 'instansi_pendidikan', 'jurusan',
|
||||||
|
'divisi', 'jabatan', 'start_join', 'pekerja', 'status',
|
||||||
|
'no_rekening', 'bank_keterangan'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if ($request->filled($field)) {
|
||||||
|
// khusus no_hp -> hilangkan 0 di depan
|
||||||
|
$validated[$field] = $field === 'no_hp'
|
||||||
|
? ltrim($request->no_hp, '0')
|
||||||
|
: $request->$field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default jenis kelamin
|
||||||
|
$validated['jenis_kelamin'] = $validated['jenis_kelamin'] ?? 'L';
|
||||||
|
|
||||||
|
// Upload file
|
||||||
|
if ($request->hasFile('talent_mapping')) {
|
||||||
|
if ($guru->talent_mapping && file_exists(storage_path('app/public/' . $guru->talent_mapping))) {
|
||||||
|
unlink(storage_path('app/public/' . $guru->talent_mapping));
|
||||||
|
}
|
||||||
|
$validated['talent_mapping'] = $request->file('talent_mapping')
|
||||||
|
->storeAs('talent-mapping', md5(time() . $request->file('talent_mapping')->getClientOriginalName()) . '.' . $request->file('talent_mapping')->getClientOriginalExtension(), 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('foto')) {
|
||||||
|
if ($guru->foto && file_exists(storage_path('app/public/' . $guru->foto))) {
|
||||||
|
unlink(storage_path('app/public/' . $guru->foto));
|
||||||
|
}
|
||||||
|
$validated['foto'] = $request->file('foto')
|
||||||
|
->storeAs('guru', md5(time() . $request->file('foto')->getClientOriginalName()) . '.' . $request->file('foto')->getClientOriginalExtension(), 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$guru->update($validated);
|
||||||
|
|
||||||
|
$firstWord = strtolower(strtok($guru->nama, ' '));
|
||||||
|
$user = User::find($guru->user_id);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $guru->nama,
|
||||||
|
'username' => $firstWord,
|
||||||
|
'email' => $guru->email,
|
||||||
|
'no_hp_orangtua' => $guru->no_hp,
|
||||||
|
'password' => bcrypt('12345'),
|
||||||
|
'role' => 'guru',
|
||||||
|
]);
|
||||||
|
$guru->update(['user_id' => $user->id]);
|
||||||
|
} else {
|
||||||
|
$user->update([
|
||||||
|
'name' => $guru->nama,
|
||||||
|
'username' => $firstWord,
|
||||||
|
'email' => $guru->email,
|
||||||
|
'no_hp_orangtua' => $guru->no_hp,
|
||||||
|
'role' => 'guru',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Data diri berhasil diperbarui',
|
||||||
|
]);
|
||||||
|
} catch (QueryException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'fail',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatePassword(Request $request){
|
||||||
|
$request->validate(
|
||||||
|
[
|
||||||
|
'old_password' => ['required'],
|
||||||
|
'new_password' => ['required', 'min:6', 'confirmed'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'old_password.required' => 'Password lama harus diisi',
|
||||||
|
'new_password.required' => 'Password baru harus diisi',
|
||||||
|
'new_password.min' => 'Password baru minimal 6 karakter',
|
||||||
|
'new_password.confirmed' => 'Konfirmasi password baru harus sama'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
$id = auth()->user()->id;
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
|
||||||
|
// cek password lama
|
||||||
|
if (!Hash::check($request->old_password, $user->password)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'old_password' => ['Password lama tidak sesuai.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update password baru
|
||||||
|
$user->password = Hash::make($request->new_password);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Password berhasil diperbarui',
|
||||||
|
'data' => null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ketersediaan()
|
||||||
|
{
|
||||||
|
$guru = auth()->user()->Guru;
|
||||||
|
if (!$guru) {
|
||||||
|
return redirect()->back()->with('error', 'Data guru tidak ditemukan');
|
||||||
|
}
|
||||||
|
|
||||||
|
$isLocked = \App\Models\KetersediaanSetting::getRecord()->lock_edit;
|
||||||
|
$sesiList = \App\Models\Sesi::orderBy('id', 'asc')->get();
|
||||||
|
|
||||||
|
// Fetch ketersediaan
|
||||||
|
$ketersediaan = $guru->ketersediaanMengajar->groupBy('hari');
|
||||||
|
$days = [1, 2, 3, 4, 5, 6];
|
||||||
|
$ketersediaanData = [];
|
||||||
|
foreach ($days as $day) {
|
||||||
|
$ketersediaanData[$day] = $ketersediaan->has($day)
|
||||||
|
? $ketersediaan->get($day)->pluck('sesi_id')->toArray()
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('guru.pages.ketersediaan', compact('guru', 'sesiList', 'ketersediaanData', 'isLocked'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateKetersediaan(Request $request)
|
||||||
|
{
|
||||||
|
$isLocked = \App\Models\KetersediaanSetting::getRecord()->lock_edit;
|
||||||
|
if ($isLocked) {
|
||||||
|
return redirect()->back()->with('error', 'Pengubahan ketersediaan mengajar dikunci oleh Admin.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$guru = auth()->user()->Guru;
|
||||||
|
if (!$guru) {
|
||||||
|
return redirect()->back()->with('error', 'Data guru tidak ditemukan');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'ketersediaan' => 'nullable|array',
|
||||||
|
'ketersediaan.*' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $request->input('ketersediaan', []);
|
||||||
|
|
||||||
|
foreach ([1, 2, 3, 4, 5, 6] as $day) {
|
||||||
|
$daySesiIds = $data[$day] ?? [];
|
||||||
|
|
||||||
|
\App\Models\KetersediaanMengajar::where('guru_id', $guru->id)
|
||||||
|
->where('hari', $day)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
foreach ($daySesiIds as $sesiId) {
|
||||||
|
\App\Models\KetersediaanMengajar::create([
|
||||||
|
'guru_id' => $guru->id,
|
||||||
|
'hari' => $day,
|
||||||
|
'sesi_id' => $sesiId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('guru.ketersediaan')
|
||||||
|
->with('success', 'Ketersediaan mengajar berhasil diperbarui');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class isAdmin
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (!auth()->check()) {
|
||||||
|
return redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth()->user()->role !== 'admin') {
|
||||||
|
if (auth()->user()->role === 'siswa') {
|
||||||
|
return redirect('/siswa');
|
||||||
|
}elseif(auth()->user()->role === 'guru'){
|
||||||
|
return redirect('/guru')->with('success', 'Anda telah login sebagai Guru!');
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class isGuesOrtAuth
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class isGuest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (auth()->check()) {
|
||||||
|
if (auth()->user()->role === 'admin') {
|
||||||
|
return redirect('/admin')->with('success', 'Anda telah login sebagai Admin!');
|
||||||
|
}elseif (auth()->user()->role === 'guru') {
|
||||||
|
return redirect('/guru')->with('success', 'Anda telah login sebagai Guru!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class isGuru
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (!auth()->check()) {
|
||||||
|
return redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth()->user()->role !== 'guru') {
|
||||||
|
if (auth()->user()->role === 'admin') {
|
||||||
|
return redirect('/admin')->with('success', 'Anda telah login sebagai Admin!');
|
||||||
|
}elseif(auth()->user()->role === 'siswa'){
|
||||||
|
return redirect('/siswa')->with('success', 'Anda telah login sebagai Siswa!');
|
||||||
|
}else{
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class isSiswa
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (!auth()->check()) {
|
||||||
|
return redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth()->user()->role !== 'siswa') {
|
||||||
|
if (auth()->user()->role === 'admin') {
|
||||||
|
return back();
|
||||||
|
}else{
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Imports;
|
||||||
|
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Maatwebsite\Excel\Concerns\ToModel;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||||
|
|
||||||
|
class LogbookImport implements ToModel, WithValidation, WithHeadingRow
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array $row
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Model|null
|
||||||
|
*/
|
||||||
|
public function model(array $row)
|
||||||
|
{
|
||||||
|
return new Logbooks([
|
||||||
|
'pertemuan' => $row['pertemuan'],
|
||||||
|
'kelas_id' => $row['kelas_id'],
|
||||||
|
'tanggal' => $this->transformDate($row['tanggal'] ?? null),
|
||||||
|
'materi' => $row['materi'] ?? null,
|
||||||
|
'catatan' => $row['catatan'] ?? null,
|
||||||
|
'status' => $row['status'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function transformDate($date)
|
||||||
|
{
|
||||||
|
if (!$date) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tanggal dalam format Excel (angka serial), ubah ke format tanggal
|
||||||
|
if (is_numeric($date)) {
|
||||||
|
return Carbon::instance(Date::excelToDateTimeObject($date))->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tanggal dalam format teks, coba parsing
|
||||||
|
try {
|
||||||
|
return Carbon::parse($date)->format('Y-m-d');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null; // Jika gagal, set ke null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'kelas_id' => 'required',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function customValidationMessages()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'kelas_id.required' => 'ID kelas tidak ditemukan',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable; // ✅ ini yang benar
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable; // ✅ bukan dari Queue
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Intervention\Image\Laravel\Facades\Image;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class CompressLogbookImages implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$directory = storage_path('app/public/foto-logbook');
|
||||||
|
$files = glob($directory . '/*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
|
||||||
|
$batch = array_chunk($files, 100);
|
||||||
|
$total = 0;
|
||||||
|
|
||||||
|
foreach ($batch as $index => $chunk) {
|
||||||
|
foreach ($chunk as $fullPath) {
|
||||||
|
try {
|
||||||
|
$image = Image::read($fullPath)
|
||||||
|
->scaleDown(width: 800)
|
||||||
|
->save($fullPath, quality: 85);
|
||||||
|
$total++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Gagal resize {$fullPath}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log::info("Batch ke-".($index+1)." selesai diproses ({$total} gambar total)");
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("Resize selesai total {$total} gambar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DetailKelas extends Model
|
||||||
|
{
|
||||||
|
protected $table = "detail_kelas";
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function siswa()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Siswa::class, 'siswa_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Pesertas()
|
||||||
|
{
|
||||||
|
return $this->siswa();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DivisiGuru extends Model
|
||||||
|
{
|
||||||
|
protected $table = "divisi_guru";
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
// public function Guru()
|
||||||
|
// {
|
||||||
|
// return $this->hasMany(Guru::class, 'divisi_id', 'id');
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class FotoLogbooks extends Model
|
||||||
|
{
|
||||||
|
protected $table = "foto_logbook";
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function Logbooks()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Logbooks::class, 'logbook_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Guru extends Model
|
||||||
|
{
|
||||||
|
protected $table = "guru";
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function detailGuru()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Kelas::class, 'guru_id');
|
||||||
|
}
|
||||||
|
public function User()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
public function ketersediaanMengajar()
|
||||||
|
{
|
||||||
|
return $this->hasMany(KetersediaanMengajar::class, 'guru_id')->orderBy('hari', 'asc');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Hari extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'hari';
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class HistoriSiswa extends Model
|
||||||
|
{
|
||||||
|
protected $table = "histori_siswa";
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function siswa()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Siswa::class, 'siswa_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Peserta()
|
||||||
|
{
|
||||||
|
return $this->siswa();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class JadwalKelas extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'jadwal_kelas';
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function kelas()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Kelas::class, 'kelas_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sesi()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Sesi::class, 'sesi_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ruangan()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Ruangan::class, 'ruangan_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function guru()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Guru::class, 'guru_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hariModel()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Hari::class, 'hari');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Kelas extends Model
|
||||||
|
{
|
||||||
|
protected $table = "kelas";
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function Programs()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Programs::class, 'program_id');
|
||||||
|
}
|
||||||
|
public function siswa()
|
||||||
|
{
|
||||||
|
return $this->hasMany(DetailKelas::class, 'kelas_id');
|
||||||
|
}
|
||||||
|
public function logbooks()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Logbooks::class, 'kelas_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sekolah()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Sekolah::class, 'sekolah_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ruanganFix()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Ruangan::class, 'ruangan_fix_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function guru()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Guru::class, 'guru_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class KetersediaanMengajar extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'ketersediaan_mengajars';
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public static $namaHari = [
|
||||||
|
1 => 'Senin',
|
||||||
|
2 => 'Selasa',
|
||||||
|
3 => 'Rabu',
|
||||||
|
4 => 'Kamis',
|
||||||
|
5 => 'Jumat',
|
||||||
|
6 => 'Sabtu',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function guru()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Guru::class, 'guru_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sesi()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Sesi::class, 'sesi_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNamaHariAttribute(): string
|
||||||
|
{
|
||||||
|
return self::$namaHari[$this->hari] ?? 'Tidak Diketahui';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class KetersediaanSetting extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'ketersediaan_settings';
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get the single settings record
|
||||||
|
*/
|
||||||
|
public static function getRecord()
|
||||||
|
{
|
||||||
|
$record = self::first();
|
||||||
|
if (!$record) {
|
||||||
|
$record = self::create(['lock_edit' => false]);
|
||||||
|
}
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Logbooks extends Model
|
||||||
|
{
|
||||||
|
protected $table = "logbook";
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function Kelas()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Kelas::class, 'kelas_id');
|
||||||
|
}
|
||||||
|
public function FotoLogbook()
|
||||||
|
{
|
||||||
|
return $this->hasMany(FotoLogbooks::class, 'logbook_id');
|
||||||
|
}
|
||||||
|
public function Sesi()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Sesi::class, 'sesi_id');
|
||||||
|
}
|
||||||
|
public function Guru()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Guru::class, 'guru_id');
|
||||||
|
}
|
||||||
|
public function Hari()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Hari::class, 'hari_id');
|
||||||
|
}
|
||||||
|
public function Ruangan()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Ruangan::class, 'ruangan_id');
|
||||||
|
}
|
||||||
|
public function Pengajar()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PengajarLogbook::class, 'logbook_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PengajarLogbook extends Model
|
||||||
|
{
|
||||||
|
protected $table = "pengajar_logbook";
|
||||||
|
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function Guru()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Guru::class, 'guru_id');
|
||||||
|
}
|
||||||
|
public function Logbook()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Logbooks::class, 'logbook_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Programs extends Model
|
||||||
|
{
|
||||||
|
protected $table = "programs";
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function siswa()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Siswa::class, 'program_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Pesertas()
|
||||||
|
{
|
||||||
|
return $this->siswa();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Ruangan extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'ruangan';
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function sekolah()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Sekolah::class, 'sekolah_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Sekolah extends Model
|
||||||
|
{
|
||||||
|
protected $table = "sekolah";
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function siswa()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Siswa::class, 'sekolah_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Peserta()
|
||||||
|
{
|
||||||
|
return $this->siswa();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Sesi extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'sesi';
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function ketersediaanMengajar()
|
||||||
|
{
|
||||||
|
return $this->hasMany(KetersediaanMengajar::class, 'sesi_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Siswa extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'siswa';
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function Programs()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Programs::class, 'program_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Sekolah()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Sekolah::class, 'sekolah_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'username',
|
||||||
|
'email',
|
||||||
|
'no_hp_orangtua',
|
||||||
|
'password',
|
||||||
|
'role',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function Guru()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Guru::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\JadwalKelas;
|
||||||
|
use App\Models\Logbooks;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class LogbookGeneratorService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generate entri logbook berdasarkan Master Jadwal GA (jadwal_kelas)
|
||||||
|
* untuk rentang tanggal tertentu, dibatasi oleh total sesi/pertemuan per kelas (jenis_kelas).
|
||||||
|
*
|
||||||
|
* @param string $startDate (Format: YYYY-MM-DD)
|
||||||
|
* @param string $endDate (Format: YYYY-MM-DD)
|
||||||
|
* @param array $options
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function generateFromJadwal(string $startDate, string $endDate, array $options = []): array
|
||||||
|
{
|
||||||
|
$start = Carbon::parse($startDate)->startOfDay();
|
||||||
|
$end = Carbon::parse($endDate)->endOfDay();
|
||||||
|
|
||||||
|
// 1. Ambil Master Jadwal Kelas beserta relasinya
|
||||||
|
$masterJadwal = JadwalKelas::with(['kelas', 'guru', 'ruangan', 'sesi'])->get();
|
||||||
|
|
||||||
|
if ($masterJadwal->isEmpty()) {
|
||||||
|
throw new \Exception('Belum ada data Master Jadwal Mingguan (`jadwal_kelas`). Silakan jalankan generate GA terlebih dahulu.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$createdCount = 0;
|
||||||
|
$updatedCount = 0;
|
||||||
|
$skippedCount = 0;
|
||||||
|
$deletedExcessCount = 0;
|
||||||
|
|
||||||
|
// Peta maksimal pertemuan per kelas dari jenis_kelas (ex: "12 Sesi" => 12)
|
||||||
|
$maxMeetingsMap = [];
|
||||||
|
foreach ($masterJadwal as $j) {
|
||||||
|
if ($j->kelas && !isset($maxMeetingsMap[$j->kelas_id])) {
|
||||||
|
$maxM = null;
|
||||||
|
if (!empty($j->kelas->jenis_kelas)) {
|
||||||
|
if (preg_match('/\d+/', $j->kelas->jenis_kelas, $matches)) {
|
||||||
|
$maxM = (int) $matches[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$maxMeetingsMap[$j->kelas_id] = $maxM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track per-kelas pertemuan counter
|
||||||
|
$pertemuanCounter = [];
|
||||||
|
|
||||||
|
// 2. Iterasi per hari dari startDate hingga endDate
|
||||||
|
$currentDate = $start->copy();
|
||||||
|
while ($currentDate->lte($end)) {
|
||||||
|
$dayIso = $currentDate->dayOfWeekIso; // 1 = Senin, ..., 7 = Minggu
|
||||||
|
$dateString = $currentDate->format('Y-m-d');
|
||||||
|
|
||||||
|
// Cari jadwal yang sesuai dengan hari pada tanggal ini
|
||||||
|
$schedulesForDay = $masterJadwal->where('hari', $dayIso);
|
||||||
|
|
||||||
|
foreach ($schedulesForDay as $jadwal) {
|
||||||
|
$kelasId = $jadwal->kelas_id;
|
||||||
|
$maxMeetings = $maxMeetingsMap[$kelasId] ?? null;
|
||||||
|
|
||||||
|
if (!isset($pertemuanCounter[$kelasId])) {
|
||||||
|
// Hitung berapa entri logbook yang sudah ada untuk kelas ini secara keseluruhan
|
||||||
|
$totalExisting = Logbooks::where('kelas_id', $kelasId)->count();
|
||||||
|
$pertemuanCounter[$kelasId] = $totalExisting;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah logbook untuk kelas, tanggal, dan sesi ini sudah ada
|
||||||
|
$existingLogbook = Logbooks::where('kelas_id', $kelasId)
|
||||||
|
->where('tanggal', $dateString)
|
||||||
|
->where('sesi_id', $jadwal->sesi_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Ekstrak jam start & end dari Sesi (ex: "08:00 - 10:00")
|
||||||
|
$jamStart = null;
|
||||||
|
$jamEnd = null;
|
||||||
|
if ($jadwal->sesi && $jadwal->sesi->jam) {
|
||||||
|
$jamParts = explode('-', $jadwal->sesi->jam);
|
||||||
|
$jamStart = isset($jamParts[0]) ? trim($jamParts[0]) : null;
|
||||||
|
$jamEnd = isset($jamParts[1]) ? trim($jamParts[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existingLogbook) {
|
||||||
|
// Jika logbook sudah completed, lewati agar data presensi guru tidak hilang
|
||||||
|
if ($existingLogbook->status === 'completed') {
|
||||||
|
$skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update logbook pending dengan info master jadwal terbaru
|
||||||
|
$existingLogbook->update([
|
||||||
|
'guru_id' => $jadwal->guru_id,
|
||||||
|
'ruangan_id' => $jadwal->ruangan_id,
|
||||||
|
'hari_id' => $dayIso,
|
||||||
|
'kode_kelas' => $jadwal->kelas->kode_kelas ?? $existingLogbook->kode_kelas,
|
||||||
|
'start' => $jamStart ?? $existingLogbook->start,
|
||||||
|
'end' => $jamEnd ?? $existingLogbook->end,
|
||||||
|
]);
|
||||||
|
$updatedCount++;
|
||||||
|
} else {
|
||||||
|
// Cek apakah kelas sudah mencapai batas maksimal pertemuan (misal 12 sesi)
|
||||||
|
if ($maxMeetings !== null && $pertemuanCounter[$kelasId] >= $maxMeetings) {
|
||||||
|
$skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inkremen nomor pertemuan
|
||||||
|
$pertemuanCounter[$kelasId]++;
|
||||||
|
$pertemuanKe = (string) $pertemuanCounter[$kelasId];
|
||||||
|
|
||||||
|
Logbooks::create([
|
||||||
|
'kelas_id' => $kelasId,
|
||||||
|
'kode_kelas' => $jadwal->kelas->kode_kelas ?? null,
|
||||||
|
'guru_id' => $jadwal->guru_id,
|
||||||
|
'ruangan_id' => $jadwal->ruangan_id,
|
||||||
|
'sesi_id' => $jadwal->sesi_id,
|
||||||
|
'hari_id' => $dayIso,
|
||||||
|
'tanggal' => $dateString,
|
||||||
|
'start' => $jamStart,
|
||||||
|
'end' => $jamEnd,
|
||||||
|
'pertemuan' => $pertemuanKe,
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
$createdCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentDate->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Bersihkan entri logbook berstatus 'pending' yang melebihi maxMeetings per kelas
|
||||||
|
foreach ($maxMeetingsMap as $kelasId => $maxM) {
|
||||||
|
if ($maxM !== null) {
|
||||||
|
$excessLogbooks = Logbooks::where('kelas_id', $kelasId)
|
||||||
|
->where('status', 'pending')
|
||||||
|
->get()
|
||||||
|
->filter(function ($lb) use ($maxM) {
|
||||||
|
return is_numeric($lb->pertemuan) && (int)$lb->pertemuan > $maxM;
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($excessLogbooks as $excess) {
|
||||||
|
$excess->delete();
|
||||||
|
$deletedExcessCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'created' => $createdCount,
|
||||||
|
'updated' => $updatedCount,
|
||||||
|
'skipped' => $skippedCount,
|
||||||
|
'deleted_excess' => $deletedExcessCount,
|
||||||
|
'total' => $createdCount + $updatedCount + $skippedCount
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chromosome merepresentasikan 1 individu solusi jadwal mingguan lengkap.
|
||||||
|
* Terdiri dari sekumpulan Gen (Gene[]) untuk seluruh kelas aktif.
|
||||||
|
*/
|
||||||
|
class Chromosome
|
||||||
|
{
|
||||||
|
/** @var array<Gene> */
|
||||||
|
public array $genes = [];
|
||||||
|
public float $fitness = 0.0;
|
||||||
|
public int $totalPenalty = 0;
|
||||||
|
|
||||||
|
/** @var array<string, int> Rincian penalti per batasan (B1..B7) */
|
||||||
|
public array $penaltyBreakdown = [
|
||||||
|
'B1' => 0, // Bentrok Guru
|
||||||
|
'B2' => 0, // Bentrok Ruangan
|
||||||
|
'B3' => 0, // Ketersediaan Guru
|
||||||
|
'B4' => 0, // Bentrok Kelas
|
||||||
|
'B5' => 0, // Kualifikasi Level
|
||||||
|
'B6' => 0, // Sebaran Hari
|
||||||
|
'B7' => 0, // Konsistensi Guru
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<Gene> $genes
|
||||||
|
*/
|
||||||
|
public function __construct(array $genes = [])
|
||||||
|
{
|
||||||
|
$this->genes = $genes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set hasil evaluasi fitness
|
||||||
|
*/
|
||||||
|
public function setEvaluationResult(int $totalPenalty, array $breakdown): void
|
||||||
|
{
|
||||||
|
$this->totalPenalty = $totalPenalty;
|
||||||
|
$this->penaltyBreakdown = $breakdown;
|
||||||
|
$this->fitness = 1.0 / (1.0 + $totalPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kloning kromosom beserta seluruh gen di dalamnya
|
||||||
|
*/
|
||||||
|
public function clone(): self
|
||||||
|
{
|
||||||
|
$clonedGenes = array_map(fn(Gene $g) => $g->clone(), $this->genes);
|
||||||
|
$chromosome = new self($clonedGenes);
|
||||||
|
$chromosome->fitness = $this->fitness;
|
||||||
|
$chromosome->totalPenalty = $this->totalPenalty;
|
||||||
|
$chromosome->penaltyBreakdown = $this->penaltyBreakdown;
|
||||||
|
return $chromosome;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling\Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO merepresentasikan seorang guru untuk keperluan GA.
|
||||||
|
* Hanya berisi atribut yang dipakai algoritma — tidak termasuk data personal
|
||||||
|
* seperti alamat, foto, atau tanggal lahir.
|
||||||
|
*/
|
||||||
|
class Guru
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param int $id ID guru di database
|
||||||
|
* @param string $nama Nama guru (untuk display saja)
|
||||||
|
* @param int $levelMaksimal Level tertinggi yang mampu diajar (1-4)
|
||||||
|
* @param array<string, bool> $ketersediaan Map slot key → bool.
|
||||||
|
* Contoh: ["1-1" => true, "1-2" => true, "2-3" => true, ...]
|
||||||
|
* Slot yang tidak ada di map dianggap tidak tersedia.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly int $id,
|
||||||
|
public readonly string $nama,
|
||||||
|
public readonly int $levelMaksimal,
|
||||||
|
public readonly array $ketersediaan
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah guru tersedia di slot tertentu.
|
||||||
|
* Ini yang dipakai H3 (constraint ketersediaan guru).
|
||||||
|
*/
|
||||||
|
public function tersediaAt(SchedulingSlot $slot): bool
|
||||||
|
{
|
||||||
|
return $this->ketersediaan[$slot->toKey()] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah guru mampu mengajar level tertentu (berjenjang).
|
||||||
|
* Guru level 3 → mampu mengajar level 1, 2, 3, tapi tidak 4.
|
||||||
|
* Ini yang dipakai H4 (constraint kualifikasi).
|
||||||
|
*/
|
||||||
|
public function mampuMengajarLevel(int $level): bool
|
||||||
|
{
|
||||||
|
return $this->levelMaksimal >= $level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghitung total slot yang tersedia untuk guru ini.
|
||||||
|
* Berguna untuk diagnostik dan penghitungan bobot fitness.
|
||||||
|
*/
|
||||||
|
public function totalSlotTersedia(): int
|
||||||
|
{
|
||||||
|
return count(array_filter($this->ketersediaan));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling\Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO merepresentasikan sebuah kelas untuk keperluan GA.
|
||||||
|
*
|
||||||
|
* Ada dua jenis kelas:
|
||||||
|
* 1. Kelas Reguler: 4 komponen (hari, sesi, ruangan, guru) dioptimasi GA
|
||||||
|
* 2. Kelas Ekstrakurikuler: hari/sesi/ruangan sudah tetap, hanya guru yang dioptimasi
|
||||||
|
*/
|
||||||
|
class Kelas
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param int $id ID kelas di database
|
||||||
|
* @param string $nama Nama kelas (untuk display)
|
||||||
|
* @param int $level Level kelas (1-4)
|
||||||
|
* @param int $frekuensiPerMinggu Berapa kali kelas ini bertemu per minggu
|
||||||
|
* @param bool $isEkstrakurikuler Apakah kelas ini ekstrakurikuler
|
||||||
|
* @param ?int $hariFix Hari yang sudah tetap (hanya untuk ekstrakurikuler)
|
||||||
|
* @param ?int $sesiFix Sesi yang sudah tetap (hanya untuk ekstrakurikuler)
|
||||||
|
* @param ?int $ruanganFixId ID ruangan yang sudah tetap (hanya untuk ekstrakurikuler)
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly int $id,
|
||||||
|
public readonly string $nama,
|
||||||
|
public readonly int $level,
|
||||||
|
public readonly int $frekuensiPerMinggu,
|
||||||
|
public readonly bool $isEkstrakurikuler,
|
||||||
|
public readonly ?int $hariFix = null,
|
||||||
|
public readonly ?int $sesiFix = null,
|
||||||
|
public readonly ?int $ruanganFixId = null,
|
||||||
|
) {
|
||||||
|
// Guard: untuk kelas ekstrakurikuler, hari/sesi/ruangan_fix wajib terisi
|
||||||
|
if ($isEkstrakurikuler) {
|
||||||
|
if ($hariFix === null || $sesiFix === null || $ruanganFixId === null) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"Kelas ekstrakurikuler '{$nama}' (id={$id}) harus punya hari_fix, sesi_fix, dan ruangan_fix_id."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengembalikan slot fix untuk kelas ekstrakurikuler.
|
||||||
|
* Return null untuk kelas reguler.
|
||||||
|
*/
|
||||||
|
public function slotFix(): ?SchedulingSlot
|
||||||
|
{
|
||||||
|
if (!$this->isEkstrakurikuler) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new SchedulingSlot($this->hariFix, $this->sesiFix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling\Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kontainer utama yang menampung semua data untuk satu run GA.
|
||||||
|
* Dibuat sekali oleh DatasetLoader di awal, lalu dipass ke GA.
|
||||||
|
*/
|
||||||
|
class SchedulingDataset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int, Guru> $guruList Map guru_id → Guru
|
||||||
|
* @param array<int, Kelas> $kelasList Map kelas_id → Kelas
|
||||||
|
* @param array<int> $ruanganRobotkuIds Daftar id ruangan Robotku
|
||||||
|
* (untuk domain pilihan ruang di kelas reguler)
|
||||||
|
* @param array<SchedulingSlot> $allSlots Semua slot valid (6 hari × 5 sesi = 30 slot)
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly array $guruList,
|
||||||
|
public readonly array $kelasList,
|
||||||
|
public readonly array $ruanganRobotkuIds,
|
||||||
|
public readonly array $allSlots,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil guru berdasarkan ID. Throw exception jika tidak ditemukan.
|
||||||
|
*/
|
||||||
|
public function getGuru(int $id): Guru
|
||||||
|
{
|
||||||
|
if (!isset($this->guruList[$id])) {
|
||||||
|
throw new \OutOfBoundsException("Guru dengan id={$id} tidak ada di dataset.");
|
||||||
|
}
|
||||||
|
return $this->guruList[$id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil kelas berdasarkan ID.
|
||||||
|
*/
|
||||||
|
public function getKelas(int $id): Kelas
|
||||||
|
{
|
||||||
|
if (!isset($this->kelasList[$id])) {
|
||||||
|
throw new \OutOfBoundsException("Kelas dengan id={$id} tidak ada di dataset.");
|
||||||
|
}
|
||||||
|
return $this->kelasList[$id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semua kelas reguler (kandidat untuk dioptimasi penuh oleh GA).
|
||||||
|
*
|
||||||
|
* @return array<Kelas>
|
||||||
|
*/
|
||||||
|
public function kelasReguler(): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(
|
||||||
|
$this->kelasList,
|
||||||
|
fn(Kelas $k) => !$k->isEkstrakurikuler
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semua kelas ekstrakurikuler (jadwal fix, hanya guru yang dioptimasi).
|
||||||
|
*
|
||||||
|
* @return array<Kelas>
|
||||||
|
*/
|
||||||
|
public function kelasEkstrakurikuler(): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(
|
||||||
|
$this->kelasList,
|
||||||
|
fn(Kelas $k) => $k->isEkstrakurikuler
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guru yang mampu mengajar level tertentu (berjenjang).
|
||||||
|
*
|
||||||
|
* @return array<Guru>
|
||||||
|
*/
|
||||||
|
public function guruMampuLevel(int $level): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(
|
||||||
|
$this->guruList,
|
||||||
|
fn(Guru $g) => $g->mampuMengajarLevel($level)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total pertemuan per minggu yang harus dijadwalkan (= panjang kromosom N).
|
||||||
|
*/
|
||||||
|
public function totalPertemuan(): int
|
||||||
|
{
|
||||||
|
return array_sum(array_map(
|
||||||
|
fn(Kelas $k) => $k->frekuensiPerMinggu,
|
||||||
|
$this->kelasList
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistik ringkas untuk keperluan diagnostik & pelaporan.
|
||||||
|
*/
|
||||||
|
public function summary(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'total_guru' => count($this->guruList),
|
||||||
|
'total_kelas' => count($this->kelasList),
|
||||||
|
'kelas_reguler' => count($this->kelasReguler()),
|
||||||
|
'kelas_ekstrakurikuler' => count($this->kelasEkstrakurikuler()),
|
||||||
|
'total_ruangan_robotku' => count($this->ruanganRobotkuIds),
|
||||||
|
'total_slot' => count($this->allSlots),
|
||||||
|
'total_pertemuan_per_minggu' => $this->totalPertemuan(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling\Data;
|
||||||
|
|
||||||
|
class SchedulingSlot
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly int $hari,
|
||||||
|
public readonly int $sesi
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membandingkan apakah dua slot sama (hari & sesi identik).
|
||||||
|
*/
|
||||||
|
public function equals(SchedulingSlot $other): bool
|
||||||
|
{
|
||||||
|
return $this->hari === $other->hari && $this->sesi === $other->sesi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengembalikan string unik untuk keperluan hash/lookup.
|
||||||
|
* Contoh: hari=2, sesi=3 → "2-3"
|
||||||
|
*/
|
||||||
|
public function toKey(): string
|
||||||
|
{
|
||||||
|
return "{$this->hari}-{$this->sesi}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membuat SchedulingSlot dari string key.
|
||||||
|
* Contoh: "2-3" → SchedulingSlot(2, 3)
|
||||||
|
*/
|
||||||
|
public static function fromKey(string $key): self
|
||||||
|
{
|
||||||
|
[$hari, $sesi] = explode('-', $key);
|
||||||
|
return new self((int) $hari, (int) $sesi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling;
|
||||||
|
|
||||||
|
use App\Services\Scheduling\Data\Guru;
|
||||||
|
use App\Services\Scheduling\Data\Kelas;
|
||||||
|
use App\Services\Scheduling\Data\SchedulingDataset;
|
||||||
|
use App\Services\Scheduling\Data\SchedulingSlot;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service yang bertanggung jawab membaca data mentah dari database
|
||||||
|
* dan mengubahnya menjadi SchedulingDataset yang siap dipakai GA.
|
||||||
|
*
|
||||||
|
* Ini adalah anti-corruption layer: seluruh dependensi pada struktur
|
||||||
|
* database ada di sini. GA tidak tahu apa-apa tentang tabel/kolom.
|
||||||
|
*/
|
||||||
|
class DatasetLoader
|
||||||
|
{
|
||||||
|
// Semester default untuk skripsi ini
|
||||||
|
public const DEFAULT_SEMESTER = 'Genap';
|
||||||
|
public const DEFAULT_TAHUN_AJARAN = '2025/2026';
|
||||||
|
|
||||||
|
// Konfigurasi operasional Robotku (bisa dipindah ke config file kalau perlu)
|
||||||
|
public const HARI_LIST = [1, 2, 3, 4, 5, 6]; // Senin-Sabtu
|
||||||
|
public const SESI_LIST = [1, 2, 3, 4, 5]; // Sesi 1-5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method utama: baca database → return SchedulingDataset lengkap.
|
||||||
|
*
|
||||||
|
* @param string|null $semester Default: 'Genap'
|
||||||
|
* @param string|null $tahunAjaran Default: '2025/2026'
|
||||||
|
* @return SchedulingDataset
|
||||||
|
* @throws \RuntimeException jika dataset tidak lolos validasi
|
||||||
|
*/
|
||||||
|
public function load(
|
||||||
|
?string $semester = null,
|
||||||
|
?string $tahunAjaran = null
|
||||||
|
): SchedulingDataset {
|
||||||
|
$semester = $semester ?? self::DEFAULT_SEMESTER;
|
||||||
|
$tahunAjaran = $tahunAjaran ?? self::DEFAULT_TAHUN_AJARAN;
|
||||||
|
|
||||||
|
$guruList = $this->loadGuruList();
|
||||||
|
$kelasList = $this->loadKelasList($semester, $tahunAjaran);
|
||||||
|
$ruanganRobotkuIds = $this->loadRuanganRobotku();
|
||||||
|
$allSlots = $this->buildAllSlots();
|
||||||
|
|
||||||
|
$dataset = new SchedulingDataset(
|
||||||
|
guruList: $guruList,
|
||||||
|
kelasList: $kelasList,
|
||||||
|
ruanganRobotkuIds: $ruanganRobotkuIds,
|
||||||
|
allSlots: $allSlots,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->validate($dataset);
|
||||||
|
|
||||||
|
return $dataset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load semua guru yang aktif mengajar & punya level_maksimal.
|
||||||
|
*
|
||||||
|
* @return array<int, Guru> Map guru_id => Guru
|
||||||
|
*/
|
||||||
|
private function loadGuruList(): array
|
||||||
|
{
|
||||||
|
// Ambil semua guru aktif yang punya level_maksimal terisi.
|
||||||
|
// Guru administratif tanpa level_maksimal otomatis dikecualikan.
|
||||||
|
$guruRows = DB::table('guru')
|
||||||
|
->select('id', 'nama', 'level_maksimal')
|
||||||
|
->where('status', 'Aktif')
|
||||||
|
->whereNotNull('level_maksimal')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Ambil semua ketersediaan sekaligus (satu query), lalu di-index per guru_id.
|
||||||
|
$guruIds = $guruRows->pluck('id')->toArray();
|
||||||
|
$ketersediaanByGuru = $this->loadKetersediaanByGuru($guruIds);
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($guruRows as $row) {
|
||||||
|
$result[$row->id] = new Guru(
|
||||||
|
id: (int) $row->id,
|
||||||
|
nama: (string) $row->nama,
|
||||||
|
levelMaksimal: (int) $row->level_maksimal,
|
||||||
|
ketersediaan: $ketersediaanByGuru[$row->id] ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load ketersediaan_mengajars dan grup berdasarkan guru_id.
|
||||||
|
*
|
||||||
|
* @param array<int> $guruIds
|
||||||
|
* @return array<int, array<string, bool>> Map guru_id => (map slot_key => true)
|
||||||
|
*/
|
||||||
|
private function loadKetersediaanByGuru(array $guruIds): array
|
||||||
|
{
|
||||||
|
if (empty($guruIds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// sesi_id di tabel ketersediaan mengacu ke tabel sesi (id 1-5).
|
||||||
|
// Kita asumsikan sesi.id === nomor sesi (dikonfirmasi user).
|
||||||
|
$rows = DB::table('ketersediaan_mengajars')
|
||||||
|
->select('guru_id', 'hari', 'sesi_id')
|
||||||
|
->whereIn('guru_id', $guruIds)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$slotKey = "{$row->hari}-{$row->sesi_id}";
|
||||||
|
$result[$row->guru_id][$slotKey] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load semua kelas aktif untuk semester tertentu.
|
||||||
|
*
|
||||||
|
* @return array<int, Kelas> Map kelas_id => Kelas
|
||||||
|
*/
|
||||||
|
private function loadKelasList(string $semester, string $tahunAjaran): array
|
||||||
|
{
|
||||||
|
// Join dengan programs untuk ambil level_number kelas.
|
||||||
|
$rows = DB::table('kelas')
|
||||||
|
->join('programs', 'kelas.program_id', '=', 'programs.id')
|
||||||
|
->select(
|
||||||
|
'kelas.id',
|
||||||
|
'kelas.nama_kelas',
|
||||||
|
'programs.level_number',
|
||||||
|
'kelas.frekuensi_per_minggu',
|
||||||
|
'kelas.is_ekstrakurikuler',
|
||||||
|
'kelas.hari_fix',
|
||||||
|
'kelas.sesi_fix',
|
||||||
|
'kelas.ruangan_fix_id',
|
||||||
|
)
|
||||||
|
->where('kelas.semester', $semester)
|
||||||
|
->where('kelas.tahun_ajaran', $tahunAjaran)
|
||||||
|
->where('kelas.status', 'Aktif')
|
||||||
|
->orderBy('kelas.id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
// level_number bisa null jika data programs belum lengkap
|
||||||
|
if ($row->level_number === null) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
"Kelas '{$row->nama_kelas}' (id={$row->id}) merujuk ke program tanpa level_number. "
|
||||||
|
. "Isi level_number di tabel programs terlebih dahulu."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[$row->id] = new Kelas(
|
||||||
|
id: (int) $row->id,
|
||||||
|
nama: (string) $row->nama_kelas,
|
||||||
|
level: (int) $row->level_number,
|
||||||
|
frekuensiPerMinggu: (int) $row->frekuensi_per_minggu,
|
||||||
|
isEkstrakurikuler: (bool) $row->is_ekstrakurikuler,
|
||||||
|
hariFix: $row->hari_fix !== null ? (int) $row->hari_fix : null,
|
||||||
|
sesiFix: $row->sesi_fix !== null ? (int) $row->sesi_fix : null,
|
||||||
|
ruanganFixId: $row->ruangan_fix_id !== null ? (int) $row->ruangan_fix_id : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load 5 ruangan Robotku (lokasi_tipe='robotku').
|
||||||
|
* Ruangan sekolah tidak masuk pool ini karena tidak dipakai kelas reguler.
|
||||||
|
*
|
||||||
|
* @return array<int> Array id ruangan
|
||||||
|
*/
|
||||||
|
private function loadRuanganRobotku(): array
|
||||||
|
{
|
||||||
|
return DB::table('ruangan')
|
||||||
|
->where('lokasi_tipe', 'robotku')
|
||||||
|
->where('status', 'aktif')
|
||||||
|
->orderBy('id')
|
||||||
|
->pluck('id')
|
||||||
|
->map(fn($id) => (int) $id)
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate semua slot valid: 6 hari × 5 sesi = 30 slot.
|
||||||
|
*
|
||||||
|
* @return array<SchedulingSlot>
|
||||||
|
*/
|
||||||
|
private function buildAllSlots(): array
|
||||||
|
{
|
||||||
|
$slots = [];
|
||||||
|
foreach (self::HARI_LIST as $hari) {
|
||||||
|
foreach (self::SESI_LIST as $sesi) {
|
||||||
|
$slots[] = new SchedulingSlot($hari, $sesi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validasi dataset: pastikan tidak ada kondisi mustahil sebelum GA jalan.
|
||||||
|
* Kalau ada masalah, throw exception dengan pesan jelas.
|
||||||
|
*/
|
||||||
|
private function validate(SchedulingDataset $dataset): void
|
||||||
|
{
|
||||||
|
// 1. Minimal harus ada guru
|
||||||
|
if (empty($dataset->guruList)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
"Tidak ada guru dengan status='Aktif' dan level_maksimal terisi. "
|
||||||
|
. "Sistem tidak bisa menjadwalkan."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Minimal harus ada kelas
|
||||||
|
if (empty($dataset->kelasList)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
"Tidak ada kelas aktif untuk semester ini. "
|
||||||
|
. "Cek filter semester & tahun_ajaran."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Minimal harus ada ruangan Robotku
|
||||||
|
if (empty($dataset->ruanganRobotkuIds)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
"Tidak ada ruangan Robotku (lokasi_tipe='robotku') yang aktif."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Untuk tiap kelas ekstrakurikuler, ruangan_fix_id harus valid
|
||||||
|
// (ruangan tersebut harus benar-benar ada di tabel ruangan)
|
||||||
|
$ruanganIdsEks = collect($dataset->kelasEkstrakurikuler())
|
||||||
|
->pluck('ruanganFixId')
|
||||||
|
->unique()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
if (!empty($ruanganIdsEks)) {
|
||||||
|
$existingRuangan = DB::table('ruangan')
|
||||||
|
->whereIn('id', $ruanganIdsEks)
|
||||||
|
->pluck('id')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$missing = array_diff($ruanganIdsEks, $existingRuangan);
|
||||||
|
if (!empty($missing)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
"Kelas ekstrakurikuler merujuk ke ruangan_fix_id yang tidak ada di database: "
|
||||||
|
. implode(', ', $missing)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Untuk tiap level yang butuh guru, minimal ada 1 guru yang mampu.
|
||||||
|
// (Feasibility check paling dasar untuk H4)
|
||||||
|
$levelKelas = collect($dataset->kelasList)->pluck('level')->unique();
|
||||||
|
foreach ($levelKelas as $level) {
|
||||||
|
$guruMampu = $dataset->guruMampuLevel($level);
|
||||||
|
if (empty($guruMampu)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
"Tidak ada guru dengan level_maksimal >= {$level}. "
|
||||||
|
. "Kelas level {$level} tidak dapat dijadwalkan."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling;
|
||||||
|
|
||||||
|
use App\Services\Scheduling\Data\SchedulingDataset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FitnessEvaluator mengevaluasi 7 Hard Constraints (B1 - B7) pada sebuah Chromosome.
|
||||||
|
* Setiap pelanggaran aturan menambah penalti +1.
|
||||||
|
*
|
||||||
|
* Fitness = 1.0 / (1.0 + Total Penalty)
|
||||||
|
*/
|
||||||
|
class FitnessEvaluator
|
||||||
|
{
|
||||||
|
public function evaluate(Chromosome $chromosome, SchedulingDataset $dataset): void
|
||||||
|
{
|
||||||
|
$breakdown = [
|
||||||
|
'B1' => 0, // Bentrok Guru
|
||||||
|
'B2' => 0, // Bentrok Ruangan
|
||||||
|
'B3' => 0, // Ketersediaan Guru
|
||||||
|
'B4' => 0, // Bentrok Kelas
|
||||||
|
'B5' => 0, // Kualifikasi Level
|
||||||
|
'B6' => 0, // Sebaran Hari
|
||||||
|
'B7' => 0, // Konsistensi Guru
|
||||||
|
];
|
||||||
|
|
||||||
|
// Struktur pembantu untuk deteksi bentrok
|
||||||
|
// key guruSlot: "guruId_hariId_sesiId" -> count
|
||||||
|
$guruSlotMap = [];
|
||||||
|
// key ruanganSlot: "ruanganId_hariId_sesiId" -> count (khusus ruangan reguler LKP)
|
||||||
|
$ruanganSlotMap = [];
|
||||||
|
// key kelasSlot: "kelasId_hariId_sesiId" -> count
|
||||||
|
$kelasSlotMap = [];
|
||||||
|
// grouping gen per kelasId -> Gene[]
|
||||||
|
$kelasGenesMap = [];
|
||||||
|
|
||||||
|
foreach ($chromosome->genes as $gene) {
|
||||||
|
$kelas = $dataset->getKelas($gene->kelasId);
|
||||||
|
$guru = $dataset->getGuru($gene->guruId);
|
||||||
|
|
||||||
|
// B1: Bentrok Guru (1 Guru mengajar >1 kelas di (H, S) sama)
|
||||||
|
$guruSlotKey = "{$gene->guruId}_{$gene->hariId}_{$gene->sesiId}";
|
||||||
|
if (isset($guruSlotMap[$guruSlotKey])) {
|
||||||
|
$breakdown['B1']++;
|
||||||
|
} else {
|
||||||
|
$guruSlotMap[$guruSlotKey] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B2: Bentrok Ruangan (1 Ruangan LKP dipakai >1 kelas di (H, S) sama, khusus reguler)
|
||||||
|
if (!$kelas->isEkstrakurikuler) {
|
||||||
|
$ruanganSlotKey = "{$gene->ruanganId}_{$gene->hariId}_{$gene->sesiId}";
|
||||||
|
if (isset($ruanganSlotMap[$ruanganSlotKey])) {
|
||||||
|
$breakdown['B2']++;
|
||||||
|
} else {
|
||||||
|
$ruanganSlotMap[$ruanganSlotKey] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// B3: Ketersediaan Guru (Guru diplot pada (H, S) yang TIDAK terdaftar di ketersediaannya)
|
||||||
|
if (!$guru->tersediaAt(new \App\Services\Scheduling\Data\SchedulingSlot($gene->hariId, $gene->sesiId))) {
|
||||||
|
$breakdown['B3']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B4: Bentrok Kelas (1 Kelas diplot di (H, S) yang sama)
|
||||||
|
$kelasSlotKey = "{$gene->kelasId}_{$gene->hariId}_{$gene->sesiId}";
|
||||||
|
if (isset($kelasSlotMap[$kelasSlotKey])) {
|
||||||
|
$breakdown['B4']++;
|
||||||
|
} else {
|
||||||
|
$kelasSlotMap[$kelasSlotKey] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B5: Kualifikasi Level (Level Maksimal Guru < Level Program Kelas)
|
||||||
|
if ($guru->levelMaksimal < $kelas->level) {
|
||||||
|
$breakdown['B5']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grouping per kelas untuk B6 dan B7
|
||||||
|
$kelasGenesMap[$gene->kelasId][] = $gene;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluasi B6 & B7 (Khusus kelas 2x per minggu)
|
||||||
|
foreach ($kelasGenesMap as $kelasId => $genes) {
|
||||||
|
if (count($genes) == 2) {
|
||||||
|
$g1 = $genes[0];
|
||||||
|
$g2 = $genes[1];
|
||||||
|
|
||||||
|
// B6: Sebaran Hari (Hari tidak boleh sama, dan harus berjarak minimal 2 hari)
|
||||||
|
$selisihHari = abs($g1->hariId - $g2->hariId);
|
||||||
|
if ($g1->hariId == $g2->hariId || $selisihHari < 2) {
|
||||||
|
$breakdown['B6']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B7: Konsistensi Guru (Guru pada kedua pertemuan harus sama)
|
||||||
|
if ($g1->guruId != $g2->guruId) {
|
||||||
|
$breakdown['B7']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalPenalty = array_sum($breakdown);
|
||||||
|
$chromosome->setEvaluationResult($totalPenalty, $breakdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling;
|
||||||
|
|
||||||
|
class GaPresentationSimulationService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Menghasilkan data simulasi GA lengkap dengan dataset sampel 10 kelas
|
||||||
|
* Gen 0 Memiliki Penalti (Best Fitness 0.5000) -> Berevolusi lewat Selection, Crossover, Mutasi/Repair -> Gen 1 Mencapai Fitness 1.0000 (Optimal Goal Achieved & Engine Stops!)
|
||||||
|
*/
|
||||||
|
public function generateSimulationData(): array
|
||||||
|
{
|
||||||
|
// 1. Data Master Sampel 10 Kelas
|
||||||
|
$kelas = [
|
||||||
|
[
|
||||||
|
'id' => 1,
|
||||||
|
'nama' => 'KD26 - Basic 1 (Robotics)',
|
||||||
|
'tipe' => 'Reguler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 1)]',
|
||||||
|
'ruang_info' => '[Diacak oleh GA (R1/R2/R3)]',
|
||||||
|
'status_jadwal' => '[Diacak oleh GA (H1-H5, S1-S4)]',
|
||||||
|
'level' => 1, 'freq' => '1x/minggu'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 2,
|
||||||
|
'nama' => 'PO1 - Scratch Beginner',
|
||||||
|
'tipe' => 'Reguler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 1)]',
|
||||||
|
'ruang_info' => '[Diacak oleh GA (R1/R2/R3)]',
|
||||||
|
'status_jadwal' => '[Diacak oleh GA (H1-H5, S1-S4)]',
|
||||||
|
'level' => 1, 'freq' => '1x/minggu'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 3,
|
||||||
|
'nama' => 'B26 (R1) - Basic 1 (Coding)',
|
||||||
|
'tipe' => 'Reguler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 2)]',
|
||||||
|
'ruang_info' => '[Diacak oleh GA (R1/R2/R3)]',
|
||||||
|
'status_jadwal' => '[Diacak oleh GA (H1-H5, S1-S4)]',
|
||||||
|
'level' => 2, 'freq' => '2x/minggu (Pertemuan 1)'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 4,
|
||||||
|
'nama' => 'B26 (R2) - Basic 1 (Coding)',
|
||||||
|
'tipe' => 'Reguler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 2)]',
|
||||||
|
'ruang_info' => '[Diacak oleh GA (R1/R2/R3)]',
|
||||||
|
'status_jadwal' => '[Diacak oleh GA (H1-H5, S1-S4)]',
|
||||||
|
'level' => 2, 'freq' => '2x/minggu (Pertemuan 2)'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 5,
|
||||||
|
'nama' => 'Ekskul SD Bintang Harapan',
|
||||||
|
'tipe' => 'Ekstrakurikuler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 1)]',
|
||||||
|
'ruang_info' => 'SD Bintang Harapan (R4 - FIX)',
|
||||||
|
'status_jadwal' => 'FIX: Jumat Sesi 1 (08:00-10:00)',
|
||||||
|
'level' => 1, 'freq' => '1x/minggu'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 6,
|
||||||
|
'nama' => 'A28 - Beginner 1 (Python)',
|
||||||
|
'tipe' => 'Reguler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 3)]',
|
||||||
|
'ruang_info' => '[Diacak oleh GA (R1/R2/R3)]',
|
||||||
|
'status_jadwal' => '[Diacak oleh GA (H1-H5, S1-S4)]',
|
||||||
|
'level' => 3, 'freq' => '1x/minggu'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 7,
|
||||||
|
'nama' => 'Ekskul TK Namira Probolinggo',
|
||||||
|
'tipe' => 'Ekstrakurikuler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 1)]',
|
||||||
|
'ruang_info' => 'TK Namira (R5 - FIX)',
|
||||||
|
'status_jadwal' => 'FIX: Selasa Sesi 1 (08:00-10:00)',
|
||||||
|
'level' => 1, 'freq' => '1x/minggu'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 8,
|
||||||
|
'nama' => 'E26 (R1) - Beginner 4 (Arduino)',
|
||||||
|
'tipe' => 'Reguler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 4)]',
|
||||||
|
'ruang_info' => '[Diacak oleh GA (R1/R2/R3)]',
|
||||||
|
'status_jadwal' => '[Diacak oleh GA (H1-H5, S1-S4)]',
|
||||||
|
'level' => 4, 'freq' => '2x/minggu (Pertemuan 1)'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 9,
|
||||||
|
'nama' => 'E26 (R2) - Beginner 4 (Arduino)',
|
||||||
|
'tipe' => 'Reguler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 4)]',
|
||||||
|
'ruang_info' => '[Diacak oleh GA (R1/R2/R3)]',
|
||||||
|
'status_jadwal' => '[Diacak oleh GA (H1-H5, S1-S4)]',
|
||||||
|
'level' => 4, 'freq' => '2x/minggu (Pertemuan 2)'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 10,
|
||||||
|
'nama' => 'P1 - Beginner 2 (Robotics)',
|
||||||
|
'tipe' => 'Reguler',
|
||||||
|
'guru_status' => '[Diacak oleh GA (Lvl >= 2)]',
|
||||||
|
'ruang_info' => '[Diacak oleh GA (R1/R2/R3)]',
|
||||||
|
'status_jadwal' => '[Diacak oleh GA (H1-H5, S1-S4)]',
|
||||||
|
'level' => 2, 'freq' => '1x/minggu'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$guruList = [
|
||||||
|
['id' => 1, 'nama' => 'Ahmad Dandy (G1)', 'level_max' => 3, 'ketersediaan' => 'Senin-Selasa (Sesi 1-2)'],
|
||||||
|
['id' => 2, 'nama' => 'Agus Dwi Priyono (G2)', 'level_max' => 3, 'ketersediaan' => 'Senin-Kamis (Sesi 1-3)'],
|
||||||
|
['id' => 3, 'nama' => 'Putri Lailatul (G3)', 'level_max' => 4, 'ketersediaan' => 'Selasa-Jumat (Sesi 1-4)'],
|
||||||
|
['id' => 4, 'nama' => 'Abdul Halim Al-Asrori (G4)', 'level_max' => 5, 'ketersediaan' => 'Rabu-Jumat (Sesi 1-2)'],
|
||||||
|
['id' => 5, 'nama' => 'Vicko Effendy (G5)', 'level_max' => 4, 'ketersediaan' => 'Senin-Jumat (Sesi 1-4)'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$ruanganList = [
|
||||||
|
['id' => 1, 'nama' => 'Lab Juara (R1)', 'tipe' => 'Reguler LKP'],
|
||||||
|
['id' => 2, 'nama' => 'Lab Cemerlang (R2)', 'tipe' => 'Reguler LKP'],
|
||||||
|
['id' => 3, 'nama' => 'Lab Gembita (R3)', 'tipe' => 'Reguler LKP'],
|
||||||
|
['id' => 4, 'nama' => 'SD Bintang Harapan (R4)', 'tipe' => 'Ekstrakurikuler (Fix Sekolah)'],
|
||||||
|
['id' => 5, 'nama' => 'TK Namira (R5)', 'tipe' => 'Ekstrakurikuler (Fix Sekolah)'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$sesiList = [
|
||||||
|
['id' => 1, 'nama' => 'Sesi 1', 'jam' => '08:00 - 10:00'],
|
||||||
|
['id' => 2, 'nama' => 'Sesi 2', 'jam' => '10:00 - 12:00'],
|
||||||
|
['id' => 3, 'nama' => 'Sesi 3', 'jam' => '13:00 - 15:00'],
|
||||||
|
['id' => 4, 'nama' => 'Sesi 4', 'jam' => '15:00 - 17:00'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$hariList = [
|
||||||
|
1 => 'Senin (H1)',
|
||||||
|
2 => 'Selasa (H2)',
|
||||||
|
3 => 'Rabu (H3)',
|
||||||
|
4 => 'Kamis (H4)',
|
||||||
|
5 => 'Jumat (H5)'
|
||||||
|
];
|
||||||
|
|
||||||
|
$availabilityMatrix = [
|
||||||
|
1 => ['1_1'=>1, '1_2'=>1, '2_1'=>1, '2_2'=>1, '3_1'=>0, '3_2'=>0, '4_1'=>0, '4_2'=>0, '5_1'=>0, '5_2'=>0],
|
||||||
|
2 => ['1_1'=>1, '1_2'=>1, '1_3'=>1, '2_1'=>1, '2_2'=>1, '2_3'=>1, '3_1'=>1, '3_2'=>1, '3_3'=>1, '4_1'=>1, '4_2'=>1, '4_3'=>1],
|
||||||
|
3 => ['2_1'=>1, '2_2'=>1, '2_3'=>1, '2_4'=>1, '3_1'=>1, '3_2'=>1, '3_3'=>1, '3_4'=>1, '4_1'=>1, '4_2'=>1, '4_3'=>1, '4_4'=>1, '5_1'=>1, '5_2'=>1, '5_3'=>1, '5_4'=>1],
|
||||||
|
4 => ['3_1'=>1, '3_2'=>1, '4_1'=>1, '4_2'=>1, '5_1'=>1, '5_2'=>1],
|
||||||
|
5 => ['1_1'=>1, '1_2'=>1, '1_3'=>1, '1_4'=>1, '2_1'=>1, '2_2'=>1, '2_3'=>1, '2_4'=>1, '3_1'=>1, '3_2'=>1, '3_3'=>1, '3_4'=>1, '4_1'=>1, '4_2'=>1, '4_3'=>1, '4_4'=>1, '5_1'=>1, '5_2'=>1, '5_3'=>1, '5_4'=>1],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 2. Formasi 6 Individu Populasi Awal (Gen 0)
|
||||||
|
// Gen 0 Belum Mengirimkan Fitness 1.0 (Best Fitness = 0.5000) agar alur Generasi GA berjalan rasional!
|
||||||
|
$populasiAwal = [
|
||||||
|
[
|
||||||
|
'index' => 1,
|
||||||
|
'genes' => [
|
||||||
|
['kelas_id' => 1, 'guru_id' => 1, 'ruang_id' => 1, 'sesi_id' => 1, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 2, 'guru_id' => 2, 'ruang_id' => 1, 'sesi_id' => 1, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 3, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 2, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 4, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 2, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 5, 'guru_id' => 5, 'ruang_id' => 4, 'sesi_id' => 1, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 6, 'guru_id' => 4, 'ruang_id' => 3, 'sesi_id' => 1, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 7, 'guru_id' => 1, 'ruang_id' => 5, 'sesi_id' => 1, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 8, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 1, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 9, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 1, 'hari_id' => 3],
|
||||||
|
['kelas_id' => 10, 'guru_id' => 2, 'ruang_id' => 3, 'sesi_id' => 2, 'hari_id' => 3],
|
||||||
|
],
|
||||||
|
'b1' => 2, 'b2' => 1, 'b3' => 2, 'b4' => 0, 'b5' => 0, 'b6' => 1, 'b7' => 0, 'total_penalti' => 6, 'fitness' => 0.1429
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'index' => 2,
|
||||||
|
'genes' => [
|
||||||
|
['kelas_id' => 1, 'guru_id' => 1, 'ruang_id' => 1, 'sesi_id' => 1, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 2, 'guru_id' => 2, 'ruang_id' => 1, 'sesi_id' => 2, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 3, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 1, 'hari_id' => 1], // Melanggar B3 (Putri Lailatul tidak tersedia Senin)
|
||||||
|
['kelas_id' => 4, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 1, 'hari_id' => 4],
|
||||||
|
['kelas_id' => 5, 'guru_id' => 5, 'ruang_id' => 4, 'sesi_id' => 1, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 6, 'guru_id' => 4, 'ruang_id' => 3, 'sesi_id' => 1, 'hari_id' => 3],
|
||||||
|
['kelas_id' => 7, 'guru_id' => 1, 'ruang_id' => 5, 'sesi_id' => 1, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 8, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 3, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 9, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 3, 'hari_id' => 4],
|
||||||
|
['kelas_id' => 10, 'guru_id' => 2, 'ruang_id' => 3, 'sesi_id' => 2, 'hari_id' => 3],
|
||||||
|
],
|
||||||
|
'b1' => 0, 'b2' => 0, 'b3' => 1, 'b4' => 0, 'b5' => 0, 'b6' => 0, 'b7' => 0, 'total_penalti' => 1, 'fitness' => 0.5000
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'index' => 3,
|
||||||
|
'genes' => [
|
||||||
|
['kelas_id' => 1, 'guru_id' => 1, 'ruang_id' => 1, 'sesi_id' => 2, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 2, 'guru_id' => 2, 'ruang_id' => 1, 'sesi_id' => 2, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 3, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 2, 'hari_id' => 3],
|
||||||
|
['kelas_id' => 4, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 2, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 5, 'guru_id' => 5, 'ruang_id' => 4, 'sesi_id' => 1, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 6, 'guru_id' => 4, 'ruang_id' => 3, 'sesi_id' => 2, 'hari_id' => 4],
|
||||||
|
['kelas_id' => 7, 'guru_id' => 1, 'ruang_id' => 5, 'sesi_id' => 1, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 8, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 4, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 9, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 4, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 10, 'guru_id' => 2, 'ruang_id' => 3, 'sesi_id' => 1, 'hari_id' => 4],
|
||||||
|
],
|
||||||
|
'b1' => 1, 'b2' => 1, 'b3' => 0, 'b4' => 0, 'b5' => 0, 'b6' => 0, 'b7' => 0, 'total_penalti' => 2, 'fitness' => 0.3333
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'index' => 4,
|
||||||
|
'genes' => [
|
||||||
|
['kelas_id' => 1, 'guru_id' => 1, 'ruang_id' => 1, 'sesi_id' => 1, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 2, 'guru_id' => 2, 'ruang_id' => 1, 'sesi_id' => 3, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 3, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 1, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 4, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 1, 'hari_id' => 3],
|
||||||
|
['kelas_id' => 5, 'guru_id' => 5, 'ruang_id' => 4, 'sesi_id' => 1, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 6, 'guru_id' => 4, 'ruang_id' => 3, 'sesi_id' => 1, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 7, 'guru_id' => 1, 'ruang_id' => 5, 'sesi_id' => 1, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 8, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 2, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 9, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 2, 'hari_id' => 4],
|
||||||
|
['kelas_id' => 10, 'guru_id' => 2, 'ruang_id' => 3, 'sesi_id' => 3, 'hari_id' => 1],
|
||||||
|
],
|
||||||
|
'b1' => 0, 'b2' => 0, 'b3' => 1, 'b4' => 0, 'b5' => 0, 'b6' => 0, 'b7' => 0, 'total_penalti' => 1, 'fitness' => 0.5000
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'index' => 5,
|
||||||
|
'genes' => [
|
||||||
|
['kelas_id' => 1, 'guru_id' => 1, 'ruang_id' => 1, 'sesi_id' => 2, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 2, 'guru_id' => 2, 'ruang_id' => 1, 'sesi_id' => 1, 'hari_id' => 3],
|
||||||
|
['kelas_id' => 3, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 3, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 4, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 3, 'hari_id' => 3],
|
||||||
|
['kelas_id' => 5, 'guru_id' => 5, 'ruang_id' => 4, 'sesi_id' => 1, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 6, 'guru_id' => 4, 'ruang_id' => 3, 'sesi_id' => 2, 'hari_id' => 3],
|
||||||
|
['kelas_id' => 7, 'guru_id' => 1, 'ruang_id' => 5, 'sesi_id' => 1, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 8, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 1, 'hari_id' => 4],
|
||||||
|
['kelas_id' => 9, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 1, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 10, 'guru_id' => 2, 'ruang_id' => 3, 'sesi_id' => 2, 'hari_id' => 2],
|
||||||
|
],
|
||||||
|
'b1' => 0, 'b2' => 0, 'b3' => 0, 'b4' => 0, 'b5' => 0, 'b6' => 2, 'b7' => 0, 'total_penalti' => 2, 'fitness' => 0.3333
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'index' => 6,
|
||||||
|
'genes' => [
|
||||||
|
['kelas_id' => 1, 'guru_id' => 1, 'ruang_id' => 1, 'sesi_id' => 1, 'hari_id' => 1],
|
||||||
|
['kelas_id' => 2, 'guru_id' => 2, 'ruang_id' => 1, 'sesi_id' => 2, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 3, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 4, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 4, 'guru_id' => 3, 'ruang_id' => 2, 'sesi_id' => 4, 'hari_id' => 4],
|
||||||
|
['kelas_id' => 5, 'guru_id' => 5, 'ruang_id' => 4, 'sesi_id' => 1, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 6, 'guru_id' => 4, 'ruang_id' => 3, 'sesi_id' => 1, 'hari_id' => 4],
|
||||||
|
['kelas_id' => 7, 'guru_id' => 1, 'ruang_id' => 5, 'sesi_id' => 1, 'hari_id' => 2],
|
||||||
|
['kelas_id' => 8, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 3, 'hari_id' => 3],
|
||||||
|
['kelas_id' => 9, 'guru_id' => 5, 'ruang_id' => 2, 'sesi_id' => 3, 'hari_id' => 5],
|
||||||
|
['kelas_id' => 10, 'guru_id' => 2, 'ruang_id' => 3, 'sesi_id' => 1, 'hari_id' => 1], // Melanggar B1 (Agus Dwi bentrok di Senin Sesi 1 dgn K01)
|
||||||
|
],
|
||||||
|
'b1' => 1, 'b2' => 0, 'b3' => 0, 'b4' => 0, 'b5' => 0, 'b6' => 0, 'b7' => 0, 'total_penalti' => 1, 'fitness' => 0.5000
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3. Simulasi Tournament Selection
|
||||||
|
$tournament1 = [
|
||||||
|
'candidates' => [$populasiAwal[0], $populasiAwal[3], $populasiAwal[2]],
|
||||||
|
'winner' => $populasiAwal[3],
|
||||||
|
'role' => 'Parent A (Induk 1)'
|
||||||
|
];
|
||||||
|
|
||||||
|
$tournament2 = [
|
||||||
|
'candidates' => [$populasiAwal[4], $populasiAwal[1], $populasiAwal[0]],
|
||||||
|
'winner' => $populasiAwal[1],
|
||||||
|
'role' => 'Parent B (Induk 2)'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 4. Simulasi Crossover
|
||||||
|
$parentA = $tournament1['winner'];
|
||||||
|
$parentB = $tournament2['winner'];
|
||||||
|
$crossoverPoint = 5;
|
||||||
|
|
||||||
|
$child1Genes = array_merge(
|
||||||
|
array_slice($parentA['genes'], 0, 5),
|
||||||
|
array_slice($parentB['genes'], 5, 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
$child2Genes = array_merge(
|
||||||
|
array_slice($parentB['genes'], 0, 5),
|
||||||
|
array_slice($parentA['genes'], 5, 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Simulasi Mutasi & Repair
|
||||||
|
// Child 1 punya 1 penalti B3 di Gen 3 (Selasa) -> Di-Smart Repair ke Hari 2 (Selasa Sesi 1) -> Menjadi Fitness 1.0000 (0 Penalti!)
|
||||||
|
$child1MutatedGenes = $child1Genes;
|
||||||
|
$child1MutatedGenes[2]['hari_id'] = 2;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'kelas' => $kelas,
|
||||||
|
'guruList' => $guruList,
|
||||||
|
'ruanganList' => $ruanganList,
|
||||||
|
'sesiList' => $sesiList,
|
||||||
|
'hariList' => $hariList,
|
||||||
|
'availabilityMatrix' => $availabilityMatrix,
|
||||||
|
'populasiAwal' => $populasiAwal,
|
||||||
|
'tournament1' => $tournament1,
|
||||||
|
'tournament2' => $tournament2,
|
||||||
|
'parentA' => $parentA,
|
||||||
|
'parentB' => $parentB,
|
||||||
|
'crossoverPoint' => $crossoverPoint,
|
||||||
|
'child1Genes' => $child1Genes,
|
||||||
|
'child2Genes' => $child2Genes,
|
||||||
|
'child1MutatedGenes' => $child1MutatedGenes,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling;
|
||||||
|
|
||||||
|
use App\Services\Scheduling\Data\SchedulingSlot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gene merepresentasikan 1 alokasi sesi kelas dalam Algoritma Genetika.
|
||||||
|
*
|
||||||
|
* Untuk kelas 1x/minggu: ada 1 Gene (indexPertemuan = 1)
|
||||||
|
* Untuk kelas 2x/minggu: ada 2 Gene (indexPertemuan = 1 dan indexPertemuan = 2)
|
||||||
|
*/
|
||||||
|
class Gene
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly int $kelasId,
|
||||||
|
public readonly int $indexPertemuan, // 1 atau 2
|
||||||
|
public int $guruId,
|
||||||
|
public int $hariId,
|
||||||
|
public int $sesiId,
|
||||||
|
public int $ruanganId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengembalikan SchedulingSlot (kombinasi Hari & Sesi) dari gen ini.
|
||||||
|
*/
|
||||||
|
public function getSlot(): SchedulingSlot
|
||||||
|
{
|
||||||
|
return new SchedulingSlot($this->hariId, $this->sesiId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kloning gen untuk keperluan individu baru
|
||||||
|
*/
|
||||||
|
public function clone(): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$this->kelasId,
|
||||||
|
$this->indexPertemuan,
|
||||||
|
$this->guruId,
|
||||||
|
$this->hariId,
|
||||||
|
$this->sesiId,
|
||||||
|
$this->ruanganId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,463 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling;
|
||||||
|
|
||||||
|
use App\Services\Scheduling\Data\SchedulingDataset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeneticAlgorithmEngine bertanggung jawab mengorganisir alur eksekusi populasi GA:
|
||||||
|
* Inisialisasi -> Evaluasi -> Seleksi Tournament -> Crossover -> Mutasi -> Elitisme.
|
||||||
|
*/
|
||||||
|
class GeneticAlgorithmEngine
|
||||||
|
{
|
||||||
|
private FitnessEvaluator $fitnessEvaluator;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private int $populationSize = 50,
|
||||||
|
private float $crossoverRate = 0.8,
|
||||||
|
private float $mutationRate = 0.1,
|
||||||
|
private int $maxGenerations = 500,
|
||||||
|
private int $tournamentSize = 3,
|
||||||
|
private int $elitismCount = 2,
|
||||||
|
private ?int $seed = null
|
||||||
|
) {
|
||||||
|
$this->fitnessEvaluator = new FitnessEvaluator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jalankan proses GA untuk mencari solusi jadwal terbaik.
|
||||||
|
*/
|
||||||
|
public function run(SchedulingDataset $dataset): array
|
||||||
|
{
|
||||||
|
if ($this->seed !== null) {
|
||||||
|
mt_srand($this->seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
$startTime = microtime(true);
|
||||||
|
|
||||||
|
// 1. Inisialisasi Populasi Awal
|
||||||
|
$population = $this->initializePopulation($dataset);
|
||||||
|
|
||||||
|
// Evaluasi Populasi Awal
|
||||||
|
foreach ($population as $chromosome) {
|
||||||
|
$this->fitnessEvaluator->evaluate($chromosome, $dataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
$bestChromosome = $this->getBestChromosome($population);
|
||||||
|
$generationCount = 0;
|
||||||
|
$generationLogs = [];
|
||||||
|
|
||||||
|
// Loop Generasi
|
||||||
|
while ($generationCount < $this->maxGenerations && $bestChromosome->fitness < 1.0) {
|
||||||
|
$currentGenNumber = $generationCount; // Generasi saat ini (0, 1, 2, dst.)
|
||||||
|
$generationCount++;
|
||||||
|
$newPopulation = [];
|
||||||
|
$sampleSelectionLogs = [];
|
||||||
|
$sampleCrossoverLogs = [];
|
||||||
|
$sampleMutationLogs = [];
|
||||||
|
|
||||||
|
// Elitisme: Ambil n individu terbaik tanpa perubahan
|
||||||
|
usort($population, fn(Chromosome $a, Chromosome $b) => $b->fitness <=> $a->fitness);
|
||||||
|
for ($i = 0; $i < $this->elitismCount; $i++) {
|
||||||
|
$newPopulation[] = $population[$i]->clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduksi hingga populasi baru penuh
|
||||||
|
while (count($newPopulation) < $this->populationSize) {
|
||||||
|
// Seleksi Tournament (k=3)
|
||||||
|
[$parent1, $tLog1] = $this->tournamentSelectWithLog($population);
|
||||||
|
[$parent2, $tLog2] = $this->tournamentSelectWithLog($population);
|
||||||
|
|
||||||
|
if (count($sampleSelectionLogs) < 2) {
|
||||||
|
$sampleSelectionLogs[] = $tLog1;
|
||||||
|
$sampleSelectionLogs[] = $tLog2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crossover
|
||||||
|
$rCross = round(mt_rand() / mt_getrandmax(), 4);
|
||||||
|
if ($rCross <= $this->crossoverRate) {
|
||||||
|
[$child1, $child2, $swappedKelas] = $this->crossoverWithLog($parent1, $parent2);
|
||||||
|
if (count($sampleCrossoverLogs) < 2) {
|
||||||
|
$kelasStr = !empty($swappedKelas) ? implode(', ', array_map(fn($k) => "K{$k}", array_slice($swappedKelas, 0, 5))) : 'K01';
|
||||||
|
$sampleCrossoverLogs[] = "Uniform Crossover (r={$rCross} <= Pc {$this->crossoverRate}): Menukarkan alokasi gen kelas [{$kelasStr}...] antara Induk 1 & Induk 2 -> Menghasilkan Anak 1 & 2.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$child1 = $parent1->clone();
|
||||||
|
$child2 = $parent2->clone();
|
||||||
|
if (count($sampleCrossoverLogs) < 2) {
|
||||||
|
$sampleCrossoverLogs[] = "Crossover Dilewati (r={$rCross} > Pc {$this->crossoverRate}): Anak mengklon penuh gen dari Induk.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutasi
|
||||||
|
$mutLog1 = $this->mutateWithLog($child1, $dataset);
|
||||||
|
$mutLog2 = $this->mutateWithLog($child2, $dataset);
|
||||||
|
|
||||||
|
if (!empty($mutLog1) && count($sampleMutationLogs) < 2) {
|
||||||
|
$sampleMutationLogs[] = "Mutasi Anak 1: " . $mutLog1;
|
||||||
|
}
|
||||||
|
if (!empty($mutLog2) && count($sampleMutationLogs) < 2) {
|
||||||
|
$sampleMutationLogs[] = "Mutasi Anak 2: " . $mutLog2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluasi Fitness Anak
|
||||||
|
$this->fitnessEvaluator->evaluate($child1, $dataset);
|
||||||
|
$this->fitnessEvaluator->evaluate($child2, $dataset);
|
||||||
|
|
||||||
|
$newPopulation[] = $child1;
|
||||||
|
if (count($newPopulation) < $this->populationSize) {
|
||||||
|
$newPopulation[] = $child2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$operatorSamples = [
|
||||||
|
'selection' => $sampleSelectionLogs,
|
||||||
|
'crossover' => $sampleCrossoverLogs,
|
||||||
|
'mutation' => empty($sampleMutationLogs) ? ["Mutasi dilewati (tidak ada gen acak yang memenuhi rate Pm = {$this->mutationRate})."] : $sampleMutationLogs,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Simpan log generasi saat ini (termasuk Generasi 0) beserta sampel operator yang memprosesnya
|
||||||
|
if ($currentGenNumber <= 10 || $currentGenNumber % 5 === 0) {
|
||||||
|
$generationLogs[] = $this->captureGenerationLog($currentGenNumber, $bestChromosome, $population, $dataset, $operatorSamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
$population = $newPopulation;
|
||||||
|
$bestChromosome = $this->getBestChromosome($population);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan log untuk generasi terakhir (Final Solusi)
|
||||||
|
$finalOperatorSamples = [
|
||||||
|
'isFinal' => true,
|
||||||
|
'reason' => $bestChromosome->fitness == 1.0 ? "Solusi Optimal (Fitness = 1.0) telah ditemukan. Kriteria Penghentian Terpenuhi." : "Batas Maksimum Generasi ({$this->maxGenerations}) telah dicapai."
|
||||||
|
];
|
||||||
|
$generationLogs[] = $this->captureGenerationLog($generationCount, $bestChromosome, $population, $dataset, $finalOperatorSamples);
|
||||||
|
|
||||||
|
$executionTime = round(microtime(true) - $startTime, 3);
|
||||||
|
$memoryUsage = round(memory_get_usage() / 1024, 2); // KiloBytes
|
||||||
|
$lastLog = !empty($generationLogs) ? end($generationLogs) : null;
|
||||||
|
$avgFitness = $lastLog['avgFitness'] ?? 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'bestChromosome' => $bestChromosome,
|
||||||
|
'generations' => $generationCount,
|
||||||
|
'executionTime' => $executionTime,
|
||||||
|
'memoryUsage' => $memoryUsage,
|
||||||
|
'avgFitness' => $avgFitness,
|
||||||
|
'isOptimal' => $bestChromosome->fitness == 1.0,
|
||||||
|
'generationLogs' => $generationLogs,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membuat snapshot log generasi untuk analisis visual persis seperti gambar acuan skripsi
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Membuat snapshot log generasi untuk analisis visual persis seperti gambar acuan skripsi
|
||||||
|
*/
|
||||||
|
private function captureGenerationLog(int $gen, Chromosome $best, array $population, SchedulingDataset $dataset, array $operatorSamples = []): array
|
||||||
|
{
|
||||||
|
$namaHariMap = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
|
||||||
|
$individus = [];
|
||||||
|
$fitnesses = [];
|
||||||
|
|
||||||
|
foreach ($population as $idx => $ind) {
|
||||||
|
$fitnesses[] = $ind->fitness;
|
||||||
|
|
||||||
|
// Analisis per gen individu untuk deteksi kode pelanggaran B1..B7
|
||||||
|
$guruSlotMap = [];
|
||||||
|
$ruanganSlotMap = [];
|
||||||
|
$kelasSlotMap = [];
|
||||||
|
$kelasGenesMap = [];
|
||||||
|
$geneViolations = [];
|
||||||
|
|
||||||
|
foreach ($ind->genes as $gIdx => $g) {
|
||||||
|
$violations = [];
|
||||||
|
$kelas = $dataset->getKelas($g->kelasId);
|
||||||
|
$guru = $dataset->getGuru($g->guruId);
|
||||||
|
|
||||||
|
// B1: Bentrok Guru
|
||||||
|
$guruSlotKey = "{$g->guruId}_{$g->hariId}_{$g->sesiId}";
|
||||||
|
if (isset($guruSlotMap[$guruSlotKey])) {
|
||||||
|
$violations[] = 'B1';
|
||||||
|
} else {
|
||||||
|
$guruSlotMap[$guruSlotKey] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B2: Bentrok Ruangan (Reguler)
|
||||||
|
if (!$kelas->isEkstrakurikuler) {
|
||||||
|
$ruanganSlotKey = "{$g->ruanganId}_{$g->hariId}_{$g->sesiId}";
|
||||||
|
if (isset($ruanganSlotMap[$ruanganSlotKey])) {
|
||||||
|
$violations[] = 'B2';
|
||||||
|
} else {
|
||||||
|
$ruanganSlotMap[$ruanganSlotKey] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// B3: Ketersediaan Guru
|
||||||
|
if (!$guru->tersediaAt(new \App\Services\Scheduling\Data\SchedulingSlot($g->hariId, $g->sesiId))) {
|
||||||
|
$violations[] = 'B3';
|
||||||
|
}
|
||||||
|
|
||||||
|
// B4: Bentrok Kelas
|
||||||
|
$kelasSlotKey = "{$g->kelasId}_{$g->hariId}_{$g->sesiId}";
|
||||||
|
if (isset($kelasSlotMap[$kelasSlotKey])) {
|
||||||
|
$violations[] = 'B4';
|
||||||
|
} else {
|
||||||
|
$kelasSlotMap[$kelasSlotKey] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B5: Kualifikasi Level
|
||||||
|
if ($guru->levelMaksimal < $kelas->level) {
|
||||||
|
$violations[] = 'B5';
|
||||||
|
}
|
||||||
|
|
||||||
|
$kelasGenesMap[$g->kelasId][] = ['gene' => $g, 'index' => $gIdx];
|
||||||
|
$geneViolations[$gIdx] = $violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B6 & B7
|
||||||
|
foreach ($kelasGenesMap as $kelasId => $gItems) {
|
||||||
|
if (count($gItems) == 2) {
|
||||||
|
$g1 = $gItems[0]['gene']; $idx1 = $gItems[0]['index'];
|
||||||
|
$g2 = $gItems[1]['gene']; $idx2 = $gItems[1]['index'];
|
||||||
|
|
||||||
|
// B6: Sebaran Hari
|
||||||
|
$selisihHari = abs($g1->hariId - $g2->hariId);
|
||||||
|
if ($g1->hariId == $g2->hariId || $selisihHari < 2) {
|
||||||
|
$geneViolations[$idx1][] = 'B6';
|
||||||
|
$geneViolations[$idx2][] = 'B6';
|
||||||
|
}
|
||||||
|
|
||||||
|
// B7: Konsistensi Guru
|
||||||
|
if ($g1->guruId != $g2->guruId) {
|
||||||
|
$geneViolations[$idx1][] = 'B7';
|
||||||
|
$geneViolations[$idx2][] = 'B7';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format Array Gen dengan Penanda Pelanggaran Terperinci
|
||||||
|
$genesFormatted = [];
|
||||||
|
foreach ($ind->genes as $gIdx => $g) {
|
||||||
|
$hariStr = $namaHariMap[$g->hariId] ?? 'Hari_' . $g->hariId;
|
||||||
|
$vCode = !empty($geneViolations[$gIdx]) ? $geneViolations[$gIdx][0] : null; // Ambil kode penalti pertama
|
||||||
|
$genesFormatted[] = [
|
||||||
|
'text' => "[K:{$g->kelasId}, G:{$g->guruId}, R:{$g->ruanganId}, S:{$g->sesiId}, {$hariStr}_{$g->indexPertemuan}]",
|
||||||
|
'violation' => $vCode
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$individus[] = [
|
||||||
|
'index' => $idx,
|
||||||
|
'fitness' => round($ind->fitness, 4),
|
||||||
|
'penalty' => $ind->totalPenalty,
|
||||||
|
'genesFormatted' => $genesFormatted
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bestGenString = [];
|
||||||
|
foreach ($best->genes as $g) {
|
||||||
|
$hariStr = $namaHariMap[$g->hariId] ?? 'Hari_' . $g->hariId;
|
||||||
|
$bestGenString[] = "[K:{$g->kelasId}, G:{$g->guruId}, R:{$g->ruanganId}, S:{$g->sesiId}, {$hariStr}_{$g->indexPertemuan}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
$minFitness = round(min($fitnesses), 4);
|
||||||
|
$maxFitness = round(max($fitnesses), 4);
|
||||||
|
$avgFitness = round(array_sum($fitnesses) / count($fitnesses), 4);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'generation' => $gen,
|
||||||
|
'fitness' => round($best->fitness, 4),
|
||||||
|
'minFitness' => $minFitness,
|
||||||
|
'maxFitness' => $maxFitness,
|
||||||
|
'avgFitness' => $avgFitness,
|
||||||
|
'penalty' => $best->totalPenalty,
|
||||||
|
'breakdown' => $best->penaltyBreakdown,
|
||||||
|
'bestIndividuString' => implode(',', $bestGenString),
|
||||||
|
'individus' => $individus,
|
||||||
|
'operatorSamples' => $operatorSamples,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tournament Selection (k=3) dengan pencatatan log
|
||||||
|
*/
|
||||||
|
private function tournamentSelectWithLog(array $population): array
|
||||||
|
{
|
||||||
|
$tournament = [];
|
||||||
|
$details = [];
|
||||||
|
for ($i = 0; $i < $this->tournamentSize; $i++) {
|
||||||
|
$randIdx = array_rand($population);
|
||||||
|
$ind = $population[$randIdx];
|
||||||
|
$tournament[] = $ind;
|
||||||
|
$details[] = "Individu[{$randIdx}] (Fitness: " . round($ind->fitness, 4) . ")";
|
||||||
|
}
|
||||||
|
$winner = $this->getBestChromosome($tournament);
|
||||||
|
$logStr = "Adu Turnamen k=3: " . implode(" VS ", $details) . " -> Winner: Fitness " . round($winner->fitness, 4);
|
||||||
|
return [$winner, $logStr];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inisialisasi populasi acak
|
||||||
|
*/
|
||||||
|
private function initializePopulation(SchedulingDataset $dataset): array
|
||||||
|
{
|
||||||
|
$population = [];
|
||||||
|
for ($i = 0; $i < $this->populationSize; $i++) {
|
||||||
|
$genes = [];
|
||||||
|
foreach ($dataset->kelasList as $kelas) {
|
||||||
|
// Tentukan berapa gen untuk kelas ini
|
||||||
|
for ($pertemuan = 1; $pertemuan <= $kelas->frekuensiPerMinggu; $pertemuan++) {
|
||||||
|
if ($kelas->isEkstrakurikuler) {
|
||||||
|
// Kunci slot (H, S, R) sesuai fix, GA cari Guru acak
|
||||||
|
$hariId = $kelas->hariFix;
|
||||||
|
$sesiId = $kelas->sesiFix;
|
||||||
|
$ruanganId = $kelas->ruanganFixId;
|
||||||
|
} else {
|
||||||
|
// Reguler: Slot (H, S) & Ruangan diacak
|
||||||
|
$slot = $dataset->allSlots[array_rand($dataset->allSlots)];
|
||||||
|
$hariId = $slot->hari;
|
||||||
|
$sesiId = $slot->sesi;
|
||||||
|
$ruanganId = $dataset->ruanganRobotkuIds[array_rand($dataset->ruanganRobotkuIds)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pilih guru yang kualifikasi level-nya memenuhi (levelMaksimal >= levelKelas)
|
||||||
|
$guruIds = array_keys(array_filter(
|
||||||
|
$dataset->guruList,
|
||||||
|
fn($g) => $g->levelMaksimal >= $kelas->level
|
||||||
|
));
|
||||||
|
if (empty($guruIds)) {
|
||||||
|
$guruIds = array_keys($dataset->guruList);
|
||||||
|
}
|
||||||
|
$guruId = $guruIds[array_rand($guruIds)];
|
||||||
|
|
||||||
|
$genes[] = new Gene(
|
||||||
|
kelasId: $kelas->id,
|
||||||
|
indexPertemuan: $pertemuan,
|
||||||
|
guruId: $guruId,
|
||||||
|
hariId: $hariId,
|
||||||
|
sesiId: $sesiId,
|
||||||
|
ruanganId: $ruanganId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$population[] = new Chromosome($genes);
|
||||||
|
}
|
||||||
|
return $population;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tournament Selection (k=3)
|
||||||
|
*/
|
||||||
|
private function tournamentSelect(array $population): Chromosome
|
||||||
|
{
|
||||||
|
$tournament = [];
|
||||||
|
for ($i = 0; $i < $this->tournamentSize; $i++) {
|
||||||
|
$tournament[] = $population[array_rand($population)];
|
||||||
|
}
|
||||||
|
return $this->getBestChromosome($tournament);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crossover (Uniform Crossover per Kelas untuk menjaga konsistensi B7)
|
||||||
|
*/
|
||||||
|
private function crossoverWithLog(Chromosome $parent1, Chromosome $parent2): array
|
||||||
|
{
|
||||||
|
$child1Genes = [];
|
||||||
|
$child2Genes = [];
|
||||||
|
$swappedKelas = [];
|
||||||
|
|
||||||
|
// Grouping per kelasId
|
||||||
|
$genesByKelas1 = [];
|
||||||
|
$genesByKelas2 = [];
|
||||||
|
foreach ($parent1->genes as $g) { $genesByKelas1[$g->kelasId][] = $g; }
|
||||||
|
foreach ($parent2->genes as $g) { $genesByKelas2[$g->kelasId][] = $g; }
|
||||||
|
|
||||||
|
foreach ($genesByKelas1 as $kelasId => $gList1) {
|
||||||
|
$gList2 = $genesByKelas2[$kelasId];
|
||||||
|
if (mt_rand(0, 1) === 1) {
|
||||||
|
foreach ($gList1 as $g) { $child1Genes[] = $g->clone(); }
|
||||||
|
foreach ($gList2 as $g) { $child2Genes[] = $g->clone(); }
|
||||||
|
$swappedKelas[] = $kelasId;
|
||||||
|
} else {
|
||||||
|
foreach ($gList2 as $g) { $child1Genes[] = $g->clone(); }
|
||||||
|
foreach ($gList1 as $g) { $child2Genes[] = $g->clone(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [new Chromosome($child1Genes), new Chromosome($child2Genes), $swappedKelas];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resetting Mutation dengan Smart Repair B7 (Konsistensi Guru)
|
||||||
|
*/
|
||||||
|
private function mutateWithLog(Chromosome $chromosome, SchedulingDataset $dataset): string
|
||||||
|
{
|
||||||
|
$namaHariMap = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
|
||||||
|
$mutatedDetails = [];
|
||||||
|
|
||||||
|
// Mutasi acak gen
|
||||||
|
foreach ($chromosome->genes as $gene) {
|
||||||
|
$r = round(mt_rand() / mt_getrandmax(), 4);
|
||||||
|
if ($r <= $this->mutationRate) {
|
||||||
|
$kelas = $dataset->getKelas($gene->kelasId);
|
||||||
|
$oldGuru = $gene->guruId;
|
||||||
|
$oldHari = $namaHariMap[$gene->hariId] ?? 'Hari_'.$gene->hariId;
|
||||||
|
$oldSesi = $gene->sesiId;
|
||||||
|
|
||||||
|
// Ganti Guru dari yang qualified
|
||||||
|
$guruIds = array_keys(array_filter(
|
||||||
|
$dataset->guruList,
|
||||||
|
fn($g) => $g->levelMaksimal >= $kelas->level
|
||||||
|
));
|
||||||
|
if (!empty($guruIds)) {
|
||||||
|
$gene->guruId = $guruIds[array_rand($guruIds)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika kelas reguler, acak ulang (H, S, R)
|
||||||
|
if (!$kelas->isEkstrakurikuler) {
|
||||||
|
$slot = $dataset->allSlots[array_rand($dataset->allSlots)];
|
||||||
|
$gene->hariId = $slot->hari;
|
||||||
|
$gene->sesiId = $slot->sesi;
|
||||||
|
$gene->ruanganId = $dataset->ruanganRobotkuIds[array_rand($dataset->ruanganRobotkuIds)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$newHari = $namaHariMap[$gene->hariId] ?? 'Hari_'.$gene->hariId;
|
||||||
|
$mutatedDetails[] = "Gen [K:{$gene->kelasId}, G:{$oldGuru}] dimutasi (r={$r} <= Pm {$this->mutationRate}) -> G:{$gene->guruId}, Slot: {$oldHari}_S{$oldSesi} diubah ke {$newHari}_S{$gene->sesiId}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smart Fix B7 (Menyamakan guru pengajar untuk kelas 2x/minggu)
|
||||||
|
$kelasMap = [];
|
||||||
|
foreach ($chromosome->genes as $g) {
|
||||||
|
$kelasMap[$g->kelasId][] = $g;
|
||||||
|
}
|
||||||
|
foreach ($kelasMap as $genes) {
|
||||||
|
if (count($genes) == 2) {
|
||||||
|
if ($genes[0]->guruId != $genes[1]->guruId) {
|
||||||
|
$genes[1]->guruId = $genes[0]->guruId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($mutatedDetails)) {
|
||||||
|
return "Mutasi dilewati pada individu ini (tidak ada gen acak dengan r <= Pm {$this->mutationRate}). Smart repair B7 aktif.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' | ', array_slice($mutatedDetails, 0, 2)) . " + Smart Repair B7";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil kromosom terbaik dari populasi
|
||||||
|
*/
|
||||||
|
private function getBestChromosome(array $population): Chromosome
|
||||||
|
{
|
||||||
|
$best = $population[0];
|
||||||
|
foreach ($population as $ind) {
|
||||||
|
if ($ind->fitness > $best->fitness) {
|
||||||
|
$best = $ind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Scheduling;
|
||||||
|
|
||||||
|
use App\Models\JadwalKelas;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScheduleGeneratorService adalah kelas koodinator utama yang menghubungkan:
|
||||||
|
* Controller <-> DatasetLoader <-> GeneticAlgorithmEngine <-> Database Persistence
|
||||||
|
*/
|
||||||
|
class ScheduleGeneratorService
|
||||||
|
{
|
||||||
|
private DatasetLoader $datasetLoader;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->datasetLoader = new DatasetLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jalankan proses penjadwalan otomatis dan simpan hasilnya ke DB.
|
||||||
|
*/
|
||||||
|
public function generate(
|
||||||
|
?string $semester = null,
|
||||||
|
?string $tahunAjaran = null,
|
||||||
|
?int $populationSize = 30,
|
||||||
|
?float $crossoverRate = 0.5,
|
||||||
|
?float $mutationRate = 0.1,
|
||||||
|
?int $maxGenerations = 100,
|
||||||
|
?int $seed = null
|
||||||
|
): array {
|
||||||
|
// 1. Load Dataset
|
||||||
|
$dataset = $this->datasetLoader->load($semester, $tahunAjaran);
|
||||||
|
|
||||||
|
// 2. Inisialisasi Engine GA
|
||||||
|
$engine = new GeneticAlgorithmEngine(
|
||||||
|
populationSize: $populationSize ?? 30,
|
||||||
|
crossoverRate: $crossoverRate ?? 0.5,
|
||||||
|
mutationRate: $mutationRate ?? 0.1,
|
||||||
|
maxGenerations: $maxGenerations ?? 100,
|
||||||
|
seed: $seed
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Eksekusi Engine
|
||||||
|
$result = $engine->run($dataset);
|
||||||
|
/** @var Chromosome $bestChromosome */
|
||||||
|
$bestChromosome = $result['bestChromosome'];
|
||||||
|
|
||||||
|
// 4. Simpan/Replace Hasil ke Database (`jadwal_kelas`)
|
||||||
|
$this->saveToDatabase($bestChromosome, $semester, $tahunAjaran);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'bestChromosome' => $bestChromosome,
|
||||||
|
'generations' => $result['generations'],
|
||||||
|
'executionTime' => $result['executionTime'],
|
||||||
|
'memoryUsage' => $result['memoryUsage'],
|
||||||
|
'avgFitness' => $result['avgFitness'] ?? 0,
|
||||||
|
'isOptimal' => $result['isOptimal'],
|
||||||
|
'totalGenes' => count($bestChromosome->genes),
|
||||||
|
'generationLogs' => $result['generationLogs'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persistence hasil terbaik ke tabel `jadwal_kelas`
|
||||||
|
*/
|
||||||
|
private function saveToDatabase(Chromosome $chromosome, ?string $semester, ?string $tahunAjaran): void
|
||||||
|
{
|
||||||
|
// Kosongkan jadwal sebelumnya untuk periode ini
|
||||||
|
JadwalKelas::truncate();
|
||||||
|
|
||||||
|
foreach ($chromosome->genes as $gene) {
|
||||||
|
JadwalKelas::create([
|
||||||
|
'kelas_id' => $gene->kelasId,
|
||||||
|
'guru_id' => $gene->guruId,
|
||||||
|
'hari' => $gene->hariId,
|
||||||
|
'hari_id' => $gene->hariId,
|
||||||
|
'sesi_id' => $gene->sesiId,
|
||||||
|
'ruangan_id' => $gene->ruanganId,
|
||||||
|
'pertemuan_ke' => $gene->indexPertemuan,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
$status = (require_once __DIR__.'/bootstrap/app.php')
|
||||||
|
->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
//
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
|
//
|
||||||
|
})->create();
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/laravel",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": ["laravel", "framework"],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
|
"intervention/image-laravel": "^1.5",
|
||||||
|
"laravel/framework": "^11.31",
|
||||||
|
"laravel/tinker": "^2.9",
|
||||||
|
"maatwebsite/excel": "^3.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/pail": "^1.1",
|
||||||
|
"laravel/pint": "^1.13",
|
||||||
|
"laravel/sail": "^1.26",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.1",
|
||||||
|
"phpunit/phpunit": "^11.0.1"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', env('APP_PREVIOUS_KEYS', ''))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent guests provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a guests provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a guests provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple guests tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the guests provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a guests must wait before
|
||||||
|
| generating more password reset tokens. This prevents the guests from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the amount of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "octane", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => 'prefer',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Set some default values. It is possible to add all defines that can be set
|
||||||
|
| in dompdf_config.inc.php. You can also override the entire config file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||||
|
|
||||||
|
// Override public path untuk server dengan struktur non-standar (misal: Laravel di /sys, public di luar).
|
||||||
|
// realpath() terhadap base_path('public') akan gagal jika folder /sys/public tidak ada.
|
||||||
|
// Gunakan storage_path() sebagai fallback yang dijamin exist.
|
||||||
|
'public_path' => (function() {
|
||||||
|
$candidates = [
|
||||||
|
base_path('public'), // standar: /sys/public
|
||||||
|
base_path('../public_html'), // hosting: /public_html
|
||||||
|
base_path('..'), // root server
|
||||||
|
storage_path(), // pasti ada
|
||||||
|
];
|
||||||
|
foreach ($candidates as $p) {
|
||||||
|
$rp = realpath($p);
|
||||||
|
if ($rp !== false) return $rp;
|
||||||
|
}
|
||||||
|
return storage_path(); // absolute last resort (realpath tidak dipanggil lagi oleh ServiceProvider)
|
||||||
|
})(),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Dejavu Sans font is missing glyphs for converted entities, turn it off if you need to show € and £.
|
||||||
|
*/
|
||||||
|
'convert_entities' => true,
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
/**
|
||||||
|
* The location of the DOMPDF font directory
|
||||||
|
*
|
||||||
|
* The location of the directory where DOMPDF will store fonts and font metrics
|
||||||
|
* Note: This directory must exist and be writable by the webserver process.
|
||||||
|
* *Please note the trailing slash.*
|
||||||
|
*
|
||||||
|
* Notes regarding fonts:
|
||||||
|
* Additional .afm font metrics can be added by executing load_font.php from command line.
|
||||||
|
*
|
||||||
|
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
|
||||||
|
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
|
||||||
|
* increase file size unless font subsetting is enabled. Before embedding a font please
|
||||||
|
* review your rights under the font license.
|
||||||
|
*
|
||||||
|
* Any font specification in the source HTML is translated to the closest font available
|
||||||
|
* in the font directory.
|
||||||
|
*
|
||||||
|
* The pdf standard "Base 14 fonts" are:
|
||||||
|
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
|
||||||
|
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
|
||||||
|
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||||
|
* Symbol, ZapfDingbats.
|
||||||
|
*/
|
||||||
|
'font_dir' => storage_path('fonts'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of the DOMPDF font cache directory
|
||||||
|
*
|
||||||
|
* This directory contains the cached font metrics for the fonts used by DOMPDF.
|
||||||
|
* This directory can be the same as DOMPDF_FONT_DIR
|
||||||
|
*
|
||||||
|
* Note: This directory must exist and be writable by the webserver process.
|
||||||
|
*/
|
||||||
|
'font_cache' => storage_path('fonts'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of a temporary directory.
|
||||||
|
*
|
||||||
|
* The directory specified must be writeable by the webserver process.
|
||||||
|
* The temporary directory is required to download remote images and when
|
||||||
|
* using the PDFLib back end.
|
||||||
|
*/
|
||||||
|
'temp_dir' => sys_get_temp_dir(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ==== IMPORTANT ====
|
||||||
|
*
|
||||||
|
* dompdf's "chroot": Prevents dompdf from accessing system files or other
|
||||||
|
* files on the webserver. All local files opened by dompdf must be in a
|
||||||
|
* subdirectory of this directory. DO NOT set it to '/' since this could
|
||||||
|
* allow an attacker to use dompdf to read any files on the server. This
|
||||||
|
* should be an absolute path.
|
||||||
|
* This is only checked on command line call by dompdf.php, but not by
|
||||||
|
* direct class use like:
|
||||||
|
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
||||||
|
*/
|
||||||
|
'chroot' => (function() {
|
||||||
|
// realpath bisa gagal di server non-standar karena symlink atau struktur khusus.
|
||||||
|
// Gunakan storage_path() sebagai fallback yang aman karena pasti ada di dalam folder app.
|
||||||
|
$bp = realpath(base_path());
|
||||||
|
if ($bp !== false) return $bp;
|
||||||
|
$sp = realpath(storage_path());
|
||||||
|
if ($sp !== false) return dirname($sp); // parent of storage = base
|
||||||
|
return storage_path(); // last resort
|
||||||
|
})(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protocol whitelist
|
||||||
|
*
|
||||||
|
* Protocols and PHP wrappers allowed in URIs, and the validation rules
|
||||||
|
* that determine if a resouce may be loaded. Full support is not guaranteed
|
||||||
|
* for the protocols/wrappers specified
|
||||||
|
* by this array.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
'allowed_protocols' => [
|
||||||
|
'data://' => ['rules' => []],
|
||||||
|
'file://' => ['rules' => []],
|
||||||
|
'http://' => ['rules' => []],
|
||||||
|
'https://' => ['rules' => []],
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operational artifact (log files, temporary files) path validation
|
||||||
|
*/
|
||||||
|
'artifactPathValidation' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
'log_output_file' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enable font subsetting or not.
|
||||||
|
*/
|
||||||
|
'enable_font_subsetting' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PDF rendering backend to use
|
||||||
|
*
|
||||||
|
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
|
||||||
|
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
|
||||||
|
* fall back on CPDF. 'GD' renders PDFs to graphic files.
|
||||||
|
* {@link * Canvas_Factory} ultimately determines which rendering class to
|
||||||
|
* instantiate based on this setting.
|
||||||
|
*
|
||||||
|
* Both PDFLib & CPDF rendering backends provide sufficient rendering
|
||||||
|
* capabilities for dompdf, however additional features (e.g. object,
|
||||||
|
* image and font support, etc.) differ between backends. Please see
|
||||||
|
* {@link PDFLib_Adapter} for more information on the PDFLib backend
|
||||||
|
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
|
||||||
|
* on CPDF. Also see the documentation for each backend at the links
|
||||||
|
* below.
|
||||||
|
*
|
||||||
|
* The GD rendering backend is a little different than PDFLib and
|
||||||
|
* CPDF. Several features of CPDF and PDFLib are not supported or do
|
||||||
|
* not make any sense when creating image files. For example,
|
||||||
|
* multiple pages are not supported, nor are PDF 'objects'. Have a
|
||||||
|
* look at {@link GD_Adapter} for more information. GD support is
|
||||||
|
* experimental, so use it at your own risk.
|
||||||
|
*
|
||||||
|
* @link http://www.pdflib.com
|
||||||
|
* @link http://www.ros.co.nz/pdf
|
||||||
|
* @link http://www.php.net/image
|
||||||
|
*/
|
||||||
|
'pdf_backend' => 'CPDF',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* html target media view which should be rendered into pdf.
|
||||||
|
* List of types and parsing rules for future extensions:
|
||||||
|
* http://www.w3.org/TR/REC-html40/types.html
|
||||||
|
* screen, tty, tv, projection, handheld, print, braille, aural, all
|
||||||
|
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
|
||||||
|
* Note, even though the generated pdf file is intended for print output,
|
||||||
|
* the desired content might be different (e.g. screen or projection view of html file).
|
||||||
|
* Therefore allow specification of content here.
|
||||||
|
*/
|
||||||
|
'default_media_type' => 'screen',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default paper size.
|
||||||
|
*
|
||||||
|
* North America standard is "letter"; other countries generally "a4"
|
||||||
|
*
|
||||||
|
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
||||||
|
*/
|
||||||
|
'default_paper_size' => 'a4',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default paper orientation.
|
||||||
|
*
|
||||||
|
* The orientation of the page (portrait or landscape).
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
'default_paper_orientation' => 'portrait',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default font family
|
||||||
|
*
|
||||||
|
* Used if no suitable fonts can be found. This must exist in the font folder.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
'default_font' => 'serif',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image DPI setting
|
||||||
|
*
|
||||||
|
* This setting determines the default DPI setting for images and fonts. The
|
||||||
|
* DPI may be overridden for inline images by explictly setting the
|
||||||
|
* image's width & height style attributes (i.e. if the image's native
|
||||||
|
* width is 600 pixels and you specify the image's width as 72 points,
|
||||||
|
* the image will have a DPI of 600 in the rendered PDF. The DPI of
|
||||||
|
* background images can not be overridden and is controlled entirely
|
||||||
|
* via this parameter.
|
||||||
|
*
|
||||||
|
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
|
||||||
|
* If a size in html is given as px (or without unit as image size),
|
||||||
|
* this tells the corresponding size in pt.
|
||||||
|
* This adjusts the relative sizes to be similar to the rendering of the
|
||||||
|
* html page in a reference browser.
|
||||||
|
*
|
||||||
|
* In pdf, always 1 pt = 1/72 inch
|
||||||
|
*
|
||||||
|
* Rendering resolution of various browsers in px per inch:
|
||||||
|
* Windows Firefox and Internet Explorer:
|
||||||
|
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
|
||||||
|
* Linux Firefox:
|
||||||
|
* about:config *resolution: Default:96
|
||||||
|
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
|
||||||
|
*
|
||||||
|
* Take care about extra font/image zoom factor of browser.
|
||||||
|
*
|
||||||
|
* In images, <img> size in pixel attribute, img css style, are overriding
|
||||||
|
* the real image dimension in px for rendering.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
'dpi' => 96,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable embedded PHP
|
||||||
|
*
|
||||||
|
* If this setting is set to true then DOMPDF will automatically evaluate embedded PHP contained
|
||||||
|
* within <script type="text/php"> ... </script> tags.
|
||||||
|
*
|
||||||
|
* ==== IMPORTANT ==== Enabling this for documents you do not trust (e.g. arbitrary remote html pages)
|
||||||
|
* is a security risk.
|
||||||
|
* Embedded scripts are run with the same level of system access available to dompdf.
|
||||||
|
* Set this option to false (recommended) if you wish to process untrusted documents.
|
||||||
|
* This setting may increase the risk of system exploit.
|
||||||
|
* Do not change this settings without understanding the consequences.
|
||||||
|
* Additional documentation is available on the dompdf wiki at:
|
||||||
|
* https://github.com/dompdf/dompdf/wiki
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_php' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable inline JavaScript
|
||||||
|
*
|
||||||
|
* If this setting is set to true then DOMPDF will automatically insert JavaScript code contained
|
||||||
|
* within <script type="text/javascript"> ... </script> tags as written into the PDF.
|
||||||
|
* NOTE: This is PDF-based JavaScript to be executed by the PDF viewer,
|
||||||
|
* not browser-based JavaScript executed by Dompdf.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_javascript' => true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable remote file access
|
||||||
|
*
|
||||||
|
* If this setting is set to true, DOMPDF will access remote sites for
|
||||||
|
* images and CSS files as required.
|
||||||
|
*
|
||||||
|
* ==== IMPORTANT ====
|
||||||
|
* This can be a security risk, in particular in combination with isPhpEnabled and
|
||||||
|
* allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...);
|
||||||
|
* This allows anonymous users to download legally doubtful internet content which on
|
||||||
|
* tracing back appears to being downloaded by your server, or allows malicious php code
|
||||||
|
* in remote html pages to be executed by your server with your account privileges.
|
||||||
|
*
|
||||||
|
* This setting may increase the risk of system exploit. Do not change
|
||||||
|
* this settings without understanding the consequences. Additional
|
||||||
|
* documentation is available on the dompdf wiki at:
|
||||||
|
* https://github.com/dompdf/dompdf/wiki
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_remote' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of allowed remote hosts
|
||||||
|
*
|
||||||
|
* Each value of the array must be a valid hostname.
|
||||||
|
*
|
||||||
|
* This will be used to filter which resources can be loaded in combination with
|
||||||
|
* isRemoteEnabled. If enable_remote is FALSE, then this will have no effect.
|
||||||
|
*
|
||||||
|
* Leave to NULL to allow any remote host.
|
||||||
|
*
|
||||||
|
* @var array|null
|
||||||
|
*/
|
||||||
|
'allowed_remote_hosts' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ratio applied to the fonts height to be more like browsers' line height
|
||||||
|
*/
|
||||||
|
'font_height_ratio' => 1.1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the HTML5 Lib parser
|
||||||
|
*
|
||||||
|
* @deprecated This feature is now always on in dompdf 2.x
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_html5_parser' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,380 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Excel;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Reader\Csv;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'exports' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Chunk size
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using FromQuery, the query is automatically chunked.
|
||||||
|
| Here you can specify how big the chunk should be.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'chunk_size' => 1000,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Pre-calculate formulas during export
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
'pre_calculate_formulas' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Enable strict null comparison
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When enabling strict null comparison empty cells ('') will
|
||||||
|
| be added to the sheet.
|
||||||
|
*/
|
||||||
|
'strict_null_comparison' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| CSV Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure e.g. delimiter, enclosure and line ending for CSV exports.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'csv' => [
|
||||||
|
'delimiter' => ',',
|
||||||
|
'enclosure' => '"',
|
||||||
|
'line_ending' => PHP_EOL,
|
||||||
|
'use_bom' => false,
|
||||||
|
'include_separator_line' => false,
|
||||||
|
'excel_compatibility' => false,
|
||||||
|
'output_encoding' => '',
|
||||||
|
'test_auto_detect' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Worksheet properties
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure e.g. default title, creator, subject,...
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'properties' => [
|
||||||
|
'creator' => '',
|
||||||
|
'lastModifiedBy' => '',
|
||||||
|
'title' => '',
|
||||||
|
'description' => '',
|
||||||
|
'subject' => '',
|
||||||
|
'keywords' => '',
|
||||||
|
'category' => '',
|
||||||
|
'manager' => '',
|
||||||
|
'company' => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'imports' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Read Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with imports, you might only be interested in the
|
||||||
|
| data that the sheet exists. By default we ignore all styles,
|
||||||
|
| however if you want to do some logic based on style data
|
||||||
|
| you can enable it by setting read_only to false.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'read_only' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Ignore Empty
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with imports, you might be interested in ignoring
|
||||||
|
| rows that have null values or empty strings. By default rows
|
||||||
|
| containing empty strings or empty values are not ignored but can be
|
||||||
|
| ignored by enabling the setting ignore_empty to true.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'ignore_empty' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Heading Row Formatter
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure the heading row formatter.
|
||||||
|
| Available options: none|slug|custom
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'heading_row' => [
|
||||||
|
'formatter' => 'slug',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| CSV Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure e.g. delimiter, enclosure and line ending for CSV imports.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'csv' => [
|
||||||
|
'delimiter' => null,
|
||||||
|
'enclosure' => '"',
|
||||||
|
'escape_character' => '\\',
|
||||||
|
'contiguous' => false,
|
||||||
|
'input_encoding' => Csv::GUESS_ENCODING,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Worksheet properties
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure e.g. default title, creator, subject,...
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'properties' => [
|
||||||
|
'creator' => '',
|
||||||
|
'lastModifiedBy' => '',
|
||||||
|
'title' => '',
|
||||||
|
'description' => '',
|
||||||
|
'subject' => '',
|
||||||
|
'keywords' => '',
|
||||||
|
'category' => '',
|
||||||
|
'manager' => '',
|
||||||
|
'company' => '',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cell Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure middleware that is executed on getting a cell value
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'cells' => [
|
||||||
|
'middleware' => [
|
||||||
|
//\Maatwebsite\Excel\Middleware\TrimCellValue::class,
|
||||||
|
//\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Extension detector
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure here which writer/reader type should be used when the package
|
||||||
|
| needs to guess the correct type based on the extension alone.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'extension_detector' => [
|
||||||
|
'xlsx' => Excel::XLSX,
|
||||||
|
'xlsm' => Excel::XLSX,
|
||||||
|
'xltx' => Excel::XLSX,
|
||||||
|
'xltm' => Excel::XLSX,
|
||||||
|
'xls' => Excel::XLS,
|
||||||
|
'xlt' => Excel::XLS,
|
||||||
|
'ods' => Excel::ODS,
|
||||||
|
'ots' => Excel::ODS,
|
||||||
|
'slk' => Excel::SLK,
|
||||||
|
'xml' => Excel::XML,
|
||||||
|
'gnumeric' => Excel::GNUMERIC,
|
||||||
|
'htm' => Excel::HTML,
|
||||||
|
'html' => Excel::HTML,
|
||||||
|
'csv' => Excel::CSV,
|
||||||
|
'tsv' => Excel::TSV,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| PDF Extension
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure here which Pdf driver should be used by default.
|
||||||
|
| Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'pdf' => Excel::DOMPDF,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Value Binder
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| PhpSpreadsheet offers a way to hook into the process of a value being
|
||||||
|
| written to a cell. In there some assumptions are made on how the
|
||||||
|
| value should be formatted. If you want to change those defaults,
|
||||||
|
| you can implement your own default value binder.
|
||||||
|
|
|
||||||
|
| Possible value binders:
|
||||||
|
|
|
||||||
|
| [x] Maatwebsite\Excel\DefaultValueBinder::class
|
||||||
|
| [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class
|
||||||
|
| [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'value_binder' => [
|
||||||
|
'default' => Maatwebsite\Excel\DefaultValueBinder::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default cell caching driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By default PhpSpreadsheet keeps all cell values in memory, however when
|
||||||
|
| dealing with large files, this might result into memory issues. If you
|
||||||
|
| want to mitigate that, you can configure a cell caching driver here.
|
||||||
|
| When using the illuminate driver, it will store each value in the
|
||||||
|
| cache store. This can slow down the process, because it needs to
|
||||||
|
| store each value. You can use the "batch" store if you want to
|
||||||
|
| only persist to the store when the memory limit is reached.
|
||||||
|
|
|
||||||
|
| Drivers: memory|illuminate|batch
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'driver' => 'memory',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Batch memory caching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with the "batch" caching driver, it will only
|
||||||
|
| persist to the store when the memory limit is reached.
|
||||||
|
| Here you can tweak the memory limit to your liking.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'batch' => [
|
||||||
|
'memory_limit' => 60000,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Illuminate cache
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "illuminate" caching driver, it will automatically use
|
||||||
|
| your default cache store. However if you prefer to have the cell
|
||||||
|
| cache on a separate store, you can configure the store name here.
|
||||||
|
| You can use any store defined in your cache config. When leaving
|
||||||
|
| at "null" it will use the default store.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'illuminate' => [
|
||||||
|
'store' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Time-to-live (TTL)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The TTL of items written to cache. If you want to keep the items cached
|
||||||
|
| indefinitely, set this to null. Otherwise, set a number of seconds,
|
||||||
|
| a \DateInterval, or a callable.
|
||||||
|
|
|
||||||
|
| Allowable types: callable|\DateInterval|int|null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'default_ttl' => 10800,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Transaction Handler
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By default the import is wrapped in a transaction. This is useful
|
||||||
|
| for when an import may fail and you want to retry it. With the
|
||||||
|
| transactions, the previous import gets rolled-back.
|
||||||
|
|
|
||||||
|
| You can disable the transaction handler by setting this to null.
|
||||||
|
| Or you can choose a custom made transaction handler here.
|
||||||
|
|
|
||||||
|
| Supported handlers: null|db
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'transactions' => [
|
||||||
|
'handler' => 'db',
|
||||||
|
'db' => [
|
||||||
|
'connection' => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'temporary_files' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Local Temporary Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When exporting and importing files, we use a temporary file, before
|
||||||
|
| storing reading or downloading. Here you can customize that path.
|
||||||
|
| permissions is an array with the permission flags for the directory (dir)
|
||||||
|
| and the create file (file).
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'local_path' => storage_path('framework/cache/laravel-excel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Local Temporary Path Permissions
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Permissions is an array with the permission flags for the directory (dir)
|
||||||
|
| and the create file (file).
|
||||||
|
| If omitted the default permissions of the filesystem will be used.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'local_permissions' => [
|
||||||
|
// 'dir' => 0755,
|
||||||
|
// 'file' => 0644,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Remote Temporary Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with a multi server setup with queues in which you
|
||||||
|
| cannot rely on having a shared local temporary path, you might
|
||||||
|
| want to store the temporary file on a shared disk. During the
|
||||||
|
| queue executing, we'll retrieve the temporary file from that
|
||||||
|
| location instead. When left to null, it will always use
|
||||||
|
| the local path. This setting only has effect when using
|
||||||
|
| in conjunction with queued imports and exports.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'remote_disk' => null,
|
||||||
|
'remote_prefix' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Force Resync
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with a multi server setup as above, it's possible
|
||||||
|
| for the clean up that occurs after entire queue has been run to only
|
||||||
|
| cleanup the server that the last AfterImportJob runs on. The rest of the server
|
||||||
|
| would still have the local temporary file stored on it. In this case your
|
||||||
|
| local storage limits can be exceeded and future imports won't be processed.
|
||||||
|
| To mitigate this you can set this config value to be true, so that after every
|
||||||
|
| queued chunk is processed the local temporary file is deleted on the server that
|
||||||
|
| processed it.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'force_resync_remote' => null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => env('APP_URL').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue