Compare commits

..

No commits in common. "8c726c3ea81c8989da565ab29f27b17f879a2670" and "c96ab27f2f42f661f32ac32996153b2d67a53089" have entirely different histories.

636 changed files with 1 additions and 108208 deletions

2
.gitattributes vendored
View File

@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@ -1,4 +1,3 @@
# ta-helgafaisa
Sistem Informasi Monitoring Santri Berbasis Web dan Mobile
>>>>>>> c96ab27f2f42f661f32ac32996153b2d67a53089
Sistem Informasi Monitoring Santri Berbasis Web dan Mobile

View File

@ -1,18 +0,0 @@
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

View File

@ -1,59 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
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
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View File

@ -1,11 +0,0 @@
* 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

19
sim-pkpps/.gitignore vendored
View File

@ -1,19 +0,0 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode

View File

@ -1,66 +0,0 @@
<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).

View File

@ -1,107 +0,0 @@
<?php
// app/Console/Commands/CleanSantriAccounts.php
namespace App\Console\Commands;
use App\Models\SantriAccount;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
class CleanSantriAccounts extends Command
{
/**
* Nama dan signature command.
*/
protected $signature = 'santri:clean-accounts
{--dry-run : Tampilkan perubahan tanpa menyimpan}';
/**
* Deskripsi command.
*/
protected $description = 'Reset username & password semua akun santri/wali agar konsisten (santri=nama_lengkap, wali=nama_orang_tua, password=NIS)';
/**
* Jalankan command.
*/
public function handle()
{
$dryRun = $this->option('dry-run');
if ($dryRun) {
$this->info('=== DRY RUN MODE (tidak ada perubahan yang disimpan) ===');
}
$accounts = SantriAccount::with('santri')->get();
$updated = 0;
$skipped = 0;
$errors = [];
foreach ($accounts as $account) {
$santri = $account->santri;
// -- Skip jika santri tidak ditemukan --
if (!$santri) {
$errors[] = "Account ID {$account->id} (id_santri={$account->id_santri}): Data santri tidak ditemukan.";
$skipped++;
continue;
}
// -- Skip jika NIS kosong --
if (empty($santri->nis)) {
$errors[] = "Account ID {$account->id} ({$santri->nama_lengkap}): NIS kosong, dilewati.";
$skipped++;
continue;
}
// -- Tentukan username yang benar --
if ($account->role === 'wali') {
$usernameBenar = $santri->nama_orang_tua ?: $santri->nama_lengkap;
} else {
$usernameBenar = $santri->nama_lengkap;
}
// -- Cek apakah username sudah benar --
$usernameChanged = ($account->username !== $usernameBenar);
if ($usernameChanged) {
// -- Pastikan username unik --
$existing = SantriAccount::where('username', $usernameBenar)
->where('id', '!=', $account->id)
->exists();
if ($existing) {
$usernameBenar = $usernameBenar . '_' . $santri->nis;
}
}
if ($usernameChanged) {
$this->line(" [{$account->role}] {$santri->nama_lengkap}: username '{$account->username}' -> '{$usernameBenar}'");
}
if (!$dryRun) {
$account->username = $usernameBenar;
$account->password = Hash::make($santri->nis);
$account->save();
}
$updated++;
}
$this->newLine();
$this->info("Selesai! Updated: {$updated}, Skipped: {$skipped}");
if (count($errors) > 0) {
$this->newLine();
$this->warn('Masalah ditemukan:');
foreach ($errors as $err) {
$this->warn(" - {$err}");
}
}
if ($dryRun) {
$this->newLine();
$this->comment('Jalankan tanpa --dry-run untuk menyimpan perubahan.');
}
return Command::SUCCESS;
}
}

View File

@ -1,281 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\Santri;
use App\Models\Kelas;
use App\Models\SantriKelas;
class MigrateSantriKelasCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate:santri-kelas
{--dry-run : Run without inserting data}
{--force : Overwrite existing data}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate data kelas santri dari kolom \'kelas\' ke tabel \'santri_kelas\'';
/**
* Mapping kelas lama ke ID kelas baru
*
* @var array
*/
protected $kelasMapping = [
'PB' => 1,
'Lambatan' => 2,
'Cepatan' => 3,
];
/**
* Counters
*/
protected $totalSantri = 0;
protected $successCount = 0;
protected $skipCount = 0;
protected $errorCount = 0;
/**
* Execute the console command.
*/
public function handle()
{
$dryRun = $this->option('dry-run');
$force = $this->option('force');
// Header
$this->info('╔══════════════════════════════════════════════════════╗');
$this->info('║ Migrating Santri Kelas Data to New System ║');
$this->info('╚══════════════════════════════════════════════════════╝');
$this->newLine();
if ($dryRun) {
$this->warn('🔍 DRY RUN MODE - No data will be inserted');
$this->newLine();
}
// Get tahun ajaran aktif
$tahunAjaran = SantriKelas::getCurrentAcademicYear();
$this->info("📅 Tahun Ajaran: {$tahunAjaran}");
$this->newLine();
// Verify kelas mapping exists
if (!$this->verifyKelasMapping()) {
return 1;
}
// Get all santri dengan kelas
$santris = Santri::whereNotNull('kelas')
->whereIn('kelas', array_keys($this->kelasMapping))
->get();
$this->totalSantri = $santris->count();
if ($this->totalSantri === 0) {
$this->warn('⚠️ No santri found with kelas data');
return 0;
}
$this->info("Found {$this->totalSantri} santri to migrate");
$this->newLine();
// Confirmation
if (!$dryRun && !$force) {
if (!$this->confirm('Do you want to proceed with migration?')) {
$this->warn('Migration cancelled');
return 0;
}
$this->newLine();
}
// Progress bar
$progressBar = $this->output->createProgressBar($this->totalSantri);
$progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %message%');
$progressBar->setMessage('Starting migration...');
$progressBar->start();
// Begin transaction
DB::beginTransaction();
try {
foreach ($santris as $santri) {
$progressBar->setMessage("Processing: {$santri->nama_lengkap}");
$result = $this->migrateSantri($santri, $tahunAjaran, $dryRun, $force);
if ($result === 'success') {
$this->successCount++;
} elseif ($result === 'skip') {
$this->skipCount++;
} else {
$this->errorCount++;
}
$progressBar->advance();
}
$progressBar->setMessage('Migration completed!');
$progressBar->finish();
$this->newLine(2);
if (!$dryRun) {
DB::commit();
$this->info('✓ Transaction committed');
} else {
DB::rollBack();
$this->info('✓ Transaction rolled back (dry-run)');
}
} catch (\Exception $e) {
DB::rollBack();
$this->newLine(2);
$this->error('✗ Migration failed: ' . $e->getMessage());
Log::error('Santri Kelas Migration Error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return 1;
}
// Summary
$this->newLine();
$this->displaySummary($dryRun);
return 0;
}
/**
* Verify kelas mapping exists in database
*/
protected function verifyKelasMapping()
{
$this->info('🔍 Verifying kelas mapping...');
$missing = [];
foreach ($this->kelasMapping as $kelasName => $kelasId) {
$kelas = Kelas::find($kelasId);
if (!$kelas) {
$missing[] = "{$kelasName} (ID: {$kelasId})";
} else {
$this->line("{$kelasName} -> {$kelas->nama_kelas} (ID: {$kelasId})");
}
}
if (!empty($missing)) {
$this->error('✗ Missing kelas in database:');
foreach ($missing as $item) {
$this->error(" - {$item}");
}
$this->error('Please run: php artisan db:seed --class=KelasSeeder');
return false;
}
$this->newLine();
return true;
}
/**
* Migrate single santri
*/
protected function migrateSantri($santri, $tahunAjaran, $dryRun, $force)
{
try {
// Get ID kelas baru
$idKelas = $this->kelasMapping[$santri->kelas] ?? null;
if (!$idKelas) {
Log::warning('Santri kelas mapping not found', [
'id_santri' => $santri->id_santri,
'kelas' => $santri->kelas
]);
return 'error';
}
// Check if already exists
$existing = SantriKelas::where('id_santri', $santri->id_santri)
->where('id_kelas', $idKelas)
->where('tahun_ajaran', $tahunAjaran)
->first();
if ($existing && !$force) {
return 'skip';
}
if ($dryRun) {
return 'success';
}
// Delete existing if force
if ($existing && $force) {
$existing->delete();
}
// Create new record
SantriKelas::create([
'id_santri' => $santri->id_santri,
'id_kelas' => $idKelas,
'tahun_ajaran' => $tahunAjaran,
'is_primary' => true,
]);
return 'success';
} catch (\Exception $e) {
Log::error('Error migrating santri', [
'id_santri' => $santri->id_santri,
'error' => $e->getMessage()
]);
return 'error';
}
}
/**
* Display summary
*/
protected function displaySummary($dryRun)
{
$this->info('╔══════════════════════════════════════════════════════╗');
$this->info('║ MIGRATION SUMMARY ║');
$this->info('╚══════════════════════════════════════════════════════╝');
$this->newLine();
$this->line(" 📊 Total santri: {$this->totalSantri}");
$this->line(" ✓ Migrated: <fg=green>{$this->successCount}</>");
$this->line(" ⊘ Skipped (already exists): <fg=yellow>{$this->skipCount}</>");
$this->line(" ✗ Errors: <fg=red>{$this->errorCount}</>");
$this->newLine();
if ($dryRun) {
$this->warn('🔍 DRY RUN - No data was actually inserted');
} else {
if ($this->errorCount === 0) {
$this->info('✓ Migration completed successfully!');
} else {
$this->warn('⚠️ Migration completed with errors. Check laravel.log for details.');
}
}
$this->newLine();
// Next steps
if (!$dryRun && $this->errorCount === 0) {
$this->info('📝 Next steps:');
$this->line(' 1. Verify data: SELECT * FROM santri_kelas');
$this->line(' 2. Test backward compatibility: $santri->kelas_name');
$this->line(' 3. Scan codebase for kelas usage: php scan_kelas_usage.php');
$this->line(' 4. Consider dropping santris.kelas column after full migration');
}
}
}

View File

@ -1,309 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\Santri;
use App\Models\Kelas;
use App\Models\KelompokKelas;
use App\Models\SantriKelas;
class MigrateSantriToNewKelas extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'migrate:santri-kelas-full
{--dry-run : Preview tanpa menyimpan ke database}';
/**
* The console command description.
*/
protected $description = 'Full migration: Pindahkan data kolom kelas santri ke tabel santri_kelas (sistem baru)';
/**
* Counters
*/
protected int $totalSantri = 0;
protected int $successCount = 0;
protected int $skipCount = 0;
protected int $errorCount = 0;
/**
* Collected errors & skipped
*/
protected array $errors = [];
protected array $skipped = [];
/**
* Resolved kelas mapping cache: ['PB' => Kelas model, ...]
*/
protected array $kelasMapping = [];
/**
* Execute the console command.
*/
public function handle(): int
{
$isDryRun = $this->option('dry-run');
$this->newLine();
$this->info('╔══════════════════════════════════════════════════════╗');
$this->info('║ MIGRASI SANTRI KE SISTEM KELAS BARU ║');
$this->info('║ ' . ($isDryRun ? '🔍 MODE: DRY-RUN (Preview Only)' : '🚀 MODE: EXECUTE (Real Migration)') . ' ║');
$this->info('╚══════════════════════════════════════════════════════╝');
$this->newLine();
// ────────────────────
// STEP 1: Validasi kelompok kelas
// ────────────────────
$this->info('📋 Step 1: Validasi kelompok kelas...');
if (!$this->validateAndBuildMapping()) {
$this->error('❌ Validasi gagal! Pastikan data kelompok_kelas dan kelas sudah tersedia.');
return Command::FAILURE;
}
$this->info(' ✅ Mapping kelas berhasil di-resolve:');
foreach ($this->kelasMapping as $oldKelas => $kelasModel) {
$this->line(" <fg=cyan>{$oldKelas}</> → <fg=green>{$kelasModel->kode_kelas} ({$kelasModel->nama_kelas})</>");
}
$this->newLine();
// ────────────────────
// STEP 2: Ambil semua santri
// ────────────────────
$this->info('📋 Step 2: Mengambil data santri...');
$santris = Santri::select('id', 'id_santri', 'nama_lengkap', 'kelas')->get();
$this->totalSantri = $santris->count();
if ($this->totalSantri === 0) {
$this->warn('⚠️ Tidak ada data santri ditemukan.');
return Command::SUCCESS;
}
$this->info(" 📊 Total santri ditemukan: <fg=yellow>{$this->totalSantri}</>");
$this->newLine();
// ────────────────────
// STEP 3: Migrate
// ────────────────────
$tahunAjaran = SantriKelas::getCurrentAcademicYear();
$this->info("📋 Step 3: Memulai migrasi (Tahun Ajaran: <fg=yellow>{$tahunAjaran}</>)...");
$this->newLine();
if (!$isDryRun) {
// Wrap dalam transaction untuk safety
DB::beginTransaction();
}
try {
$this->output->progressStart($this->totalSantri);
foreach ($santris as $santri) {
$this->processSantri($santri, $tahunAjaran, $isDryRun);
$this->output->progressAdvance();
}
$this->output->progressFinish();
$this->newLine();
if (!$isDryRun) {
DB::commit();
$this->info('✅ Transaction committed.');
}
} catch (\Exception $e) {
if (!$isDryRun) {
DB::rollBack();
$this->error('❌ Transaction rolled back!');
}
$this->error("Fatal error: {$e->getMessage()}");
Log::error('MigrateSantriToNewKelas fatal error', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return Command::FAILURE;
}
// ────────────────────
// STEP 4: Summary Report
// ────────────────────
$this->printSummary($isDryRun, $tahunAjaran);
// ────────────────────
// STEP 5: Post-migration validation
// ────────────────────
if (!$isDryRun) {
$this->validatePostMigration();
}
return Command::SUCCESS;
}
/**
* Validasi kelompok kelas dan build mapping dinamis.
*/
protected function validateAndBuildMapping(): bool
{
$mappings = [
'PB' => '%PB%',
'Lambatan' => '%Lambatan%',
'Cepatan' => '%Cepatan%',
];
foreach ($mappings as $oldKelas => $likePattern) {
$kelas = Kelas::whereHas('kelompok', function ($q) use ($likePattern) {
$q->where('nama_kelompok', 'like', $likePattern);
})
->where('is_active', true)
->orderBy('urutan')
->first();
if (!$kelas) {
$this->error(" ❌ Tidak ditemukan kelas aktif untuk kelompok '{$oldKelas}' (pattern: {$likePattern})");
return false;
}
$this->kelasMapping[$oldKelas] = $kelas;
}
return true;
}
/**
* Process satu santri.
*/
protected function processSantri(Santri $santri, string $tahunAjaran, bool $isDryRun): void
{
try {
$kelasLama = $santri->kelas;
// Skip jika kelas NULL atau tidak dikenali
if (empty($kelasLama) || !isset($this->kelasMapping[$kelasLama])) {
$reason = empty($kelasLama) ? 'Kelas NULL' : "Kelas '{$kelasLama}' tidak dikenali";
$this->skipped[] = [
'id_santri' => $santri->id_santri,
'nama' => $santri->nama_lengkap,
'reason' => $reason,
];
$this->skipCount++;
return;
}
$kelasBaru = $this->kelasMapping[$kelasLama];
if ($isDryRun) {
// Dry-run: hanya tampilkan
$this->line(" <fg=green>✓</> {$santri->id_santri} ({$santri->nama_lengkap}): <fg=yellow>{$kelasLama}</> → <fg=cyan>{$kelasBaru->kode_kelas} ({$kelasBaru->nama_kelas})</>");
$this->successCount++;
return;
}
// Real execute: Insert/update ke santri_kelas
SantriKelas::updateOrCreate(
[
'id_santri' => $santri->id_santri,
'tahun_ajaran' => $tahunAjaran,
'is_primary' => true,
],
[
'id_kelas' => $kelasBaru->id,
]
);
$this->successCount++;
} catch (\Exception $e) {
$this->errors[] = [
'id_santri' => $santri->id_santri,
'nama' => $santri->nama_lengkap,
'error' => $e->getMessage(),
];
$this->errorCount++;
Log::warning('MigrateSantriToNewKelas: Error processing santri', [
'id_santri' => $santri->id_santri,
'error' => $e->getMessage(),
]);
}
}
/**
* Print summary report.
*/
protected function printSummary(bool $isDryRun, string $tahunAjaran): void
{
$this->newLine();
$this->info('╔══════════════════════════════════════════════════════╗');
$this->info('║ 📊 SUMMARY REPORT ║');
$this->info('╚══════════════════════════════════════════════════════╝');
$this->newLine();
$this->line(" Mode : <fg=" . ($isDryRun ? 'yellow>DRY-RUN (Preview)' : 'green>EXECUTED (Real)') . "</>");
$this->line(" Tahun Ajaran : <fg=cyan>{$tahunAjaran}</>");
$this->newLine();
$this->line(" Total santri : <fg=white>{$this->totalSantri}</>");
$this->line(" ✅ Berhasil : <fg=green>{$this->successCount}</>");
$this->line(" ⚠️ Skipped : <fg=yellow>{$this->skipCount}</>");
$this->line(" ❌ Error : <fg=red>{$this->errorCount}</>");
// List skipped
if (count($this->skipped) > 0) {
$this->newLine();
$this->warn(' ⚠️ Santri yang di-skip:');
foreach ($this->skipped as $item) {
$this->line(" - <fg=yellow>{$item['id_santri']}</> ({$item['nama']}): {$item['reason']}");
}
}
// List errors
if (count($this->errors) > 0) {
$this->newLine();
$this->error(' ❌ Santri yang error:');
foreach ($this->errors as $item) {
$this->line(" - <fg=red>{$item['id_santri']}</> ({$item['nama']}): {$item['error']}");
}
}
$this->newLine();
if ($isDryRun) {
$this->info('💡 Ini hanya preview. Jalankan tanpa --dry-run untuk eksekusi migrasi.');
} else {
$this->info('✅ Migrasi selesai! Data santri_kelas telah diperbarui.');
}
$this->newLine();
}
/**
* Validasi setelah migrasi.
*/
protected function validatePostMigration(): void
{
$this->info('📋 Post-migration validation...');
// Count santri yang punya kelas (kolom lama) tapi belum ada di santri_kelas
$santriDenganKelas = Santri::whereNotNull('kelas')
->where('kelas', '!=', '')
->count();
$santriDiSantriKelas = SantriKelas::where('is_primary', true)->count();
$this->line(" Santri dengan kelas (kolom lama) : <fg=yellow>{$santriDenganKelas}</>");
$this->line(" Santri di santri_kelas (primary) : <fg=cyan>{$santriDiSantriKelas}</>");
if ($santriDiSantriKelas >= $santriDenganKelas) {
$this->info(' ✅ Validasi OK! Semua santri sudah ter-migrate.');
} else {
$diff = $santriDenganKelas - $santriDiSantriKelas;
$this->warn(" ⚠️ Ada {$diff} santri yang belum ter-migrate. Periksa log di atas.");
}
$this->newLine();
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
}

View File

@ -1,382 +0,0 @@
<?php
// app/Http/Controllers/admin/AbsensiKegiatanController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AbsensiKegiatan;
use App\Models\Kegiatan;
use App\Models\Kelas;
use App\Models\Kepulangan;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AbsensiKegiatanController extends Controller
{
/**
* Daftar kegiatan untuk absensi diarahkan ke Dashboard Kegiatan (tidak redundan)
*/
public function index(Request $request)
{
return redirect()->route('admin.kegiatan.jadwal');
}
/**
* Form input absensi
*/
public function inputAbsensi($kegiatan_id)
{
// Get kegiatan dengan relasi kategori dan kelas
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok'])
->where('kegiatan_id', $kegiatan_id)
->firstOrFail();
$tanggal = request('tanggal', now()->format('Y-m-d'));
// Build santri grouped by kelas
$santriGrouped = collect();
$allSantris = Santri::where('status', 'Aktif')
->with(['kelasSantri.kelas', 'kelasPrimary.kelas'])
->orderBy('nama_lengkap')
->get();
if ($kegiatan->isForAllClasses()) {
// Kegiatan umum: group by primary kelas
$santriGrouped = $allSantris->groupBy(function($s) {
$primary = $s->kelasPrimary;
return $primary && $primary->kelas ? $primary->kelas->nama_kelas : 'Tanpa Kelas';
})->sortKeys();
} else {
// Kegiatan khusus: group by kelas yang di-assign ke kegiatan
$placedIds = [];
foreach ($kegiatan->kelasKegiatan as $kelas) {
$santriForKelas = $allSantris->filter(function($s) use ($kelas, &$placedIds) {
if (in_array($s->id_santri, $placedIds)) return false;
return $s->kelasSantri->contains('id_kelas', $kelas->id);
});
foreach ($santriForKelas as $s) {
$placedIds[] = $s->id_santri;
}
if ($santriForKelas->count() > 0) {
$santriGrouped[$kelas->nama_kelas] = $santriForKelas;
}
}
// Santri yang tidak termasuk kelas kegiatan manapun
$santriLainnya = $allSantris->whereNotIn('id_santri', $placedIds);
if ($santriLainnya->count() > 0) {
$santriGrouped['Kelas Lain'] = $santriLainnya;
}
}
// Flatten for total count
$santris = $santriGrouped->flatten()->unique('id_santri');
// Ambil data absensi yang sudah ada
$absensiData = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
->whereDate('tanggal', $tanggal)
->pluck('status', 'id_santri')
->toArray();
// Cek santri yang sedang pulang
$santriSedangPulang = Kepulangan::where('status', 'Disetujui')
->where('tanggal_pulang', '<=', $tanggal)
->where('tanggal_kembali', '>=', $tanggal)
->pluck('id_santri')
->toArray();
// Info kelas kegiatan untuk view
$kegiatanInfo = [
'is_umum' => $kegiatan->isForAllClasses(),
'kelas_list' => $kegiatan->kelasKegiatan->pluck('nama_kelas')->implode(', '),
'jumlah_kelas' => $kegiatan->kelasKegiatan->count(),
];
return view('admin.kegiatan.absensi.input', compact('kegiatan', 'santris', 'santriGrouped', 'absensiData', 'tanggal', 'kegiatanInfo', 'santriSedangPulang'));
}
/**
* Simpan absensi manual (hanya santri yang dikirim form)
*/
public function simpanAbsensi(Request $request)
{
$validated = $request->validate([
'kegiatan_id' => 'required|exists:kegiatans,kegiatan_id',
'tanggal' => 'required|date',
'absensi' => 'nullable|array',
'absensi.*' => 'nullable|in:Hadir,Izin,Sakit,Alpa,Terlambat,Pulang',
]);
// Cek santri yang sedang pulang
$santriSedangPulang = Kepulangan::where('status', 'Disetujui')
->where('tanggal_pulang', '<=', $request->tanggal)
->where('tanggal_kembali', '>=', $request->tanggal)
->pluck('id_santri')
->toArray();
$absensiInput = $request->absensi ?? [];
DB::beginTransaction();
try {
$saved = 0;
foreach ($absensiInput as $id_santri => $status) {
// Skip jika kosong (santri dilewati)
if (empty($status)) {
continue;
}
// Paksa status Pulang untuk santri yang sedang pulang
$finalStatus = in_array($id_santri, $santriSedangPulang) ? 'Pulang' : $status;
AbsensiKegiatan::updateOrCreate(
[
'kegiatan_id' => $request->kegiatan_id,
'id_santri' => $id_santri,
'tanggal' => $request->tanggal,
],
[
'status' => $finalStatus,
'metode_absen' => 'Manual',
'waktu_absen' => now()->format('H:i:s'),
]
);
$saved++;
}
DB::commit();
return redirect()->route('admin.kegiatan.index')
->with('success', "Absensi berhasil disimpan ({$saved} santri).");
} catch (\Exception $e) {
DB::rollBack();
return back()->with('error', 'Gagal menyimpan absensi: ' . $e->getMessage());
}
}
/**
* Edit single absensi record
*/
public function editAbsensi($id)
{
$absensi = AbsensiKegiatan::with(['santri', 'kegiatan.kategori'])->findOrFail($id);
return view('admin.kegiatan.absensi.edit', compact('absensi'));
}
/**
* Update single absensi record
*/
public function updateAbsensi(Request $request, $id)
{
$absensi = AbsensiKegiatan::findOrFail($id);
$validated = $request->validate([
'status' => 'required|in:Hadir,Izin,Sakit,Alpa,Terlambat,Pulang',
]);
$absensi->update([
'status' => $validated['status'],
'waktu_absen' => now()->format('H:i:s'),
]);
return redirect()->route('admin.absensi-kegiatan.rekap', $absensi->kegiatan_id)
->with('success', 'Status absensi ' . $absensi->santri->nama_lengkap . ' berhasil diperbarui.');
}
/**
* Hapus single absensi record
*/
public function hapusAbsensi($id)
{
$absensi = AbsensiKegiatan::findOrFail($id);
$kegiatanId = $absensi->kegiatan_id;
$nama = $absensi->santri->nama_lengkap;
$absensi->delete();
return redirect()->route('admin.absensi-kegiatan.rekap', $kegiatanId)
->with('success', 'Data absensi ' . $nama . ' berhasil dihapus.');
}
/**
* Rekap absensi kegiatan
*/
public function rekapAbsensi(Request $request, $kegiatan_id)
{
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan'])->where('kegiatan_id', $kegiatan_id)->firstOrFail();
$query = AbsensiKegiatan::with(['santri.kelasSantri.kelas'])
->where('kegiatan_id', $kegiatan_id);
// Filter tanggal
if ($request->filled('tanggal')) {
$query->whereDate('tanggal', $request->tanggal);
}
// Filter bulan
if ($request->filled('bulan')) {
$query->whereMonth('tanggal', date('m', strtotime($request->bulan)))
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
}
// Filter kelas
if ($request->filled('kelas_id')) {
$query->whereHas('santri.kelasSantri', function($q) use ($request) {
$q->where('id_kelas', $request->kelas_id);
});
}
$absensis = $query->orderBy('tanggal', 'desc')
->orderBy('waktu_absen', 'desc')
->get();
// Build kelas list for filter dropdown — selalu tampilkan semua kelas aktif
$kelasFilterList = Kelas::active()->ordered()->get();
// Grup per kelas — selalu group by kelas_name santri
$absensiPerKelas = $absensis->groupBy(function ($item) {
return $item->santri->kelas_name ?? 'Belum Ada Kelas';
})->sortKeys();
// Statistik
$statsQuery = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id);
if ($request->filled('tanggal')) {
$statsQuery->whereDate('tanggal', $request->tanggal);
}
if ($request->filled('bulan')) {
$statsQuery->whereMonth('tanggal', date('m', strtotime($request->bulan)))
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
}
if ($request->filled('kelas_id')) {
$statsQuery->whereHas('santri.kelasSantri', function($q) use ($request) {
$q->where('id_kelas', $request->kelas_id);
});
}
$stats = $statsQuery->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->pluck('total', 'status')
->toArray();
// ── Hitung total SEMUA santri aktif ──
$allSantriQuery = Santri::where('status', 'Aktif');
if ($request->filled('kelas_id')) {
$allSantriQuery->whereHas('kelasSantri', function($q) use ($request) {
$q->where('id_kelas', $request->kelas_id);
});
}
$totalSantriEligible = $allSantriQuery->count();
// Hitung santri unik yang sudah tercatat absensi (sesuai filter)
$recordedQuery = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id);
if ($request->filled('tanggal')) {
$recordedQuery->whereDate('tanggal', $request->tanggal);
}
if ($request->filled('bulan')) {
$recordedQuery->whereMonth('tanggal', date('m', strtotime($request->bulan)))
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
}
if ($request->filled('kelas_id')) {
$recordedQuery->whereHas('santri.kelasSantri', function($q) use ($request) {
$q->where('id_kelas', $request->kelas_id);
});
}
$santriSudahAbsen = $recordedQuery->distinct('id_santri')->count('id_santri');
$belumAbsen = max(0, $totalSantriEligible - $santriSudahAbsen);
// Persentase kehadiran berdasarkan total semua santri aktif
$totalRecorded = array_sum($stats);
$hadirCount = ($stats['Hadir'] ?? 0) + ($stats['Terlambat'] ?? 0);
$persenHadir = $totalSantriEligible > 0 ? round($hadirCount / $totalSantriEligible * 100, 1) : 0;
// Daftar santri yang belum absen (selalu ditampilkan)
$santriBelumAbsen = collect();
// Bangun query ID santri yang sudah absen (sesuai filter aktif)
$sudahAbsenQuery = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id);
if ($request->filled('tanggal')) {
$sudahAbsenQuery->whereDate('tanggal', $request->tanggal);
}
if ($request->filled('bulan')) {
$sudahAbsenQuery->whereMonth('tanggal', date('m', strtotime($request->bulan)))
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
}
if ($request->filled('kelas_id')) {
$sudahAbsenQuery->whereHas('santri.kelasSantri', function($q) use ($request) {
$q->where('id_kelas', $request->kelas_id);
});
}
$idSantriSudahAbsen = $sudahAbsenQuery->pluck('id_santri')->unique()->toArray();
$belumQuery = Santri::where('status', 'Aktif');
if ($request->filled('kelas_id')) {
$belumQuery->whereHas('kelasSantri', function($q) use ($request) {
$q->where('id_kelas', $request->kelas_id);
});
}
$santriBelumAbsen = $belumQuery
->whereNotIn('id_santri', $idSantriSudahAbsen)
->with(['kelasPrimary.kelas'])
->orderBy('nama_lengkap')
->get();
return view('admin.kegiatan.absensi.rekap', compact(
'kegiatan', 'absensis', 'absensiPerKelas', 'stats', 'kelasFilterList',
'totalSantriEligible', 'santriSudahAbsen', 'belumAbsen', 'persenHadir',
'totalRecorded', 'hadirCount', 'santriBelumAbsen'
));
}
/**
* Scan RFID (API untuk JavaScript)
*/
public function scanRfid(Request $request)
{
$validated = $request->validate([
'rfid_uid' => 'required|string',
'kegiatan_id' => 'required|exists:kegiatans,kegiatan_id',
'tanggal' => 'required|date',
]);
// Cari santri berdasarkan RFID
$santri = Santri::where('rfid_uid', $request->rfid_uid)
->where('status', 'Aktif')
->first();
if (!$santri) {
return response()->json([
'success' => false,
'message' => 'RFID tidak terdaftar atau santri tidak aktif.'
], 404);
}
// Cek apakah sudah absen hari ini
$existing = AbsensiKegiatan::where('kegiatan_id', $request->kegiatan_id)
->where('id_santri', $santri->id_santri)
->whereDate('tanggal', $request->tanggal)
->first();
if ($existing) {
return response()->json([
'success' => false,
'message' => $santri->nama_lengkap . ' sudah melakukan absensi (' . $existing->status . ').'
], 400);
}
// Simpan absensi
$absensi = AbsensiKegiatan::create([
'kegiatan_id' => $request->kegiatan_id,
'id_santri' => $santri->id_santri,
'tanggal' => $request->tanggal,
'status' => 'Hadir',
'metode_absen' => 'RFID',
'waktu_absen' => now()->format('H:i:s'),
]);
return response()->json([
'success' => true,
'message' => 'Absensi berhasil untuk ' . $santri->nama_lengkap,
'data' => [
'nama' => $santri->nama_lengkap,
'id_santri' => $santri->id_santri,
'kelas' => $santri->kelas,
'waktu' => now()->format('H:i:s'),
]
]);
}
}

View File

@ -1,176 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Berita;
use App\Models\Kelas;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class BeritaController extends Controller
{
/**
* Tampilkan daftar berita
*/
public function index(Request $request)
{
$query = Berita::query();
if ($request->filled('search')) {
$query->search($request->search);
}
if ($request->filled('status')) {
$query->status($request->status);
}
if ($request->filled('target')) {
$query->target($request->target);
}
$berita = $query->orderBy('created_at', 'desc')->paginate(15);
return view('admin.berita.index', compact('berita'));
}
/**
* Tampilkan form create
*/
public function create()
{
$kelasOptions = Kelas::where('is_active', true)->ordered()->get();
return view('admin.berita.create', compact('kelasOptions'));
}
/**
* Simpan berita baru
*/
public function store(Request $request)
{
$validated = $request->validate([
'judul' => 'required|string|max:255',
'konten' => 'required|string',
'penulis' => 'required|string|max:255',
'gambar' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'status' => 'required|in:draft,published',
'target_berita' => 'required|in:semua,kelas_tertentu',
'target_kelas' => 'nullable|array',
'target_kelas.*' => 'exists:kelas,id',
], [
'judul.required' => 'Judul berita wajib diisi',
'konten.required' => 'Konten berita wajib diisi',
'penulis.required' => 'Nama penulis wajib diisi',
'status.required' => 'Status berita wajib dipilih',
'target_berita.required' => 'Target berita wajib dipilih',
]);
// Upload gambar jika ada
if ($request->hasFile('gambar')) {
$validated['gambar'] = $request->file('gambar')->store('berita', 'public');
}
// Konversi target_kelas ke array integer jika kelas_tertentu
if ($validated['target_berita'] === 'kelas_tertentu' && $request->filled('target_kelas')) {
$validated['target_kelas'] = array_map('intval', $request->target_kelas);
} else {
$validated['target_kelas'] = null;
}
Berita::create($validated);
return redirect()->route('admin.berita.index')
->with('success', 'Berita berhasil ditambahkan!');
}
/**
* Tampilkan detail berita
*/
public function show(Berita $berita)
{
return view('admin.berita.show', compact('berita'));
}
/**
* Tampilkan form edit
*/
public function edit(Berita $berita)
{
$kelasOptions = Kelas::where('is_active', true)->ordered()->get();
return view('admin.berita.edit', compact('berita', 'kelasOptions'));
}
/**
* Update berita
*/
public function update(Request $request, Berita $berita)
{
$validated = $request->validate([
'judul' => 'required|string|max:255',
'konten' => 'required|string',
'penulis' => 'required|string|max:255',
'gambar' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'status' => 'required|in:draft,published',
'target_berita' => 'required|in:semua,kelas_tertentu',
'target_kelas' => 'nullable|array',
'target_kelas.*' => 'exists:kelas,id',
]);
// Upload gambar baru jika ada
if ($request->hasFile('gambar')) {
if ($berita->gambar) {
Storage::disk('public')->delete($berita->gambar);
}
$validated['gambar'] = $request->file('gambar')->store('berita', 'public');
}
// Konversi target_kelas
if ($validated['target_berita'] === 'kelas_tertentu' && $request->filled('target_kelas')) {
$validated['target_kelas'] = array_map('intval', $request->target_kelas);
} else {
$validated['target_kelas'] = null;
}
$berita->update($validated);
return redirect()->route('admin.berita.index')
->with('success', 'Berita berhasil diperbarui!');
}
/**
* Hapus berita
*/
public function destroy(Berita $berita)
{
if ($berita->gambar) {
Storage::disk('public')->delete($berita->gambar);
}
$berita->delete();
return redirect()->route('admin.berita.index')
->with('success', 'Berita berhasil dihapus!');
}
/**
* Tampilkan statistik berita
*/
public function statistik()
{
$totalBerita = Berita::count();
$totalPublished = Berita::where('status', 'published')->count();
$totalDraft = Berita::where('status', 'draft')->count();
$beritaSemua = Berita::where('target_berita', 'semua')->count();
$beritaKelas = Berita::where('target_berita', 'kelas_tertentu')->count();
return view('admin.berita.statistik', compact(
'totalBerita',
'totalPublished',
'totalDraft',
'beritaSemua',
'beritaKelas'
));
}
}

View File

@ -1,784 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Capaian;
use App\Models\Santri;
use App\Models\Materi;
use App\Models\Semester;
use App\Models\Kelas;
use App\Models\SantriKelas;
use App\Services\CapaianAccessService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CapaianController extends Controller
{
// =====================================================================
// INDEX — daftar santri + total progress
// =====================================================================
public function index(Request $request)
{
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
$semesterAktif = Semester::aktif()->first();
$selectedKelas = $request->input('id_kelas');
$selectedSemester = $request->input('id_semester', $semesterAktif?->id_semester);
$search = $request->input('search');
$kelasList = Kelas::active()->ordered()->with('kelompok')->get();
$query = Santri::where('status', 'Aktif')
->with(['kelasPrimary.kelas.kelompok']);
if ($selectedKelas) {
$query->kelas($selectedKelas);
}
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('nama_lengkap', 'like', "%{$search}%")
->orWhere('nis', 'like', "%{$search}%");
});
}
$santris = $query->orderBy('nama_lengkap')->get();
$santriData = $santris->map(function ($santri) use ($selectedSemester) {
$capaians = Capaian::where('id_santri', $santri->id_santri)
->when($selectedSemester, fn($q) => $q->where('id_semester', $selectedSemester))
->get();
$capaiansBerisi = $capaians->where('persentase', '>', 0);
$totalProgress = $capaiansBerisi->isEmpty() ? 0 : $capaiansBerisi->avg('persentase');
return [
'santri' => $santri,
'total_progress' => round($totalProgress, 2),
'total_materi' => $capaiansBerisi->count(),
'capaians' => $capaians,
];
})->sortBy('total_progress')->values();
return view('admin.capaian.index', compact(
'santriData', 'semesters', 'kelasList',
'selectedKelas', 'selectedSemester', 'search'
));
}
// =====================================================================
// CREATE / STORE
// =====================================================================
public function create(Request $request)
{
$santris = Santri::aktif()->select('id', 'id_santri', 'nis', 'nama_lengkap')
->with(['kelasPrimary.kelas'])->orderBy('nama_lengkap')->get();
$semesterAktif = Semester::aktif()->first();
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
$selectedSantri = null;
$materiOptions = [];
if ($request->filled('id_santri')) {
$selectedSantri = Santri::where('id_santri', $request->id_santri)
->with(['kelasSantri.kelas'])->first();
if ($selectedSantri) {
$kelasNames = $selectedSantri->kelasSantri
->map(fn($sk) => $sk->kelas?->nama_kelas)->filter()->unique()->toArray();
$materiOptions = Materi::whereIn('kelas', $kelasNames ?: [''])
->orderBy('kategori')->orderBy('nama_kitab')->get();
}
}
return view('admin.capaian.create', compact(
'santris', 'semesters', 'semesterAktif', 'selectedSantri', 'materiOptions'
));
}
public function getMateriByKelas(Request $request)
{
$santri = Santri::where('id_santri', $request->id_santri)
->with(['kelasSantri.kelas'])->first();
if (!$santri) return response()->json(['error' => 'Santri tidak ditemukan'], 404);
$kelasNames = $santri->kelasSantri
->map(fn($sk) => $sk->kelas?->nama_kelas)->filter()->unique()->toArray();
$materis = Materi::whereIn('kelas', $kelasNames ?: [''])
->select('id', 'id_materi', 'kategori', 'nama_kitab', 'halaman_mulai', 'halaman_akhir', 'total_halaman')
->orderBy('kategori')->orderBy('nama_kitab')->get();
return response()->json(['kelas' => $santri->kelas, 'materis' => $materis]);
}
public function getDetailMateri(Request $request)
{
$materi = Materi::where('id_materi', $request->id_materi)->first();
if (!$materi) return response()->json(['error' => 'Materi tidak ditemukan'], 404);
$existingCapaian = null;
if ($request->filled('id_santri') && $request->filled('id_semester')) {
$existingCapaian = Capaian::where('id_santri', $request->id_santri)
->where('id_materi', $request->id_materi)
->where('id_semester', $request->id_semester)->first();
}
return response()->json(['materi' => $materi, 'existing_capaian' => $existingCapaian]);
}
public function store(Request $request)
{
$validated = $request->validate([
'id_santri' => 'required|exists:santris,id_santri',
'id_materi' => 'required|exists:materi,id_materi',
'id_semester' => 'required|exists:semester,id_semester',
'halaman_selesai' => 'required|string',
'catatan' => 'nullable|string',
'tanggal_input' => 'required|date',
]);
$existing = Capaian::where('id_santri', $validated['id_santri'])
->where('id_materi', $validated['id_materi'])
->where('id_semester', $validated['id_semester'])->first();
if ($existing) {
$existing->update([
'halaman_selesai' => $validated['halaman_selesai'],
'catatan' => $validated['catatan'],
'tanggal_input' => $validated['tanggal_input'],
]);
return redirect()->route('admin.capaian.show', $existing)
->with('success', 'Capaian berhasil diperbarui.');
}
$capaian = Capaian::create($validated);
return redirect()->route('admin.capaian.show', $capaian)
->with('success', 'Capaian berhasil ditambahkan.');
}
// =====================================================================
// SHOW / EDIT / UPDATE / DESTROY
// =====================================================================
public function show(Capaian $capaian)
{
$capaian->load(['santri.kelasPrimary.kelas', 'materi', 'semester']);
return view('admin.capaian.show', compact('capaian'));
}
public function edit(Capaian $capaian)
{
$capaian->load(['santri.kelasPrimary.kelas', 'materi', 'semester']);
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
return view('admin.capaian.edit', compact('capaian', 'semesters'));
}
public function update(Request $request, Capaian $capaian)
{
$validated = $request->validate([
'halaman_selesai' => 'required|string',
'catatan' => 'nullable|string',
'tanggal_input' => 'required|date',
]);
$capaian->update($validated);
return redirect()->route('admin.capaian.show', $capaian)
->with('success', 'Capaian berhasil diperbarui.');
}
public function destroy(Capaian $capaian)
{
$santriNama = $capaian->santri->nama_lengkap;
$materiNama = $capaian->materi->nama_kitab;
$capaian->delete();
return redirect()->route('admin.capaian.index')
->with('success', "Capaian {$santriNama} untuk materi {$materiNama} berhasil dihapus.");
}
// =====================================================================
// RIWAYAT SANTRI
// =====================================================================
public function riwayatSantri($id_santri, Request $request)
{
$santri = Santri::where('id_santri', $id_santri)
->with('kelasPrimary.kelas')->firstOrFail();
$query = Capaian::with(['materi', 'semester'])->bySantri($id_santri);
if ($request->filled('id_semester')) $query->bySemester($request->id_semester);
if ($request->filled('search')) {
$search = $request->search;
$query->whereHas('materi', fn($q) => $q->where('nama_kitab', 'like', "%{$search}%"));
}
$capaians = $query->orderBy('created_at', 'desc')->paginate(15)->appends(request()->query());
$totalCapaian = $capaians->total();
$rataRataPersentase = Capaian::bySantri($id_santri)->avg('persentase') ?? 0;
$statistikKategori = Capaian::bySantri($id_santri)
->join('materi', 'capaian.id_materi', '=', 'materi.id_materi')
->select('materi.kategori', DB::raw('AVG(capaian.persentase) as rata_rata'))
->groupBy('materi.kategori')->get()
->pluck('rata_rata', 'kategori')->toArray();
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
return view('admin.capaian.riwayat-santri', compact(
'santri', 'capaians', 'totalCapaian', 'rataRataPersentase', 'statistikKategori', 'semesters'
));
}
// =====================================================================
// CALCULATE PERSENTASE (AJAX)
// =====================================================================
public function calculatePersentase(Request $request)
{
if (empty($request->halaman_selesai) || empty($request->id_materi)) {
return response()->json(['persentase' => 0, 'jumlah' => 0]);
}
try {
$persentase = Capaian::calculatePersentase($request->halaman_selesai, $request->id_materi);
$pages = Capaian::parseHalamanSelesai($request->halaman_selesai);
return response()->json([
'persentase' => number_format($persentase, 2),
'jumlah' => count($pages),
'pages' => $pages,
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
// =====================================================================
// DASHBOARD
// =====================================================================
public function dashboard(Request $request)
{
// --- Filters ---
$kelas = $request->input('kelas');
$idSemester = $request->input('id_semester');
$filterSantri = $request->input('filter_santri', 'all');
$semesterAktif = Semester::aktif()->first();
$selectedSemester = $idSemester ?: ($semesterAktif?->id_semester);
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->orderBy('periode', 'desc')->get();
$allSemestersOrdered = Semester::orderBy('tahun_ajaran')->orderBy('periode')->get();
$materis = Materi::orderBy('kategori')->orderBy('nama_kitab')->get();
// Kelas yang punya santri primary
$primaryKelasIds = SantriKelas::where('is_primary', true)->distinct()->pluck('id_kelas');
$kelasModels = Kelas::active()->whereIn('id', $primaryKelasIds)->ordered()->with('kelompok')->get();
$kelasList = $kelasModels->pluck('nama_kelas')->unique()->values()->toArray();
$santrisAktif = Santri::where('status', 'Aktif')
->with(['kelasPrimary.kelas'])
->when($kelas, fn($q) => $q->primaryKelasByName($kelas))
->orderBy('nama_lengkap')->get();
$santrisKhatam = Santri::where('status', 'Khatam')
->with(['kelasPrimary.kelas'])
->when($kelas, fn($q) => $q->primaryKelasByName($kelas))
->orderBy('nama_lengkap')->get();
// Load semua capaian sekali saja
$allCapaian = Capaian::with(['santri.kelasPrimary.kelas', 'materi', 'semester'])
->when($kelas, fn($q) => $q->whereHas('santri', fn($sq) => $sq->primaryKelasByName($kelas)))
->get();
$filteredCapaian = $selectedSemester
? $allCapaian->where('id_semester', $selectedSemester)
: $allCapaian;
// --- KPI ---
$totalSantriAktif = $santrisAktif->count();
$rataRataProgress = round($filteredCapaian->avg('persentase') ?? 0, 1);
$statistikKategori = [];
foreach (["Al-Qur'an", 'Hadist', 'Materi Tambahan'] as $kat) {
$katCap = $filteredCapaian->filter(fn($c) => $c->materi && $c->materi->kategori === $kat);
$statistikKategori[$kat] = [
'count' => $katCap->count(),
'avg' => round($katCap->avg('persentase') ?? 0, 2),
'selesai' => $katCap->where('persentase', '>=', 100)->count(),
];
}
$distribusiProgress = [
'0-25%' => $filteredCapaian->where('persentase', '>=', 0)->where('persentase', '<=', 25)->count(),
'26-50%' => $filteredCapaian->where('persentase', '>', 25)->where('persentase', '<=', 50)->count(),
'51-75%' => $filteredCapaian->where('persentase', '>', 50)->where('persentase', '<=', 75)->count(),
'76-99%' => $filteredCapaian->where('persentase', '>', 75)->where('persentase', '<', 100)->count(),
'100%' => $filteredCapaian->where('persentase', '>=', 100)->count(),
];
// --- Rekap per kelas (Ranking tab) ---
$rekapKelas = [];
foreach ($kelasList as $k) {
$kelasCapaian = $filteredCapaian->filter(
fn($c) => $c->santri && $c->santri->kelas === $k && $c->santri->status === 'Aktif'
);
$santriIds = $kelasCapaian->pluck('id_santri')->unique();
$ranking = [];
foreach ($santriIds as $sid) {
$sc = $kelasCapaian->where('id_santri', $sid);
$santri = $sc->first()->santri;
$kelasMateris = $materis->where('kelas', $k);
$totalMateriKelas = $kelasMateris->count();
$selesai = $sc->where('persentase', '>=', 100)->count();
$avgProg = round($sc->avg('persentase') ?? 0, 2);
$isFullKhatam = $totalMateriKelas > 0 && $selesai >= $totalMateriKelas;
$ranking[] = [
'santri' => $santri,
'avg_progress' => $avgProg,
'selesai' => $selesai,
'total_materi_kelas' => $totalMateriKelas,
'is_full_khatam' => $isFullKhatam,
'alquran' => round($sc->filter(fn($c) => $c->materi->kategori == "Al-Qur'an")->avg('persentase') ?? 0, 1),
'hadist' => round($sc->filter(fn($c) => $c->materi->kategori == 'Hadist')->avg('persentase') ?? 0, 1),
'tambahan' => round($sc->filter(fn($c) => $c->materi->kategori == 'Materi Tambahan')->avg('persentase') ?? 0, 1),
];
}
usort($ranking, fn($a, $b) => $b['avg_progress'] <=> $a['avg_progress']);
$khatamSantris = Santri::primaryKelasByName($k)->where('status', 'Khatam')->get();
$totalSantri = count($ranking);
$rekapKelas[$k] = [
'ranking' => $ranking,
'khatam' => $khatamSantris,
'total_aktif' => Santri::primaryKelasByName($k)->where('status', 'Aktif')->count(),
'summary' => [
'total_santri' => $totalSantri,
'avg_progress' => $totalSantri > 0 ? round(collect($ranking)->avg('avg_progress'), 1) : 0,
'total_selesai' => collect($ranking)->sum('selesai'),
'santri_tuntas' => collect($ranking)->where('avg_progress', '>=', 100)->count(),
],
];
}
// --- Semester comparison (Line chart) ---
$semesterLabels = $allSemestersOrdered->pluck('nama_semester')->toArray();
$semesterComparison = [];
foreach ($kelasList as $k) {
$dataPoints = [];
foreach ($allSemestersOrdered as $sem) {
$semCap = $allCapaian->where('id_semester', $sem->id_semester)
->filter(fn($c) => $c->santri && $c->santri->kelas === $k);
$dataPoints[] = round($semCap->avg('persentase') ?? 0, 2);
}
$semesterComparison[$k] = $dataPoints;
}
// --- Materi completion rate ---
$filteredMateris = $kelas ? $materis->where('kelas', $kelas) : $materis;
$materiCompletionRate = [];
foreach ($filteredMateris as $materi) {
$rates = [];
foreach ($allSemestersOrdered as $sem) {
$semMatCap = $allCapaian->where('id_materi', $materi->id_materi)
->where('id_semester', $sem->id_semester);
$total = $semMatCap->count();
$selesai = $semMatCap->where('persentase', '>=', 100)->count();
$rates[$sem->id_semester] = $total > 0 ? round(($selesai / $total) * 100, 1) : null;
}
$materiCompletionRate[] = ['materi' => $materi, 'rates' => $rates];
}
// --- Bottleneck ---
$bottleneckMateri = [];
foreach ($filteredMateris as $materi) {
$matCap = $filteredCapaian->where('id_materi', $materi->id_materi);
if ($matCap->isEmpty()) continue;
$totalS = $matCap->count();
$stuckS = $matCap->where('persentase', '<', 50)->count();
$stuckPct = $totalS > 0 ? round(($stuckS / $totalS) * 100, 1) : 0;
$bottleneckMateri[] = [
'materi' => $materi,
'avg_progress' => round($matCap->avg('persentase') ?? 0, 2),
'total_santri' => $totalS,
'stuck_santri' => $stuckS,
'stuck_percentage' => $stuckPct,
];
}
usort($bottleneckMateri, fn($a, $b) => $b['stuck_percentage'] <=> $a['stuck_percentage']);
$bottleneckMateri = array_slice($bottleneckMateri, 0, 10);
// --- Projected Graduation (per kelas, tab Progress Santri) ---
$projectedByKelas = [];
foreach ($santrisAktif as $santri) {
$santriCap = $allCapaian->where('id_santri', $santri->id_santri);
if ($santriCap->isEmpty()) continue;
$progressPerSem = [];
foreach ($allSemestersOrdered as $sem) {
$semCap = $santriCap->where('id_semester', $sem->id_semester);
if ($semCap->isNotEmpty()) {
$progressPerSem[] = [
'sem' => $sem->nama_semester,
'avg' => round($semCap->avg('persentase'), 2),
];
}
}
$currentProgress = round($santriCap->avg('persentase') ?? 0, 2);
$growthRate = 0;
if (count($progressPerSem) >= 2) {
$diffs = [];
for ($i = 1; $i < count($progressPerSem); $i++) {
$diffs[] = $progressPerSem[$i]['avg'] - $progressPerSem[$i - 1]['avg'];
}
$growthRate = count($diffs) > 0 ? round(array_sum($diffs) / count($diffs), 2) : 0;
} elseif (count($progressPerSem) === 1) {
$growthRate = $progressPerSem[0]['avg'];
}
$remaining = 100 - $currentProgress;
$semestersToGrad = ($growthRate > 0 && $currentProgress < 100)
? ceil($remaining / $growthRate)
: ($currentProgress >= 100 ? 0 : null);
$item = [
'santri' => $santri,
'current_progress' => $currentProgress,
'growth_rate' => $growthRate,
'semesters_to_grad' => $semestersToGrad,
'history' => $progressPerSem,
];
$kelasKey = $santri->kelas ?? 'Tanpa Kelas';
$projectedByKelas[$kelasKey][] = $item;
}
foreach ($projectedByKelas as &$kItems) {
usort($kItems, fn($a, $b) => $b['current_progress'] <=> $a['current_progress']);
}
unset($kItems);
// --- Semester Summary Report ---
$semesterSummary = null;
if ($selectedSemester) {
$selectedSem = $semesters->where('id_semester', $selectedSemester)->first();
$semCap = $allCapaian->where('id_semester', $selectedSemester);
$currentIdx = $allSemestersOrdered->search(fn($s) => $s->id_semester === $selectedSemester);
$prevSemester = $currentIdx > 0 ? $allSemestersOrdered[$currentIdx - 1] : null;
$prevSemCap = $prevSemester
? $allCapaian->where('id_semester', $prevSemester->id_semester)
: collect();
$avgProgressSem = $semCap->avg('persentase') ?? 0;
$avgProgressPrev = $prevSemCap->isNotEmpty() ? ($prevSemCap->avg('persentase') ?? 0) : 0;
$santriIds = $semCap->pluck('id_santri')->unique();
$santriFullKhatam = 0;
$santriRemedialCount = 0;
$santriRemedialList = [];
foreach ($santriIds as $sid) {
$sCap = $semCap->where('id_santri', $sid);
if ($sCap->every(fn($c) => $c->persentase >= 100)) $santriFullKhatam++;
if (($sCap->avg('persentase') ?? 0) < 30) {
$santriRemedialCount++;
$s = $santrisAktif->where('id_santri', $sid)->first();
if ($s) $santriRemedialList[] = $s;
}
}
$materiKhatamList = $semCap->where('persentase', '>=', 100)
->groupBy('id_materi')
->map(fn($g) => ['count' => $g->count(), 'materi' => $g->first()->materi])
->sortByDesc('count')->take(5)->values();
$materiMinList = $semCap->groupBy('id_materi')
->map(fn($g) => ['avg' => round($g->avg('persentase'), 2), 'materi' => $g->first()->materi])
->sortBy('avg')->take(5)->values();
$semesterSummary = [
'semester' => $selectedSem,
'prev_semester' => $prevSemester,
'total_santri' => $santriIds->count(),
'avg_progress' => round($avgProgressSem, 2),
'avg_progress_prev' => round($avgProgressPrev, 2),
'kenaikan' => round($avgProgressSem - $avgProgressPrev, 2),
'santri_khatam' => $santriFullKhatam,
'santri_remedial_count' => $santriRemedialCount,
'santri_remedial' => $santriRemedialList,
'materi_khatam' => $materiKhatamList,
'materi_min' => $materiMinList,
];
}
// --- Santri Ringkasan (overview tab) ---
$santriProgressList = [];
foreach ($santrisAktif as $santri) {
$sc = $filteredCapaian->where('id_santri', $santri->id_santri);
$avg = $sc->isNotEmpty() ? round($sc->avg('persentase'), 1) : 0;
$santriProgressList[] = ['santri' => $santri, 'avg' => $avg];
}
usort($santriProgressList, fn($a, $b) => $b['avg'] <=> $a['avg']);
$DISPLAY_LIMIT = 8;
$santriRingkasan = match ($filterSantri) {
'top' => array_slice($santriProgressList, 0, $DISPLAY_LIMIT),
'perhatian' => array_slice(array_reverse($santriProgressList), 0, $DISPLAY_LIMIT),
default => array_slice($santriProgressList, 0, $DISPLAY_LIMIT),
};
$totalSantriFiltered = count($santriProgressList);
// --- Status akses input capaian santri ---
$capaianAccessOpen = CapaianAccessService::isOpen();
$capaianAccessConfig = CapaianAccessService::getConfig();
return view('admin.capaian.dashboard', compact(
'semesters', 'allSemestersOrdered', 'selectedSemester', 'semesterAktif',
'kelas', 'kelasList', 'kelasModels', 'santrisAktif', 'santrisKhatam', 'materis',
'totalSantriAktif', 'rataRataProgress',
'statistikKategori', 'distribusiProgress',
'rekapKelas',
'semesterLabels', 'semesterComparison',
'materiCompletionRate',
'bottleneckMateri',
'projectedByKelas',
'semesterSummary',
'santriRingkasan', 'totalSantriFiltered', 'filterSantri',
'capaianAccessOpen', 'capaianAccessConfig'
));
}
// =====================================================================
// TANDAI / BATAL KHATAM — FIX: DB::table agar tidak truncate enum
// =====================================================================
public function tandaiKhatam($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
DB::table('santris')
->where('id', $santri->id)
->update(['status' => 'Khatam', 'updated_at' => now()]);
return redirect()->back()
->with('success', "Santri {$santri->nama_lengkap} berhasil ditandai sebagai Khatam.");
}
public function batalKhatam($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
DB::table('santris')
->where('id', $santri->id)
->update(['status' => 'Aktif', 'updated_at' => now()]);
return redirect()->back()
->with('success', "Status Khatam santri {$santri->nama_lengkap} berhasil dibatalkan.");
}
// =====================================================================
// EXPORT RAPOR
// =====================================================================
public function exportRapor($id_santri, $id_semester)
{
$santri = Santri::where('id_santri', $id_santri)->with('kelasPrimary.kelas')->firstOrFail();
$semester = Semester::where('id_semester', $id_semester)->firstOrFail();
$capaians = Capaian::where('id_santri', $id_santri)
->where('id_semester', $id_semester)
->with('materi')->orderBy('created_at')->get();
$allSem = Semester::orderBy('tahun_ajaran')->orderBy('periode')->get();
$curIdx = $allSem->search(fn($s) => $s->id_semester === $id_semester);
$prevSemester = $curIdx > 0 ? $allSem[$curIdx - 1] : null;
$prevCapaians = $prevSemester
? Capaian::where('id_santri', $id_santri)
->where('id_semester', $prevSemester->id_semester)
->with('materi')->get()
: collect();
$avgProgress = $capaians->avg('persentase') ?? 0;
$avgPrev = $prevCapaians->avg('persentase') ?? 0;
$selesai = $capaians->where('persentase', '>=', 100)->count();
$totalMateri = $capaians->count();
$perKategori = [];
foreach (["Al-Qur'an", 'Hadist', 'Materi Tambahan'] as $kat) {
$katCap = $capaians->filter(fn($c) => $c->materi && $c->materi->kategori === $kat);
$katPrev = $prevCapaians->filter(fn($c) => $c->materi && $c->materi->kategori === $kat);
$perKategori[$kat] = [
'avg' => round($katCap->avg('persentase') ?? 0, 2),
'prev' => round($katPrev->avg('persentase') ?? 0, 2),
'count' => $katCap->count(),
'selesai' => $katCap->where('persentase', '>=', 100)->count(),
];
}
return view('admin.capaian.export-rapor', compact(
'santri', 'semester', 'capaians', 'prevSemester', 'prevCapaians',
'avgProgress', 'avgPrev', 'selesai', 'totalMateri', 'perKategori'
));
}
// =====================================================================
// DETAIL MATERI
// =====================================================================
public function detailMateri($id_materi, Request $request)
{
$materi = Materi::where('id_materi', $id_materi)->firstOrFail();
$semesterAktif = Semester::aktif()->first();
$selectedSemester = $request->input('id_semester', $semesterAktif?->id_semester);
$capaians = Capaian::where('id_materi', $id_materi)
->when($selectedSemester, fn($q) => $q->where('id_semester', $selectedSemester))
->with(['santri.kelasPrimary.kelas', 'semester'])
->orderBy('persentase', 'desc')->get();
$totalSantri = $capaians->count();
$rataRataPersentase = $capaians->avg('persentase') ?? 0;
$santriSelesai = $capaians->where('persentase', '>=', 100)->count();
$santriMulai = $capaians->where('persentase', '>', 0)->where('persentase', '<', 100)->count();
$distribusi = [
'0-25%' => $capaians->whereBetween('persentase', [0, 25])->count(),
'26-50%' => $capaians->whereBetween('persentase', [26, 50])->count(),
'51-75%' => $capaians->whereBetween('persentase', [51, 75])->count(),
'76-99%' => $capaians->whereBetween('persentase', [76, 99])->count(),
'100%' => $capaians->where('persentase', '>=', 100)->count(),
];
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
return view('admin.capaian.detail-materi', compact(
'materi', 'capaians', 'totalSantri', 'rataRataPersentase',
'santriSelesai', 'santriMulai', 'distribusi', 'semesters', 'selectedSemester'
));
}
// =====================================================================
// API GRAFIK (AJAX)
// =====================================================================
public function apiGrafikData(Request $request)
{
$type = $request->input('type', 'kategori');
$idSemester = $request->input('id_semester');
$kelas = $request->input('kelas');
$query = Capaian::with(['santri', 'materi']);
if ($idSemester) $query->bySemester($idSemester);
if ($kelas) {
$query->whereHas('santri', fn($q) => $q->kelasByName($kelas));
}
$data = [];
switch ($type) {
case 'kategori':
$data = [
'labels' => ["Al-Qur'an", 'Hadist', 'Materi Tambahan'],
'datasets' => [[
'label' => 'Rata-rata Progress (%)',
'data' => [
$query->clone()->byKategori("Al-Qur'an")->avg('persentase') ?? 0,
$query->clone()->byKategori('Hadist')->avg('persentase') ?? 0,
$query->clone()->byKategori('Materi Tambahan')->avg('persentase') ?? 0,
],
'backgroundColor' => ['rgba(111,186,157,0.8)', 'rgba(129,198,232,0.8)', 'rgba(255,213,107,0.8)'],
]]
];
break;
case 'distribusi':
$capaians = $query->get();
$data = [
'labels' => ['0-25%', '26-50%', '51-75%', '76-99%', '100%'],
'datasets' => [[
'label' => 'Jumlah Santri',
'data' => [
$capaians->whereBetween('persentase', [0, 25])->count(),
$capaians->whereBetween('persentase', [26, 50])->count(),
$capaians->whereBetween('persentase', [51, 75])->count(),
$capaians->whereBetween('persentase', [76, 99])->count(),
$capaians->where('persentase', '>=', 100)->count(),
],
'backgroundColor' => [
'rgba(255,139,148,0.8)', 'rgba(255,171,145,0.8)',
'rgba(255,213,107,0.8)', 'rgba(129,198,232,0.8)',
'rgba(111,186,157,0.8)',
],
]]
];
break;
case 'trend':
$semesters = Semester::orderBy('tahun_ajaran')->orderBy('periode')->get();
$labels = [];
$dataPoints = [];
foreach ($semesters as $semester) {
$labels[] = $semester->nama_semester;
$dataPoints[] = round(
Capaian::where('id_semester', $semester->id_semester)
->when($kelas, fn($q) => $q->whereHas('santri', fn($sq) => $sq->kelasByName($kelas)))
->avg('persentase') ?? 0, 2
);
}
$data = [
'labels' => $labels,
'datasets' => [[
'label' => 'Rata-rata Progress (%)',
'data' => $dataPoints,
'borderColor' => 'rgba(111,186,157,1)',
'backgroundColor' => 'rgba(111,186,157,0.2)',
'tension' => 0.4,
]]
];
break;
}
return response()->json($data);
}
// =====================================================================
// KELOLA AKSES INPUT CAPAIAN OLEH SANTRI
// =====================================================================
/**
* Halaman pengaturan akses input capaian santri.
* GET /admin/capaian/akses-santri
*/
public function kelolaAksesSantri()
{
$config = CapaianAccessService::getConfig();
$isOpen = CapaianAccessService::isOpen();
$sisaWaktu = CapaianAccessService::getSisaWaktu();
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
$semesterAktif = Semester::aktif()->first();
return view('admin.capaian.akses-santri', compact(
'config', 'isOpen', 'sisaWaktu', 'semesters', 'semesterAktif'
));
}
/**
* Buka akses input capaian untuk santri.
* POST /admin/capaian/akses-santri/buka
*/
public function bukaAksesSantri(Request $request)
{
$request->validate([
'id_semester' => 'nullable|exists:semester,id_semester',
'durasi_jam' => 'nullable|integer|min:1|max:720',
'catatan' => 'nullable|string|max:255',
]);
CapaianAccessService::open([
'opened_by' => auth()->user()->name,
'id_semester' => $request->id_semester,
'durasi_jam' => $request->durasi_jam,
'catatan' => $request->catatan,
]);
return redirect()->route('admin.capaian.akses-santri')
->with('success', 'Akses input capaian untuk santri berhasil dibuka.');
}
/**
* Tutup akses input capaian.
* POST /admin/capaian/akses-santri/tutup
*/
public function tutupAksesSantri()
{
CapaianAccessService::close();
return redirect()->route('admin.capaian.akses-santri')
->with('success', 'Akses input capaian untuk santri berhasil ditutup.');
}
}

View File

@ -1,425 +0,0 @@
<?php
// app/Http/Controllers/Admin/ImportMesinController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AbsensiKegiatan;
use App\Models\ImportMesinLog;
use App\Models\Kegiatan;
use App\Models\Kepulangan;
use App\Models\MesinSantriMapping;
use App\Services\EpposGLogParser;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ImportMesinController extends Controller
{
public function __construct(private EpposGLogParser $parser) {}
// ──────────────────────────────────────────────────────────
// INDEX
// ──────────────────────────────────────────────────────────
public function index()
{
$belumMapping = MesinSantriMapping::where('is_active', true)
->where(function ($q) {
$q->whereNull('id_santri')->orWhere('id_santri', '');
})->count();
$riwayat = ImportMesinLog::with('user')->latest()->take(10)->get();
return view('admin.mesin.import.index', compact('belumMapping', 'riwayat'));
}
// ──────────────────────────────────────────────────────────
// PREVIEW — POST
// Proses file GLog, simpan hasil ke session, redirect ke
// showPreview (GET). PRG pattern agar refresh tidak error.
// ──────────────────────────────────────────────────────────
public function preview(Request $request)
{
$request->validate([
'file_glog' => 'required|file|max:20480',
'tol_sebelum' => 'nullable|integer|min:0|max:60',
'tol_sesudah' => 'nullable|integer|min:0|max:60',
'isi_alpa' => 'nullable',
'conflict_strategy' => 'nullable|in:mesin,exist,manual',
]);
// ── Baca toleransi PERSIS dari form (bukan hardcode) ──
// Jika field tidak dikirim (misal: disable JS), fallback ke nilai
// default form (15 dan 10) yang sama persis dengan default di index.blade.php
$tolSebelum = (int)($request->input('tol_sebelum', 15));
$tolSesudah = (int)($request->input('tol_sesudah', 10));
$isiAlpa = $request->has('isi_alpa');
$conflictStrategy = $request->input('conflict_strategy', 'mesin');
// ── Parse GLog ────────────────────────────────────────
try {
$glogRecords = $this->parser->parseGLog(
$request->file('file_glog')->getPathname()
);
} catch (\Throwable $e) {
return back()->with('error', 'Gagal membaca file GLog: ' . $e->getMessage());
}
if (empty($glogRecords)) {
return back()->with('error',
'File GLog tidak mengandung data scan yang valid. ' .
'Pastikan file yang diupload benar (format GLog dari Eppos).'
);
}
// ── Bangun infoData dari mapping yang sudah ada ───────
$mappingAll = MesinSantriMapping::where('is_active', true)->get();
$infoData = ['shifts' => [], 'jadwal' => []];
foreach ($mappingAll as $m) {
$infoData['jadwal'][$m->id_mesin] = [
'nama' => $m->nama_mesin ?? '',
'dept' => $m->dept_mesin ?? '',
'shift' => 1,
];
}
// ── Kegiatan dari DB ──────────────────────────────────
$kegiatans = Kegiatan::orderBy('hari')->orderBy('waktu_mulai')
->get()
->map(function ($k) {
$rawMulai = $k->getRawOriginal('waktu_mulai');
$rawSelesai = $k->getRawOriginal('waktu_selesai');
$mulai = $rawMulai ? substr($rawMulai, 0, 5) : '00:00';
$selesai = $rawSelesai ? substr($rawSelesai, 0, 5) : $mulai;
return [
'kegiatan_id' => $k->kegiatan_id,
'nama' => $k->nama_kegiatan,
'hari' => $k->hari,
'waktu_mulai' => $mulai,
'waktu_selesai' => $selesai,
];
})->toArray();
if (empty($kegiatans)) {
return back()->with('error',
'Tidak ada kegiatan tersimpan di database. ' .
'Tambahkan kegiatan terlebih dahulu di menu Kegiatan.'
);
}
// ── Match scan ke kegiatan (pakai toleransi dari form) ─
$glogGrouped = $this->parser->groupGLogByDay($glogRecords);
$rawHasil = $this->parser->matchToKegiatan(
$glogGrouped,
$infoData,
$kegiatans,
$tolSebelum, // ← dari form, bukan hardcode
$tolSesudah // ← dari form, bukan hardcode
);
// ── Enrich: santri web + kepulangan + deteksi konflik ─
$kepulanganCache = [];
$hasilEnriched = [];
foreach ($rawHasil as $dayData) {
$tanggal = $dayData['tanggal'];
$idMesin = $dayData['id_mesin'];
$mapping = MesinSantriMapping::where('id_mesin', $idMesin)
->where('is_active', true)
->with('santri')
->first();
$idSantri = $mapping?->santri?->id_santri;
$namaWeb = $mapping?->santri?->nama_lengkap;
$kelas = $mapping?->santri?->kelasPrimary?->kelas?->nama_kelas ?? '-';
// Cache kepulangan per tanggal
if (!isset($kepulanganCache[$tanggal])) {
$kepulanganCache[$tanggal] = Kepulangan::where('status', 'Disetujui')
->where('tanggal_pulang', '<=', $tanggal)
->where('tanggal_kembali', '>=', $tanggal)
->pluck('id_santri')->toArray();
}
$isPulang = $idSantri && in_array($idSantri, $kepulanganCache[$tanggal]);
$rows = array_map(
function ($row) use ($idSantri, $tanggal, $isPulang, $isiAlpa) {
$statusFinal = $isPulang ? 'Pulang' : $row['status'];
if (!$isiAlpa && $statusFinal === 'Alpa' && !$row['matched']) {
$statusFinal = null;
}
$existing = null;
$isConflict = false;
if ($idSantri) {
$rec = AbsensiKegiatan::where('kegiatan_id', $row['kegiatan_id'])
->where('id_santri', $idSantri)
->whereDate('tanggal', $tanggal)
->first();
if ($rec) {
$rawWaktu = $rec->getRawOriginal('waktu_absen');
$existing = [
'status' => $rec->status,
'waktu' => $rawWaktu ? substr($rawWaktu, 0, 5) : null,
'metode' => $rec->metode_absen ?? 'Manual',
];
if (!$row['matched'] && $statusFinal === 'Alpa') {
// Tidak ada scan = tidak override data manual
$statusFinal = $rec->status;
$isConflict = false;
} else {
$isConflict = ($rec->metode_absen !== 'Import_Mesin')
&& ($rec->status !== $statusFinal)
&& $statusFinal !== null;
}
}
}
return array_merge($row, [
'status_final' => $statusFinal,
'existing' => $existing,
'is_conflict' => $isConflict,
]);
},
$dayData['rows']
);
$hasilEnriched[] = array_merge($dayData, [
'id_santri' => $idSantri,
'nama_web' => $namaWeb,
'kelas' => $kelas,
'match_status' => $mapping
? ($idSantri ? 'OK' : 'NO_SANTRI')
: 'NOT_MAPPED',
'is_pulang' => $isPulang,
'rows' => $rows,
]);
}
// Urutkan: tanggal → nama
usort($hasilEnriched, fn($a, $b) =>
[$a['tanggal'], $a['nama_web'] ?? $a['nama_mesin']]
<=> [$b['tanggal'], $b['nama_web'] ?? $b['nama_mesin']]
);
// ── Simpan ke session lalu redirect (PRG pattern) ─────
session([
'eppos_hasil' => $hasilEnriched,
'tol_sebelum' => $tolSebelum, // nilai dari form
'tol_sesudah' => $tolSesudah, // nilai dari form
'isi_alpa' => $isiAlpa,
'conflict_strategy' => $conflictStrategy,
]);
return redirect()->route('admin.mesin.import.show-preview');
}
// ──────────────────────────────────────────────────────────
// SHOW PREVIEW — GET (aman di-refresh)
// ──────────────────────────────────────────────────────────
public function showPreview()
{
$hasilEnriched = session('eppos_hasil');
if (empty($hasilEnriched)) {
return redirect()->route('admin.mesin.import.index')
->with('error', 'Tidak ada data preview. Silakan upload file GLog terlebih dahulu.');
}
// Ambil toleransi dari session (nilai yang dipakai saat matching)
$tolSebelum = session('tol_sebelum', 15);
$tolSesudah = session('tol_sesudah', 10);
$isiAlpa = session('isi_alpa', true);
$conflictStrategy = session('conflict_strategy', 'mesin');
$tanggalList = array_unique(array_column($hasilEnriched, 'tanggal'));
sort($tanggalList);
$debugScans = [];
foreach ($hasilEnriched as $h) {
if (!empty($h['unmatched_scans'])) {
$debugScans[] = [
'nama' => $h['nama_web'] ?? $h['nama_mesin'],
'tanggal' => $h['tanggal'],
'id_mesin' => $h['id_mesin'],
'scans' => $h['all_scans'],
'unmatched' => $h['unmatched_scans'],
];
}
}
$allRows = collect($hasilEnriched)->flatMap(fn($h) => $h['rows']);
$stats = [
'total_santri' => count($hasilEnriched),
'ok' => collect($hasilEnriched)->where('match_status', 'OK')->count(),
'not_mapped' => collect($hasilEnriched)->where('match_status', 'NOT_MAPPED')->count(),
'hadir' => $allRows->where('status_final', 'Hadir')->count(),
'terlambat' => $allRows->where('status_final', 'Terlambat')->count(),
'alpa' => $allRows->where('status_final', 'Alpa')->count(),
'konflik' => $allRows->where('is_conflict', true)->count(),
];
return view('admin.mesin.import.preview', compact(
'hasilEnriched', 'tanggalList', 'stats',
'tolSebelum', 'tolSesudah', 'isiAlpa',
'debugScans', 'conflictStrategy'
));
}
// ──────────────────────────────────────────────────────────
// STORE — simpan ke database
// ──────────────────────────────────────────────────────────
public function store(Request $request)
{
$hasilEnriched = session('eppos_hasil', []);
if (empty($hasilEnriched)) {
return redirect()->route('admin.mesin.import.index')
->with('error', 'Sesi expired. Silakan upload ulang file GLog.');
}
$bulkStrategy = $request->input('conflict_strategy', 'manual');
$choices = $request->input('conflict_choices', []);
$counters = [
'created' => 0,
'updated' => 0,
'kept' => 0,
'skipped' => 0,
'no_santri' => 0,
'null_skip' => 0,
];
DB::beginTransaction();
try {
foreach ($hasilEnriched as $dayData) {
if (!$dayData['id_santri']) {
$counters['no_santri']++;
continue;
}
foreach ($dayData['rows'] as $row) {
// Status null = tidak perlu disimpan
if ($row['status_final'] === null) {
$counters['null_skip']++;
continue;
}
// Alpa tanpa scan + sudah ada data existing → pertahankan
if (!$row['matched'] && $row['status_final'] === 'Alpa'
&& !empty($row['existing'])) {
$counters['skipped']++;
continue;
}
// Status sama dengan existing → tidak perlu update
if (!$row['matched'] && !empty($row['existing'])
&& $row['status_final'] === $row['existing']['status']) {
$counters['skipped']++;
continue;
}
$key = "{$row['kegiatan_id']}_{$dayData['id_santri']}_{$dayData['tanggal']}";
$hasExisting = !empty($row['existing']);
$isConflict = $row['is_conflict'] ?? false;
if (!$hasExisting) {
// Belum ada data → buat baru
AbsensiKegiatan::create([
'kegiatan_id' => $row['kegiatan_id'],
'id_santri' => $dayData['id_santri'],
'tanggal' => $dayData['tanggal'],
'status' => $row['status_final'],
'metode_absen' => 'Import_Mesin',
'waktu_absen' => $row['jam_scan']
? Carbon::parse(
$dayData['tanggal'] . ' ' . $row['jam_scan']
)->format('H:i:s')
: Carbon::parse($dayData['tanggal'])->format('H:i:s'),
]);
$counters['created']++;
continue;
}
// Ada existing tapi tidak konflik → skip
if (!$isConflict) {
$counters['skipped']++;
continue;
}
// Ada konflik → cek strategi
$choice = ($bulkStrategy !== 'manual')
? $bulkStrategy
: ($choices[$key] ?? null);
if ($choice === 'mesin') {
AbsensiKegiatan::where('kegiatan_id', $row['kegiatan_id'])
->where('id_santri', $dayData['id_santri'])
->whereDate('tanggal', $dayData['tanggal'])
->update([
'status' => $row['status_final'],
'metode_absen' => 'Import_Mesin',
'waktu_absen' => $row['jam_scan']
? Carbon::parse(
$dayData['tanggal'] . ' ' . $row['jam_scan']
)->format('H:i:s')
: null,
'konflik_catatan' => 'Ditimpa import mesin '
. now()->format('d/m/Y H:i')
. ' (sebelumnya: '
. $row['existing']['status']
. ' via '
. ($row['existing']['metode'] ?? 'Manual')
. ')',
]);
$counters['updated']++;
} else {
// Pertahankan data lama
$counters['kept']++;
}
}
}
// Catat ke log
ImportMesinLog::create([
'user_id' => auth()->id(),
'jumlah_scan' => collect($hasilEnriched)
->flatMap(fn($h) => $h['all_scans'])->count(),
'berhasil' => $counters['created'],
'konflik_selesai' => $counters['updated'],
'dilewati' => $counters['skipped'],
'no_santri' => $counters['no_santri'],
]);
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
return back()->with('error', 'Import gagal: ' . $e->getMessage());
}
// Hapus session setelah berhasil
session()->forget([
'eppos_hasil', 'tol_sebelum', 'tol_sesudah',
'isi_alpa', 'conflict_strategy',
]);
$msg = "Import selesai! "
. "{$counters['created']} data baru tersimpan, "
. "{$counters['updated']} konflik (pilih mesin), "
. "{$counters['kept']} konflik (pertahankan data lama), "
. "{$counters['skipped']} duplikat dilewati.";
if ($counters['no_santri'] > 0) {
$msg .= " | {$counters['no_santri']} santri belum ada mapping (tidak tersimpan).";
}
return redirect()->route('admin.riwayat-kegiatan.index')
->with('success', $msg);
}
}

View File

@ -1,208 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Santri;
use Illuminate\Http\Request;
use Mpdf\Mpdf;
use Mpdf\Config\ConfigVariables;
use Mpdf\Config\FontVariables;
class KartuRfidController extends Controller
{
public function index(Request $request)
{
$query = Santri::where('status', 'Aktif');
if ($request->filled('filter')) {
if ($request->filter == 'ada_rfid') {
$query->whereNotNull('rfid_uid');
} elseif ($request->filter == 'belum_rfid') {
$query->whereNull('rfid_uid');
}
}
$santris = $query
->select('id', 'id_santri', 'nis', 'nama_lengkap', 'rfid_uid', 'foto', 'status')
->with(['kelasSantri.kelas'])
->orderBy('nama_lengkap')
->paginate(15);
return view('admin.kegiatan.kartu.index', compact('santris'));
}
public function daftarRfid($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
return view('admin.kegiatan.kartu.daftar', compact('santri'));
}
public function simpanRfid(Request $request, $id_santri)
{
$request->validate([
'rfid_uid' => 'required|string|max:50|unique:santris,rfid_uid',
], [
'rfid_uid.required' => 'UID RFID wajib diisi.',
'rfid_uid.unique' => 'UID RFID ini sudah terdaftar pada santri lain.',
]);
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
$santri->update(['rfid_uid' => $request->rfid_uid]);
return redirect()->route('admin.kartu-rfid.index')
->with('success', 'RFID berhasil didaftarkan untuk ' . $santri->nama_lengkap);
}
public function hapusRfid($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
$santri->update(['rfid_uid' => null]);
return redirect()->route('admin.kartu-rfid.index')
->with('success', 'RFID berhasil dihapus dari ' . $santri->nama_lengkap);
}
public function cetakKartu($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)
->with([
'kelasPrimary.kelas',
'kelasSantri' => fn($q) => $q->orderByDesc('is_primary')->orderBy('id'),
'kelasSantri.kelas',
])
->firstOrFail();
if (!$santri->rfid_uid) {
return back()->with('error', 'Santri belum memiliki RFID yang terdaftar.');
}
// ── Siapkan data untuk view ──────────────────────────────────────
$namaSantri = strtoupper($santri->nama_lengkap ?? 'NAMA SANTRI');
// Potong nama max 28 karakter agar muat di kartu
if (mb_strlen($namaSantri) > 28) {
$namaSantri = mb_substr($namaSantri, 0, 27) . '…';
}
$initial = strtoupper(substr($santri->nama_lengkap ?? 'S', 0, 1));
$nis = !empty($santri->nis) ? $santri->nis : '-';
$uid = !empty($santri->rfid_uid) ? $santri->rfid_uid : '-';
// Kelas: pakai kelasPrimary, fallback ke first kelasSantri
$kelasNama = '-';
if ($santri->kelasPrimary && $santri->kelasPrimary->kelas) {
$kelasNama = strtoupper($santri->kelasPrimary->kelas->nama_kelas);
} elseif ($santri->kelasSantri->first() && $santri->kelasSantri->first()->kelas) {
$kelasNama = strtoupper($santri->kelasSantri->first()->kelas->nama_kelas);
}
// Logo — embed base64 (tidak butuh GD)
$logoBase64 = '';
$logoMime = 'image/png';
foreach ([
public_path('images/logo.png'),
public_path('images/logo.jpg'),
public_path('img/logo.png'),
public_path('logo.png'),
] as $lp) {
if (file_exists($lp)) {
$ext = strtolower(pathinfo($lp, PATHINFO_EXTENSION));
$logoMime = $ext === 'png' ? 'image/png' : 'image/jpeg';
$logoBase64 = base64_encode(file_get_contents($lp));
break;
}
}
// Foto santri — resize ke ukuran kartu lalu embed base64
$fotoBase64 = '';
$fotoMime = 'image/jpeg';
if (!empty($santri->foto)) {
foreach ([
storage_path('app/public/' . $santri->foto),
public_path('storage/' . $santri->foto),
public_path($santri->foto),
] as $fp) {
if (file_exists($fp)) {
$ext = strtolower(pathinfo($fp, PATHINFO_EXTENSION));
$fotoMime = in_array($ext, ['png', 'gif', 'webp']) ? 'image/' . $ext : 'image/jpeg';
// Resize agar base64 tidak terlalu besar (max 400×400)
if (extension_loaded('gd')) {
$imgData = file_get_contents($fp);
$src = @imagecreatefromstring($imgData);
if ($src) {
$origW = imagesx($src);
$origH = imagesy($src);
$max = 400;
if ($origW > $max || $origH > $max) {
$ratio = min($max / $origW, $max / $origH);
$newW = (int) round($origW * $ratio);
$newH = (int) round($origH * $ratio);
$dst = imagecreatetruecolor($newW, $newH);
imagecopyresampled($dst, $src, 0, 0, 0, 0, $newW, $newH, $origW, $origH);
imagedestroy($src);
ob_start();
imagejpeg($dst, null, 80);
$resized = ob_get_clean();
imagedestroy($dst);
$fotoBase64 = base64_encode($resized);
$fotoMime = 'image/jpeg';
} else {
imagedestroy($src);
$fotoBase64 = base64_encode($imgData);
}
} else {
$fotoBase64 = base64_encode($imgData);
}
} else {
$fotoBase64 = base64_encode(file_get_contents($fp));
}
break;
}
}
}
// ── Render HTML dari blade ────────────────────────────────────────
$html = view('admin.kegiatan.kartu.cetak', compact(
'santri',
'namaSantri', 'initial', 'nis', 'uid', 'kelasNama',
'logoBase64', 'logoMime',
'fotoBase64', 'fotoMime'
))->render();
// ── Inisialisasi mPDF ─────────────────────────────────────────────
// Format: 54mm × 85.6mm (ukuran kartu ID standar)
$mpdf = new Mpdf([
'mode' => 'utf-8',
'format' => [54, 85.6],
'orientation' => 'P',
'margin_top' => 0,
'margin_bottom' => 0,
'margin_left' => 0,
'margin_right' => 0,
'margin_header' => 0,
'margin_footer' => 0,
'default_font' => 'dejavusans',
'tempDir' => storage_path('app/mpdf_tmp'),
'autoScriptToLang' => false,
'autoLangToFont' => false,
// Aktifkan dukungan SVG (untuk foto bulat)
'enableImports' => true,
]);
// Naikkan limit regex agar mPDF tidak error pada HTML besar
$prevLimit = ini_get('pcre.backtrack_limit');
ini_set('pcre.backtrack_limit', '5000000');
// Matikan page break otomatis
$mpdf->SetAutoPageBreak(false);
$mpdf->SetDisplayMode('fullpage');
$mpdf->WriteHTML($html);
// Kembalikan limit semula
ini_set('pcre.backtrack_limit', $prevLimit);
return response($mpdf->Output('Kartu_RFID_' . $santri->id_santri . '.pdf', 'S'))
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'inline; filename="Kartu_RFID_' . $santri->id_santri . '.pdf"');
}
}

View File

@ -1,119 +0,0 @@
<?php
// app/Http/Controllers/admin/KategoriKegiatanController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\KategoriKegiatan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class KategoriKegiatanController extends Controller
{
/**
* Tampilkan daftar kategori kegiatan
*/
public function index(Request $request)
{
$query = KategoriKegiatan::query();
// Search
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('nama_kategori', 'like', "%{$search}%")
->orWhere('kategori_id', 'like', "%{$search}%")
->orWhere('keterangan', 'like', "%{$search}%");
});
}
$kategoris = $query->select('id', 'kategori_id', 'nama_kategori', 'keterangan', 'created_at')
->orderBy('created_at', 'desc')
->paginate(10)
->appends(request()->query());
return view('admin.kegiatan.kategori.index', compact('kategoris'));
}
/**
* Form tambah kategori
*/
public function create()
{
// Preview ID berikutnya
$nextId = Cache::remember('next_kategori_id', 60, function () {
$last = KategoriKegiatan::select('kategori_id')->orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->kategori_id, 2)) + 1 : 1;
return 'KT' . str_pad($num, 3, '0', STR_PAD_LEFT);
});
return view('admin.kegiatan.kategori.create', compact('nextId'));
}
/**
* Simpan kategori baru
*/
public function store(Request $request)
{
$validated = $request->validate([
'nama_kategori' => 'required|string|max:100|unique:kategori_kegiatans,nama_kategori',
'keterangan' => 'nullable|string',
], [
'nama_kategori.required' => 'Nama kategori wajib diisi.',
'nama_kategori.unique' => 'Nama kategori sudah digunakan.',
]);
KategoriKegiatan::create($validated);
Cache::forget('next_kategori_id');
return redirect()->route('admin.kategori-kegiatan.index')
->with('success', 'Kategori kegiatan berhasil ditambahkan.');
}
/**
* Tampilkan detail kategori
*/
public function show(KategoriKegiatan $kategoriKegiatan)
{
return view('admin.kegiatan.kategori.show', compact('kategoriKegiatan'));
}
/**
* Form edit kategori
*/
public function edit(KategoriKegiatan $kategoriKegiatan)
{
return view('admin.kegiatan.kategori.edit', compact('kategoriKegiatan'));
}
/**
* Update kategori
*/
public function update(Request $request, KategoriKegiatan $kategoriKegiatan)
{
$validated = $request->validate([
'nama_kategori' => 'required|string|max:100|unique:kategori_kegiatans,nama_kategori,' . $kategoriKegiatan->id,
'keterangan' => 'nullable|string',
], [
'nama_kategori.required' => 'Nama kategori wajib diisi.',
'nama_kategori.unique' => 'Nama kategori sudah digunakan.',
]);
$kategoriKegiatan->update($validated);
return redirect()->route('admin.kategori-kegiatan.index')
->with('success', 'Kategori kegiatan berhasil diperbarui.');
}
/**
* Hapus kategori
*/
public function destroy(KategoriKegiatan $kategoriKegiatan)
{
$nama = $kategoriKegiatan->nama_kategori;
$kategoriKegiatan->delete();
Cache::forget('next_kategori_id');
return redirect()->route('admin.kategori-kegiatan.index')
->with('success', "Kategori \"$nama\" berhasil dihapus.");
}
}

View File

@ -1,106 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\KategoriPelanggaran;
use App\Models\KlasifikasiPelanggaran;
use Illuminate\Http\Request;
class KategoriPelanggaranController extends Controller
{
public function index(Request $request)
{
$query = KategoriPelanggaran::with('klasifikasi');
// Filter klasifikasi
if ($request->filled('id_klasifikasi')) {
$query->byKlasifikasi($request->id_klasifikasi);
}
// Filter status
if ($request->filled('is_active')) {
$query->where('is_active', $request->is_active);
}
$data = $query->orderBy('created_at', 'desc')->get();
$klasifikasiList = KlasifikasiPelanggaran::aktif()->byUrutan()->get();
return view('admin.kategori_pelanggaran.index', compact('data', 'klasifikasiList'));
}
public function create()
{
$last = KategoriPelanggaran::orderBy('id', 'desc')->first();
$nextNum = $last ? intval(substr($last->id_kategori, 2)) + 1 : 1;
$nextId = 'KP' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
$klasifikasiList = KlasifikasiPelanggaran::aktif()->byUrutan()->get();
return view('admin.kategori_pelanggaran.create', compact('nextId', 'klasifikasiList'));
}
public function store(Request $request)
{
$validated = $request->validate([
'id_klasifikasi' => 'required|exists:klasifikasi_pelanggarans,id_klasifikasi',
'nama_pelanggaran' => 'required|string|max:255',
'poin' => 'required|integer|min:1|max:100',
'kafaroh' => 'nullable|string',
'is_active' => 'boolean',
]);
KategoriPelanggaran::create($validated);
return redirect()->route('admin.kategori-pelanggaran.index')
->with('success', 'Pelanggaran berhasil ditambahkan.');
}
public function show(KategoriPelanggaran $kategoriPelanggaran)
{
$kategoriPelanggaran->load(['klasifikasi', 'riwayatPelanggaran.santri']);
return view('admin.kategori_pelanggaran.show', [
'kategori' => $kategoriPelanggaran
]);
}
public function edit(KategoriPelanggaran $kategoriPelanggaran)
{
$klasifikasiList = KlasifikasiPelanggaran::aktif()->byUrutan()->get();
return view('admin.kategori_pelanggaran.edit', [
'kategori' => $kategoriPelanggaran,
'klasifikasiList' => $klasifikasiList
]);
}
public function update(Request $request, KategoriPelanggaran $kategoriPelanggaran)
{
$validated = $request->validate([
'id_klasifikasi' => 'required|exists:klasifikasi_pelanggarans,id_klasifikasi',
'nama_pelanggaran' => 'required|string|max:255',
'poin' => 'required|integer|min:1|max:100',
'kafaroh' => 'nullable|string',
'is_active' => 'boolean',
]);
$kategoriPelanggaran->update($validated);
return redirect()->route('admin.kategori-pelanggaran.index')
->with('success', 'Pelanggaran berhasil diperbarui.');
}
public function destroy(KategoriPelanggaran $kategoriPelanggaran)
{
if ($kategoriPelanggaran->riwayatPelanggaran()->count() > 0) {
return redirect()->route('admin.kategori-pelanggaran.index')
->with('error', 'Pelanggaran tidak dapat dihapus karena masih digunakan.');
}
$kategoriPelanggaran->delete();
return redirect()->route('admin.kategori-pelanggaran.index')
->with('success', 'Pelanggaran berhasil dihapus.');
}
}

View File

@ -1,616 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Kegiatan;
use App\Models\KategoriKegiatan;
use App\Models\KelompokKelas;
use App\Models\Kelas;
use App\Models\AbsensiKegiatan;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Carbon\Carbon;
class KegiatanController extends Controller
{
/**
* Dashboard Kegiatan Hari Ini
*/
public function index(Request $request)
{
$selectedDate = $request->filled('tanggal')
? Carbon::parse($request->tanggal)
: Carbon::now();
$hariIndonesia = [
'Monday' => 'Senin',
'Tuesday' => 'Selasa',
'Wednesday' => 'Rabu',
'Thursday' => 'Kamis',
'Friday' => 'Jumat',
'Saturday' => 'Sabtu',
'Sunday' => 'Ahad',
];
$selectedHari = $hariIndonesia[$selectedDate->format('l')];
$selectedKelasId = $request->filled('kelas') ? $request->kelas : null;
$query = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok', 'absensis' => function ($q) use ($selectedDate) {
$q->whereDate('tanggal', $selectedDate->format('Y-m-d'));
}])->where('hari', $selectedHari);
if ($selectedKelasId) {
if ($selectedKelasId === 'umum') {
$query->doesntHave('kelasKegiatan');
} else {
$query->whereHas('kelasKegiatan', function ($q) use ($selectedKelasId) {
$q->where('kelas.id', $selectedKelasId);
});
}
}
if ($request->filled('kategori_id')) {
$query->where('kategori_id', $request->kategori_id);
}
$kegiatanHariIni = $query->orderBy('waktu_mulai')->get();
// ── Total santri aktif (dipakai sebagai denominator kegiatan umum) ──────
$totalSantriAktif = Santri::where('status', 'Aktif')->count();
// ── Pre-load semua santri aktif beserta kelas mereka (1 query) ────────────
// Ini dipakai untuk menghitung "berapa santri yang seharusnya hadir" per kegiatan
$allSantriAktif = Santri::where('status', 'Aktif')
->with('kelasSantri')
->get();
$kegiatanHariIni->each(function ($kegiatan) use ($totalSantriAktif, $selectedDate, $allSantriAktif) {
$absensis = $kegiatan->absensis; // sudah di-eager-load
$totalAbsensi = $absensis->count();
// ── FIX 1: Terlambat = hadir (masuk), bukan alpha ──────────────────
$hadirEfektif = $absensis->whereIn('status', ['Hadir', 'Terlambat'])->count();
$terlambat = $absensis->where('status', 'Terlambat')->count();
$izin = $absensis->where('status', 'Izin')->count();
$sakit = $absensis->where('status', 'Sakit')->count();
$alpa = $absensis->where('status', 'Alpa')->count();
// ── FIX 2: Denominator = jumlah santri yg seharusnya ikut kegiatan ini ──
if ($kegiatan->kelasKegiatan->isEmpty()) {
// Kegiatan Umum → semua santri aktif
$totalSantriKegiatan = $totalSantriAktif;
} else {
// Kegiatan Khusus → hitung santri yang terdaftar di kelas kegiatan
$kelasIds = $kegiatan->kelasKegiatan->pluck('id')->toArray();
$totalSantriKegiatan = $allSantriAktif->filter(function ($s) use ($kelasIds) {
return $s->kelasSantri->whereIn('id_kelas', $kelasIds)->count() > 0;
})->count();
// Fallback jika tidak ada santri terdaftar di kelas
if ($totalSantriKegiatan === 0) $totalSantriKegiatan = $totalSantriAktif;
}
// ── FIX 3: Persentase berdasarkan total santri kegiatan, bukan yg sudah absen ──
$persenKehadiran = $totalSantriKegiatan > 0
? round(($hadirEfektif / $totalSantriKegiatan) * 100)
: 0;
// ── FIX 4: Info per kelas — berapa kelas sudah/belum input ───────────
$infoPerKelas = collect();
if (!$kegiatan->kelasKegiatan->isEmpty()) {
$absensiByKelas = $absensis->groupBy(function ($ab) {
// group berdasarkan kelas santri yg hadir (ambil kelas pertama yg sesuai)
return $ab->santri->kelas_name ?? 'Tanpa Kelas';
});
foreach ($kegiatan->kelasKegiatan as $kls) {
$kelasId = $kls->id;
$santriDiKelas = $allSantriAktif->filter(function ($s) use ($kelasId) {
return $s->kelasSantri->where('id_kelas', $kelasId)->count() > 0;
})->count();
// Berapa dari kelas ini yang sudah diinput absensi hari ini
// Cek dari absensis: santri yg ada di kelas ini
$sudahInputKelas = 0; // akan diisi di blade via data yg dikirim
$infoPerKelas->push([
'nama' => $kls->nama_kelas,
'total_santri' => $santriDiKelas,
]);
}
}
// ── Status kegiatan (belum / berlangsung / selesai) ─────────────────
$now = Carbon::now();
$waktuMulaiStr = is_string($kegiatan->waktu_mulai) ? $kegiatan->waktu_mulai : $kegiatan->waktu_mulai->format('H:i');
$waktuSelesaiStr = is_string($kegiatan->waktu_selesai) ? $kegiatan->waktu_selesai : $kegiatan->waktu_selesai->format('H:i');
$waktuMulai = Carbon::parse($selectedDate->format('Y-m-d') . ' ' . $waktuMulaiStr);
$waktuSelesai = Carbon::parse($selectedDate->format('Y-m-d') . ' ' . $waktuSelesaiStr);
if ($selectedDate->isToday()) {
if ($now->lt($waktuMulai)) $status = 'belum';
elseif ($now->between($waktuMulai, $waktuSelesai)) $status = 'berlangsung';
else $status = 'selesai';
} elseif ($selectedDate->isFuture()) {
$status = 'belum';
} else {
$status = 'selesai';
}
// Set semua property ke object kegiatan
$kegiatan->total_hadir = $hadirEfektif; // hadir + terlambat
$kegiatan->total_hadir_murni = $absensis->where('status', 'Hadir')->count();
$kegiatan->total_terlambat = $terlambat;
$kegiatan->total_izin = $izin;
$kegiatan->total_sakit = $sakit;
$kegiatan->total_alpa = $alpa;
$kegiatan->total_absensi = $totalAbsensi; // sudah diinput
$kegiatan->total_santri_kegiatan = $totalSantriKegiatan; // seharusnya hadir
$kegiatan->belum_absen = max(0, $totalSantriKegiatan - $totalAbsensi);
$kegiatan->persen_kehadiran = $persenKehadiran;
$kegiatan->status_kegiatan = $status;
$kegiatan->info_per_kelas = $infoPerKelas;
});
$totalKegiatanHariIni = $kegiatanHariIni->count();
$kegiatanSelesai = $kegiatanHariIni->where('status_kegiatan', 'selesai')->count();
$kegiatanBerlangsung = $kegiatanHariIni->where('status_kegiatan', 'berlangsung')->count();
$avgKehadiran = $kegiatanHariIni->count() > 0 ? round($kegiatanHariIni->avg('persen_kehadiran')) : 0;
$lastWeekDate = $selectedDate->copy()->subWeek();
$lastWeekHari = $hariIndonesia[$lastWeekDate->format('l')];
$kegiatanLastWeekCount = Kegiatan::where('hari', $lastWeekHari)->count();
$comparisonTotal = $totalKegiatanHariIni - $kegiatanLastWeekCount;
$avgKehadiranLastWeek = Cache::remember('avg_kehadiran_' . $lastWeekDate->format('Y-m-d'), 600, function () use ($lastWeekDate, $lastWeekHari) {
$list = Kegiatan::where('hari', $lastWeekHari)->get();
$totalPersen = 0;
$count = 0;
foreach ($list as $kg) {
$abs = AbsensiKegiatan::where('kegiatan_id', $kg->kegiatan_id)
->whereDate('tanggal', $lastWeekDate->format('Y-m-d'))->get();
if ($abs->count() > 0) {
// FIX: hitung hadir + terlambat, bukan hadir saja
$hadirCount = $abs->whereIn('status', ['Hadir', 'Terlambat'])->count();
$totalPersen += ($hadirCount / $abs->count()) * 100;
$count++;
}
}
return $count > 0 ? round($totalPersen / $count) : 0;
});
$comparisonAvg = $avgKehadiran - $avgKehadiranLastWeek;
$kelasList = Kelas::with('kelompok')->active()->ordered()->get();
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
$insights = $this->generateInsights($kegiatanHariIni, $totalSantriAktif, $selectedDate);
$heatmapData = Cache::remember('heatmap_30days_' . now()->format('Y-m-d'), 600, function () {
return $this->generateHeatmapData();
});
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
return view('admin.kegiatan.data.dashboard', compact(
'kegiatanHariIni', 'totalKegiatanHariIni', 'kegiatanSelesai',
'kegiatanBerlangsung', 'avgKehadiran', 'totalSantriAktif',
'selectedDate', 'selectedHari', 'hariList', 'kelasList',
'selectedKelasId', 'comparisonTotal', 'comparisonAvg',
'insights', 'heatmapData', 'kategoris'
));
}
/**
* Generate Quick Insights (Rule-Based AI)
*/
private function generateInsights($kegiatanHariIni, $totalSantriAktif, $selectedDate)
{
$insights = [];
foreach ($kegiatanHariIni as $kegiatan) {
if ($kegiatan->total_absensi > 0 && $kegiatan->persen_kehadiran < 70) {
$insights[] = [
'type' => 'warning',
'icon' => 'exclamation-triangle',
'message' => "Kegiatan {$kegiatan->nama_kegiatan} kehadiran rendah ({$kegiatan->persen_kehadiran}%)",
'detail' => "{$kegiatan->total_hadir} hadir dari {$kegiatan->total_santri_kegiatan} santri",
'action_url' => route('admin.absensi-kegiatan.input', $kegiatan->kegiatan_id) . '?tanggal=' . $selectedDate->format('Y-m-d'),
'action_text' => 'Input Absensi',
];
}
}
foreach ($kegiatanHariIni as $kegiatan) {
if ($kegiatan->persen_kehadiran == 100 && $kegiatan->total_absensi > 0) {
$insights[] = [
'type' => 'success',
'icon' => 'check-circle',
'message' => "Perfect! {$kegiatan->nama_kegiatan} kehadiran 100%",
'detail' => "Semua {$kegiatan->total_santri_kegiatan} santri hadir",
'action_url' => null,
'action_text' => null,
];
}
}
$kegiatanLive = $kegiatanHariIni->where('status_kegiatan', 'berlangsung')->first();
if ($kegiatanLive) {
$insights[] = [
'type' => 'info',
'icon' => 'clock',
'message' => "Kegiatan {$kegiatanLive->nama_kegiatan} sedang berlangsung",
'detail' => "Progress absensi: {$kegiatanLive->total_absensi}/{$kegiatanLive->total_santri_kegiatan} santri ({$kegiatanLive->persen_kehadiran}%)",
'action_url' => route('admin.absensi-kegiatan.input', $kegiatanLive->kegiatan_id) . '?tanggal=' . $selectedDate->format('Y-m-d'),
'action_text' => 'Input Absensi Sekarang',
];
}
foreach ($kegiatanHariIni as $kegiatan) {
if ($kegiatan->status_kegiatan == 'selesai' && $kegiatan->total_absensi == 0) {
$waktuSelesai = is_string($kegiatan->waktu_selesai) ? $kegiatan->waktu_selesai : $kegiatan->waktu_selesai->format('H:i');
$insights[] = [
'type' => 'danger',
'icon' => 'exclamation-circle',
'message' => "Kegiatan {$kegiatan->nama_kegiatan} belum input absensi",
'detail' => "Sudah selesai pukul {$waktuSelesai}, {$kegiatan->total_santri_kegiatan} santri belum diinput",
'action_url' => route('admin.absensi-kegiatan.input', $kegiatan->kegiatan_id) . '?tanggal=' . $selectedDate->format('Y-m-d'),
'action_text' => 'Input Sekarang',
];
}
}
return collect($insights)->take(5)->toArray();
}
/**
* Generate Heatmap Data (30 hari terakhir)
* FIX: hitung hadir + terlambat, bukan hadir saja
*/
private function generateHeatmapData()
{
$heatmapData = [];
$startDate = Carbon::now()->subDays(29);
for ($i = 0; $i < 30; $i++) {
$date = $startDate->copy()->addDays($i);
$dateStr = $date->format('Y-m-d');
$absensi = AbsensiKegiatan::whereDate('tanggal', $dateStr)->get();
$percentage = $absensi->count() > 0
? round(($absensi->whereIn('status', ['Hadir', 'Terlambat'])->count() / $absensi->count()) * 100, 1)
: 0;
$heatmapData[] = [
'date' => $dateStr,
'day_name' => $date->locale('id')->isoFormat('ddd'),
'percentage' => $percentage,
'level' => $this->getHeatmapLevel($percentage),
'is_today' => $date->isToday(),
];
}
return $heatmapData;
}
private function getHeatmapLevel($percentage)
{
if ($percentage >= 90) return 4;
if ($percentage >= 80) return 3;
if ($percentage >= 70) return 2;
if ($percentage > 0) return 1;
return 0;
}
/**
* AJAX: Get Detail Kegiatan untuk Modal
* FIX: persen_hadir juga ikut hitung Terlambat
*/
public function getDetailModal($kegiatan_id, Request $request)
{
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok'])
->where('kegiatan_id', $kegiatan_id)->firstOrFail();
$absensis = AbsensiKegiatan::with(['santri.kelasSantri.kelas'])
->where('kegiatan_id', $kegiatan_id)
->whereDate('tanggal', $tanggal)
->orderBy('waktu_absen', 'desc')->get();
$isUmum = $kegiatan->isForAllClasses();
// Grup absensi per kelas
if ($isUmum) {
$absensiPerKelas = $absensis->groupBy(fn($item) => $item->santri->kelas_name ?? 'Belum Ada Kelas')->sortKeys();
} else {
$absensiPerKelas = collect();
foreach ($kegiatan->kelasKegiatan as $kelas) {
$filtered = $absensis->filter(fn($item) => $item->santri->kelasSantri->contains('id_kelas', $kelas->id));
if ($filtered->count() > 0) $absensiPerKelas[$kelas->nama_kelas] = $filtered;
}
$placedIds = $absensiPerKelas->flatten()->pluck('id')->toArray();
$lainnya = $absensis->filter(fn($item) => !in_array($item->id, $placedIds));
if ($lainnya->count() > 0) $absensiPerKelas['Kelas Lain'] = $lainnya;
}
// FIX: hadir efektif = Hadir + Terlambat
$hadirEfektif = $absensis->whereIn('status', ['Hadir', 'Terlambat'])->count();
// Total santri yang seharusnya hadir
if ($isUmum) {
$totalSantri = Santri::where('status', 'Aktif')->count();
} else {
$kelasIds = $kegiatan->kelasKegiatan->pluck('id')->toArray();
$totalSantri = Santri::where('status', 'Aktif')
->whereHas('kelasSantri', fn($q) => $q->whereIn('id_kelas', $kelasIds))
->count();
if ($totalSantri === 0) $totalSantri = Santri::where('status', 'Aktif')->count();
}
$stats = [
'hadir' => $absensis->where('status', 'Hadir')->count(),
'terlambat' => $absensis->where('status', 'Terlambat')->count(),
'hadir_efektif'=> $hadirEfektif, // hadir + terlambat
'izin' => $absensis->where('status', 'Izin')->count(),
'sakit' => $absensis->where('status', 'Sakit')->count(),
'alpa' => $absensis->where('status', 'Alpa')->count(),
];
$stats['belum_absen'] = max(0, $totalSantri - $absensis->count());
$stats['sudah_absen'] = $absensis->count();
$stats['total'] = $totalSantri;
// FIX: persen hadir pakai hadir_efektif bukan hadir saja
$stats['persen_hadir'] = $totalSantri > 0
? round(($hadirEfektif / $totalSantri) * 100, 1)
: 0;
// ── Info per kelas untuk modal: sudah/belum input ───────────────────────
$infoKelasModal = collect();
if (!$isUmum) {
$allSantriAktif = Santri::where('status', 'Aktif')->with('kelasSantri')->get();
foreach ($kegiatan->kelasKegiatan as $kls) {
$kelasId = $kls->id;
$santriDiKelas = $allSantriAktif->filter(fn($s) => $s->kelasSantri->where('id_kelas', $kelasId)->count() > 0)->count();
$sudahInputKelas = $absensis->filter(fn($ab) =>
$ab->santri->kelasSantri->contains('id_kelas', $kelasId)
)->count();
$infoKelasModal->push([
'nama' => $kls->nama_kelas,
'total_santri' => $santriDiKelas,
'sudah_input' => $sudahInputKelas,
'belum_input' => max(0, $santriDiKelas - $sudahInputKelas),
'sudah_semua' => $sudahInputKelas >= $santriDiKelas && $santriDiKelas > 0,
]);
}
}
// Daftar santri belum absen
$idSantriSudahAbsen = $absensis->pluck('id_santri')->toArray();
$belumQuery = Santri::where('status', 'Aktif')
->whereNotIn('id_santri', $idSantriSudahAbsen)
->with(['kelasSantri.kelas', 'kelasPrimary.kelas'])
->orderBy('nama_lengkap');
if (!$isUmum) {
$kelasIds = $kegiatan->kelasKegiatan->pluck('id')->toArray();
$belumQuery->whereHas('kelasSantri', fn($q) => $q->whereIn('id_kelas', $kelasIds));
}
$allBelumAbsen = $belumQuery->get();
if ($isUmum) {
$santriBelumAbsenPerKelas = $allBelumAbsen->groupBy(function($s) {
return optional(optional($s->kelasPrimary)->kelas)->nama_kelas ?? 'Tanpa Kelas';
})->sortKeys();
} else {
$santriBelumAbsenPerKelas = collect();
$placedBelumIds = [];
foreach ($kegiatan->kelasKegiatan as $kelas) {
$inKelas = $allBelumAbsen->filter(function($s) use ($kelas, &$placedBelumIds) {
if (in_array($s->id_santri, $placedBelumIds)) return false;
return $s->kelasSantri->contains('id_kelas', $kelas->id);
});
foreach ($inKelas as $s) $placedBelumIds[] = $s->id_santri;
if ($inKelas->count() > 0) $santriBelumAbsenPerKelas[$kelas->nama_kelas] = $inKelas;
}
$lainnyaBelum = $allBelumAbsen->whereNotIn('id_santri', $placedBelumIds);
if ($lainnyaBelum->count() > 0) $santriBelumAbsenPerKelas['Kelas Lain'] = $lainnyaBelum;
}
$santriBelumAbsen = $allBelumAbsen;
return view('admin.kegiatan.data.partials.detail-modal', compact(
'kegiatan', 'absensis', 'absensiPerKelas', 'stats', 'tanggal',
'santriBelumAbsen', 'santriBelumAbsenPerKelas', 'infoKelasModal'
));
}
/**
* Jadwal Kegiatan Lengkap
*/
public function jadwal(Request $request)
{
$query = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok']);
if ($request->filled('hari')) $query->where('hari', $request->hari);
if ($request->filled('kategori_id')) $query->where('kategori_id', $request->kategori_id);
if ($request->filled('kelas_id')) {
if ($request->kelas_id === 'umum') {
$query->doesntHave('kelasKegiatan');
} else {
$query->whereHas('kelasKegiatan', fn($q) => $q->where('kelas.id', $request->kelas_id));
}
}
if ($request->filled('search')) $query->search($request->search);
$kegiatans = $query->select('id', 'kegiatan_id', 'kategori_id', 'nama_kegiatan', 'hari', 'waktu_mulai', 'waktu_selesai', 'materi')
->orderBy('hari')->orderBy('waktu_mulai')
->paginate(15)->appends(request()->query());
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
$kelasList = Kelas::with('kelompok')->active()->ordered()->get();
return view('admin.kegiatan.data.index', compact('kegiatans', 'kategoris', 'hariList', 'kelasList'));
}
/**
* Form tambah kegiatan
*/
public function create()
{
$nextId = Cache::remember('next_kegiatan_id', 60, function () {
$last = Kegiatan::select('kegiatan_id')->orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->kegiatan_id, 2)) + 1 : 1;
return 'KG' . str_pad($num, 3, '0', STR_PAD_LEFT);
});
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
$kelompokKelas = KelompokKelas::with(['kelas' => fn($q) => $q->where('is_active', true)->orderBy('urutan')])
->active()->ordered()->get();
return view('admin.kegiatan.data.create', compact('nextId', 'kategoris', 'hariList', 'kelompokKelas'));
}
/**
* Simpan kegiatan baru
*/
public function store(Request $request)
{
$validated = $request->validate([
'kategori_id' => 'required|exists:kategori_kegiatans,kategori_id',
'nama_kegiatan' => 'required|string|max:150',
'hari' => 'required|array|min:1',
'hari.*' => 'in:Senin,Selasa,Rabu,Kamis,Jumat,Sabtu,Ahad',
'waktu_mulai' => 'required|date_format:H:i',
'waktu_selesai' => 'required|date_format:H:i|after:waktu_mulai',
'materi' => 'nullable|string|max:200',
'keterangan' => 'nullable|string',
'kelas_ids' => 'nullable|array',
'kelas_ids.*' => 'exists:kelas,id',
], [
'kategori_id.required' => 'Kategori wajib dipilih.',
'nama_kegiatan.required' => 'Nama kegiatan wajib diisi.',
'hari.required' => 'Minimal pilih satu hari.',
'hari.min' => 'Minimal pilih satu hari.',
'waktu_mulai.required' => 'Waktu mulai wajib diisi.',
'waktu_selesai.required' => 'Waktu selesai wajib diisi.',
'waktu_selesai.after' => 'Waktu selesai harus lebih dari waktu mulai.',
]);
$hariList = $validated['hari'];
unset($validated['hari']);
$createdCount = 0;
foreach ($hariList as $hari) {
$kg = Kegiatan::create(array_merge($validated, ['hari' => $hari]));
if ($request->has('kelas_ids') && !empty($request->kelas_ids)) {
$kg->assignKelas($request->kelas_ids);
}
$createdCount++;
}
Cache::forget('next_kegiatan_id');
$message = $createdCount > 1
? "Berhasil menambahkan kegiatan untuk {$createdCount} hari."
: 'Kegiatan berhasil ditambahkan.';
return redirect()->route('admin.kegiatan.jadwal')->with('success', $message);
}
/**
* Tampilkan detail kegiatan
*/
public function show(Kegiatan $kegiatan)
{
$kegiatan->load(['kategori', 'kelasKegiatan.kelompok']);
return view('admin.kegiatan.data.show', compact('kegiatan'));
}
/**
* Form edit kegiatan
*/
public function edit(Kegiatan $kegiatan)
{
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
$kelompokKelas = KelompokKelas::with(['kelas' => fn($q) => $q->where('is_active', true)->orderBy('urutan')])
->active()->ordered()->get();
$kegiatan->load('kelasKegiatan');
return view('admin.kegiatan.data.edit', compact('kegiatan', 'kategoris', 'hariList', 'kelompokKelas'));
}
/**
* Update kegiatan smart multi-hari
*/
public function update(Request $request, Kegiatan $kegiatan)
{
$validated = $request->validate([
'kategori_id' => 'required|exists:kategori_kegiatans,kategori_id',
'nama_kegiatan' => 'required|string|max:150',
'hari' => 'required|array|min:1',
'hari.*' => 'in:Senin,Selasa,Rabu,Kamis,Jumat,Sabtu,Ahad',
'waktu_mulai' => 'required|date_format:H:i',
'waktu_selesai' => 'required|date_format:H:i|after:waktu_mulai',
'materi' => 'nullable|string|max:200',
'keterangan' => 'nullable|string',
'kelas_ids' => 'nullable|array',
'kelas_ids.*' => 'exists:kelas,id',
], [
'kategori_id.required' => 'Kategori wajib dipilih.',
'nama_kegiatan.required' => 'Nama kegiatan wajib diisi.',
'hari.required' => 'Minimal pilih satu hari.',
'hari.min' => 'Minimal pilih satu hari.',
'waktu_selesai.after' => 'Waktu selesai harus lebih dari waktu mulai.',
]);
$hariDipilih = $validated['hari'];
$kelasIds = $request->input('kelas_ids', []);
$baseData = collect($validated)->except(['hari', 'kelas_ids'])->toArray();
$saudara = Kegiatan::where('nama_kegiatan', $kegiatan->nama_kegiatan)
->where('kategori_id', $kegiatan->kategori_id)
->get()
->keyBy('hari');
$updatedCount = 0;
$createdCount = 0;
foreach ($hariDipilih as $hari) {
if ($saudara->has($hari)) {
$target = $saudara->get($hari);
$target->update(array_merge($baseData, ['hari' => $hari]));
$target->assignKelas($kelasIds);
$updatedCount++;
} else {
$newKg = Kegiatan::create(array_merge($baseData, ['hari' => $hari]));
$newKg->assignKelas($kelasIds);
$createdCount++;
}
}
Cache::forget('next_kegiatan_id');
$parts = [];
if ($updatedCount > 0) $parts[] = "{$updatedCount} kegiatan diperbarui";
if ($createdCount > 0) $parts[] = "{$createdCount} kegiatan baru dibuat";
return redirect()->route('admin.kegiatan.jadwal')
->with('success', 'Berhasil: ' . implode(', ', $parts) . '.');
}
/**
* Hapus kegiatan
*/
public function destroy(Kegiatan $kegiatan)
{
$nama = $kegiatan->nama_kegiatan;
$kegiatan->delete();
Cache::forget('next_kegiatan_id');
return redirect()->route('admin.kegiatan.jadwal')
->with('success', "Kegiatan \"{$nama}\" berhasil dihapus.");
}
}

View File

@ -1,418 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Kelas;
use App\Models\KelompokKelas;
use App\Models\Santri;
use App\Models\SantriKelas;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class KelasController extends Controller
{
// ==========================================
// SECTION 1: CRUD KELAS
// ==========================================
public function index(Request $request)
{
$query = Kelas::with('kelompok');
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('nama_kelas', 'like', "%{$search}%")
->orWhere('kode_kelas', 'like', "%{$search}%");
});
}
if ($request->filled('kelompok')) {
$query->where('id_kelompok', $request->kelompok);
}
if ($request->filled('status')) {
$query->where('is_active', $request->status === 'active');
}
$kelas = $query->orderBy('id_kelompok', 'asc')
->orderBy('urutan', 'asc')
->paginate(15)
->appends(request()->query());
$kelompokKelas = KelompokKelas::active()->ordered()->get();
return view('admin.kelas.index', compact('kelas', 'kelompokKelas'));
}
public function create()
{
$nextKodeKelas = Cache::remember('next_kelas_kode', 60, function () {
$lastKelas = Kelas::orderBy('id', 'desc')->first();
$nextNum = $lastKelas ? intval(substr($lastKelas->kode_kelas, 3)) + 1 : 1;
return 'KLS' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
});
$kelompokKelas = KelompokKelas::active()->ordered()->get();
return view('admin.kelas.create', compact('nextKodeKelas', 'kelompokKelas'));
}
public function store(Request $request)
{
$validated = $request->validate([
'nama_kelas' => 'required|string|max:100|unique:kelas,nama_kelas',
'id_kelompok' => 'required|string|exists:kelompok_kelas,id_kelompok',
'urutan' => 'required|integer|min:0',
'is_active' => 'boolean',
]);
$validated['is_active'] = $request->has('is_active');
Kelas::create($validated);
Cache::forget('next_kelas_kode');
return redirect()->route('admin.kelas.index')
->with('success', 'Kelas berhasil ditambahkan.');
}
public function show(Kelas $kela)
{
$kela->load(['kelompok', 'santriKelas.santri']);
$santriCount = $kela->santriKelas()
->whereHas('santri', fn($q) => $q->where('status', 'Aktif'))
->count();
return view('admin.kelas.show', compact('kela', 'santriCount'));
}
public function edit(Kelas $kela)
{
$kelompokKelas = KelompokKelas::active()->ordered()->get();
return view('admin.kelas.edit', compact('kela', 'kelompokKelas'));
}
public function update(Request $request, Kelas $kela)
{
$validated = $request->validate([
'nama_kelas' => 'required|string|max:100|unique:kelas,nama_kelas,' . $kela->id,
'id_kelompok' => 'required|string|exists:kelompok_kelas,id_kelompok',
'urutan' => 'required|integer|min:0',
'is_active' => 'boolean',
]);
$validated['is_active'] = $request->has('is_active');
$kela->update($validated);
return redirect()->route('admin.kelas.index')
->with('success', 'Kelas berhasil diperbarui.');
}
public function destroy(Kelas $kela)
{
$santriCount = $kela->santriKelas()->count();
$kegiatanCount = $kela->kegiatans()->count();
if ($santriCount > 0) {
return redirect()->route('admin.kelas.index')
->with('error', "Kelas tidak dapat dihapus karena masih digunakan oleh {$santriCount} santri.");
}
if ($kegiatanCount > 0) {
return redirect()->route('admin.kelas.index')
->with('error', "Kelas tidak dapat dihapus karena masih memiliki {$kegiatanCount} kegiatan.");
}
$kela->delete();
return redirect()->route('admin.kelas.index')
->with('success', 'Kelas berhasil dihapus.');
}
// ==========================================
// SECTION 2: CRUD KELOMPOK KELAS
// ==========================================
public function kelompokIndex(Request $request)
{
$query = KelompokKelas::withCount('kelas');
if ($request->filled('search')) {
$query->where('nama_kelompok', 'like', '%' . $request->search . '%');
}
if ($request->filled('status')) {
$query->where('is_active', $request->status === 'active');
}
$kelompokKelas = $query->orderBy('urutan', 'asc')
->paginate(15)
->appends(request()->query());
return view('admin.kelas.kelompok.index', compact('kelompokKelas'));
}
public function kelompokCreate()
{
$nextIdKelompok = Cache::remember('next_kelompok_id', 60, function () {
$last = KelompokKelas::orderBy('id', 'desc')->first();
$nextNum = $last ? intval(substr($last->id_kelompok, 3)) + 1 : 1;
return 'KEL' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
});
return view('admin.kelas.kelompok.create', compact('nextIdKelompok'));
}
public function kelompokStore(Request $request)
{
$validated = $request->validate([
'nama_kelompok' => 'required|string|max:100|unique:kelompok_kelas,nama_kelompok',
'deskripsi' => 'nullable|string|max:500',
'urutan' => 'required|integer|min:0',
'is_active' => 'boolean',
]);
$validated['is_active'] = $request->has('is_active');
KelompokKelas::create($validated);
Cache::forget('next_kelompok_id');
return redirect()->route('admin.kelas.kelompok.index')
->with('success', 'Kelompok kelas berhasil ditambahkan.');
}
public function kelompokEdit($id)
{
$kelompok = KelompokKelas::findOrFail($id);
$kelompok->loadCount('kelas');
return view('admin.kelas.kelompok.edit', compact('kelompok'));
}
public function kelompokUpdate(Request $request, $id)
{
$kelompok = KelompokKelas::findOrFail($id);
$validated = $request->validate([
'nama_kelompok' => 'required|string|max:100|unique:kelompok_kelas,nama_kelompok,' . $kelompok->id,
'deskripsi' => 'nullable|string|max:500',
'urutan' => 'required|integer|min:0',
'is_active' => 'boolean',
]);
$validated['is_active'] = $request->has('is_active');
$kelompok->update($validated);
return redirect()->route('admin.kelas.kelompok.index')
->with('success', 'Kelompok kelas berhasil diperbarui.');
}
public function kelompokDestroy($id)
{
$kelompok = KelompokKelas::findOrFail($id);
$kelasCount = $kelompok->kelas()->count();
if ($kelasCount > 0) {
return redirect()->route('admin.kelas.kelompok.index')
->with('error', "Kelompok tidak dapat dihapus karena masih memiliki {$kelasCount} kelas.");
}
$kelompok->delete();
return redirect()->route('admin.kelas.kelompok.index')
->with('success', 'Kelompok kelas berhasil dihapus.');
}
// ==========================================
// SECTION 3: KENAIKAN KELAS MASSAL
// ==========================================
public function kenaikanIndex(Request $request)
{
$tahunAjaranAktif = $this->getActiveTahunAjaran();
$tahunAjaranBaru = $this->getNextAcademicYear($tahunAjaranAktif);
$totalSantriAktif = Santri::where('status', 'Aktif')->count();
$kelompokKelas = KelompokKelas::with([
'kelas' => fn($q) => $q->where('is_active', true)->orderBy('urutan'),
])->active()->ordered()->get();
$selectedKelompok = $request->get('kelompok');
if (!$selectedKelompok && $kelompokKelas->isNotEmpty()) {
$selectedKelompok = $kelompokKelas->first()->id_kelompok;
}
$kelasList = Kelas::with('kelompok')
->where('is_active', true)
->when($selectedKelompok, fn($q) => $q->where('id_kelompok', $selectedKelompok))
->withCount([
'santriKelas as santri_aktif_count' => fn($q) => $q->whereHas('santri', fn($s) => $s->where('status', 'Aktif')),
])
->orderBy('urutan', 'asc')
->get();
$allKelasList = Kelas::with('kelompok')
->where('is_active', true)
->orderBy('id_kelompok', 'asc')
->orderBy('urutan', 'asc')
->get();
return view('admin.kelas.kenaikan.index', compact(
'tahunAjaranAktif', 'tahunAjaranBaru', 'totalSantriAktif',
'kelompokKelas', 'kelasList', 'allKelasList', 'selectedKelompok'
));
}
public function kenaikanPreview($id)
{
$kelas = Kelas::with('kelompok')->findOrFail($id);
$tahunAjaranAktif = $this->getActiveTahunAjaran();
$tahunAjaranBaru = $this->getNextAcademicYear($tahunAjaranAktif);
$santriList = Santri::whereHas('kelasSantri', fn($q) => $q->where('id_kelas', $id))
->where('status', 'Aktif')
->orderBy('nama_lengkap')
->get();
$kelasOptions = KelompokKelas::with([
'kelas' => fn($q) => $q->where('is_active', true)->orderBy('urutan'),
])->active()->ordered()->get();
return view('admin.kelas.kenaikan.preview', compact(
'kelas', 'santriList', 'tahunAjaranAktif', 'tahunAjaranBaru', 'kelasOptions'
));
}
public function kenaikanProcess(Request $request)
{
$request->validate([
'id_kelas_asal' => 'required|exists:kelas,id',
'id_kelas_tujuan' => 'required|exists:kelas,id|different:id_kelas_asal',
], [
'id_kelas_tujuan.different' => 'Kelas tujuan tidak boleh sama dengan kelas asal.',
]);
$kelasAsal = Kelas::findOrFail($request->id_kelas_asal);
$kelasTujuan = Kelas::findOrFail($request->id_kelas_tujuan);
// Ambil semua id kelas yang satu kelompok dengan kelas asal
$kelasSeKelompok = Kelas::where('id_kelompok', $kelasAsal->id_kelompok)
->pluck('id');
$santriIds = Santri::whereHas('kelasSantri', fn($q) => $q->where('id_kelas', $request->id_kelas_asal))
->where('status', 'Aktif')
->pluck('id_santri');
if ($santriIds->isEmpty()) {
return redirect()->route('admin.kelas.kenaikan.index')
->with('error', 'Tidak ada santri aktif di kelas ' . $kelasAsal->nama_kelas . '.');
}
$processed = 0;
DB::beginTransaction();
try {
foreach ($santriIds as $idSantri) {
// Ambil record kelas asal
$recordAsal = SantriKelas::where('id_santri', $idSantri)
->where('id_kelas', $kelasAsal->id)
->orderBy('tahun_ajaran', 'desc')
->first();
if (!$recordAsal) continue;
// Hapus record lain milik santri ini yang satu kelompok dengan kelas asal
// (kecuali record asal itu sendiri, karena akan kita update)
SantriKelas::where('id_santri', $idSantri)
->whereIn('id_kelas', $kelasSeKelompok)
->where('id', '!=', $recordAsal->id)
->delete();
// Pindahkan record kelas asal ke kelas tujuan
$recordAsal->update(['id_kelas' => $kelasTujuan->id]);
$processed++;
}
DB::commit();
return redirect()->route('admin.kelas.kenaikan.index')
->with('success', "Berhasil menaikkan {$processed} santri dari {$kelasAsal->nama_kelas} ke {$kelasTujuan->nama_kelas}.");
} catch (\Exception $e) {
DB::rollBack();
return redirect()->route('admin.kelas.kenaikan.index')
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
}
}
public function kenaikanProcessSelected(Request $request)
{
$request->validate([
'id_kelas_asal' => 'required|exists:kelas,id',
'id_kelas_tujuan' => 'required|exists:kelas,id|different:id_kelas_asal',
'santri_ids' => 'required|array|min:1',
'santri_ids.*' => 'exists:santris,id_santri',
], [
'santri_ids.required' => 'Pilih minimal 1 santri untuk dinaikkan kelasnya.',
'santri_ids.min' => 'Pilih minimal 1 santri untuk dinaikkan kelasnya.',
'id_kelas_tujuan.different' => 'Kelas tujuan tidak boleh sama dengan kelas asal.',
]);
$kelasAsal = Kelas::findOrFail($request->id_kelas_asal);
$kelasTujuan = Kelas::findOrFail($request->id_kelas_tujuan);
// Ambil semua id kelas yang satu kelompok dengan kelas asal
$kelasSeKelompok = Kelas::where('id_kelompok', $kelasAsal->id_kelompok)
->pluck('id');
$processed = 0;
DB::beginTransaction();
try {
foreach ($request->santri_ids as $idSantri) {
// Ambil record kelas asal
$recordAsal = SantriKelas::where('id_santri', $idSantri)
->where('id_kelas', $kelasAsal->id)
->orderBy('tahun_ajaran', 'desc')
->first();
if (!$recordAsal) continue;
// Hapus record lain milik santri ini yang satu kelompok dengan kelas asal
// (kecuali record asal itu sendiri, karena akan kita update)
SantriKelas::where('id_santri', $idSantri)
->whereIn('id_kelas', $kelasSeKelompok)
->where('id', '!=', $recordAsal->id)
->delete();
// Pindahkan record kelas asal ke kelas tujuan
$recordAsal->update(['id_kelas' => $kelasTujuan->id]);
$processed++;
}
DB::commit();
return redirect()->route('admin.kelas.kenaikan.index')
->with('success', "Berhasil menaikkan {$processed} santri dari {$kelasAsal->nama_kelas} ke {$kelasTujuan->nama_kelas}.");
} catch (\Exception $e) {
DB::rollBack();
return redirect()->route('admin.kelas.kenaikan.preview', $request->id_kelas_asal)
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
}
}
/**
* Helper: tahun ajaran aktif berdasarkan data yang ada di santri_kelas.
* Menggunakan tahun ajaran terbaru yang punya record, fallback ke kalkulasi.
*/
private function getActiveTahunAjaran(): string
{
return SantriKelas::max('tahun_ajaran') ?? SantriKelas::getCurrentAcademicYear();
}
/**
* Helper: hitung tahun ajaran berikutnya
* Contoh: "2024/2025" -> "2025/2026"
*/
private function getNextAcademicYear(string $currentYear): string
{
$parts = explode('/', $currentYear);
return ((int) $parts[0] + 1) . '/' . ((int) $parts[1] + 1);
}
}

View File

@ -1,758 +0,0 @@
<?php
// app/Http/Controllers/Admin/KepulanganController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Kepulangan;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Barryvdh\DomPDF\Facade\Pdf;
class KepulanganController extends Controller
{
/**
* Display a listing of kepulangan
*/
public function index(Request $request)
{
$query = Kepulangan::with('santri');
// Search
if ($request->filled('search')) {
$query->search($request->search);
}
// Filter status
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Filter tahun
if ($request->filled('tahun')) {
$query->whereYear('tanggal_pulang', $request->tahun);
}
// Filter bulan
if ($request->filled('bulan')) {
$query->whereMonth('tanggal_pulang', $request->bulan);
}
// Get data dengan pagination
$kepulangan = $query->orderBy('created_at', 'desc')->paginate(15);
// Statistics
$stats = [
'total_data' => Kepulangan::count(),
'menunggu_approval' => Kepulangan::where('status', 'Menunggu')->count(),
'sedang_izin' => Kepulangan::aktif()->count(),
'over_limit_santri' => count(Kepulangan::getSantriOverLimit()),
];
// Get unique years for filter
$tahunList = Kepulangan::selectRaw('YEAR(tanggal_pulang) as tahun')
->distinct()
->orderBy('tahun', 'desc')
->pluck('tahun');
// Get santri yang over limit untuk highlight
$santriOverLimit = Kepulangan::getSantriOverLimit();
// Get settings untuk info periode
$settings = Kepulangan::getSettings();
return view('admin.kepulangan.index', compact(
'kepulangan',
'stats',
'tahunList',
'santriOverLimit',
'settings'
));
}
/**
* Show the form for creating a new kepulangan
*/
public function create()
{
$santriList = Santri::where('status', 'Aktif')
->orderBy('nama_lengkap')
->get();
$settings = Kepulangan::getSettings();
return view('admin.kepulangan.create', compact('santriList', 'settings'));
}
/**
* Store a newly created kepulangan
* PERBAIKAN: Hapus validasi minimal karakter
*/
public function store(Request $request)
{
$validated = $request->validate([
'id_santri' => 'required|exists:santris,id_santri',
'tanggal_pulang' => 'required|date|after_or_equal:today',
'tanggal_kembali' => 'required|date|after:tanggal_pulang',
'alasan' => 'required|string|max:500',
], [
'id_santri.required' => 'Santri wajib dipilih.',
'id_santri.exists' => 'Santri tidak ditemukan.',
'tanggal_pulang.required' => 'Tanggal pulang wajib diisi.',
'tanggal_pulang.after_or_equal' => 'Tanggal pulang tidak boleh kurang dari hari ini.',
'tanggal_kembali.required' => 'Tanggal kembali wajib diisi.',
'tanggal_kembali.after' => 'Tanggal kembali harus setelah tanggal pulang.',
'alasan.required' => 'Alasan kepulangan wajib diisi.',
'alasan.max' => 'Alasan maksimal 500 karakter.',
]);
// Create kepulangan (durasi_izin akan otomatis dihitung di model)
$kepulangan = Kepulangan::create($validated);
// Check apakah over limit
$kuota = Kepulangan::getSisaKuotaSantri($validated['id_santri']);
$message = 'Izin kepulangan berhasil diajukan.';
if ($kuota['status'] === 'melebihi') {
$message .= ' ⚠️ PERHATIAN: Santri ini sudah melebihi kuota ' . $kuota['kuota_maksimal'] . ' hari per tahun (Total: ' . $kuota['total_terpakai'] . ' hari).';
} elseif ($kuota['status'] === 'hampir_habis') {
$message .= ' ⚠️ Kuota hampir habis. Sisa: ' . $kuota['sisa_kuota'] . ' hari.';
}
return redirect()->route('admin.kepulangan.index')
->with('success', $message);
}
/**
* Display the specified kepulangan
*/
public function show($id_kepulangan)
{
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)
->with('santri')
->firstOrFail();
// Get detail kuota santri
$kuotaSantri = Kepulangan::getSisaKuotaSantri($kepulangan->id_santri);
// Get settings
$settings = Kepulangan::getSettings();
// Get detail izin tahun ini
$detailIzin = $this->getDetailIzinSantri(
$kepulangan->id_santri,
$settings->periode_mulai,
$settings->periode_akhir
);
// Get history kepulangan santri (exclude current)
$history = Kepulangan::where('id_santri', $kepulangan->id_santri)
->where('id_kepulangan', '!=', $id_kepulangan)
->orderBy('tanggal_pulang', 'desc')
->limit(5)
->get();
return view('admin.kepulangan.show', compact(
'kepulangan',
'kuotaSantri',
'detailIzin',
'history',
'settings'
));
}
/**
* Show the form for editing kepulangan
*/
public function edit($id_kepulangan)
{
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
if ($kepulangan->status !== 'Menunggu') {
return redirect()->route('admin.kepulangan.index')
->with('error', 'Hanya izin dengan status "Menunggu" yang bisa diedit.');
}
$santriList = Santri::where('status', 'Aktif')
->orderBy('nama_lengkap')
->get();
$settings = Kepulangan::getSettings();
$kuotaSantri = Kepulangan::getSisaKuotaSantri($kepulangan->id_santri);
return view('admin.kepulangan.edit', compact(
'kepulangan',
'santriList',
'settings',
'kuotaSantri'
));
}
/**
* Update the specified kepulangan
* PERBAIKAN: Hapus validasi minimal karakter
*/
public function update(Request $request, $id_kepulangan)
{
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
if ($kepulangan->status !== 'Menunggu') {
return redirect()->route('admin.kepulangan.index')
->with('error', 'Hanya izin dengan status "Menunggu" yang bisa diubah.');
}
$validated = $request->validate([
'tanggal_pulang' => 'required|date|after_or_equal:today',
'tanggal_kembali' => 'required|date|after:tanggal_pulang',
'alasan' => 'required|string|max:500',
], [
'tanggal_pulang.required' => 'Tanggal pulang wajib diisi.',
'tanggal_kembali.required' => 'Tanggal kembali wajib diisi.',
'tanggal_kembali.after' => 'Tanggal kembali harus setelah tanggal pulang.',
'alasan.required' => 'Alasan kepulangan wajib diisi.',
]);
// Update (durasi_izin akan otomatis dihitung ulang di model)
$kepulangan->update($validated);
// Check apakah over limit setelah update
$kuota = Kepulangan::getSisaKuotaSantri($kepulangan->id_santri);
$message = 'Data kepulangan berhasil diperbarui.';
if ($kuota['status'] === 'melebihi') {
$message .= ' ⚠️ PERHATIAN: Santri ini sudah melebihi kuota ' . $kuota['kuota_maksimal'] . ' hari.';
}
return redirect()->route('admin.kepulangan.show', $id_kepulangan)
->with('success', $message);
}
/**
* Remove the specified kepulangan
* PERBAIKAN: Bisa hapus data Selesai juga (untuk data lama)
*/
public function destroy($id_kepulangan)
{
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
// PERBAIKAN: Bisa hapus Menunggu, Ditolak, atau Selesai
if (!in_array($kepulangan->status, ['Menunggu', 'Ditolak', 'Selesai'])) {
return response()->json([
'success' => false,
'message' => 'Hanya izin dengan status "Menunggu", "Ditolak", atau "Selesai" yang bisa dihapus.'
], 403);
}
$kepulangan->delete();
return response()->json([
'success' => true,
'message' => 'Data kepulangan berhasil dihapus.'
]);
}
/**
* Approve kepulangan
*/
public function approve(Request $request, $id_kepulangan)
{
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
if ($kepulangan->status !== 'Menunggu') {
return response()->json([
'success' => false,
'message' => 'Izin sudah diproses sebelumnya.'
], 400);
}
$kepulangan->update([
'status' => 'Disetujui',
'approved_by' => Auth::user()->name,
'approved_at' => now(),
'catatan' => $request->catatan ?? null,
]);
return response()->json([
'success' => true,
'message' => 'Izin kepulangan berhasil disetujui.'
]);
}
/**
* Reject kepulangan
*/
public function reject(Request $request, $id_kepulangan)
{
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
$validated = $request->validate([
'alasan_penolakan' => 'required|string',
], [
'alasan_penolakan.required' => 'Alasan penolakan wajib diisi.',
]);
if ($kepulangan->status !== 'Menunggu') {
return response()->json([
'success' => false,
'message' => 'Izin sudah diproses sebelumnya.'
], 400);
}
$kepulangan->update([
'status' => 'Ditolak',
'approved_by' => Auth::user()->name,
'approved_at' => now(),
'catatan' => $validated['alasan_penolakan'],
]);
return response()->json([
'success' => true,
'message' => 'Izin kepulangan telah ditolak.'
]);
}
/**
* Complete kepulangan dengan input tanggal kembali aktual
*/
public function complete(Request $request, $id_kepulangan)
{
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
if ($kepulangan->status !== 'Disetujui') {
return response()->json([
'success' => false,
'message' => 'Hanya izin yang disetujui yang bisa diselesaikan.'
], 400);
}
// Validasi tanggal kembali aktual
$validated = $request->validate([
'tanggal_kembali_aktual' => 'required|date',
], [
'tanggal_kembali_aktual.required' => 'Tanggal kembali aktual wajib diisi.',
'tanggal_kembali_aktual.date' => 'Format tanggal tidak valid.',
]);
// Validasi manual: tanggal kembali tidak boleh sebelum tanggal pulang
$tanggalKembaliAktual = Carbon::parse($validated['tanggal_kembali_aktual']);
if ($tanggalKembaliAktual->lt($kepulangan->tanggal_pulang)) {
return response()->json([
'success' => false,
'message' => 'Tanggal kembali aktual tidak boleh sebelum tanggal pulang (' . $kepulangan->tanggal_pulang->format('d M Y') . ').'
], 400);
}
// Simpan durasi rencana untuk perbandingan
$durasiRencana = $kepulangan->durasi_izin;
$tanggalKembaliRencana = $kepulangan->tanggal_kembali->format('Y-m-d');
// Update tanggal_kembali dengan tanggal aktual
// Durasi_izin akan otomatis recalculate di model (via updating event)
$kepulangan->update([
'tanggal_kembali' => $validated['tanggal_kembali_aktual'],
'status' => 'Selesai'
]);
// Refresh untuk mendapat durasi yang sudah dihitung ulang
$kepulangan->refresh();
$durasiAktual = $kepulangan->durasi_izin;
// Buat pesan informatif
$message = 'Kepulangan santri berhasil diselesaikan.';
if ($durasiAktual < $durasiRencana) {
$selisih = $durasiRencana - $durasiAktual;
$message .= " Santri pulang {$selisih} hari lebih cepat dari rencana (Rencana: {$durasiRencana} hari, Aktual: {$durasiAktual} hari). Kuota telah disesuaikan.";
} elseif ($durasiAktual > $durasiRencana) {
$selisih = $durasiAktual - $durasiRencana;
$message .= " Santri pulang {$selisih} hari lebih lambat dari rencana (Rencana: {$durasiRencana} hari, Aktual: {$durasiAktual} hari). Kuota telah disesuaikan.";
} else {
$message .= " Santri pulang sesuai rencana ({$durasiAktual} hari).";
}
return response()->json([
'success' => true,
'message' => $message,
'data' => [
'durasi_rencana' => $durasiRencana,
'durasi_aktual' => $durasiAktual,
'tanggal_kembali_rencana' => $tanggalKembaliRencana,
'tanggal_kembali_aktual' => $validated['tanggal_kembali_aktual'],
]
]);
}
/**
* Print surat izin kepulangan
*/
public function print($id_kepulangan)
{
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)
->with('santri')
->firstOrFail();
if ($kepulangan->status !== 'Disetujui') {
return redirect()->route('admin.kepulangan.show', $id_kepulangan)
->with('error', 'Hanya izin yang disetujui yang bisa dicetak.');
}
$santri = $kepulangan->santri;
$tanggalCetak = Carbon::now()->format('d F Y');
$pdf = Pdf::loadView('admin.kepulangan.surat-pdf', compact(
'kepulangan',
'santri',
'tanggalCetak'
));
return $pdf->stream('Surat-Izin-' . $kepulangan->id_kepulangan . '.pdf');
}
/**
* API: Get santri data with penggunaan kuota
* PERBAIKAN: Return JSON yang benar, tidak ada HTML error
*/
public function getSantriData($idSantri)
{
try {
$santri = Santri::where('id_santri', $idSantri)->first();
if (!$santri) {
return response()->json([
'success' => false,
'message' => 'Santri tidak ditemukan.'
], 404);
}
$kuotaSantri = Kepulangan::getSisaKuotaSantri($idSantri);
return response()->json([
'success' => true,
'santri' => $santri,
'penggunaan_izin' => $kuotaSantri
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage()
], 500);
}
}
/**
* ========================================
* FITUR PENGATURAN KUOTA
* ========================================
*/
/**
* Show settings page
*/
public function settings()
{
$settings = Kepulangan::getSettings();
$resetLogs = Kepulangan::getResetLogs(20);
// Statistik periode saat ini
$totalSantri = Santri::where('status', 'Aktif')->count();
$santriOverLimit = Kepulangan::getSantriOverLimit();
$totalIzinPeriodeIni = Kepulangan::whereBetween('tanggal_pulang', [
$settings->periode_mulai,
$settings->periode_akhir
])->whereIn('status', ['Disetujui', 'Selesai'])->count();
return view('admin.kepulangan.settings', compact(
'settings',
'resetLogs',
'totalSantri',
'santriOverLimit',
'totalIzinPeriodeIni'
));
}
/**
* Update settings
*/
public function updateSettings(Request $request)
{
$validated = $request->validate([
'kuota_maksimal' => 'required|integer|min:1|max:365',
'periode_mulai' => 'required|date',
'periode_akhir' => 'required|date|after:periode_mulai',
], [
'kuota_maksimal.required' => 'Kuota maksimal wajib diisi.',
'kuota_maksimal.min' => 'Kuota minimal 1 hari.',
'kuota_maksimal.max' => 'Kuota maksimal 365 hari.',
'periode_mulai.required' => 'Periode mulai wajib diisi.',
'periode_akhir.required' => 'Periode akhir wajib diisi.',
'periode_akhir.after' => 'Periode akhir harus setelah periode mulai.',
]);
Kepulangan::updateSettings(
$validated['kuota_maksimal'],
$validated['periode_mulai'],
$validated['periode_akhir']
);
return redirect()->route('admin.kepulangan.settings')
->with('success', 'Pengaturan kuota berhasil diperbarui.');
}
/**
* Reset kuota satu santri
*/
public function resetKuotaSantri(Request $request, $idSantri)
{
$validated = $request->validate([
'catatan' => 'nullable|string|max:500',
]);
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
$result = Kepulangan::resetKuotaSantri(
$idSantri,
Auth::user()->name,
$validated['catatan'] ?? 'Reset kuota individual untuk ' . $santri->nama_lengkap
);
return response()->json([
'success' => true,
'message' => 'Kuota santri ' . $santri->nama_lengkap . ' berhasil direset. Total ' . $result['total_hari_direset'] . ' hari telah direset.'
]);
}
/**
* Reset kuota semua santri
*/
public function resetKuotaSemuaSantri(Request $request)
{
$validated = $request->validate([
'catatan' => 'nullable|string|max:500',
'konfirmasi' => 'required|accepted',
], [
'konfirmasi.accepted' => 'Anda harus mencentang konfirmasi untuk melanjutkan reset massal.',
]);
$result = Kepulangan::resetKuotaSemuaSantri(
Auth::user()->name,
$validated['catatan'] ?? 'Reset kuota tahunan massal'
);
return response()->json([
'success' => true,
'message' => 'Kuota berhasil direset untuk ' . $result['total_santri'] . ' santri. Total ' . $result['total_hari_direset'] . ' hari telah direset.',
'data' => $result
]);
}
/**
* Show list santri over limit
*/
public function santriOverLimit()
{
$settings = Kepulangan::getSettings();
$santriOverLimitIds = Kepulangan::getSantriOverLimit();
$santriList = Santri::whereIn('id_santri', array_keys($santriOverLimitIds))
->with(['kepulangan' => function($query) use ($settings) {
$query->whereBetween('tanggal_pulang', [
$settings->periode_mulai,
$settings->periode_akhir
])->whereIn('status', ['Disetujui', 'Selesai']);
}])
->get()
->map(function($santri) use ($santriOverLimitIds) {
$kuota = Kepulangan::getSisaKuotaSantri($santri->id_santri);
$santri->total_hari_izin = $santriOverLimitIds[$santri->id_santri];
$santri->kuota_info = $kuota;
return $santri;
})
->sortByDesc('total_hari_izin');
return view('admin.kepulangan.over-limit', compact('santriList', 'settings'));
}
/**
* Helper: Get detail izin santri
*/
private function getDetailIzinSantri($idSantri, $periodeMulai, $periodeAkhir)
{
$kepulanganList = Kepulangan::where('id_santri', $idSantri)
->whereIn('status', ['Disetujui', 'Selesai'])
->whereBetween('tanggal_pulang', [$periodeMulai, $periodeAkhir])
->orderBy('tanggal_pulang', 'desc')
->get();
$settings = Kepulangan::getSettings();
$totalHari = $kepulanganList->sum('durasi_izin');
$totalIzin = $kepulanganList->count();
$sisaKuota = max(0, $settings->kuota_maksimal - $totalHari);
$overLimit = $totalHari > $settings->kuota_maksimal;
$details = $kepulanganList->map(function($item) {
return [
'id' => $item->id_kepulangan,
'tanggal' => $item->tanggal_pulang_formatted . ' - ' . $item->tanggal_kembali_formatted,
'durasi' => $item->durasi_izin,
'alasan' => $item->alasan,
'status' => $item->status,
];
});
return [
'total_hari' => $totalHari,
'total_izin' => $totalIzin,
'sisa_kuota' => $sisaKuota,
'over_limit' => $overLimit,
'details' => $details,
];
}
/**
* ========================================
* PENGAJUAN DARI MOBILE
* ========================================
*/
/**
* Tampilkan daftar pengajuan kepulangan dari mobile
*/
public function pengajuan(Request $request)
{
$query = \App\Models\PengajuanKepulangan::with('santri');
// Search
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('id_pengajuan', 'like', "%{$search}%")
->orWhere('alasan', 'like', "%{$search}%")
->orWhereHas('santri', function($q2) use ($search) {
$q2->where('nama_lengkap', 'like', "%{$search}%")
->orWhere('id_santri', 'like', "%{$search}%");
});
});
}
// Filter status
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Get data dengan pagination
$pengajuan = $query->orderBy('created_at', 'desc')->paginate(15);
// Statistics
$stats = [
'total_data' => \App\Models\PengajuanKepulangan::count(),
'menunggu' => \App\Models\PengajuanKepulangan::where('status', 'Menunggu')->count(),
'disetujui' => \App\Models\PengajuanKepulangan::where('status', 'Disetujui')->count(),
'ditolak' => \App\Models\PengajuanKepulangan::where('status', 'Ditolak')->count(),
];
return view('admin.kepulangan.pengajuan', compact('pengajuan', 'stats'));
}
/**
* Approve pengajuan kepulangan
*/
public function approvePengajuan(Request $request, $id)
{
try {
$validated = $request->validate([
'catatan_review' => 'nullable|string|max:500',
]);
$pengajuan = \App\Models\PengajuanKepulangan::findOrFail($id);
// Cegah review ulang
if ($pengajuan->status !== 'Menunggu') {
return response()->json([
'success' => false,
'message' => 'Pengajuan sudah direview sebelumnya'
], 400);
}
// Simpan ID pengajuan untuk catatan sebelum dihapus
$id_pengajuan = $pengajuan->id_pengajuan;
// Pindahkan ke tabel kepulangan
$kepulangan = Kepulangan::create([
'id_santri' => $pengajuan->id_santri,
'tanggal_pulang' => $pengajuan->tanggal_pulang,
'tanggal_kembali' => $pengajuan->tanggal_kembali,
'durasi_izin' => $pengajuan->durasi_izin,
'alasan' => $pengajuan->alasan,
'status' => 'Disetujui',
'catatan' => 'Disetujui dari pengajuan mobile: ' . $id_pengajuan . ($validated['catatan_review'] ? ' - ' . $validated['catatan_review'] : ''),
'approved_by' => Auth::user()->name,
'approved_at' => now(),
]);
// Hapus dari tabel pengajuan setelah dipindahkan
$pengajuan->delete();
// TODO: Kirim notifikasi FCM ke mobile
// $this->sendNotification($pengajuan->id_santri, 'approved');
return response()->json([
'success' => true,
'message' => 'Pengajuan berhasil disetujui dan ditambahkan ke data kepulangan',
'kepulangan_id' => $kepulangan->id_kepulangan,
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage()
], 500);
}
}
/**
* Reject pengajuan kepulangan
*/
public function rejectPengajuan(Request $request, $id)
{
try {
$validated = $request->validate([
'catatan_review' => 'required|string|max:500',
], [
'catatan_review.required' => 'Catatan penolakan wajib diisi',
]);
$pengajuan = \App\Models\PengajuanKepulangan::findOrFail($id);
// Cegah review ulang
if ($pengajuan->status !== 'Menunggu') {
return response()->json([
'success' => false,
'message' => 'Pengajuan sudah direview sebelumnya'
], 400);
}
// Simpan data untuk notifikasi sebelum dihapus
$id_santri = $pengajuan->id_santri;
$catatan = $validated['catatan_review'];
// Hapus pengajuan yang ditolak
$pengajuan->delete();
// TODO: Kirim notifikasi FCM ke mobile
// $this->sendNotification($id_santri, 'rejected', $catatan);
return response()->json([
'success' => true,
'message' => 'Pengajuan berhasil ditolak dan dihapus dari daftar'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage()
], 500);
}
}
}

View File

@ -1,240 +0,0 @@
<?php
// app/Http/Controllers/Admin/KesehatanSantriController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\KesehatanSantri;
use App\Models\Santri;
use Illuminate\Http\Request;
class KesehatanSantriController extends Controller
{
/**
* Tampilkan daftar data kesehatan santri
*/
public function index(Request $request)
{
$query = KesehatanSantri::with('santri');
// Search
if ($request->filled('search')) {
$query->search($request->search);
}
// Filter status
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Filter bulan
if ($request->filled('month')) {
$query->whereMonth('tanggal_masuk', $request->month);
}
// Filter tahun
$year = $request->filled('year') ? $request->year : date('Y');
$query->whereYear('tanggal_masuk', $year);
// Urutkan terbaru
$kesehatanSantri = $query->orderBy('tanggal_masuk', 'desc')->paginate(15);
// Data untuk filter
$statusOptions = ['dirawat', 'sembuh', 'izin'];
$monthOptions = [
1 => 'Januari', 2 => 'Februari', 3 => 'Maret',
4 => 'April', 5 => 'Mei', 6 => 'Juni',
7 => 'Juli', 8 => 'Agustus', 9 => 'September',
10 => 'Oktober', 11 => 'November', 12 => 'Desember'
];
$yearOptions = range(date('Y'), date('Y') - 5);
return view('admin.kesehatan-santri.index', compact(
'kesehatanSantri',
'statusOptions',
'monthOptions',
'yearOptions'
));
}
/**
* Tampilkan form tambah data
*/
public function create()
{
// Ambil semua santri aktif
$santri = Santri::where('status', 'Aktif')
->orderBy('nama_lengkap')
->get();
return view('admin.kesehatan-santri.create', compact('santri'));
}
/**
* Simpan data kesehatan baru
*/
public function store(Request $request)
{
$validated = $request->validate([
'id_santri' => 'required|exists:santris,id_santri',
'tanggal_masuk' => 'required|date|before_or_equal:today',
'tanggal_keluar' => 'nullable|date|after_or_equal:tanggal_masuk|before_or_equal:today',
'keluhan' => 'required|string|max:1000',
'catatan' => 'nullable|string|max:1000',
'status' => 'required|in:dirawat,sembuh,izin',
], [
'id_santri.required' => 'Santri wajib dipilih.',
'id_santri.exists' => 'Santri tidak ditemukan.',
'tanggal_masuk.required' => 'Tanggal masuk wajib diisi.',
'tanggal_masuk.before_or_equal' => 'Tanggal masuk tidak boleh melebihi hari ini.',
'tanggal_keluar.after_or_equal' => 'Tanggal keluar harus setelah tanggal masuk.',
'keluhan.required' => 'Keluhan wajib diisi.',
'keluhan.max' => 'Keluhan maksimal 1000 karakter.',
'status.required' => 'Status wajib dipilih.',
]);
// Validasi: Jika status bukan dirawat, tanggal keluar wajib diisi
if ($validated['status'] != 'dirawat' && empty($validated['tanggal_keluar'])) {
return back()->withErrors([
'tanggal_keluar' => 'Tanggal keluar wajib diisi untuk status ' . $validated['status']
])->withInput();
}
// Jika status dirawat, kosongkan tanggal keluar
if ($validated['status'] == 'dirawat') {
$validated['tanggal_keluar'] = null;
}
KesehatanSantri::create($validated);
return redirect()->route('admin.kesehatan-santri.index')
->with('success', 'Data kesehatan santri berhasil ditambahkan.');
}
/**
* Tampilkan detail data kesehatan
*/
public function show(KesehatanSantri $kesehatanSantri)
{
// Load relasi santri
$kesehatanSantri->load('santri');
// Ambil riwayat kesehatan santri lainnya (5 data terbaru, kecuali data saat ini)
$riwayatKesehatan = KesehatanSantri::where('id_santri', $kesehatanSantri->id_santri)
->where('id', '!=', $kesehatanSantri->id)
->orderBy('tanggal_masuk', 'desc')
->take(5)
->get();
return view('admin.kesehatan-santri.show', compact('kesehatanSantri', 'riwayatKesehatan'));
}
/**
* Tampilkan form edit
*/
public function edit(KesehatanSantri $kesehatanSantri)
{
// Ambil semua santri aktif
$santri = Santri::where('status', 'Aktif')
->orderBy('nama_lengkap')
->get();
return view('admin.kesehatan-santri.edit', compact('kesehatanSantri', 'santri'));
}
/**
* Update data kesehatan
*/
public function update(Request $request, KesehatanSantri $kesehatanSantri)
{
$validated = $request->validate([
'id_santri' => 'required|exists:santris,id_santri',
'tanggal_masuk' => 'required|date|before_or_equal:today',
'tanggal_keluar' => 'nullable|date|after_or_equal:tanggal_masuk|before_or_equal:today',
'keluhan' => 'required|string|max:1000',
'catatan' => 'nullable|string|max:1000',
'status' => 'required|in:dirawat,sembuh,izin',
], [
'id_santri.required' => 'Santri wajib dipilih.',
'tanggal_masuk.required' => 'Tanggal masuk wajib diisi.',
'tanggal_keluar.after_or_equal' => 'Tanggal keluar harus setelah tanggal masuk.',
'keluhan.required' => 'Keluhan wajib diisi.',
'status.required' => 'Status wajib dipilih.',
]);
// Validasi: Jika status bukan dirawat, tanggal keluar wajib diisi
if ($validated['status'] != 'dirawat' && empty($validated['tanggal_keluar'])) {
return back()->withErrors([
'tanggal_keluar' => 'Tanggal keluar wajib diisi untuk status ' . $validated['status']
])->withInput();
}
// Jika status dirawat, kosongkan tanggal keluar
if ($validated['status'] == 'dirawat') {
$validated['tanggal_keluar'] = null;
}
$kesehatanSantri->update($validated);
return redirect()->route('admin.kesehatan-santri.index')
->with('success', 'Data kesehatan santri berhasil diperbarui.');
}
/**
* Hapus data kesehatan
*/
public function destroy(KesehatanSantri $kesehatanSantri)
{
$namaSantri = $kesehatanSantri->santri->nama_lengkap;
$kesehatanSantri->delete();
return redirect()->route('admin.kesehatan-santri.index')
->with('success', 'Data kesehatan "' . $namaSantri . '" berhasil dihapus.');
}
/**
* Update status keluar UKP (via AJAX/Modal)
*/
public function keluarUkp(Request $request, KesehatanSantri $kesehatanSantri)
{
$validated = $request->validate([
'tanggal_keluar' => 'required|date|after_or_equal:' . $kesehatanSantri->tanggal_masuk . '|before_or_equal:today',
'status' => 'required|in:sembuh,izin',
], [
'tanggal_keluar.required' => 'Tanggal keluar wajib diisi.',
'tanggal_keluar.after_or_equal' => 'Tanggal keluar harus setelah tanggal masuk.',
'status.required' => 'Status wajib dipilih.',
]);
$kesehatanSantri->update($validated);
return redirect()->route('admin.kesehatan-santri.index')
->with('success', 'Santri berhasil keluar dari UKP.');
}
/**
* Tampilkan riwayat kesehatan per santri
*/
public function riwayat($id_santri)
{
// Cari santri
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
// Ambil semua riwayat kesehatan santri
$riwayatKesehatan = KesehatanSantri::where('id_santri', $id_santri)
->orderBy('tanggal_masuk', 'desc')
->paginate(15);
return view('admin.kesehatan-santri.riwayat', compact('santri', 'riwayatKesehatan'));
}
/**
* Cetak surat izin sakit
*/
public function cetakSurat(KesehatanSantri $kesehatanSantri)
{
$kesehatanSantri->load('santri');
return view('admin.kesehatan-santri.cetak-surat', compact('kesehatanSantri'));
}
}

View File

@ -1,141 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Keuangan;
use App\Models\PembayaranSpp;
use Illuminate\Http\Request;
use Carbon\Carbon;
class KeuanganController extends Controller
{
public function index(Request $request)
{
$query = Keuangan::query();
if ($request->filled('search')) {
$query->search($request->search);
}
if ($request->filled('jenis')) {
$query->where('jenis', $request->jenis);
}
if ($request->filled('bulan') && $request->filled('tahun')) {
$query->bulan($request->bulan, $request->tahun);
}
$transaksi = $query->orderByDesc('tanggal')
->orderByDesc('created_at')
->paginate(20)
->appends(request()->query());
return view('admin.keuangan.index', compact('transaksi'));
}
public function create()
{
return view('admin.keuangan.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'jenis' => 'required|in:pemasukan,pengeluaran',
'nominal' => 'required|numeric|min:1',
'keterangan' => 'nullable|string|max:500',
'tanggal' => 'required|date',
], [
'jenis.required' => 'Jenis transaksi wajib dipilih.',
'nominal.required' => 'Nominal wajib diisi.',
'nominal.min' => 'Nominal minimal Rp 1.',
'tanggal.required' => 'Tanggal wajib diisi.',
]);
Keuangan::create($validated);
return redirect()->route('admin.keuangan.index')
->with('success', 'Transaksi keuangan berhasil ditambahkan.');
}
public function show($id)
{
$transaksi = Keuangan::findOrFail($id);
return view('admin.keuangan.show', compact('transaksi'));
}
public function edit($id)
{
$transaksi = Keuangan::findOrFail($id);
return view('admin.keuangan.edit', compact('transaksi'));
}
public function update(Request $request, $id)
{
$transaksi = Keuangan::findOrFail($id);
$validated = $request->validate([
'jenis' => 'required|in:pemasukan,pengeluaran',
'nominal' => 'required|numeric|min:1',
'keterangan' => 'nullable|string|max:500',
'tanggal' => 'required|date',
]);
$transaksi->update($validated);
return redirect()->route('admin.keuangan.index')
->with('success', 'Transaksi berhasil diperbarui.');
}
public function destroy($id)
{
Keuangan::findOrFail($id)->delete();
return redirect()->route('admin.keuangan.index')
->with('success', 'Transaksi berhasil dihapus.');
}
/**
* Laporan Neraca: SPP terkumpul vs pengeluaran pondok = sisa kas
*/
public function laporan(Request $request)
{
$bulan = $request->get('bulan', (int) date('n'));
$tahun = $request->get('tahun', (int) date('Y'));
// SPP terkumpul bulan ini
$sppTerkumpul = PembayaranSpp::where('bulan', $bulan)
->where('tahun', $tahun)
->lunas()
->sum('nominal');
// Pemasukan pondok (kas masuk non-SPP)
$pemasukanPondok = Keuangan::pemasukan()->bulan($bulan, $tahun)->sum('nominal');
// Pengeluaran pondok
$pengeluaranPondok = Keuangan::pengeluaran()->bulan($bulan, $tahun)->sum('nominal');
$totalPemasukan = $sppTerkumpul + $pemasukanPondok;
$sisaKas = $totalPemasukan - $pengeluaranPondok;
// Detail pengeluaran terbesar
$detailPengeluaran = Keuangan::pengeluaran()
->bulan($bulan, $tahun)
->orderByDesc('nominal')
->limit(10)
->get();
// Detail pemasukan non-SPP
$detailPemasukan = Keuangan::pemasukan()
->bulan($bulan, $tahun)
->orderByDesc('nominal')
->limit(10)
->get();
return view('admin.keuangan.laporan', compact(
'bulan', 'tahun',
'sppTerkumpul', 'pemasukanPondok', 'pengeluaranPondok',
'totalPemasukan', 'sisaKas',
'detailPengeluaran', 'detailPemasukan'
));
}
}

View File

@ -1,89 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\KlasifikasiPelanggaran;
use Illuminate\Http\Request;
class KlasifikasiPelanggaranController extends Controller
{
public function index()
{
$data = KlasifikasiPelanggaran::withCount('pelanggarans')
->byUrutan()
->get();
return view('admin.klasifikasi_pelanggaran.index', compact('data'));
}
public function create()
{
$last = KlasifikasiPelanggaran::orderBy('id', 'desc')->first();
$nextNum = $last ? intval(substr($last->id_klasifikasi, 2)) + 1 : 1;
$nextId = 'KL' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
return view('admin.klasifikasi_pelanggaran.create', compact('nextId'));
}
public function store(Request $request)
{
$validated = $request->validate([
'nama_klasifikasi' => 'required|string|max:100',
'deskripsi' => 'nullable|string',
'urutan' => 'nullable|integer|min:0',
'is_active' => 'boolean',
]);
KlasifikasiPelanggaran::create($validated);
return redirect()->route('admin.klasifikasi-pelanggaran.index')
->with('success', 'Klasifikasi berhasil ditambahkan.');
}
public function show(KlasifikasiPelanggaran $klasifikasiPelanggaran)
{
$klasifikasiPelanggaran->load(['pelanggarans' => function($q) {
$q->aktif()->orderBy('nama_pelanggaran');
}]);
return view('admin.klasifikasi_pelanggaran.show', [
'klasifikasi' => $klasifikasiPelanggaran
]);
}
public function edit(KlasifikasiPelanggaran $klasifikasiPelanggaran)
{
return view('admin.klasifikasi_pelanggaran.edit', [
'klasifikasi' => $klasifikasiPelanggaran
]);
}
public function update(Request $request, KlasifikasiPelanggaran $klasifikasiPelanggaran)
{
$validated = $request->validate([
'nama_klasifikasi' => 'required|string|max:100',
'deskripsi' => 'nullable|string',
'urutan' => 'nullable|integer|min:0',
'is_active' => 'boolean',
]);
$klasifikasiPelanggaran->update($validated);
return redirect()->route('admin.klasifikasi-pelanggaran.index')
->with('success', 'Klasifikasi berhasil diperbarui.');
}
public function destroy(KlasifikasiPelanggaran $klasifikasiPelanggaran)
{
if ($klasifikasiPelanggaran->pelanggarans()->count() > 0) {
return redirect()->route('admin.klasifikasi-pelanggaran.index')
->with('error', 'Klasifikasi tidak dapat dihapus karena masih memiliki pelanggaran.');
}
$klasifikasiPelanggaran->delete();
return redirect()->route('admin.klasifikasi-pelanggaran.index')
->with('success', 'Klasifikasi berhasil dihapus.');
}
}

View File

@ -1,629 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AbsensiKegiatan;
use App\Models\Kegiatan;
use App\Models\KategoriKegiatan;
use App\Models\Santri;
use App\Models\Kelas;
use App\Models\KelompokKelas;
use App\Models\SantriKelas;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Carbon\Carbon;
class LaporanKegiatanController extends Controller
{
// ════════════════════════════════════════════════════════
// A. INDEX
// ════════════════════════════════════════════════════════
public function index(Request $request)
{
$periode = $request->get('periode', 'minggu_ini');
[$startDate, $endDate] = $this->getPeriodeRange($periode, $request);
[$prevStart, $prevEnd] = $this->getPreviousPeriodeRange($periode, $startDate, $endDate);
$periodeLabel = $this->getPeriodeLabel($periode, $startDate, $endDate);
$kpi = $this->calculateKpi($startDate, $endDate);
$kpiPrev = $this->calculateKpi($prevStart, $prevEnd);
$kpiComparison = [
'avg_kehadiran' => round($kpi['avg_kehadiran'] - $kpiPrev['avg_kehadiran'], 1),
'santri_perlu_perhatian' => $kpi['santri_perlu_perhatian'] - $kpiPrev['santri_perlu_perhatian'],
];
$trendData = $this->getTrendData($startDate, $endDate);
$topKegiatan = $this->getTopBottomKegiatan($startDate, $endDate, 'top', 5);
$bottomKegiatan = $this->getTopBottomKegiatan($startDate, $endDate, 'bottom', 5);
$patterns = Cache::remember('laporan_patterns_' . now()->format('YmdH'), 3600, fn() => $this->buildPatterns());
$santriPerluPerhatianList = $this->getSantriPerluPerhatianList($startDate, $endDate, 8);
$kelasRingkasan = $this->getKelasRingkasan($startDate, $endDate);
$distribusiSantri = $this->getDistribusiSantri($startDate, $endDate);
return view('admin.kegiatan.laporan.index', compact(
'periode', 'startDate', 'endDate', 'periodeLabel',
'kpi', 'kpiComparison',
'trendData', 'topKegiatan', 'bottomKegiatan',
'patterns', 'santriPerluPerhatianList',
'kelasRingkasan', 'distribusiSantri'
));
}
// ════════════════════════════════════════════════════════
// B. DETAIL SANTRI → redirect
// ════════════════════════════════════════════════════════
public function detailSantri($id_santri, Request $request)
{
return redirect()->route('admin.riwayat-kegiatan.detail-santri', $id_santri)
->with('info', 'Detail santri tersedia di Riwayat Kegiatan.');
}
// ════════════════════════════════════════════════════════
// C. ANALISIS KEGIATAN → redirect
// ════════════════════════════════════════════════════════
public function analisKegiatan($kegiatan_id, Request $request)
{
$kegiatan = Kegiatan::where('kegiatan_id', $kegiatan_id)->firstOrFail();
return redirect()->route('admin.riwayat-kegiatan.show', $kegiatan->id)
->with('info', 'Detail kegiatan tersedia di Riwayat Kegiatan.');
}
// ════════════════════════════════════════════════════════
// D. SANTRI PERLU PERHATIAN (halaman penuh)
// ════════════════════════════════════════════════════════
public function santriPerluPerhatian(Request $request)
{
[$startDate, $endDate] = $this->getPeriodeRange($request->get('periode', 'bulan_ini'), $request);
$periodeLabel = $this->getPeriodeLabel($request->get('periode', 'bulan_ini'), $startDate, $endDate);
$totalSantriAktif = Santri::where('status', 'Aktif')->count();
$query = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
->where('santris.status', 'Aktif')
->select(
'santris.id_santri', 'santris.nama_lengkap',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Terlambat" THEN 1 ELSE 0 END) as terlambat'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Alpa" THEN 1 ELSE 0 END) as alpa'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Izin" THEN 1 ELSE 0 END) as izin'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
)
->groupBy('santris.id_santri', 'santris.nama_lengkap')
->having('persen', '<', 70)
->orderBy('persen', 'asc');
if ($request->filled('id_kelas')) {
$sIds = SantriKelas::where('id_kelas', $request->id_kelas)->pluck('id_santri');
$query->whereIn('santris.id_santri', $sIds);
}
$santris = $query->paginate(20)->appends(request()->query());
$kelasList = Kelas::active()->ordered()->with('kelompok')->get();
return view('admin.kegiatan.laporan.santri-perlu-perhatian', compact(
'santris', 'kelasList', 'startDate', 'endDate',
'periodeLabel', 'totalSantriAktif'
));
}
// ════════════════════════════════════════════════════════
// E. PATTERN DETECTION
// ════════════════════════════════════════════════════════
public function patternDetection(Request $request)
{
$patterns = $this->buildPatterns();
if ($request->ajax()) return response()->json($patterns);
return $patterns;
}
// ════════════════════════════════════════════════════════
// F. EXPORT CSV
// ════════════════════════════════════════════════════════
public function exportExcel(Request $request)
{
[$startDate, $endDate] = $this->getPeriodeRange($request->get('periode', 'bulan_ini'), $request);
$filename = 'laporan_kegiatan_' . $startDate->format('Ymd') . '_' . $endDate->format('Ymd') . '.csv';
return response()->streamDownload(function () use ($startDate, $endDate) {
$handle = fopen('php://output', 'w');
fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
$kpi = $this->calculateKpi($startDate, $endDate);
fputcsv($handle, ['=== RINGKASAN LAPORAN KEGIATAN ===']);
fputcsv($handle, ['Periode', $startDate->format('d/m/Y') . ' s/d ' . $endDate->format('d/m/Y')]);
fputcsv($handle, ['Total Kegiatan Unik', $kpi['total_kegiatan']]);
fputcsv($handle, ['Total Absensi Tercatat', $kpi['total_absensi']]);
fputcsv($handle, ['Hadir Efektif', $kpi['total_hadir'] . ' dari ' . $kpi['total_absensi']]);
fputcsv($handle, ['Rata-rata Kehadiran', $kpi['avg_kehadiran'] . '%']);
fputcsv($handle, ['Santri Perlu Perhatian', $kpi['santri_perlu_perhatian'] . ' dari ' . $kpi['total_santri_aktif'] . ' santri aktif']);
fputcsv($handle, []);
fputcsv($handle, ['=== KEHADIRAN PER KEGIATAN ===']);
fputcsv($handle, ['Kegiatan', 'Kategori', 'Hari', 'Total Absensi', 'Hadir Efektif', 'Terlambat', 'Izin', 'Sakit', 'Alpa', '% Hadir']);
$perKeg = $this->getTopBottomKegiatan($startDate, $endDate, 'top', 999);
foreach ($perKeg as $k) {
fputcsv($handle, [
$k['nama_kegiatan'], $k['nama_kategori'] ?? '-', $k['hari'] ?? '-',
$k['total'], $k['hadir'], $k['terlambat'] ?? 0,
$k['izin'] ?? 0, $k['sakit'] ?? 0, $k['alpa'] ?? 0,
$k['persen'] . '%'
]);
}
fputcsv($handle, []);
fputcsv($handle, ['=== DETAIL PER SANTRI ===']);
fputcsv($handle, ['ID Santri', 'Nama', 'Total Absensi', 'Hadir Efektif', 'Terlambat', 'Izin', 'Sakit', 'Alpa', '% Hadir']);
$perSantri = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
->where('santris.status', 'Aktif')
->select(
'santris.id_santri', 'santris.nama_lengkap',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Terlambat" THEN 1 ELSE 0 END) as terlambat'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Izin" THEN 1 ELSE 0 END) as izin'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Alpa" THEN 1 ELSE 0 END) as alpa')
)
->groupBy('santris.id_santri', 'santris.nama_lengkap')
->orderBy('santris.nama_lengkap')
->get();
foreach ($perSantri as $s) {
$persen = $s->total > 0 ? round($s->hadir / $s->total * 100, 1) : 0;
fputcsv($handle, [
$s->id_santri, $s->nama_lengkap, $s->total,
$s->hadir, $s->terlambat, $s->izin, $s->sakit, $s->alpa,
$persen . '%'
]);
}
fclose($handle);
}, $filename, ['Content-Type' => 'text/csv']);
}
// ════════════════════════════════════════════════════════
// PRIVATE HELPERS
// ════════════════════════════════════════════════════════
private function getPeriodeRange($periode, Request $request = null)
{
switch ($periode) {
case 'hari_ini': return [Carbon::today(), Carbon::today()];
case 'minggu_ini': return [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()];
case 'bulan_ini': return [Carbon::now()->startOfMonth(), Carbon::now()->endOfMonth()];
case 'semester_ini':
$m = Carbon::now()->month;
return $m >= 7
? [Carbon::create(now()->year, 7, 1), Carbon::create(now()->year, 12, 31)]
: [Carbon::create(now()->year, 1, 1), Carbon::create(now()->year, 6, 30)];
case 'custom':
$dari = $request?->get('tanggal_dari', now()->startOfMonth()->format('Y-m-d'));
$sampai = $request?->get('tanggal_sampai', now()->format('Y-m-d'));
return [Carbon::parse($dari), Carbon::parse($sampai)];
default:
return [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()];
}
}
private function getPreviousPeriodeRange($periode, $startDate, $endDate)
{
$diff = $startDate->diffInDays($endDate) + 1;
return [
Carbon::parse($startDate)->subDays($diff),
Carbon::parse($endDate)->subDays($diff),
];
}
private function getPeriodeLabel($periode, $startDate, $endDate)
{
return match ($periode) {
'hari_ini' => 'Hari Ini (' . Carbon::today()->locale('id')->isoFormat('D MMMM Y') . ')',
'minggu_ini' => 'Minggu Ini (' . $startDate->locale('id')->isoFormat('D MMM') . '' . $endDate->locale('id')->isoFormat('D MMM Y') . ')',
'bulan_ini' => Carbon::now()->locale('id')->isoFormat('MMMM Y'),
'semester_ini' => 'Semester ' . (now()->month >= 7 ? 'Ganjil' : 'Genap') . ' ' . now()->year,
'custom' => $startDate->format('d/m/Y') . ' ' . $endDate->format('d/m/Y'),
default => $startDate->format('d/m/Y') . ' ' . $endDate->format('d/m/Y'),
};
}
private function calculateKpi($startDate, $endDate)
{
$totalSantriAktif = Santri::where('status', 'Aktif')->count();
$totalKegiatan = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
->distinct('kegiatan_id')->count('kegiatan_id');
$agg = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
->selectRaw('
COUNT(*) as total,
SUM(CASE WHEN status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir,
SUM(CASE WHEN status = "Terlambat" THEN 1 ELSE 0 END) as terlambat,
SUM(CASE WHEN status = "Alpa" THEN 1 ELSE 0 END) as alpa,
SUM(CASE WHEN status = "Izin" THEN 1 ELSE 0 END) as izin,
SUM(CASE WHEN status = "Sakit" THEN 1 ELSE 0 END) as sakit
')->first();
$totalAbsensi = $agg->total ?? 0;
$totalHadir = $agg->hadir ?? 0;
$avgKehadiran = $totalAbsensi > 0 ? round($totalHadir / $totalAbsensi * 100, 1) : 0;
$kegiatanTerbaik = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
->select(
'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir'),
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
)
->groupBy('kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan')
->having('total', '>=', 5)
->orderByDesc('persen')->first();
// COUNT via subquery — tidak load semua baris ke PHP
$santriPerluPerhatian = DB::table(function ($q) use ($startDate, $endDate) {
$q->from('absensi_kegiatans')
->whereBetween('absensi_kegiatans.tanggal', [$startDate, $endDate])
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
->where('santris.status', 'Aktif')
->select(
'santris.id_santri',
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
)
->groupBy('santris.id_santri')
->having('persen', '<', 70);
}, 'sub')->count();
return [
'total_kegiatan' => $totalKegiatan,
'total_absensi' => $totalAbsensi,
'total_hadir' => $totalHadir,
'total_terlambat' => $agg->terlambat ?? 0,
'total_alpa' => $agg->alpa ?? 0,
'total_izin' => $agg->izin ?? 0,
'total_sakit' => $agg->sakit ?? 0,
'avg_kehadiran' => $avgKehadiran,
'total_santri_aktif' => $totalSantriAktif,
'santri_perlu_perhatian' => $santriPerluPerhatian,
'kegiatan_terbaik' => $kegiatanTerbaik ? [
'nama' => $kegiatanTerbaik->nama_kegiatan,
'persen'=> $kegiatanTerbaik->persen,
'hadir' => $kegiatanTerbaik->hadir,
'total' => $kegiatanTerbaik->total,
] : ['nama' => '-', 'persen' => 0, 'hadir' => 0, 'total' => 0],
];
}
/**
* 1 query bulk GROUP BY tanggal+kategori, pivot di PHP.
*/
private function getTrendData($startDate, $endDate)
{
$diffDays = $startDate->diffInDays($endDate);
$byWeek = $diffDays > 14;
$dateExpr = $byWeek
? "DATE_FORMAT(absensi_kegiatans.tanggal, '%Y-%u')"
: "DATE(absensi_kegiatans.tanggal)";
$rows = DB::table('absensi_kegiatans')
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
->join('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id')
->whereBetween('absensi_kegiatans.tanggal', [$startDate, $endDate])
->selectRaw("
{$dateExpr} as periode_key,
MIN(absensi_kegiatans.tanggal) as periode_date,
kategori_kegiatans.nama_kategori,
COUNT(*) as total,
SUM(CASE WHEN absensi_kegiatans.status IN ('Hadir','Terlambat') THEN 1 ELSE 0 END) as hadir
")
->groupByRaw("{$dateExpr}, kategori_kegiatans.nama_kategori")
->orderBy('periode_date')
->get();
$periodeKeys = $rows->pluck('periode_key')->unique()->sort()->values();
$kategoris = $rows->pluck('nama_kategori')->unique()->values();
$lookup = [];
foreach ($rows as $r) {
$lookup[$r->periode_key][$r->nama_kategori] =
$r->total > 0 ? round($r->hadir / $r->total * 100, 1) : null;
}
$labels = [];
if ($byWeek) {
foreach ($periodeKeys as $i => $_) { $labels[] = 'Mg ' . ($i + 1); }
} else {
foreach ($periodeKeys as $key) { $labels[] = Carbon::parse($key)->format('d/m'); }
}
$datasets = [];
foreach ($kategoris as $kat) {
$data = [];
foreach ($periodeKeys as $key) { $data[] = $lookup[$key][$kat] ?? null; }
$datasets[] = ['kategori' => $kat, 'data' => $data];
}
return ['labels' => $labels, 'datasets' => $datasets];
}
private function getTopBottomKegiatan($startDate, $endDate, $type = 'top', $limit = 5)
{
return AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
->leftJoin('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id')
->select(
'kegiatans.id',
'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan', 'kegiatans.hari',
'kategori_kegiatans.nama_kategori',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Terlambat" THEN 1 ELSE 0 END) as terlambat'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Alpa" THEN 1 ELSE 0 END) as alpa'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Izin" THEN 1 ELSE 0 END) as izin'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
)
->groupBy('kegiatans.id', 'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan', 'kegiatans.hari', 'kategori_kegiatans.nama_kategori')
->having('total', '>=', 3)
->orderBy('persen', $type === 'top' ? 'desc' : 'asc')
->limit($limit)
->get()->toArray();
}
private function getKelasRingkasan($startDate, $endDate)
{
$kelompoks = KelompokKelas::active()->ordered()
->with(['kelas' => fn($q) => $q->active()->ordered()])
->get();
$allKelasIds = $kelompoks->flatMap(fn($k) => $k->kelas->pluck('id'));
if ($allKelasIds->isEmpty()) return [];
// 1 query: semua santri di semua kelas
$santriPerKelas = DB::table('santri_kelas')
->whereIn('id_kelas', $allKelasIds)
->select('id_kelas', 'id_santri')
->get()
->groupBy('id_kelas');
$allSantriIds = DB::table('santri_kelas')
->whereIn('id_kelas', $allKelasIds)
->pluck('id_santri')->unique();
if ($allSantriIds->isEmpty()) return [];
// 1 query: absensi semua santri sekaligus
$absensiRows = DB::table('absensi_kegiatans')
->whereIn('id_santri', $allSantriIds)
->whereBetween('tanggal', [$startDate, $endDate])
->select('id_santri',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir')
)
->groupBy('id_santri')
->get()
->keyBy('id_santri');
$result = [];
foreach ($kelompoks as $kelompok) {
$kelasData = [];
foreach ($kelompok->kelas as $kelas) {
$sIds = $santriPerKelas->get($kelas->id, collect())->pluck('id_santri');
if ($sIds->isEmpty()) continue;
$total = 0; $hadir = 0;
foreach ($sIds as $sid) {
if (isset($absensiRows[$sid])) {
$total += $absensiRows[$sid]->total;
$hadir += $absensiRows[$sid]->hadir;
}
}
if ($total === 0) continue;
$kelasData[] = [
'id' => $kelas->id,
'nama_kelas' => $kelas->nama_kelas,
'hadir' => $hadir,
'total' => $total,
'persen' => round($hadir / $total * 100, 1),
];
}
if (!empty($kelasData)) {
$result[] = ['nama_kelompok' => $kelompok->nama_kelompok, 'kelas' => $kelasData];
}
}
return $result;
}
private function getSantriPerluPerhatianList($startDate, $endDate, $limit = 8)
{
return AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
->where('santris.status', 'Aktif')
->select(
'santris.id_santri', 'santris.nama_lengkap',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Alpa" THEN 1 ELSE 0 END) as alpa'),
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
)
->groupBy('santris.id_santri', 'santris.nama_lengkap')
->having('persen', '<', 70)
->orderBy('persen', 'asc')
->limit($limit)->get();
}
private function getDistribusiSantri($startDate, $endDate): array
{
// Bucketing langsung di SQL
$agg = DB::table(function ($q) use ($startDate, $endDate) {
$q->from('absensi_kegiatans')
->whereBetween('absensi_kegiatans.tanggal', [$startDate, $endDate])
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
->where('santris.status', 'Aktif')
->select(
'santris.id_santri',
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
)
->groupBy('santris.id_santri');
}, 'sub')
->selectRaw('
COUNT(*) as total,
SUM(CASE WHEN persen >= 95 THEN 1 ELSE 0 END) as sangat_baik,
SUM(CASE WHEN persen >= 85 AND persen < 95 THEN 1 ELSE 0 END) as baik,
SUM(CASE WHEN persen >= 70 AND persen < 85 THEN 1 ELSE 0 END) as cukup,
SUM(CASE WHEN persen < 70 THEN 1 ELSE 0 END) as perlu_perhatian
')
->first();
$total = $agg->total ?? 0;
$defs = [
['label' => 'Sangat Baik', 'key' => 'sangat_baik', 'color' => '#10B981'],
['label' => 'Baik', 'key' => 'baik', 'color' => '#34D399'],
['label' => 'Cukup', 'key' => 'cukup', 'color' => '#F59E0B'],
['label' => 'Perlu Perhatian','key' => 'perlu_perhatian','color' => '#EF4444'],
];
$buckets = [];
foreach ($defs as $d) {
$count = (int)($agg->{$d['key']} ?? 0);
$buckets[] = [
'label' => $d['label'],
'color' => $d['color'],
'count' => $count,
'persen' => $total > 0 ? round($count / $total * 100, 1) : 0,
];
}
return ['buckets' => $buckets, 'total' => $total];
}
private function buildPatterns(): array
{
$patterns = [];
// 1. Kegiatan konsisten rendah — sertakan kegiatans.id agar tidak query ulang
$lowAttendance = AbsensiKegiatan::where('tanggal', '>=', Carbon::now()->subWeeks(3))
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
->select(
'kegiatans.id',
'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir'),
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
)
->groupBy('kegiatans.id', 'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan')
->having('persen', '<', 75)
->having('total', '>=', 10)
->get();
foreach ($lowAttendance as $la) {
$patterns[] = [
'type' => 'warning',
'title' => $la->nama_kegiatan,
'description' => "Kehadiran {$la->persen}% ({$la->hadir} hadir dari {$la->total} tercatat) — konsisten di bawah 75% selama 3 minggu.",
'action_url' => route('admin.riwayat-kegiatan.show', $la->id),
'action_text' => 'Lihat Santri',
];
}
// 2. Hari tertentu rendah — 1 query
$dayStats = AbsensiKegiatan::where('tanggal', '>=', Carbon::now()->subMonth())
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
->select(
'kegiatans.hari',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir'),
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
)
->groupBy('kegiatans.hari')
->having('persen', '<', 70)
->having('total', '>=', 5)
->get();
foreach ($dayStats as $ds) {
$patterns[] = [
'type' => 'info',
'title' => "Hari {$ds->hari}",
'description' => "Rata-rata kehadiran {$ds->persen}% ({$ds->hadir} hadir dari {$ds->total} tercatat) dalam sebulan terakhir.",
'action_url' => null,
'action_text' => null,
];
}
// 3. Kelas turun >10% — JOIN langsung, tanpa whereIn besar
$tw = [Carbon::now()->startOfWeek()->format('Y-m-d'), Carbon::now()->endOfWeek()->format('Y-m-d')];
$lw = [Carbon::now()->subWeek()->startOfWeek()->format('Y-m-d'), Carbon::now()->subWeek()->endOfWeek()->format('Y-m-d')];
$buildKelasStats = function ($range) {
return DB::table('absensi_kegiatans')
->join('santri_kelas', 'absensi_kegiatans.id_santri', '=', 'santri_kelas.id_santri')
->join('kelas', 'santri_kelas.id_kelas', '=', 'kelas.id')
->where('kelas.is_active', true)
->whereBetween('absensi_kegiatans.tanggal', $range)
->select(
'kelas.id',
'kelas.nama_kelas',
DB::raw('COUNT(DISTINCT absensi_kegiatans.id) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir')
)
->groupBy('kelas.id', 'kelas.nama_kelas')
->get()
->keyBy('id');
};
$twStats = $buildKelasStats($tw);
$lwStats = $buildKelasStats($lw);
foreach ($twStats as $kelasId => $tw_) {
if (!isset($lwStats[$kelasId])) continue;
$lw_ = $lwStats[$kelasId];
$pTw = $tw_->total > 0 ? round($tw_->hadir / $tw_->total * 100, 1) : null;
$pLw = $lw_->total > 0 ? round($lw_->hadir / $lw_->total * 100, 1) : null;
if ($pTw !== null && $pLw !== null && ($pLw - $pTw) > 10) {
$drop = round($pLw - $pTw, 1);
$patterns[] = [
'type' => 'danger',
'title' => "Kelas {$tw_->nama_kelas} turun {$drop}%",
'description' => "Dari {$pLw}% (minggu lalu) → {$pTw}% (minggu ini). Penurunan signifikan.",
'action_url' => null,
'action_text' => null,
];
}
}
// 4. Santri ≥3 Alpa dalam 2 minggu — 1 query
$streaks = DB::select("
SELECT s.id_santri, s.nama_lengkap, COUNT(*) as jml_alpa
FROM absensi_kegiatans a
JOIN santris s ON a.id_santri = s.id_santri
WHERE a.status = 'Alpa'
AND a.tanggal >= ?
AND s.status = 'Aktif'
GROUP BY s.id_santri, s.nama_lengkap
HAVING jml_alpa >= 3
ORDER BY jml_alpa DESC
LIMIT 10
", [Carbon::now()->subWeeks(2)->format('Y-m-d')]);
foreach ($streaks as $s) {
$patterns[] = [
'type' => 'danger',
'title' => $s->nama_lengkap,
'description' => "{$s->jml_alpa}× Alpa dalam 2 minggu terakhir. Perlu tindak lanjut segera.",
'action_url' => route('admin.riwayat-kegiatan.detail-santri', $s->id_santri),
'action_text' => 'Lihat Riwayat',
];
}
return $patterns;
}
}

View File

@ -1,210 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Materi;
use App\Models\Santri;
use App\Models\Capaian;
use App\Models\Semester;
use App\Models\Kelas;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class MateriController extends Controller
{
/**
* Display a listing of materi with filters
*/
public function index(Request $request)
{
$query = Materi::query();
// Filter berdasarkan kategori
if ($request->filled('kategori')) {
$query->kategori($request->kategori);
}
// Filter berdasarkan kelas
if ($request->filled('kelas')) {
$query->kelas($request->kelas);
}
// Search
if ($request->filled('search')) {
$query->search($request->search);
}
// Select kolom yang diperlukan untuk optimasi
$materis = $query->select(
'id',
'id_materi',
'kategori',
'kelas',
'nama_kitab',
'halaman_mulai',
'halaman_akhir',
'total_halaman',
'created_at'
)
->orderBy('kategori')
->orderBy('kelas')
->orderBy('nama_kitab')
->paginate(20)
->appends(request()->query());
// Dynamic kelas list dari tabel kelas
$kelasList = Kelas::active()->ordered()->get();
return view('admin.materi.index', compact('materis', 'kelasList'));
}
/**
* Show the form for creating a new materi
*/
public function create()
{
// Generate next ID untuk preview
$nextIdMateri = Cache::remember('next_materi_id', 60, function () {
$lastMateri = Materi::select('id_materi')
->orderBy('id', 'desc')
->first();
$nextNum = $lastMateri ? intval(substr($lastMateri->id_materi, 1)) + 1 : 1;
return 'M' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
});
// Dynamic kelas list dari tabel kelas
$kelasList = Kelas::active()->ordered()->get();
return view('admin.materi.create', compact('nextIdMateri', 'kelasList'));
}
/**
* Store a newly created materi in storage
*/
public function store(Request $request)
{
// Ambil nama kelas yang valid dari tabel kelas
$validKelasNames = Kelas::active()->pluck('nama_kelas')->implode(',');
$validated = $request->validate([
'kategori' => 'required|in:Al-Qur\'an,Hadist,Materi Tambahan',
'kelas' => 'required|in:' . $validKelasNames,
'nama_kitab' => 'required|string|max:255',
'halaman_mulai' => 'required|integer|min:1',
'halaman_akhir' => 'required|integer|min:1|gte:halaman_mulai',
'deskripsi' => 'nullable|string',
], [
'kategori.required' => 'Kategori wajib dipilih.',
'kelas.required' => 'Kelas wajib dipilih.',
'kelas.in' => 'Kelas yang dipilih tidak valid.',
'nama_kitab.required' => 'Nama kitab wajib diisi.',
'halaman_mulai.required' => 'Halaman mulai wajib diisi.',
'halaman_mulai.min' => 'Halaman mulai minimal 1.',
'halaman_akhir.required' => 'Halaman akhir wajib diisi.',
'halaman_akhir.gte' => 'Halaman akhir harus lebih besar atau sama dengan halaman mulai.',
]);
// Create materi
$materi = Materi::create($validated);
// Auto-create capaian untuk semua santri di kelas tersebut (via relasi baru)
$santris = Santri::kelasByName($validated['kelas'])
->where('status', 'Aktif')
->get();
// Get semester aktif
$semesterAktif = Semester::aktif()->first();
if ($semesterAktif && $santris->count() > 0) {
foreach ($santris as $santri) {
// Create capaian dengan progress 0
Capaian::create([
'id_santri' => $santri->id_santri,
'id_materi' => $materi->id_materi,
'id_semester' => $semesterAktif->id_semester,
'halaman_selesai' => '',
'persentase' => 0,
'catatan' => 'Auto-created untuk materi baru',
'tanggal_input' => now(),
]);
}
}
// Clear cache
Cache::forget('next_materi_id');
return redirect()->route('admin.materi.index')
->with('success', "Data materi berhasil ditambahkan. Capaian otomatis dibuat untuk {$santris->count()} santri kelas {$validated['kelas']}.");
}
/**
* Display the specified materi
*/
public function show(Materi $materi)
{
// Load relasi capaian jika ada (nanti di langkah 2)
// $materi->load('capaian.santri');
return view('admin.materi.show', compact('materi'));
}
/**
* Show the form for editing the specified materi
*/
public function edit(Materi $materi)
{
$kelasList = Kelas::active()->ordered()->get();
return view('admin.materi.edit', compact('materi', 'kelasList'));
}
/**
* Update the specified materi in storage
*/
public function update(Request $request, Materi $materi)
{
$validKelasNames = Kelas::active()->pluck('nama_kelas')->implode(',');
$validated = $request->validate([
'kategori' => 'required|in:Al-Qur\'an,Hadist,Materi Tambahan',
'kelas' => 'required|in:' . $validKelasNames,
'nama_kitab' => 'required|string|max:255',
'halaman_mulai' => 'required|integer|min:1',
'halaman_akhir' => 'required|integer|min:1|gte:halaman_mulai',
'deskripsi' => 'nullable|string',
], [
'kategori.required' => 'Kategori wajib dipilih.',
'kelas.required' => 'Kelas wajib dipilih.',
'kelas.in' => 'Kelas yang dipilih tidak valid.',
'nama_kitab.required' => 'Nama kitab wajib diisi.',
'halaman_mulai.required' => 'Halaman mulai wajib diisi.',
'halaman_mulai.min' => 'Halaman mulai minimal 1.',
'halaman_akhir.required' => 'Halaman akhir wajib diisi.',
'halaman_akhir.gte' => 'Halaman akhir harus lebih besar atau sama dengan halaman mulai.',
]);
$materi->update($validated);
return redirect()->route('admin.materi.index')
->with('success', 'Data materi berhasil diperbarui.');
}
/**
* Remove the specified materi from storage
*/
public function destroy(Materi $materi)
{
$namaKitab = $materi->nama_kitab;
// TODO: Check jika ada capaian yang terkait (Langkah 2)
// if ($materi->capaian()->exists()) {
// return redirect()->route('admin.materi.index')
// ->with('error', 'Tidak dapat menghapus materi yang sudah memiliki data capaian.');
// }
$materi->delete();
return redirect()->route('admin.materi.index')
->with('success', 'Data materi "' . $namaKitab . '" berhasil dihapus.');
}
}

View File

@ -1,226 +0,0 @@
<?php
// app/Http/Controllers/Admin/MesinMappingController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\MesinSantriMapping;
use App\Models\Santri;
use App\Services\EpposGLogParser;
use Illuminate\Http\Request;
class MesinMappingController extends Controller
{
// ──────────────────────────────────────────────────────────
// INDEX
// ──────────────────────────────────────────────────────────
public function index()
{
$mappings = MesinSantriMapping::with('santri')
->orderByRaw('CAST(id_mesin AS UNSIGNED)')
->get();
$santris = Santri::where('status', 'Aktif')
->orderBy('nama_lengkap')
->get(['id_santri', 'nama_lengkap']);
return view('admin.mesin.mapping-santri.index', compact('mappings', 'santris'));
}
// ──────────────────────────────────────────────────────────
// STORE (tambah manual)
// ──────────────────────────────────────────────────────────
public function store(Request $request)
{
$request->validate([
'id_mesin' => 'required|string|unique:mesin_santri_mappings,id_mesin',
'id_santri' => 'nullable|exists:santris,id_santri',
'nama_mesin' => 'nullable|string|max:100',
'catatan' => 'nullable|string|max:255',
]);
MesinSantriMapping::create($request->only(
'id_mesin', 'id_santri', 'nama_mesin', 'catatan'
));
return back()->with('success', "Mapping ID Mesin {$request->id_mesin} berhasil ditambahkan.");
}
// ──────────────────────────────────────────────────────────
// UPDATE (ganti santri lewat dropdown)
// ──────────────────────────────────────────────────────────
public function update(Request $request, $id)
{
$mapping = MesinSantriMapping::findOrFail($id);
$request->validate([
'id_santri' => 'nullable|exists:santris,id_santri',
]);
$mapping->update(['id_santri' => $request->id_santri ?: null]);
return back()->with('success', 'Mapping berhasil diperbarui.');
}
// ──────────────────────────────────────────────────────────
// DESTROY
// ──────────────────────────────────────────────────────────
public function destroy($id)
{
$mapping = MesinSantriMapping::findOrFail($id);
$idMesin = $mapping->id_mesin;
$mapping->delete();
return back()->with('success', "Mapping ID Mesin {$idMesin} berhasil dihapus.");
}
// ──────────────────────────────────────────────────────────
// IMPORT FROM INFO.XLS
// ──────────────────────────────────────────────────────────
public function importFromInfo(Request $request)
{
$request->validate([
'file_info' => 'required|file|mimes:xls,xlsx|max:10240',
]);
$parser = app(EpposGLogParser::class);
$infoData = $parser->parseInfoFile($request->file('file_info')->getPathname());
$jadwal = $infoData['jadwal'];
$added = 0;
$skipped = 0;
$matched = 0;
// Ambil semua santri aktif sekali saja (efisien, tidak query per-santri)
$semuaSantri = Santri::where('status', 'Aktif')
->get(['id_santri', 'nama_lengkap']);
foreach ($jadwal as $idMesin => $data) {
// Skip jika mapping sudah ada
if (MesinSantriMapping::where('id_mesin', $idMesin)->exists()) {
$skipped++;
continue;
}
// Coba cocokkan nama dengan berbagai strategi
$santri = $this->cariSantriByNama($data['nama'], $semuaSantri);
MesinSantriMapping::create([
'id_mesin' => $idMesin,
'id_santri' => $santri?->id_santri,
'nama_mesin' => $data['nama'],
'dept_mesin' => $data['dept'] ?? null,
]);
if ($santri) $matched++;
$added++;
}
$msg = "{$added} mapping ditambahkan ({$matched} otomatis cocok nama), {$skipped} sudah ada.";
if ($added > $matched) {
$belum = $added - $matched;
$msg .= " {$belum} perlu dipetakan manual (nama tidak cocok).";
}
return back()->with('success', $msg);
}
// ──────────────────────────────────────────────────────────
// HELPER: Cari Santri Berdasarkan Nama (Fuzzy Matching)
//
// Strategi (urutan prioritas):
// 1. Exact match (nama lengkap sama persis, case-insensitive)
// 2. Nama mesin ada di dalam nama lengkap santri
// → "helga faisa" ditemukan di "helga faisa fahar"
// 3. Nama lengkap santri ada di dalam nama mesin
// → "helga" ditemukan di "helga faisa fahar"
// 4. Semua kata dari nama mesin ada di nama santri
// → nama mesin "helga faisa" → cari santri yang punya "helga" DAN "faisa"
// 5. Minimal 1 kata dari nama mesin cocok, pilih santri
// dengan skor kata paling banyak cocok
// ──────────────────────────────────────────────────────────
private function cariSantriByNama(string $namaMesin, $semuaSantri): ?Santri
{
$namaMesinBersih = strtolower(trim($namaMesin));
if (empty($namaMesinBersih)) return null;
// ── Strategi 1: Exact match ───────────────────────────
foreach ($semuaSantri as $santri) {
$namaDb = strtolower(trim($santri->nama_lengkap));
if ($namaDb === $namaMesinBersih) {
return $santri;
}
}
// ── Strategi 2: nama mesin ada di nama santri ─────────
// Contoh: nama mesin "helga faisa" → santri "helga faisa fahar" ✓
foreach ($semuaSantri as $santri) {
$namaDb = strtolower(trim($santri->nama_lengkap));
if (str_contains($namaDb, $namaMesinBersih)) {
return $santri;
}
}
// ── Strategi 3: nama santri ada di nama mesin ─────────
// Contoh: nama mesin "helga faisa fahar" → santri "helga faisa" ✓
foreach ($semuaSantri as $santri) {
$namaDb = strtolower(trim($santri->nama_lengkap));
if (str_contains($namaMesinBersih, $namaDb)) {
return $santri;
}
}
// ── Strategi 4 & 5: Skor berdasarkan kata ────────────
// Pecah nama mesin jadi kata-kata
// Contoh: "helga faisa" → ['helga', 'faisa']
$kataMesin = array_filter(explode(' ', $namaMesinBersih));
if (empty($kataMesin)) return null;
$kandidatTerbaik = null;
$skorTerbaik = 0;
foreach ($semuaSantri as $santri) {
$namaDb = strtolower(trim($santri->nama_lengkap));
$kataDb = array_filter(explode(' ', $namaDb));
$skorCocok = 0;
foreach ($kataMesin as $kata) {
// Minimal 3 karakter agar tidak false positive (mis. "al", "bin")
if (strlen($kata) < 3) continue;
foreach ($kataDb as $kataDbItem) {
if (
$kataDbItem === $kata || // kata persis sama
str_contains($kataDbItem, $kata) || // kata mesin ada di kata db
str_contains($kata, $kataDbItem) // kata db ada di kata mesin
) {
$skorCocok++;
break; // sudah cocok, lanjut kata berikutnya
}
}
}
// Hitung persentase kata yang cocok
$persenCocok = $skorCocok / count($kataMesin);
// Update kandidat jika skor lebih tinggi
if ($persenCocok > $skorTerbaik) {
$skorTerbaik = $persenCocok;
$kandidatTerbaik = $santri;
}
}
// Ambil kandidat hanya jika minimal 50% kata cocok
// Contoh: nama mesin "helga faisa" (2 kata) → butuh minimal 1 kata cocok
// Contoh: nama mesin "helga faisa fahar" (3 kata) → butuh minimal 2 kata cocok
if ($skorTerbaik >= 0.5) {
return $kandidatTerbaik;
}
// Tidak ada yang cocok
return null;
}
}

View File

@ -1,465 +0,0 @@
<?php
// app/Http/Controllers/Admin/PembayaranSppController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\PembayaranSpp;
use App\Models\Santri;
use Illuminate\Http\Request;
use Carbon\Carbon;
class PembayaranSppController extends Controller
{
// ══════════════════════════════════════════════════════
// INDEX
// ══════════════════════════════════════════════════════
public function index(Request $request)
{
// Default tab
$tab = $request->get('tab', 'belum-bayar');
// Default bulan dan tahun ke bulan/tahun saat ini jika tidak ada filter
$bulan = $request->filled('bulan') ? $request->bulan : date('n');
$tahun = $request->filled('tahun') ? $request->tahun : date('Y');
// Data untuk filter tahun
$tahunList = PembayaranSpp::selectRaw('DISTINCT tahun')
->orderBy('tahun', 'desc')
->pluck('tahun');
// Tambahkan tahun saat ini jika belum ada
if (!$tahunList->contains(date('Y'))) {
$tahunList->prepend(date('Y'));
}
// Get santri dengan status pembayaran untuk periode yang dipilih
$santriList = Santri::where('status', 'Aktif')
->with(['pembayaranSpp' => function ($q) use ($bulan, $tahun) {
$q->where('bulan', $bulan)->where('tahun', $tahun);
}])
->get()
->map(function ($santri) {
$p = $santri->pembayaranSpp->first();
return [
'id_santri' => $santri->id_santri,
'nama_lengkap' => $santri->nama_lengkap,
'nis' => $santri->nis,
'kelas' => $santri->kelas,
'pembayaran' => $p,
// status virtual: Lunas / Cicilan / Belum Lunas / Belum Ada Tagihan
'status' => $p ? ($p->status === 'Lunas' ? 'Lunas' : ($p->isCicilan() ? 'Cicilan' : 'Belum Lunas')) : 'Belum Ada Tagihan',
'is_telat' => $p ? $p->isTelat() : false,
'nominal' => $p ? (float) $p->nominal : 0,
'tanggal_bayar'=> $p ? $p->tanggal_bayar : null,
'batas_bayar' => $p ? $p->batas_bayar : null,
];
});
// ─── KPI (hitung dari data PENUH sebelum filter tab) ─────────
$totalSantriAll = $santriList->count();
$totalLunas = $santriList->where('status', 'Lunas')->count();
$totalCicilan = $santriList->where('status', 'Cicilan')->count();
$totalBelumBayar = $santriList->whereIn('status', ['Belum Lunas', 'Belum Ada Tagihan'])->count();
$totalTelat = $santriList->where('is_telat', true)->count();
$totalBelumAdaTagihan = $santriList->where('status', 'Belum Ada Tagihan')->count();
$nominalLunas = $santriList->where('status', 'Lunas')->sum('nominal');
$nominalBelumLunas = $santriList->whereIn('status', ['Belum Lunas', 'Cicilan'])->sum('nominal');
// ─── Filter tab ───────────────────────────────────────────────
if ($tab === 'sudah-bayar') {
$santriList = $santriList
->filter(fn($i) => $i['pembayaran'] && $i['status'] === 'Lunas')
->sortByDesc(fn($i) => $i['tanggal_bayar'] ? $i['tanggal_bayar']->timestamp : 0);
} elseif ($tab === 'cicilan') {
$santriList = $santriList
->filter(fn($i) => $i['pembayaran'] && $i['status'] === 'Cicilan')
->sortBy('nama_lengkap');
} else {
// belum-bayar: status Belum Lunas atau Belum Ada Tagihan
$santriList = $santriList
->filter(fn($i) => in_array($i['status'], ['Belum Lunas', 'Belum Ada Tagihan']))
->sortBy('nama_lengkap');
}
// ─── Search ───────────────────────────────────────────────────
if ($request->filled('search')) {
$search = strtolower($request->search);
$santriList = $santriList->filter(fn($i) =>
str_contains(strtolower($i['nama_lengkap']), $search) ||
str_contains(strtolower($i['id_santri']), $search) ||
str_contains(strtolower($i['nis']), $search)
);
}
// ─── Filter status spesifik (tab belum-bayar) ─────────────────
if ($request->filled('filter_status')) {
if ($request->filter_status === 'Telat') {
$santriList = $santriList->filter(fn($i) => $i['is_telat']);
} elseif ($request->filter_status === 'Belum Ada Tagihan') {
$santriList = $santriList->filter(fn($i) => !$i['pembayaran']);
} else {
$santriList = $santriList->filter(fn($i) => $i['status'] === $request->filter_status);
}
}
// ─── Pagination manual ────────────────────────────────────────
$santriList = $santriList->values();
$perPage = 20;
$currentPage = $request->get('page', 1);
$offset = ($currentPage - 1) * $perPage;
$santriPaginated = $santriList->slice($offset, $perPage)->values();
$totalPages = ceil($santriList->count() / $perPage);
$totalSantri = $santriList->count();
return view('admin.pembayaran-spp.index', compact(
'santriPaginated', 'tab', 'bulan', 'tahun', 'tahunList',
'totalSantri', 'totalSantriAll',
'totalLunas', 'totalCicilan', 'totalBelumBayar',
'totalTelat', 'totalBelumAdaTagihan',
'nominalLunas', 'nominalBelumLunas',
'currentPage', 'totalPages'
));
}
// ══════════════════════════════════════════════════════
// CREATE / STORE
// ══════════════════════════════════════════════════════
public function create()
{
$santris = Santri::where('status', 'Aktif')->orderBy('nama_lengkap', 'asc')->get();
$last = PembayaranSpp::orderBy('id', 'desc')->first();
$nextNum = $last ? intval(substr($last->id_pembayaran, 3)) + 1 : 1;
$nextId = 'SPP' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
return view('admin.pembayaran-spp.create', compact('santris', 'nextId'));
}
public function store(Request $request)
{
$validated = $request->validate([
'id_santri' => 'required|exists:santris,id_santri',
'bulan' => 'required|integer|min:1|max:12',
'tahun' => 'required|integer|min:2020|max:2100',
'nominal' => 'required|numeric|min:0',
'status' => 'required|in:Lunas,Belum Lunas',
'tanggal_bayar'=> 'nullable|date',
'batas_bayar' => 'required|date',
'keterangan' => 'nullable|string',
], [
'id_santri.required' => 'Santri wajib dipilih.',
'id_santri.exists' => 'Santri tidak ditemukan.',
'bulan.required' => 'Bulan wajib diisi.',
'bulan.min' => 'Bulan harus antara 1-12.',
'bulan.max' => 'Bulan harus antara 1-12.',
'tahun.required' => 'Tahun wajib diisi.',
'nominal.required' => 'Nominal wajib diisi.',
'nominal.min' => 'Nominal minimal 0.',
'status.required' => 'Status wajib dipilih.',
'batas_bayar.required' => 'Batas bayar wajib diisi.',
]);
// Cek duplikasi — jika sudah ada, arahkan ke edit
$existing = PembayaranSpp::where('id_santri', $validated['id_santri'])
->where('bulan', $validated['bulan'])
->where('tahun', $validated['tahun'])
->first();
if ($existing) {
return redirect()->route('admin.pembayaran-spp.edit', $existing->id)
->with('info', 'Data SPP untuk periode ini sudah ada. Silakan edit data berikut untuk mengubah status pembayaran.');
}
// Jika status lunas dan tanggal_bayar kosong, set ke hari ini
if ($validated['status'] === 'Lunas' && empty($validated['tanggal_bayar'])) {
$validated['tanggal_bayar'] = Carbon::now()->format('Y-m-d');
}
PembayaranSpp::create($validated);
return redirect()->route('admin.pembayaran-spp.index')
->with('success', 'Data pembayaran SPP berhasil ditambahkan.');
}
// ══════════════════════════════════════════════════════
// SHOW / EDIT / UPDATE / DESTROY
// ══════════════════════════════════════════════════════
public function show(PembayaranSpp $pembayaranSpp)
{
$pembayaranSpp->load('santri');
return view('admin.pembayaran-spp.show', compact('pembayaranSpp'));
}
public function edit(PembayaranSpp $pembayaranSpp)
{
$santris = Santri::orderBy('nama_lengkap', 'asc')->get();
return view('admin.pembayaran-spp.edit', compact('pembayaranSpp', 'santris'));
}
public function update(Request $request, PembayaranSpp $pembayaranSpp)
{
$validated = $request->validate([
'id_santri' => 'required|exists:santris,id_santri',
'bulan' => 'required|integer|min:1|max:12',
'tahun' => 'required|integer|min:2020|max:2100',
'nominal' => 'required|numeric|min:0',
'status' => 'required|in:Lunas,Belum Lunas',
'tanggal_bayar'=> 'nullable|date',
'batas_bayar' => 'required|date',
'keterangan' => 'nullable|string',
], [
'id_santri.required' => 'Santri wajib dipilih.',
'bulan.required' => 'Bulan wajib diisi.',
'tahun.required' => 'Tahun wajib diisi.',
'nominal.required' => 'Nominal wajib diisi.',
'status.required' => 'Status wajib dipilih.',
'batas_bayar.required' => 'Batas bayar wajib diisi.',
]);
// Cek duplikasi (kecuali data sendiri)
$exists = PembayaranSpp::where('id_santri', $validated['id_santri'])
->where('bulan', $validated['bulan'])
->where('tahun', $validated['tahun'])
->where('id', '!=', $pembayaranSpp->id)
->exists();
if ($exists) {
return back()->withInput()->with('error', 'Data pembayaran untuk periode ini sudah ada.');
}
// Jika status lunas dan tanggal_bayar kosong, set ke hari ini
if ($validated['status'] === 'Lunas' && empty($validated['tanggal_bayar'])) {
$validated['tanggal_bayar'] = Carbon::now()->format('Y-m-d');
}
// Jika diubah ke Lunas, hapus data cicilan dari keterangan
if ($validated['status'] === 'Lunas' && $pembayaranSpp->isCicilan()) {
$validated['keterangan'] = $pembayaranSpp->catatan_teks; // simpan teks catatan saja
}
$pembayaranSpp->update($validated);
return redirect()->route('admin.pembayaran-spp.index')
->with('success', 'Data pembayaran SPP berhasil diperbarui.');
}
public function destroy(PembayaranSpp $pembayaranSpp)
{
$periode = $pembayaranSpp->periode_lengkap;
$santri = $pembayaranSpp->santri->nama_lengkap;
$pembayaranSpp->delete();
return redirect()->route('admin.pembayaran-spp.index')
->with('success', "Data pembayaran SPP {$periode} untuk {$santri} berhasil dihapus.");
}
// ══════════════════════════════════════════════════════
// QUICK ACTIONS
// ══════════════════════════════════════════════════════
/**
* Tandai Lunas langsung (quick pay)
*/
public function bayar(Request $request, PembayaranSpp $pembayaranSpp)
{
if ($pembayaranSpp->status === 'Lunas') {
return redirect()->back()->with('info', 'Pembayaran ini sudah berstatus Lunas.');
}
$pembayaranSpp->update([
'status' => 'Lunas',
'tanggal_bayar' => $request->filled('tanggal_bayar')
? $request->tanggal_bayar
: Carbon::now()->format('Y-m-d'),
// Bersihkan data cicilan dari keterangan, simpan catatan teks jika ada
'keterangan' => $pembayaranSpp->catatan_teks,
]);
$nama = $pembayaranSpp->santri->nama_lengkap;
$periode = $pembayaranSpp->periode_lengkap;
return redirect()->route('admin.pembayaran-spp.index', [
'tab' => 'sudah-bayar',
'bulan' => $pembayaranSpp->bulan,
'tahun' => $pembayaranSpp->tahun,
])->with('success', "Pembayaran SPP {$periode} untuk {$nama} berhasil ditandai Lunas.");
}
/**
* Catat cicilan (tambah nominal terbayar)
* Status DB tetap "Belum Lunas" cicilan disimpan di keterangan (JSON).
*/
public function catatCicilan(Request $request, PembayaranSpp $pembayaranSpp)
{
$request->validate([
'nominal_cicilan' => 'required|numeric|min:1',
'catatan' => 'nullable|string|max:200',
]);
$sudahTerbayar = $pembayaranSpp->nominal_terbayar;
$totalTagihan = (float) $pembayaranSpp->nominal;
$baru = $sudahTerbayar + (float) $request->nominal_cicilan;
// Jika total cicilan >= tagihan → otomatis Lunas
if ($baru >= $totalTagihan) {
$pembayaranSpp->update([
'status' => 'Lunas',
'tanggal_bayar' => Carbon::now()->format('Y-m-d'),
'keterangan' => $request->catatan ?? $pembayaranSpp->catatan_teks,
]);
return redirect()->route('admin.pembayaran-spp.index', [
'tab' => 'sudah-bayar',
'bulan' => $pembayaranSpp->bulan,
'tahun' => $pembayaranSpp->tahun,
])->with('success', "Cicilan terakhir diterima. SPP {$pembayaranSpp->periode_lengkap} untuk {$pembayaranSpp->santri->nama_lengkap} sekarang Lunas.");
}
// Masih cicilan — update keterangan saja, status tetap "Belum Lunas"
$pembayaranSpp->setCicilan($baru, $request->catatan ?? $pembayaranSpp->catatan_teks);
$pembayaranSpp->save();
$sisaFormat = 'Rp ' . number_format($totalTagihan - $baru, 0, ',', '.');
$cicilanFormat = 'Rp ' . number_format((float) $request->nominal_cicilan, 0, ',', '.');
return redirect()->back()
->with('success', "Cicilan {$cicilanFormat} berhasil dicatat. Sisa: {$sisaFormat}");
}
// ══════════════════════════════════════════════════════
// RIWAYAT PER SANTRI
// ══════════════════════════════════════════════════════
public function riwayat($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
$pembayaranSpp = PembayaranSpp::where('id_santri', $id_santri)
->orderBy('tahun', 'desc')
->orderBy('bulan', 'desc')
->paginate(15);
// Statistik
$totalBayar = PembayaranSpp::where('id_santri', $id_santri)
->where('status', 'Lunas')
->sum('nominal');
$totalTunggakan = PembayaranSpp::where('id_santri', $id_santri)
->where('status', 'Belum Lunas')
->sum('nominal');
$jumlahTelat = PembayaranSpp::where('id_santri', $id_santri)
->where('status', 'Belum Lunas')
->where('batas_bayar', '<', Carbon::now())
->count();
return view('admin.pembayaran-spp.riwayat', compact(
'santri', 'pembayaranSpp', 'totalBayar', 'totalTunggakan', 'jumlahTelat'
));
}
// ══════════════════════════════════════════════════════
// GENERATE SPP MASSAL
// ══════════════════════════════════════════════════════
public function generate(Request $request)
{
if ($request->isMethod('post')) {
$validated = $request->validate([
'bulan' => 'required|integer|min:1|max:12',
'tahun' => 'required|integer|min:2020|max:2100',
'nominal' => 'required|numeric|min:0',
'batas_bayar' => 'required|date',
]);
$santris = Santri::where('status', 'Aktif')->get();
$generated = 0;
$skipped = 0;
foreach ($santris as $santri) {
$exists = PembayaranSpp::where('id_santri', $santri->id_santri)
->where('bulan', $validated['bulan'])
->where('tahun', $validated['tahun'])
->exists();
if (!$exists) {
PembayaranSpp::create([
'id_santri' => $santri->id_santri,
'bulan' => $validated['bulan'],
'tahun' => $validated['tahun'],
'nominal' => $validated['nominal'],
'status' => 'Belum Lunas',
'batas_bayar' => $validated['batas_bayar'],
]);
$generated++;
} else {
$skipped++;
}
}
return redirect()->route('admin.pembayaran-spp.index')
->with('success', "Berhasil generate {$generated} data SPP. {$skipped} data dilewati (sudah ada).");
}
return view('admin.pembayaran-spp.generate');
}
// ══════════════════════════════════════════════════════
// LAPORAN & CETAK
// ══════════════════════════════════════════════════════
public function laporan()
{
return view('admin.pembayaran-spp.laporan');
}
public function cetakLaporan(Request $request)
{
$query = PembayaranSpp::with('santri');
if ($request->filled('bulan')) $query->where('bulan', $request->bulan);
if ($request->filled('tahun')) $query->where('tahun', $request->tahun);
if ($request->filled('status')) {
if ($request->status === 'Telat') {
$query->telat();
} else {
$query->where('status', $request->status);
}
}
$pembayaranSpp = $query->orderBy('tahun', 'desc')->orderBy('bulan', 'desc')->get();
$totalLunas = $pembayaranSpp->where('status', 'Lunas')->sum('nominal');
$totalTunggakan = $pembayaranSpp->where('status', 'Belum Lunas')->sum('nominal');
$jumlahTelat = $pembayaranSpp->filter(fn($s) => $s->isTelat())->count();
return view('admin.pembayaran-spp.cetak-laporan', compact(
'pembayaranSpp', 'totalLunas', 'totalTunggakan', 'jumlahTelat'
));
}
public function cetakLaporanSantri($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
$pembayaranSpp = PembayaranSpp::where('id_santri', $id_santri)
->orderBy('tahun', 'desc')
->orderBy('bulan', 'desc')
->get();
$totalLunas = $pembayaranSpp->where('status', 'Lunas')->sum('nominal');
$totalTunggakan = $pembayaranSpp->where('status', 'Belum Lunas')->sum('nominal');
$jumlahTelat = $pembayaranSpp->filter(fn($s) => $s->isTelat())->count();
return view('admin.pembayaran-spp.cetak-laporan-santri', compact(
'santri', 'pembayaranSpp', 'totalLunas', 'totalTunggakan', 'jumlahTelat'
));
}
public function cetakBukti(PembayaranSpp $pembayaranSpp)
{
$pembayaranSpp->load('santri');
return view('admin.pembayaran-spp.cetak-bukti', compact('pembayaranSpp'));
}
}

View File

@ -1,78 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\PembinaanSanksi;
use Illuminate\Http\Request;
class PembinaanSanksiController extends Controller
{
public function index()
{
$data = PembinaanSanksi::byUrutan()->get();
return view('admin.pembinaan_sanksi.index', compact('data'));
}
public function create()
{
$last = PembinaanSanksi::orderBy('id', 'desc')->first();
$nextNum = $last ? intval(substr($last->id_pembinaan, 2)) + 1 : 1;
$nextId = 'PS' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
return view('admin.pembinaan_sanksi.create', compact('nextId'));
}
public function store(Request $request)
{
$validated = $request->validate([
'judul' => 'required|string|max:255',
'konten' => 'required|string',
'urutan' => 'nullable|integer|min:0',
'is_active' => 'boolean',
]);
PembinaanSanksi::create($validated);
return redirect()->route('admin.pembinaan-sanksi.index')
->with('success', 'Pembinaan & Sanksi berhasil ditambahkan.');
}
public function show(PembinaanSanksi $pembinaanSanksi)
{
return view('admin.pembinaan_sanksi.show', [
'pembinaan' => $pembinaanSanksi
]);
}
public function edit(PembinaanSanksi $pembinaanSanksi)
{
return view('admin.pembinaan_sanksi.edit', [
'pembinaan' => $pembinaanSanksi
]);
}
public function update(Request $request, PembinaanSanksi $pembinaanSanksi)
{
$validated = $request->validate([
'judul' => 'required|string|max:255',
'konten' => 'required|string',
'urutan' => 'nullable|integer|min:0',
'is_active' => 'boolean',
]);
$pembinaanSanksi->update($validated);
return redirect()->route('admin.pembinaan-sanksi.index')
->with('success', 'Pembinaan & Sanksi berhasil diperbarui.');
}
public function destroy(PembinaanSanksi $pembinaanSanksi)
{
$pembinaanSanksi->delete();
return redirect()->route('admin.pembinaan-sanksi.index')
->with('success', 'Pembinaan & Sanksi berhasil dihapus.');
}
}

View File

@ -1,328 +0,0 @@
<?php
// app/Http/Controllers/admin/RiwayatKegiatanController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AbsensiKegiatan;
use App\Models\Kegiatan;
use App\Models\KategoriKegiatan;
use App\Models\Santri;
use App\Models\Kelas;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class RiwayatKegiatanController extends Controller
{
/**
* Halaman utama riwayat default: HARI INI
*
* Mode hari_ini flat list kegiatan (paginated cards)
* Mode minggu_ini / custom grouped by actual date, tiap tanggal = 1 tabel
*/
public function index(Request $request)
{
$hariMap = [
'Monday' => 'Senin', 'Tuesday' => 'Selasa', 'Wednesday' => 'Rabu',
'Thursday' => 'Kamis', 'Friday' => 'Jumat', 'Saturday' => 'Sabtu',
'Sunday' => 'Ahad',
];
// ── Tentukan mode & rentang tanggal ───────────────────────────────────
$mode = $request->get('mode', 'hari_ini');
if ($mode === 'minggu_ini') {
$dari = now()->startOfWeek(Carbon::MONDAY)->format('Y-m-d');
$sampai = now()->endOfWeek(Carbon::SUNDAY)->format('Y-m-d');
$tanggal = null;
} elseif ($mode === 'custom') {
$dari = $request->get('dari', now()->subDays(6)->format('Y-m-d'));
$sampai = $request->get('sampai', now()->format('Y-m-d'));
if ($dari > $sampai) { [$dari, $sampai] = [$sampai, $dari]; }
$tanggal = null;
} else {
$mode = 'hari_ini';
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
$dari = $tanggal;
$sampai = $tanggal;
}
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->orderBy('nama_kategori')->get();
$kategoriId = $request->get('kategori_id', '');
// ── Label periode ─────────────────────────────────────────────────────
if ($mode === 'hari_ini') {
$periodeLabel = Carbon::parse($dari)->locale('id')->isoFormat('dddd, D MMMM Y');
} elseif ($mode === 'minggu_ini') {
$periodeLabel = Carbon::parse($dari)->locale('id')->isoFormat('D MMM') . ' '
. Carbon::parse($sampai)->locale('id')->isoFormat('D MMM Y');
} else {
$periodeLabel = Carbon::parse($dari)->locale('id')->isoFormat('D MMM Y') . ' '
. Carbon::parse($sampai)->locale('id')->isoFormat('D MMM Y');
}
// ═══════════════════════════════════════════════════════════════════
// MODE HARI INI — flat list kegiatan (paginated)
// ═══════════════════════════════════════════════════════════════════
if ($mode === 'hari_ini') {
$hariDipilih = $hariMap[Carbon::parse($dari)->format('l')] ?? null;
$baseQuery = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok'])
->when($hariDipilih, fn($q) => $q->where('hari', $hariDipilih))
->when($kategoriId, fn($q) => $q->where('kategori_id', $kategoriId))
->withCount([
'absensis as total_absensi' => fn($q) => $q->whereDate('tanggal', $dari),
'absensis as hadir' => fn($q) => $q->where('status', 'Hadir')->whereDate('tanggal', $dari),
'absensis as terlambat' => fn($q) => $q->where('status', 'Terlambat')->whereDate('tanggal', $dari),
'absensis as izin' => fn($q) => $q->where('status', 'Izin')->whereDate('tanggal', $dari),
'absensis as sakit' => fn($q) => $q->where('status', 'Sakit')->whereDate('tanggal', $dari),
'absensis as alpa' => fn($q) => $q->where('status', 'Alpa')->whereDate('tanggal', $dari),
'absensis as pulang' => fn($q) => $q->where('status', 'Pulang')->whereDate('tanggal', $dari),
])
->orderBy('waktu_mulai');
$kegiatans = (clone $baseQuery)->paginate(20)->appends($request->query());
$kegiatanPerTanggal = null;
$allItems = (clone $baseQuery)->get();
$summary = [
'hadir' => $allItems->sum('hadir') + $allItems->sum('terlambat'),
'terlambat' => $allItems->sum('terlambat'),
'izin' => $allItems->sum('izin'),
'sakit' => $allItems->sum('sakit'),
'alpa' => $allItems->sum('alpa'),
'total_absensi' => $allItems->sum('total_absensi'),
'jumlah_kegiatan' => $allItems->count(),
'jumlah_hari' => 1,
];
// ═══════════════════════════════════════════════════════════════════
// MODE MULTI-HARI — grouped by actual date
// ═══════════════════════════════════════════════════════════════════
} else {
$kegiatans = null;
// Ambil semua tanggal yang ada absensi dalam range (terbaru dulu)
$tanggalList = AbsensiKegiatan::selectRaw('DATE(tanggal) as tgl')
->whereDate('tanggal', '>=', $dari)
->whereDate('tanggal', '<=', $sampai)
->when($kategoriId, fn($q) => $q->whereHas('kegiatan', fn($qq) => $qq->where('kategori_id', $kategoriId)))
->groupBy('tgl')
->orderBy('tgl', 'desc')
->pluck('tgl');
// Untuk tiap tanggal: kegiatan + stats khusus tanggal itu saja
$kegiatanPerTanggal = collect();
foreach ($tanggalList as $tgl) {
$hariIndo = $hariMap[Carbon::parse($tgl)->format('l')] ?? null;
$items = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok'])
->when($hariIndo, fn($q) => $q->where('hari', $hariIndo))
->when($kategoriId, fn($q) => $q->where('kategori_id', $kategoriId))
->whereHas('absensis', fn($q) => $q->whereDate('tanggal', $tgl))
->withCount([
'absensis as total_absensi' => fn($q) => $q->whereDate('tanggal', $tgl),
'absensis as hadir' => fn($q) => $q->where('status', 'Hadir')->whereDate('tanggal', $tgl),
'absensis as terlambat' => fn($q) => $q->where('status', 'Terlambat')->whereDate('tanggal', $tgl),
'absensis as izin' => fn($q) => $q->where('status', 'Izin')->whereDate('tanggal', $tgl),
'absensis as sakit' => fn($q) => $q->where('status', 'Sakit')->whereDate('tanggal', $tgl),
'absensis as alpa' => fn($q) => $q->where('status', 'Alpa')->whereDate('tanggal', $tgl),
'absensis as pulang' => fn($q) => $q->where('status', 'Pulang')->whereDate('tanggal', $tgl),
])
->orderBy('waktu_mulai')
->get();
if ($items->count() > 0) {
$kegiatanPerTanggal[$tgl] = $items;
}
}
$allKeg = $kegiatanPerTanggal->flatten();
$summary = [
'hadir' => $allKeg->sum('hadir') + $allKeg->sum('terlambat'),
'terlambat' => $allKeg->sum('terlambat'),
'izin' => $allKeg->sum('izin'),
'sakit' => $allKeg->sum('sakit'),
'alpa' => $allKeg->sum('alpa'),
'total_absensi' => $allKeg->sum('total_absensi'),
'jumlah_kegiatan' => $allKeg->count(),
'jumlah_hari' => $kegiatanPerTanggal->count(),
];
}
return view('admin.kegiatan.riwayat.index', compact(
'kegiatans', 'kegiatanPerTanggal', 'kategoris', 'summary', 'mode',
'dari', 'sampai', 'tanggal', 'periodeLabel', 'kategoriId'
));
}
/**
* Detail riwayat per kegiatan santri per kelas + filter
*/
public function show($id, Request $request)
{
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok'])
->findOrFail($id);
// ── Ambil parameter periode dari index ────────────────────────────────
$mode = $request->get('mode', 'hari_ini');
$dari = $request->get('dari', now()->format('Y-m-d'));
$sampai = $request->get('sampai', now()->format('Y-m-d'));
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
if ($mode === 'hari_ini') {
$dari = $tanggal;
$sampai = $tanggal;
}
// ── Query absensi ─────────────────────────────────────────────────────
$query = AbsensiKegiatan::with(['santri.kelasSantri.kelas.kelompok'])
->where('kegiatan_id', $kegiatan->kegiatan_id)
->whereDate('tanggal', '>=', $dari)
->whereDate('tanggal', '<=', $sampai);
if ($request->filled('id_santri')) $query->where('id_santri', $request->id_santri);
if ($request->filled('id_kelas')) $query->whereHas('santri.kelasSantri', fn($q) => $q->where('id_kelas', $request->id_kelas));
if ($request->filled('status')) $query->where('status', $request->status);
if ($request->filled('tanggal_spesifik')) $query->whereDate('tanggal', $request->tanggal_spesifik);
$riwayats = $query->orderBy('tanggal', 'desc')->orderBy('waktu_absen')->paginate(50)->appends($request->query());
// ── Statistik ─────────────────────────────────────────────────────────
$statsQuery = AbsensiKegiatan::where('kegiatan_id', $kegiatan->kegiatan_id)
->whereDate('tanggal', '>=', $dari)->whereDate('tanggal', '<=', $sampai);
if ($request->filled('id_kelas')) {
$statsQuery->whereHas('santri.kelasSantri', fn($q) => $q->where('id_kelas', $request->id_kelas));
}
$stats = $statsQuery->select('status', DB::raw('count(*) as total'))
->groupBy('status')->pluck('total', 'status')->toArray();
// ── Total santri yang seharusnya hadir ────────────────────────────────
if ($kegiatan->kelasKegiatan->isEmpty()) {
$totalSantriEligible = Santri::where('status', 'Aktif')->count();
} else {
$kelasIds = $kegiatan->kelasKegiatan->pluck('id')->toArray();
$totalSantriEligible = Santri::where('status', 'Aktif')
->whereHas('kelasSantri', fn($q) => $q->whereIn('id_kelas', $kelasIds))
->count();
if ($totalSantriEligible === 0) $totalSantriEligible = Santri::where('status', 'Aktif')->count();
}
$totalRecorded = array_sum($stats);
$hadirCount = ($stats['Hadir'] ?? 0) + ($stats['Terlambat'] ?? 0);
$persenHadir = $totalSantriEligible > 0 ? round($hadirCount / $totalSantriEligible * 100, 1) : 0;
// ── Filter dropdown ───────────────────────────────────────────────────
$santris = Santri::where('status', 'Aktif')->select('id_santri', 'nama_lengkap')->orderBy('nama_lengkap')->get();
$kelasList = Kelas::active()->ordered()->with('kelompok')->get();
// ── Label periode ─────────────────────────────────────────────────────
if ($mode === 'hari_ini') {
$periodeLabel = Carbon::parse($dari)->locale('id')->isoFormat('dddd, D MMMM Y');
} else {
$periodeLabel = Carbon::parse($dari)->locale('id')->isoFormat('D MMM Y')
. ' '
. Carbon::parse($sampai)->locale('id')->isoFormat('D MMM Y');
}
return view('admin.kegiatan.riwayat.show', compact(
'kegiatan', 'riwayats', 'santris', 'kelasList', 'stats',
'totalSantriEligible', 'totalRecorded', 'persenHadir',
'mode', 'dari', 'sampai', 'tanggal', 'periodeLabel'
));
}
/**
* Riwayat kehadiran per santri tabbed per kategori
*/
public function detailSantri($id_santri, Request $request)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
// ── Statistik ringkasan (Terlambat = Hadir, bukan Alpa) ──────────────
$rawStats = AbsensiKegiatan::where('id_santri', $id_santri)
->select('status', DB::raw('count(*) as total'))
->groupBy('status')->pluck('total', 'status')->toArray();
// Hadir efektif = Hadir + Terlambat
$stats = $rawStats;
$stats['_hadir_efektif'] = ($rawStats['Hadir'] ?? 0) + ($rawStats['Terlambat'] ?? 0);
// ── Tren 30 hari — Hadir efektif (Hadir + Terlambat) ─────────────────
$riwayat30Hari = AbsensiKegiatan::where('id_santri', $id_santri)
->whereDate('tanggal', '>=', now()->subDays(29))
->select(
DB::raw('DATE(tanggal) as tanggal'),
DB::raw('SUM(CASE WHEN status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir'),
DB::raw('COUNT(*) as total')
)
->groupBy('tanggal')->orderBy('tanggal', 'asc')->get();
// ── Daftar semua kategori yang punya riwayat untuk santri ini ─────────
$kategoriList = AbsensiKegiatan::where('id_santri', $id_santri)
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
->join('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id')
->select(
'kategori_kegiatans.kategori_id',
'kategori_kegiatans.nama_kategori',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status IN ("Hadir","Terlambat") THEN 1 ELSE 0 END) as hadir_efektif')
)
->groupBy('kategori_kegiatans.kategori_id', 'kategori_kegiatans.nama_kategori')
->orderBy('kategori_kegiatans.nama_kategori')
->get();
// ── Tab aktif & per_page ───────────────────────────────────────────────
$activeKategori = $request->get('tab_kat', $kategoriList->first()?->kategori_id ?? '');
// per_page: 15 | 50 | 100 | 'all' (semua data, tanpa paginasi)
$perPageRaw = $request->get('per_page', 15);
$showAll = ($perPageRaw === 'all');
$perPage = $showAll ? 'all' : (in_array((int)$perPageRaw, [15, 50, 100]) ? (int)$perPageRaw : 15);
// ── Riwayat untuk tab aktif ────────────────────────────────────────────
$riwayatsQuery = AbsensiKegiatan::with('kegiatan.kategori')
->where('id_santri', $id_santri)
->whereHas('kegiatan', fn($q) => $q->where('kategori_id', $activeKategori))
->orderBy('tanggal', 'desc');
if ($showAll) {
// Wrap sebagai LengthAwarePaginator manual agar view tetap konsisten
$allItems = $riwayatsQuery->get();
$riwayats = new \Illuminate\Pagination\LengthAwarePaginator(
$allItems, $allItems->count(), max($allItems->count(), 1), 1,
['path' => $request->url(), 'query' => $request->query()]
);
} else {
$riwayats = $riwayatsQuery->paginate($perPage)->appends($request->query());
}
return view('admin.kegiatan.riwayat.detail-santri', compact(
'santri', 'stats', 'riwayat30Hari', 'riwayats',
'kategoriList', 'activeKategori', 'perPage', 'showAll'
));
}
public function edit(AbsensiKegiatan $riwayat)
{
$riwayat->load(['santri', 'kegiatan']);
return view('admin.kegiatan.riwayat.edit', compact('riwayat'));
}
public function update(Request $request, AbsensiKegiatan $riwayat)
{
$validated = $request->validate([
'status' => 'required|in:Hadir,Izin,Sakit,Alpa,Terlambat,Pulang',
'waktu_absen' => 'nullable|date_format:H:i',
]);
$riwayat->update($validated);
return redirect()->route('admin.riwayat-kegiatan.index')->with('success', 'Riwayat absensi berhasil diperbarui.');
}
public function destroy(AbsensiKegiatan $riwayat)
{
$nama = $riwayat->santri->nama_lengkap;
$riwayat->delete();
return redirect()->route('admin.riwayat-kegiatan.index')->with('success', "Riwayat absensi $nama berhasil dihapus.");
}
}

View File

@ -1,348 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\RiwayatPelanggaran;
use App\Models\KategoriPelanggaran;
use App\Models\KlasifikasiPelanggaran;
use App\Models\Santri;
use Illuminate\Http\Request;
use Carbon\Carbon;
class RiwayatPelanggaranController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = RiwayatPelanggaran::with(['santri', 'kategori.klasifikasi']);
// Filter berdasarkan pencarian
if ($request->has('search') && $request->search != '') {
$query->search($request->search);
}
// Filter berdasarkan santri
if ($request->has('id_santri') && $request->id_santri != '') {
$query->bySantri($request->id_santri);
}
// Filter berdasarkan kategori
if ($request->has('id_kategori') && $request->id_kategori != '') {
$query->byKategori($request->id_kategori);
}
// Filter berdasarkan klasifikasi (BARU)
if ($request->has('id_klasifikasi') && $request->id_klasifikasi != '') {
$query->whereHas('kategori', function($q) use ($request) {
$q->where('id_klasifikasi', $request->id_klasifikasi);
});
}
// Filter berdasarkan status kafaroh (BARU)
if ($request->has('status_kafaroh') && $request->status_kafaroh != '') {
if ($request->status_kafaroh == '1') {
$query->kafarohSelesai();
} else {
$query->kafarohBelumSelesai();
}
}
// Filter berdasarkan status publish (BARU)
if ($request->has('status_publish') && $request->status_publish != '') {
if ($request->status_publish == '1') {
$query->publishedToParent();
} else {
$query->notPublishedToParent();
}
}
// Filter berdasarkan tanggal
if ($request->has('tanggal_mulai') && $request->tanggal_mulai != '') {
$tanggalSelesai = $request->tanggal_selesai ?? $request->tanggal_mulai;
$query->byTanggal($request->tanggal_mulai, $tanggalSelesai);
}
// Filter bulan ini
if ($request->has('bulan_ini') && $request->bulan_ini == '1') {
$query->bulanIni();
}
$data = $query->terbaru()->paginate(15);
// Data untuk filter dropdown
$santriList = Santri::aktif()->orderBy('nama_lengkap')->get();
$kategoriList = KategoriPelanggaran::with('klasifikasi')
->orderBy('nama_pelanggaran')
->get();
$klasifikasiList = KlasifikasiPelanggaran::aktif()->byUrutan()->get();
// Statistik
$totalPelanggaran = RiwayatPelanggaran::count();
$pelanggaranBulanIni = RiwayatPelanggaran::bulanIni()->count();
$totalPoin = RiwayatPelanggaran::sum('poin');
$totalKafarohSelesai = RiwayatPelanggaran::kafarohSelesai()->count();
$totalPublished = RiwayatPelanggaran::publishedToParent()->count();
return view('admin.riwayat_pelanggaran.index', compact(
'data',
'santriList',
'kategoriList',
'klasifikasiList',
'totalPelanggaran',
'pelanggaranBulanIni',
'totalPoin',
'totalKafarohSelesai',
'totalPublished'
));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
// Generate preview ID riwayat berikutnya
$lastRiwayat = RiwayatPelanggaran::orderBy('id', 'desc')->first();
$nextNum = $lastRiwayat ? intval(substr($lastRiwayat->id_riwayat, 1)) + 1 : 1;
$nextIdRiwayat = 'P' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
// Data untuk dropdown
$santriList = Santri::aktif()->orderBy('nama_lengkap')->get();
$klasifikasiList = KlasifikasiPelanggaran::aktif()->byUrutan()->get();
$kategoriList = KategoriPelanggaran::with('klasifikasi')
->aktif()
->orderBy('id_klasifikasi')
->orderBy('nama_pelanggaran')
->get();
return view('admin.riwayat_pelanggaran.create', compact(
'nextIdRiwayat',
'santriList',
'klasifikasiList',
'kategoriList'
));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'id_santri' => 'required|exists:santris,id_santri',
'id_kategori' => 'required|exists:kategori_pelanggarans,id_kategori',
'tanggal' => 'required|date',
'keterangan' => 'nullable|string|max:1000',
], [
'id_santri.required' => 'Santri wajib dipilih.',
'id_santri.exists' => 'Santri tidak ditemukan.',
'id_kategori.required' => 'Kategori pelanggaran wajib dipilih.',
'id_kategori.exists' => 'Kategori tidak ditemukan.',
'tanggal.required' => 'Tanggal wajib diisi.',
'tanggal.date' => 'Format tanggal tidak valid.',
]);
// Ambil poin dari kategori
$kategori = KategoriPelanggaran::where('id_kategori', $validated['id_kategori'])->first();
$validated['poin'] = $kategori->poin;
$validated['poin_asli'] = $kategori->poin;
RiwayatPelanggaran::create($validated);
return redirect()->route('admin.riwayat-pelanggaran.index')
->with('success', 'Riwayat pelanggaran berhasil ditambahkan.');
}
/**
* Display the specified resource.
*/
public function show(RiwayatPelanggaran $riwayatPelanggaran)
{
$riwayatPelanggaran->load([
'santri',
'kategori.klasifikasi',
'adminKafaroh',
'adminPublished'
]);
// Riwayat pelanggaran santri lainnya
$riwayatLainnya = RiwayatPelanggaran::where('id_santri', $riwayatPelanggaran->id_santri)
->where('id', '!=', $riwayatPelanggaran->id)
->with('kategori')
->terbaru()
->limit(5)
->get();
return view('admin.riwayat_pelanggaran.show', compact(
'riwayatPelanggaran',
'riwayatLainnya'
));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(RiwayatPelanggaran $riwayatPelanggaran)
{
$riwayatPelanggaran->load(['santri', 'kategori']);
// Data untuk dropdown
$santriList = Santri::aktif()->orderBy('nama_lengkap')->get();
$kategoriList = KategoriPelanggaran::with('klasifikasi')
->aktif()
->orderBy('id_klasifikasi')
->orderBy('nama_pelanggaran')
->get();
return view('admin.riwayat_pelanggaran.edit', compact(
'riwayatPelanggaran',
'santriList',
'kategoriList'
));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, RiwayatPelanggaran $riwayatPelanggaran)
{
$validated = $request->validate([
'id_santri' => 'required|exists:santris,id_santri',
'id_kategori' => 'required|exists:kategori_pelanggarans,id_kategori',
'tanggal' => 'required|date',
'keterangan' => 'nullable|string|max:1000',
], [
'id_santri.required' => 'Santri wajib dipilih.',
'id_santri.exists' => 'Santri tidak ditemukan.',
'id_kategori.required' => 'Kategori pelanggaran wajib dipilih.',
'id_kategori.exists' => 'Kategori tidak ditemukan.',
'tanggal.required' => 'Tanggal wajib diisi.',
'tanggal.date' => 'Format tanggal tidak valid.',
]);
// Ambil poin dari kategori
$kategori = KategoriPelanggaran::where('id_kategori', $validated['id_kategori'])->first();
// Jika kategori berubah dan kafaroh belum selesai, update poin
if ($riwayatPelanggaran->id_kategori != $validated['id_kategori'] && !$riwayatPelanggaran->is_kafaroh_selesai) {
$validated['poin'] = $kategori->poin;
$validated['poin_asli'] = $kategori->poin;
}
$riwayatPelanggaran->update($validated);
return redirect()->route('admin.riwayat-pelanggaran.index')
->with('success', 'Riwayat pelanggaran berhasil diperbarui.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(RiwayatPelanggaran $riwayatPelanggaran)
{
$idRiwayat = $riwayatPelanggaran->id_riwayat;
$namaSantri = $riwayatPelanggaran->santri->nama_lengkap ?? 'Unknown';
$riwayatPelanggaran->delete();
return redirect()->route('admin.riwayat-pelanggaran.index')
->with('success', 'Riwayat pelanggaran ' . $idRiwayat . ' untuk santri ' . $namaSantri . ' berhasil dihapus.');
}
/**
* Tampilkan riwayat pelanggaran per santri
*/
public function riwayatSantri($idSantri)
{
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
$riwayat = RiwayatPelanggaran::with('kategori')
->bySantri($idSantri)
->terbaru()
->paginate(10);
$totalPoin = RiwayatPelanggaran::bySantri($idSantri)->sum('poin');
$totalPelanggaran = RiwayatPelanggaran::bySantri($idSantri)->count();
$totalKafarohSelesai = RiwayatPelanggaran::bySantri($idSantri)->kafarohSelesai()->count();
return view('admin.riwayat_pelanggaran.riwayat_santri', compact(
'santri',
'riwayat',
'totalPoin',
'totalPelanggaran',
'totalKafarohSelesai'
));
}
/**
* Selesaikan Kafaroh
*/
public function selesaikanKafaroh(Request $request, RiwayatPelanggaran $riwayatPelanggaran)
{
// Validasi jika kafaroh sudah selesai
if ($riwayatPelanggaran->is_kafaroh_selesai) {
return redirect()->back()
->with('error', 'Kafaroh sudah diselesaikan sebelumnya.');
}
$validated = $request->validate([
'catatan_kafaroh' => 'nullable|string|max:500',
]);
$riwayatPelanggaran->update([
'is_kafaroh_selesai' => true,
'tanggal_kafaroh_selesai' => now(),
'admin_kafaroh_id' => auth()->id(),
'catatan_kafaroh' => $validated['catatan_kafaroh'] ?? null,
'poin' => 0, // Poin dilebur menjadi 0
]);
return redirect()->back()
->with('success', 'Kafaroh berhasil diselesaikan. Poin telah dilebur menjadi 0.');
}
/**
* Publish ke Wali Santri
*/
public function publishToParent(RiwayatPelanggaran $riwayatPelanggaran)
{
// Validasi jika sudah dipublish
if ($riwayatPelanggaran->is_published_to_parent) {
return redirect()->back()
->with('error', 'Riwayat pelanggaran sudah dikirim ke wali santri sebelumnya.');
}
$riwayatPelanggaran->update([
'is_published_to_parent' => true,
'tanggal_published' => now(),
'admin_published_id' => auth()->id(),
]);
return redirect()->back()
->with('success', 'Riwayat pelanggaran berhasil dikirim ke wali santri.');
}
/**
* Batalkan Publish
*/
public function unpublishFromParent(RiwayatPelanggaran $riwayatPelanggaran)
{
// Validasi jika belum dipublish
if (!$riwayatPelanggaran->is_published_to_parent) {
return redirect()->back()
->with('error', 'Riwayat pelanggaran belum dikirim ke wali santri.');
}
$riwayatPelanggaran->update([
'is_published_to_parent' => false,
'tanggal_published' => null,
'admin_published_id' => null,
]);
return redirect()->back()
->with('success', 'Pengiriman ke wali santri berhasil dibatalkan.');
}
}

View File

@ -1,295 +0,0 @@
<?php
// app/Http/Controllers/Admin/SantriController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Santri;
use App\Models\KelompokKelas;
use App\Models\SantriKelas;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
class SantriController extends Controller
{
/**
* Tampilkan daftar data santri dengan fitur search.
*/
public function index(Request $request)
{
$query = Santri::with(['kelasSantri.kelas.kelompok']);
// Search berdasarkan nama, NIS, atau ID Santri
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('nama_lengkap', 'like', "%{$search}%")
->orWhere('nis', 'like', "%{$search}%")
->orWhere('id_santri', 'like', "%{$search}%");
});
}
// Filter berdasarkan status
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Filter berdasarkan kelas spesifik
if ($request->filled('id_kelas')) {
$query->whereHas('kelasSantri', function($q) use ($request) {
$q->where('id_kelas', $request->id_kelas);
});
}
// Select kolom yang diperlukan saja
$santris = $query->select(
'id',
'id_santri',
'nis',
'nama_lengkap',
'jenis_kelamin',
'status',
'foto',
'created_at'
)
->orderBy('created_at', 'desc')
->paginate(20)
->appends(request()->query());
// Load kelompok kelas untuk filter dropdown
$kelompokKelas = KelompokKelas::with(['kelas' => function($q) {
$q->where('is_active', true)->orderBy('urutan');
}])->active()->ordered()->get();
return view('admin.santri.index', compact('santris', 'kelompokKelas'));
}
/**
* Tampilkan form untuk membuat santri baru.
*/
public function create()
{
// Cache last santri ID selama 1 menit
$nextIdSantri = Cache::remember('next_santri_id', 60, function () {
$lastSantri = Santri::select('id_santri')
->orderBy('id', 'desc')
->first();
$nextNum = $lastSantri ? intval(substr($lastSantri->id_santri, 1)) + 1 : 1;
return 'S' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
});
// Load kelompok kelas untuk dropdown bertingkat
$kelompokKelas = KelompokKelas::with(['kelas' => function($q) {
$q->where('is_active', true)->orderBy('urutan');
}])->active()->ordered()->get();
return view('admin.santri.create', compact('nextIdSantri', 'kelompokKelas'));
}
/**
* Simpan santri baru ke database.
*/
public function store(Request $request)
{
$validated = $request->validate([
'nis' => 'nullable|string|max:255|unique:santris,nis',
'nama_lengkap' => 'required|string|max:255',
'jenis_kelamin' => 'required|in:Laki-laki,Perempuan',
'kelas_ids' => 'nullable|array',
'kelas_ids.*' => 'nullable|array',
'kelas_ids.*.*' => 'exists:kelas,id',
'status' => 'required|in:Aktif,Lulus,Tidak Aktif',
'alamat_santri' => 'nullable|string',
'daerah_asal' => 'nullable|string|max:255',
'nama_orang_tua' => 'nullable|string|max:255',
'nomor_hp_ortu' => 'nullable|string|max:20',
'foto' => 'nullable|image|mimes:jpg,jpeg,png|max:2048',
], [
'nis.unique' => 'NIS sudah digunakan oleh santri lain.',
'nama_lengkap.required' => 'Nama lengkap wajib diisi.',
'jenis_kelamin.required' => 'Jenis kelamin wajib dipilih.',
'status.required' => 'Status wajib dipilih.',
'foto.image' => 'File harus berupa gambar.',
'foto.mimes' => 'Foto harus berformat JPG, JPEG, atau PNG.',
'foto.max' => 'Ukuran foto maksimal 2 MB.',
]);
// Flatten nested array: kelas_ids[kelompok][] → flat array of kelas IDs
$kelasIdsFlat = [];
if (isset($validated['kelas_ids']) && is_array($validated['kelas_ids'])) {
foreach ($validated['kelas_ids'] as $kelompok => $kelasArray) {
if (is_array($kelasArray)) {
$kelasIdsFlat = array_merge($kelasIdsFlat, $kelasArray);
}
}
}
// Validasi minimal 1 kelas dipilih
if (empty($kelasIdsFlat)) {
return back()->withInput()->withErrors(['kelas_ids' => 'Minimal satu kelas wajib dipilih.']);
}
// Hapus kelas_ids dari validated (bukan kolom santri)
unset($validated['kelas_ids']);
// Buat santri
$santri = Santri::create($validated);
// Assign semua kelas yang dipilih
$tahunAjaran = SantriKelas::getCurrentAcademicYear();
$isFirst = true;
foreach ($kelasIdsFlat as $idKelas) {
$santri->assignKelas($idKelas, $tahunAjaran, $isFirst);
$isFirst = false;
}
// Handle upload foto
if ($request->hasFile('foto')) {
$file = $request->file('foto');
$extension = $file->getClientOriginalExtension();
$filename = $santri->id_santri . '.' . $extension;
// Simpan file ke storage/app/public/santri
$path = $file->storeAs('santri', $filename, 'public');
// Update path foto di database
$santri->update(['foto' => $path]);
}
// Clear cache
Cache::forget('next_santri_id');
Cache::forget('santris_tanpa_akun');
Cache::forget('santri_aktif_list');
return redirect()->route('admin.santri.index')
->with('success', 'Data santri berhasil ditambahkan.');
}
/**
* Tampilkan detail santri.
*/
public function show(Santri $santri)
{
$santri->load('kelasSantri.kelas.kelompok');
return view('admin.santri.show', compact('santri'));
}
/**
* Tampilkan form untuk mengedit santri.
*/
public function edit(Santri $santri)
{
$santri->load('kelasSantri.kelas.kelompok');
// Load kelompok kelas untuk dropdown bertingkat
$kelompokKelas = KelompokKelas::with(['kelas' => function($q) {
$q->where('is_active', true)->orderBy('urutan');
}])->active()->ordered()->get();
return view('admin.santri.edit', compact('santri', 'kelompokKelas'));
}
/**
* Update data santri di database.
*/
public function update(Request $request, Santri $santri)
{
$validated = $request->validate([
'nis' => 'nullable|string|max:255|unique:santris,nis,' . $santri->id,
'nama_lengkap' => 'required|string|max:255',
'jenis_kelamin' => 'required|in:Laki-laki,Perempuan',
'kelas_ids' => 'nullable|array',
'kelas_ids.*' => 'nullable|array',
'kelas_ids.*.*' => 'exists:kelas,id',
'status' => 'required|in:Aktif,Lulus,Tidak Aktif',
'alamat_santri' => 'nullable|string',
'daerah_asal' => 'nullable|string|max:255',
'nama_orang_tua' => 'nullable|string|max:255',
'nomor_hp_ortu' => 'nullable|string|max:20',
'foto' => 'nullable|image|mimes:jpg,jpeg,png|max:2048',
], [
'nis.unique' => 'NIS sudah digunakan oleh santri lain.',
'nama_lengkap.required' => 'Nama lengkap wajib diisi.',
'jenis_kelamin.required' => 'Jenis kelamin wajib dipilih.',
'status.required' => 'Status wajib dipilih.',
'foto.image' => 'File harus berupa gambar.',
'foto.mimes' => 'Foto harus berformat JPG, JPEG, atau PNG.',
'foto.max' => 'Ukuran foto maksimal 2 MB.',
]);
// Flatten nested array: kelas_ids[kelompok][] → flat array of kelas IDs
$kelasIdsFlat = [];
if (isset($validated['kelas_ids']) && is_array($validated['kelas_ids'])) {
foreach ($validated['kelas_ids'] as $kelompok => $kelasArray) {
if (is_array($kelasArray)) {
$kelasIdsFlat = array_merge($kelasIdsFlat, $kelasArray);
}
}
}
// Validasi minimal 1 kelas dipilih
if (empty($kelasIdsFlat)) {
return back()->withInput()->withErrors(['kelas_ids' => 'Minimal satu kelas wajib dipilih.']);
}
// Hapus kelas_ids dari validated (bukan kolom santri)
unset($validated['kelas_ids']);
// Handle upload foto baru
if ($request->hasFile('foto')) {
if ($santri->foto && Storage::disk('public')->exists($santri->foto)) {
Storage::disk('public')->delete($santri->foto);
}
$file = $request->file('foto');
$extension = $file->getClientOriginalExtension();
$filename = $santri->id_santri . '.' . $extension;
$path = $file->storeAs('santri', $filename, 'public');
$validated['foto'] = $path;
}
$santri->update($validated);
// Sync kelas: hapus semua kelas tahun ini, lalu assign ulang
$tahunAjaran = SantriKelas::getCurrentAcademicYear();
$santri->kelasSantri()
->where('tahun_ajaran', $tahunAjaran)
->delete();
$isFirst = true;
foreach ($kelasIdsFlat as $idKelas) {
$santri->assignKelas($idKelas, $tahunAjaran, $isFirst);
$isFirst = false;
}
// Clear cache
Cache::forget('santris_tanpa_akun');
Cache::forget('santri_aktif_list');
return redirect()->route('admin.santri.index')
->with('success', 'Data santri berhasil diperbarui.');
}
/**
* Hapus data santri dari database.
*/
public function destroy(Santri $santri)
{
$namaSantri = $santri->nama_lengkap;
// Hapus foto jika ada
if ($santri->foto && Storage::disk('public')->exists($santri->foto)) {
Storage::disk('public')->delete($santri->foto);
}
$santri->delete();
// Clear cache
Cache::forget('santris_tanpa_akun');
Cache::forget('santri_aktif_list');
return redirect()->route('admin.santri.index')
->with('success', 'Data santri "' . $namaSantri . '" berhasil dihapus.');
}
}

View File

@ -1,156 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Semester;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class SemesterController extends Controller
{
/**
* Display a listing of semester
*/
public function index(Request $request)
{
$query = Semester::query();
// Filter tahun ajaran
if ($request->filled('tahun_ajaran')) {
$query->tahunAjaran($request->tahun_ajaran);
}
$semesters = $query->orderBy('tahun_ajaran', 'desc')
->orderBy('periode', 'desc')
->paginate(10)
->appends(request()->query());
return view('admin.semester.index', compact('semesters'));
}
/**
* Show the form for creating a new semester
*/
public function create()
{
$nextIdSemester = Cache::remember('next_semester_id', 60, function () {
$last = Semester::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_semester, 3)) + 1 : 1;
return 'SEM' . str_pad($num, 3, '0', STR_PAD_LEFT);
});
return view('admin.semester.create', compact('nextIdSemester'));
}
/**
* Store a newly created semester
*/
public function store(Request $request)
{
$validated = $request->validate([
'tahun_ajaran' => 'required|string|max:20',
'periode' => 'required|in:1,2',
'tanggal_mulai' => 'required|date',
'tanggal_akhir' => 'required|date|after:tanggal_mulai',
'is_active' => 'boolean',
], [
'tahun_ajaran.required' => 'Tahun ajaran wajib diisi.',
'periode.required' => 'Periode wajib dipilih.',
'tanggal_mulai.required' => 'Tanggal mulai wajib diisi.',
'tanggal_akhir.required' => 'Tanggal akhir wajib diisi.',
'tanggal_akhir.after' => 'Tanggal akhir harus setelah tanggal mulai.',
]);
$validated['is_active'] = $request->has('is_active') ? 1 : 0;
Semester::create($validated);
Cache::forget('next_semester_id');
return redirect()->route('admin.semester.index')
->with('success', 'Semester berhasil ditambahkan.');
}
/**
* Show the specified semester
*/
public function show(Semester $semester)
{
// Load statistik capaian
$semester->load(['capaian.santri', 'capaian.materi']);
$totalCapaian = $semester->capaian()->count();
$santriUnik = $semester->capaian()->distinct('id_santri')->count('id_santri');
$rataRataPersentase = $semester->capaian()->avg('persentase') ?? 0;
return view('admin.semester.show', compact('semester', 'totalCapaian', 'santriUnik', 'rataRataPersentase'));
}
/**
* Show the form for editing the specified semester
*/
public function edit(Semester $semester)
{
return view('admin.semester.edit', compact('semester'));
}
/**
* Update the specified semester
*/
public function update(Request $request, Semester $semester)
{
$validated = $request->validate([
'tahun_ajaran' => 'required|string|max:20',
'periode' => 'required|in:1,2',
'tanggal_mulai' => 'required|date',
'tanggal_akhir' => 'required|date|after:tanggal_mulai',
'is_active' => 'boolean',
], [
'tahun_ajaran.required' => 'Tahun ajaran wajib diisi.',
'periode.required' => 'Periode wajib dipilih.',
'tanggal_mulai.required' => 'Tanggal mulai wajib diisi.',
'tanggal_akhir.required' => 'Tanggal akhir wajib diisi.',
'tanggal_akhir.after' => 'Tanggal akhir harus setelah tanggal mulai.',
]);
$validated['is_active'] = $request->has('is_active') ? 1 : 0;
$semester->update($validated);
return redirect()->route('admin.semester.index')
->with('success', 'Semester berhasil diperbarui.');
}
/**
* Remove the specified semester
*/
public function destroy(Semester $semester)
{
// Check jika ada capaian terkait
if ($semester->capaian()->exists()) {
return redirect()->route('admin.semester.index')
->with('error', 'Tidak dapat menghapus semester yang sudah memiliki data capaian.');
}
$namaSemester = $semester->nama_semester;
$semester->delete();
return redirect()->route('admin.semester.index')
->with('success', 'Semester "' . $namaSemester . '" berhasil dihapus.');
}
/**
* Toggle status aktif semester
*/
public function toggleAktif(Semester $semester)
{
$semester->is_active = !$semester->is_active;
$semester->save();
$status = $semester->is_active ? 'aktif' : 'tidak aktif';
return redirect()->route('admin.semester.index')
->with('success', 'Semester berhasil diubah menjadi ' . $status . '.');
}
}

View File

@ -1,490 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\UangSaku;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Carbon\Carbon;
class UangSakuController extends Controller
{
// ────────────────────────────────────────────────────────────────
// PRIVATE: cek apakah user aktif adalah pamong
// ────────────────────────────────────────────────────────────────
private function isPamong(): bool
{
return auth()->user()->role === 'pamong';
}
private function requirePamong(): void
{
if (! $this->isPamong()) {
abort(403, 'Akses ditolak. Hanya Pamong yang dapat melakukan aksi ini.');
}
}
/**
* Tampilkan daftar uang saku Grouped per Santri
* Default: bulan ini
*/
public function index(Request $request)
{
$search = $request->get('search');
$dari = $request->get('dari', now()->startOfMonth()->format('Y-m-d'));
$sampai = $request->get('sampai', now()->endOfMonth()->format('Y-m-d'));
$sort = $request->get('sort', 'nama');
// ── KPI ringkasan periode ───────────────────────────────────
$kpiQuery = UangSaku::whereBetween('tanggal_transaksi', [$dari, $sampai]);
$kpi = [
'total_transaksi' => (clone $kpiQuery)->count(),
'total_pemasukan' => (float)(clone $kpiQuery)->where('jenis_transaksi', 'pemasukan')->sum('nominal'),
'total_pengeluaran' => (float)(clone $kpiQuery)->where('jenis_transaksi', 'pengeluaran')->sum('nominal'),
'total_santri' => (clone $kpiQuery)->distinct('id_santri')->count('id_santri'),
];
$kpi['selisih'] = $kpi['total_pemasukan'] - $kpi['total_pengeluaran'];
// ── KPI Real-time: total saldo semua santri ─────────────────
$kpi['total_saldo_realtime'] = (float) DB::table('uang_saku as a')
->whereNotExists(function ($q) {
$q->from('uang_saku as b')
->whereColumn('b.id_santri', 'a.id_santri')
->where(function ($inner) {
$inner->whereColumn('b.tanggal_transaksi', '>', 'a.tanggal_transaksi')
->orWhere(function ($tie) {
$tie->whereColumn('b.tanggal_transaksi', '=', 'a.tanggal_transaksi')
->whereColumn('b.id', '>', 'a.id');
});
});
})
->sum('saldo_sesudah');
// ── Query santri ────────────────────────────────────────────
$santriQuery = Santri::aktif()
->select('id_santri', 'nama_lengkap')
->has('uangSaku');
if ($search) {
$santriQuery->where(function ($q) use ($search) {
$q->where('nama_lengkap', 'like', "%{$search}%")
->orWhere('id_santri', 'like', "%{$search}%");
});
}
$santriQuery->orderBy('nama_lengkap');
$santriList = $santriQuery->paginate(20)->appends(request()->query());
$ids = $santriList->pluck('id_santri');
// ── Saldo terakhir per santri (NOT EXISTS) ──────────────────
$latestIds = DB::table('uang_saku as a')
->whereIn('a.id_santri', $ids)
->whereNotExists(function ($q) {
$q->from('uang_saku as b')
->whereColumn('b.id_santri', 'a.id_santri')
->where(function ($inner) {
$inner->whereColumn('b.tanggal_transaksi', '>', 'a.tanggal_transaksi')
->orWhere(function ($tie) {
$tie->whereColumn('b.tanggal_transaksi', '=', 'a.tanggal_transaksi')
->whereColumn('b.id', '>', 'a.id');
});
});
})
->select('a.id_santri', 'a.id')
->pluck('a.id', 'a.id_santri');
$saldoMap = UangSaku::whereIn('id', $latestIds->values())
->get()
->keyBy('id_santri');
// ── Statistik per santri mengikuti PERIODE filter ───────────
$periodeStats = UangSaku::whereIn('id_santri', $ids)
->whereBetween('tanggal_transaksi', [$dari, $sampai])
->select(
'id_santri',
DB::raw('SUM(CASE WHEN jenis_transaksi="pemasukan" THEN nominal ELSE 0 END) as pemasukan_periode'),
DB::raw('SUM(CASE WHEN jenis_transaksi="pengeluaran" THEN nominal ELSE 0 END) as pengeluaran_periode'),
DB::raw('COUNT(*) as total_periode')
)
->groupBy('id_santri')
->get()
->keyBy('id_santri');
// ── Transaksi terbaru per santri (max 5) ────────────────────
$transaksiMap = UangSaku::whereIn('id_santri', $ids)
->orderByDesc('tanggal_transaksi')
->orderByDesc('id')
->get()
->groupBy('id_santri')
->map(fn($g) => $g->take(5));
// ── Attach data ke santri objects ───────────────────────────
$collection = $santriList->getCollection()->map(function ($santri) use ($saldoMap, $periodeStats, $transaksiMap) {
$saldoRow = $saldoMap[$santri->id_santri] ?? null;
$periode = $periodeStats[$santri->id_santri] ?? null;
$santri->saldo_terakhir = $saldoRow ? (float)$saldoRow->saldo_sesudah : 0;
$santri->transaksi_terakhir_tgl = $saldoRow ? $saldoRow->tanggal_transaksi : null;
$santri->pemasukan_periode = $periode ? (float)$periode->pemasukan_periode : 0;
$santri->pengeluaran_periode = $periode ? (float)$periode->pengeluaran_periode : 0;
$santri->transaksi_periode = $periode ? (int)$periode->total_periode : 0;
$santri->transaksi_terbaru = $transaksiMap[$santri->id_santri] ?? collect();
return $santri;
});
$sorted = match($sort) {
'saldo_asc' => $collection->sortBy('saldo_terakhir'),
'saldo_desc' => $collection->sortByDesc('saldo_terakhir'),
'transaksi_desc' => $collection->sortByDesc('transaksi_periode'),
'terakhir' => $collection->sortByDesc('transaksi_terakhir_tgl'),
default => $collection->sortBy('nama_lengkap'),
};
$santriList->setCollection($sorted->values());
// Kirim flag ke view agar tampilan bisa menyesuaikan
$canCrud = $this->isPamong();
return view('admin.uang-saku.index', compact('santriList', 'kpi', 'dari', 'sampai', 'sort', 'canCrud'));
}
/**
* AJAX: Info santri untuk form create/edit
*/
public function santriInfo($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
$bulanIni = now();
$lastTx = UangSaku::where('id_santri', $id_santri)
->orderByDesc('tanggal_transaksi')
->orderByDesc('id')
->first();
$saldo = $lastTx ? (float)$lastTx->saldo_sesudah : 0;
$pemasukanBulanIni = UangSaku::where('id_santri', $id_santri)
->where('jenis_transaksi', 'pemasukan')
->whereMonth('tanggal_transaksi', $bulanIni->month)
->whereYear('tanggal_transaksi', $bulanIni->year)
->sum('nominal');
$pengeluaranBulanIni = UangSaku::where('id_santri', $id_santri)
->where('jenis_transaksi', 'pengeluaran')
->whereMonth('tanggal_transaksi', $bulanIni->month)
->whereYear('tanggal_transaksi', $bulanIni->year)
->sum('nominal');
$transaksiTerakhir = UangSaku::where('id_santri', $id_santri)
->orderByDesc('tanggal_transaksi')
->orderByDesc('id')
->limit(3)
->get()
->map(fn($t) => [
'tanggal' => $t->tanggal_transaksi->format('d/m/Y'),
'jenis' => $t->jenis_transaksi,
'nominal' => number_format($t->nominal, 0, ',', '.'),
'keterangan' => $t->keterangan ?? '-',
]);
return response()->json([
'nama' => $santri->nama_lengkap,
'saldo_terakhir' => number_format($saldo, 0, ',', '.'),
'saldo_raw' => $saldo,
'total_pemasukan_bulan_ini' => number_format($pemasukanBulanIni, 0, ',', '.'),
'total_pengeluaran_bulan_ini' => number_format($pengeluaranBulanIni, 0, ',', '.'),
'transaksi_terakhir' => $transaksiTerakhir,
]);
}
public function create()
{
$this->requirePamong();
$santriList = Santri::where('status', 'Aktif')
->select('id_santri', 'nama_lengkap')
->orderBy('nama_lengkap')
->get();
return view('admin.uang-saku.create', compact('santriList'));
}
public function store(Request $request)
{
$this->requirePamong();
$validated = $request->validate([
'id_santri' => 'required|exists:santris,id_santri',
'jenis_transaksi' => 'required|in:pemasukan,pengeluaran',
'nominal' => 'required|numeric|min:1|max:99999999',
'keterangan' => 'nullable|string|max:500',
'tanggal_transaksi' => 'required|date',
]);
DB::beginTransaction();
try {
UangSaku::create($validated);
$this->recalculateSaldoAfter($validated['id_santri'], $validated['tanggal_transaksi']);
if ($validated['jenis_transaksi'] === 'pengeluaran'
&& $this->hasSaldoNegatif($validated['id_santri'], $validated['tanggal_transaksi'])) {
DB::rollBack();
return back()->withInput()->with(
'error',
'Transaksi gagal: Saldo tidak mencukupi. ' .
'Jumlah pengeluaran melebihi saldo yang tersedia pada tanggal tersebut.'
);
}
DB::commit();
Cache::forget('santri_aktif_uang_saku');
return redirect()->route('admin.uang-saku.index')
->with('success', 'Transaksi uang saku berhasil ditambahkan.');
} catch (\Exception $e) {
DB::rollBack();
return back()->withInput()->with('error', 'Gagal menambahkan transaksi: ' . $e->getMessage());
}
}
public function show($id)
{
$transaksi = UangSaku::with('santri')->findOrFail($id);
$canCrud = $this->isPamong();
return view('admin.uang-saku.show', compact('transaksi', 'canCrud'));
}
public function edit($id)
{
$this->requirePamong();
$transaksi = UangSaku::with('santri')->findOrFail($id);
$santriList = Santri::where('status', 'Aktif')
->select('id_santri', 'nama_lengkap')
->orderBy('nama_lengkap')
->get();
return view('admin.uang-saku.edit', compact('transaksi', 'santriList'));
}
public function update(Request $request, $id)
{
$this->requirePamong();
$transaksi = UangSaku::findOrFail($id);
$validated = $request->validate([
'jenis_transaksi' => 'required|in:pemasukan,pengeluaran',
'nominal' => 'required|numeric|min:1|max:99999999',
'keterangan' => 'nullable|string|max:500',
'tanggal_transaksi' => 'required|date',
]);
$tanggalLama = $transaksi->tanggal_transaksi->format('Y-m-d');
$tanggalBaru = $validated['tanggal_transaksi'];
$tanggalMulai = min($tanggalLama, $tanggalBaru);
DB::beginTransaction();
try {
$transaksi->fill($validated)->saveQuietly();
$this->recalculateSaldoAfter($transaksi->id_santri, $tanggalMulai);
if ($this->hasSaldoNegatif($transaksi->id_santri, $tanggalMulai)) {
DB::rollBack();
return back()->withInput()->with(
'error',
'Perubahan gagal: Perubahan ini menyebabkan saldo menjadi negatif. ' .
'Pengeluaran tidak boleh melebihi saldo yang tersedia.'
);
}
DB::commit();
Cache::forget('santri_aktif_uang_saku');
return redirect()->route('admin.uang-saku.index')
->with('success', 'Transaksi berhasil diperbarui.');
} catch (\Exception $e) {
DB::rollBack();
return back()->withInput()->with('error', 'Gagal memperbarui transaksi: ' . $e->getMessage());
}
}
public function destroy($id)
{
$this->requirePamong();
$transaksi = UangSaku::findOrFail($id);
$idSantri = $transaksi->id_santri;
$tanggal = $transaksi->tanggal_transaksi->format('Y-m-d');
DB::beginTransaction();
try {
$transaksi->delete();
$this->recalculateSaldoAfter($idSantri, $tanggal);
DB::commit();
Cache::forget('santri_aktif_uang_saku');
return redirect()->route('admin.uang-saku.index')
->with('success', 'Transaksi berhasil dihapus.');
} catch (\Exception $e) {
DB::rollBack();
return back()->with('error', 'Gagal menghapus transaksi: ' . $e->getMessage());
}
}
public function riwayat(Request $request, $id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
$tanggalDari = $request->filled('tanggal_dari')
? $request->tanggal_dari
: now()->startOfMonth()->format('Y-m-d');
$tanggalSampai = $request->filled('tanggal_sampai')
? $request->tanggal_sampai
: now()->endOfMonth()->format('Y-m-d');
// Transaksi dalam periode (paginated)
$transaksi = UangSaku::where('id_santri', $id_santri)
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
->orderBy('tanggal_transaksi', 'desc')
->orderBy('id', 'desc')
->paginate(20)
->appends($request->query());
// Total pemasukan & pengeluaran periode
$totalPemasukan = UangSaku::where('id_santri', $id_santri)
->where('jenis_transaksi', 'pemasukan')
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
->sum('nominal');
$totalPengeluaran = UangSaku::where('id_santri', $id_santri)
->where('jenis_transaksi', 'pengeluaran')
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
->sum('nominal');
// Saldo aktual real-time (kumulatif semua waktu)
$lastTx = UangSaku::where('id_santri', $id_santri)
->orderByDesc('tanggal_transaksi')
->orderByDesc('id')
->first();
$saldoTerakhir = $lastTx ? (float)$lastTx->saldo_sesudah : 0;
// Saldo awal periode = saldo_sesudah transaksi terakhir SEBELUM tanggalDari
$txSebelumPeriode = UangSaku::where('id_santri', $id_santri)
->where('tanggal_transaksi', '<', $tanggalDari)
->orderByDesc('tanggal_transaksi')
->orderByDesc('id')
->first();
$saldoAwalPeriode = $txSebelumPeriode ? (float)$txSebelumPeriode->saldo_sesudah : 0;
// Saldo akhir periode = saldo_sesudah transaksi terakhir s.d. tanggalSampai
$txAkhirPeriode = UangSaku::where('id_santri', $id_santri)
->where('tanggal_transaksi', '<=', $tanggalSampai)
->orderByDesc('tanggal_transaksi')
->orderByDesc('id')
->first();
$saldoAkhirPeriode = $txAkhirPeriode ? (float)$txAkhirPeriode->saldo_sesudah : 0;
// ── DATA GRAFIK: PERJALANAN SALDO ────────────────────────────
$saldoPerHari = UangSaku::where('id_santri', $id_santri)
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
->select(
DB::raw('DATE(tanggal_transaksi) as tgl'),
DB::raw('SUBSTRING_INDEX(GROUP_CONCAT(saldo_sesudah ORDER BY id DESC), ",", 1) as saldo_akhir_hari')
)
->groupBy('tgl')
->orderBy('tgl')
->get()
->keyBy('tgl');
$periodeDari = Carbon::parse($tanggalDari);
$periodeSampai = Carbon::parse($tanggalSampai);
$dataGrafikSaldo = [];
$saldoBerjalan = $saldoAwalPeriode;
$current = $periodeDari->copy();
$dataGrafikSaldo[] = [
'tanggal' => $periodeDari->format('Y-m-d'),
'saldo' => $saldoAwalPeriode,
'is_awal' => true,
];
while ($current->lte($periodeSampai)) {
$tgl = $current->format('Y-m-d');
if (isset($saldoPerHari[$tgl])) {
$saldoBerjalan = (float)$saldoPerHari[$tgl]->saldo_akhir_hari;
}
if ($current->eq($periodeDari)) {
if (isset($saldoPerHari[$tgl])) {
$dataGrafikSaldo[0]['saldo'] = (float)$saldoPerHari[$tgl]->saldo_akhir_hari;
$dataGrafikSaldo[0]['is_awal'] = false;
}
} else {
$dataGrafikSaldo[] = [
'tanggal' => $tgl,
'saldo' => $saldoBerjalan,
'is_awal' => false,
];
}
$current->addDay();
}
$canCrud = $this->isPamong();
return view('admin.uang-saku.riwayat', compact(
'santri', 'transaksi',
'totalPemasukan', 'totalPengeluaran',
'saldoAwalPeriode', 'saldoAkhirPeriode', 'saldoTerakhir',
'dataGrafikSaldo',
'tanggalDari', 'tanggalSampai',
'periodeDari', 'periodeSampai',
'canCrud'
));
}
// ────────────────────────────────────────────────────────────────
// PRIVATE HELPERS
// ────────────────────────────────────────────────────────────────
private function hasSaldoNegatif(string $idSantri, string $tanggal): bool
{
return UangSaku::where('id_santri', $idSantri)
->where('tanggal_transaksi', '>=', $tanggal)
->where('saldo_sesudah', '<', 0)
->exists();
}
private function recalculateSaldoAfter($idSantri, $tanggal)
{
$tanggal = $tanggal instanceof Carbon
? $tanggal->format('Y-m-d')
: $tanggal;
$transaksiSetelah = UangSaku::where('id_santri', $idSantri)
->where('tanggal_transaksi', '>=', $tanggal)
->orderBy('tanggal_transaksi')
->orderBy('created_at')
->orderBy('id')
->get();
foreach ($transaksiSetelah as $index => $trans) {
if ($index === 0) {
$prev = UangSaku::where('id_santri', $idSantri)
->where('tanggal_transaksi', '<', $tanggal)
->orderByDesc('tanggal_transaksi')
->orderByDesc('created_at')
->orderByDesc('id')
->first();
$trans->saldo_sebelum = $prev ? (float)$prev->saldo_sesudah : 0;
} else {
$trans->saldo_sebelum = (float)$transaksiSetelah[$index - 1]->saldo_sesudah;
}
$trans->saldo_sesudah = $trans->jenis_transaksi === 'pemasukan'
? $trans->saldo_sebelum + (float)$trans->nominal
: $trans->saldo_sebelum - (float)$trans->nominal;
$trans->saveQuietly();
}
}
}

View File

@ -1,372 +0,0 @@
<?php
// app/Http/Controllers/Admin/UserController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Santri;
use App\Models\SantriAccount;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
// ══════════════════ AKUN SANTRI (WEB) ══════════════════
/**
* Daftar akun santri
*/
public function santriAccounts()
{
$users = SantriAccount::where('role', 'santri')
->with(['santri:id_santri,nama_lengkap,nis,nomor_hp_ortu'])
->get();
$santris_tanpa_akun = Santri::whereDoesntHave('santriAccount', function ($q) {
$q->where('role', 'santri');
})->get();
return view('admin.users.santri_accounts', compact('users', 'santris_tanpa_akun'));
}
/**
* Buat akun santri untuk satu santri langsung (1 klik)
*/
public function buatAkunSantri(Request $request, string $idSantri)
{
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
if (!$santri->nis) {
return redirect()->back()
->with('error', 'Santri ' . $santri->nama_lengkap . ' belum memiliki NIS.');
}
$sudahAda = SantriAccount::where('role', 'santri')
->where('id_santri', $idSantri)->exists();
if ($sudahAda) {
return redirect()->back()
->with('error', 'Santri ' . $santri->nama_lengkap . ' sudah memiliki akun.');
}
SantriAccount::create([
'id_santri' => $santri->id_santri,
'username' => $santri->nama_lengkap,
'password' => Hash::make($santri->nis),
'role' => 'santri',
]);
return redirect()->back()
->with('success', 'Akun santri ' . $santri->nama_lengkap . ' berhasil dibuat. Username: ' . $santri->nama_lengkap . ' | Password: ' . $santri->nis);
}
/**
* Buat akun santri untuk semua santri yang belum punya akun (1 klik massal)
*/
public function buatSemuaAkunSantri(Request $request)
{
$santriList = Santri::whereDoesntHave('santriAccount', function ($q) {
$q->where('role', 'santri');
})->whereNotNull('nis')->get();
if ($santriList->isEmpty()) {
return redirect()->back()
->with('info', 'Semua santri sudah memiliki akun.');
}
$berhasil = 0;
foreach ($santriList as $santri) {
SantriAccount::create([
'id_santri' => $santri->id_santri,
'username' => $santri->nama_lengkap,
'password' => Hash::make($santri->nis),
'role' => 'santri',
]);
$berhasil++;
}
return redirect()->back()
->with('success', $berhasil . ' akun santri berhasil dibuat sekaligus.');
}
/**
* Hapus akun santri
*/
public function destroySantriAccount(string $id)
{
$account = SantriAccount::where('role', 'santri')->findOrFail($id);
$nama = $account->santri ? $account->santri->nama_lengkap : $account->username;
$account->delete();
return redirect()->back()
->with('success', 'Akun santri ' . $nama . ' berhasil dihapus.');
}
// ══════════════════ AKUN WALI (MOBILE) ══════════════════
/**
* Daftar akun wali
*/
public function waliAccounts()
{
$users = SantriAccount::where('role', 'wali')
->with(['santri:id_santri,nama_lengkap,nis,nama_orang_tua,nomor_hp_ortu'])
->get();
$santris_tanpa_wali = Santri::whereDoesntHave('santriAccount', function ($q) {
$q->where('role', 'wali');
})->get();
return view('admin.users.wali_accounts', compact('users', 'santris_tanpa_wali'));
}
/**
* Resolve username untuk akun wali.
*
* Aturan:
* - Default : nama_orang_tua (sama seperti sebelumnya, username = nama ortu)
* - Fallback : "nama_orang_tua - nama_santri"
* hanya dipakai jika nama_orang_tua sudah dipakai
* akun wali lain (cek DB + array in-memory untuk proses massal).
*
* @param Santri $santri
* @param array $usernameYangSudahDipakai username yang sudah dibuat dalam iterasi massal saat ini
*/
private function resolveUsernameWali(Santri $santri, array $usernameYangSudahDipakai = []): string
{
$usernameDefault = $santri->nama_orang_tua;
// Cek di database: apakah nama ortu ini sudah jadi username wali lain?
$sudahDiDbOlehLain = SantriAccount::where('role', 'wali')
->where('username', $usernameDefault)
->where('id_santri', '!=', $santri->id_santri)
->exists();
// Cek di array in-memory (untuk proses massal dalam 1 request)
$sudahDiMemoriOlehLain = in_array($usernameDefault, $usernameYangSudahDipakai);
if ($sudahDiDbOlehLain || $sudahDiMemoriOlehLain) {
// Fallback: tambahkan nama santri agar unik
return $usernameDefault . ' - ' . $santri->nama_lengkap;
}
// Normal: cukup nama orang tua saja
return $usernameDefault;
}
/**
* Buat akun wali untuk satu santri langsung (1 klik)
*/
public function buatAkunWali(Request $request, string $idSantri)
{
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
if (!$santri->nis) {
return redirect()->back()
->with('error', 'Santri ' . $santri->nama_lengkap . ' belum memiliki NIS.');
}
if (!$santri->nama_orang_tua) {
return redirect()->back()
->with('error', 'Santri ' . $santri->nama_lengkap . ' belum memiliki data nama orang tua.');
}
$sudahAda = SantriAccount::where('role', 'wali')
->where('id_santri', $idSantri)->exists();
if ($sudahAda) {
return redirect()->back()
->with('error', 'Wali santri ' . $santri->nama_lengkap . ' sudah memiliki akun.');
}
$username = $this->resolveUsernameWali($santri);
SantriAccount::create([
'id_santri' => $santri->id_santri,
'username' => $username,
'password' => Hash::make($santri->nis),
'role' => 'wali',
]);
return redirect()->back()
->with('success', 'Akun wali untuk ' . $santri->nama_lengkap . ' berhasil dibuat. Username: ' . $username . ' | Password: ' . $santri->nis);
}
/**
* Buat akun wali untuk semua santri yang belum punya akun wali (1 klik massal)
*/
public function buatSemuaAkunWali(Request $request)
{
$santriList = Santri::whereDoesntHave('santriAccount', function ($q) {
$q->where('role', 'wali');
})->whereNotNull('nis')->whereNotNull('nama_orang_tua')->get();
if ($santriList->isEmpty()) {
return redirect()->back()
->with('info', 'Semua santri sudah memiliki akun wali.');
}
$berhasil = 0;
$gagal = 0;
// Lacak username yg dibuat dalam iterasi ini agar
// santri berikut dg nama ortu sama langsung dapat fallback
$usernameYangSudahDipakai = [];
foreach ($santriList as $santri) {
if (!$santri->nama_orang_tua) {
$gagal++;
continue;
}
$username = $this->resolveUsernameWali($santri, $usernameYangSudahDipakai);
SantriAccount::create([
'id_santri' => $santri->id_santri,
'username' => $username,
'password' => Hash::make($santri->nis),
'role' => 'wali',
]);
$usernameYangSudahDipakai[] = $username;
$berhasil++;
}
$pesan = $berhasil . ' akun wali berhasil dibuat.';
if ($gagal > 0) {
$pesan .= ' ' . $gagal . ' dilewati karena data orang tua tidak lengkap.';
}
return redirect()->back()->with('success', $pesan);
}
/**
* Hapus akun wali
*/
public function destroyWaliAccount(string $id)
{
$account = SantriAccount::where('role', 'wali')->findOrFail($id);
$nama = $account->santri ? $account->santri->nama_lengkap : $account->username;
$account->delete();
return redirect()->back()
->with('success', 'Akun wali ' . $nama . ' berhasil dihapus.');
}
// ══════════════════ AKUN ADMIN ══════════════════
/**
* Daftar akun admin
*/
public function adminAccounts()
{
$admins = User::whereIn('role', ['super_admin', 'akademik', 'pamong'])
->orderByRaw("FIELD(role, 'super_admin', 'akademik', 'pamong')")
->orderBy('name')
->get();
return view('admin.users.admin_accounts', compact('admins'));
}
/**
* Form buat akun admin baru
*/
public function createAdminAccount()
{
return view('admin.users.admin_form', [
'admin' => null,
'action' => route('admin.users.admin_store'),
'method' => 'POST',
]);
}
/**
* Simpan akun admin baru
*/
public function storeAdminAccount(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'role' => 'required|in:akademik,pamong',
'password' => 'required|string|min:8|confirmed',
], [
'name.required' => 'Nama wajib diisi.',
'email.required' => 'Email wajib diisi.',
'email.unique' => 'Email sudah digunakan.',
'role.required' => 'Role wajib dipilih.',
'password.required' => 'Password wajib diisi.',
'password.min' => 'Password minimal 8 karakter.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
]);
User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'username' => $validated['email'],
'password' => Hash::make($validated['password']),
'role' => $validated['role'],
]);
return redirect()->route('admin.users.admin_accounts')
->with('success', 'Akun ' . $validated['role'] . ' untuk ' . $validated['name'] . ' berhasil dibuat.');
}
/**
* Form edit akun admin
*/
public function editAdminAccount(string $userId)
{
$admin = User::whereIn('role', ['akademik', 'pamong'])->findOrFail($userId);
return view('admin.users.admin_form', [
'admin' => $admin,
'action' => route('admin.users.admin_update', $userId),
'method' => 'PUT',
]);
}
/**
* Update akun admin
*/
public function updateAdminAccount(Request $request, string $userId)
{
$admin = User::whereIn('role', ['akademik', 'pamong'])->findOrFail($userId);
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $userId,
'role' => 'required|in:akademik,pamong',
'password' => 'nullable|string|min:8|confirmed',
]);
$admin->name = $validated['name'];
$admin->email = $validated['email'];
$admin->username = $validated['email'];
$admin->role = $validated['role'];
if (!empty($validated['password'])) {
$admin->password = Hash::make($validated['password']);
}
$admin->save();
return redirect()->route('admin.users.admin_accounts')
->with('success', 'Akun ' . $admin->name . ' berhasil diperbarui.');
}
/**
* Hapus akun admin
*/
public function destroyAdminAccount(string $userId)
{
$admin = User::whereIn('role', ['akademik', 'pamong'])->findOrFail($userId);
$nama = $admin->name;
$admin->delete();
return redirect()->route('admin.users.admin_accounts')
->with('success', 'Akun ' . $nama . ' berhasil dihapus.');
}
}

View File

@ -1,356 +0,0 @@
<?php
// app/Http/Controllers/Api/ApiAbsensiKegiatanController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\AbsensiKegiatan;
use App\Models\Kegiatan;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class ApiAbsensiKegiatanController extends Controller
{
/**
* ==========================================
* 1. DASHBOARD HARI INI (Summary + Timeline)
* ==========================================
*/
public function today(Request $request)
{
try {
$user = $request->user();
$idSantri = $user->id_santri; // id_santri dari santri_accounts
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
$selectedDate = Carbon::parse($tanggal);
// Summary Hari Ini
$summary = AbsensiKegiatan::where('id_santri', $idSantri)
->whereDate('tanggal', $selectedDate)
->select(
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
DB::raw('SUM(CASE WHEN status = "Izin" THEN 1 ELSE 0 END) as izin'),
DB::raw('SUM(CASE WHEN status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
DB::raw('SUM(CASE WHEN status = "Alpa" THEN 1 ELSE 0 END) as alpa')
)
->first();
$percentage = $summary->total > 0
? round(($summary->hadir / $summary->total) * 100, 1)
: 0;
// Timeline Absensi Hari Ini
$timeline = AbsensiKegiatan::with(['kegiatan.kategori'])
->where('id_santri', $idSantri)
->whereDate('tanggal', $selectedDate)
->orderBy('waktu_absen')
->get()
->map(function($absensi) use ($selectedDate) {
$kegiatan = $absensi->kegiatan;
// Calculate punctuality (jika RFID)
$punctuality = null;
if ($absensi->metode_absen === 'RFID' && $absensi->status === 'Hadir') {
$waktuMulai = Carbon::parse($selectedDate->format('Y-m-d') . ' ' . $kegiatan->waktu_mulai);
$waktuAbsen = Carbon::parse($absensi->waktu_absen);
$diffMinutes = $waktuAbsen->diffInMinutes($waktuMulai, false);
if ($diffMinutes <= 0) {
$punctuality = 'Tepat Waktu';
} else {
$punctuality = 'Telat ' . abs($diffMinutes) . ' menit';
}
}
return [
'absensi_id' => $absensi->absensi_id,
'kegiatan_id' => $kegiatan->kegiatan_id,
'nama_kegiatan' => $kegiatan->nama_kegiatan,
'kategori' => [
'nama' => $kegiatan->kategori->nama_kategori,
'icon' => $kegiatan->kategori->icon ?? 'fa-calendar',
'warna' => $kegiatan->kategori->warna ?? '#6FBAA5',
],
'waktu_mulai' => date('H:i', strtotime($kegiatan->waktu_mulai)),
'waktu_selesai' => date('H:i', strtotime($kegiatan->waktu_selesai)),
'status' => $absensi->status,
'waktu_absen' => $absensi->waktu_absen ? date('H:i', strtotime($absensi->waktu_absen)) : null,
'metode_absen' => $absensi->metode_absen,
'punctuality' => $punctuality,
'keterangan' => $absensi->keterangan,
];
});
return response()->json([
'success' => true,
'data' => [
'tanggal' => $selectedDate->locale('id')->isoFormat('dddd, D MMMM YYYY'),
'tanggal_raw' => $selectedDate->format('Y-m-d'),
'summary' => [
'total' => $summary->total,
'hadir' => $summary->hadir,
'izin' => $summary->izin,
'sakit' => $summary->sakit,
'alpa' => $summary->alpa,
'percentage' => $percentage,
],
'timeline' => $timeline,
],
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
/**
* ==========================================
* 2. SUMMARY MINGGU INI
* ==========================================
*/
public function week(Request $request)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
$startDate = Carbon::now()->startOfWeek();
$endDate = Carbon::now()->endOfWeek();
// Summary
$summary = AbsensiKegiatan::where('id_santri', $idSantri)
->whereBetween('tanggal', [$startDate, $endDate])
->select(
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
DB::raw('SUM(CASE WHEN status = "Izin" THEN 1 ELSE 0 END) as izin'),
DB::raw('SUM(CASE WHEN status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
DB::raw('SUM(CASE WHEN status = "Alpa" THEN 1 ELSE 0 END) as alpa')
)
->first();
$percentage = $summary->total > 0
? round(($summary->hadir / $summary->total) * 100, 1)
: 0;
// Trend 7 hari
$trend = [];
for ($i = 0; $i < 7; $i++) {
$date = $startDate->copy()->addDays($i);
$dayData = AbsensiKegiatan::where('id_santri', $idSantri)
->whereDate('tanggal', $date)
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status = "Hadir" THEN 1 ELSE 0 END) as hadir')
->first();
$trend[] = [
'date' => $date->format('Y-m-d'),
'day_name' => $date->locale('id')->isoFormat('ddd'),
'percentage' => $dayData->total > 0
? round(($dayData->hadir / $dayData->total) * 100, 1)
: 0,
];
}
// Breakdown per kategori
$perKategori = AbsensiKegiatan::where('id_santri', $idSantri)
->whereBetween('tanggal', [$startDate, $endDate])
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
->join('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id')
->select(
'kategori_kegiatans.nama_kategori',
'kategori_kegiatans.warna',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Hadir" THEN 1 ELSE 0 END) as hadir')
)
->groupBy('kategori_kegiatans.kategori_id', 'kategori_kegiatans.nama_kategori', 'kategori_kegiatans.warna')
->get()
->map(function($item) {
return [
'nama_kategori' => $item->nama_kategori,
'warna' => $item->warna ?? '#6FBAA5',
'total' => $item->total,
'hadir' => $item->hadir,
'percentage' => $item->total > 0
? round(($item->hadir / $item->total) * 100, 1)
: 0,
];
});
return response()->json([
'success' => true,
'data' => [
'periode' => $startDate->locale('id')->isoFormat('D MMM') . ' - ' . $endDate->locale('id')->isoFormat('D MMM Y'),
'start_date' => $startDate->format('Y-m-d'),
'end_date' => $endDate->format('Y-m-d'),
'summary' => [
'total' => $summary->total,
'hadir' => $summary->hadir,
'izin' => $summary->izin,
'sakit' => $summary->sakit,
'alpa' => $summary->alpa,
'percentage' => $percentage,
],
'trend' => $trend,
'per_kategori' => $perKategori,
],
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
/**
* ==========================================
* 3. RIWAYAT BULAN (dengan Pagination)
* ==========================================
*/
public function month(Request $request)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
$bulan = $request->get('bulan', now()->format('Y-m'));
$date = Carbon::parse($bulan . '-01');
$startDate = $date->copy()->startOfMonth();
$endDate = $date->copy()->endOfMonth();
// Summary
$summary = AbsensiKegiatan::where('id_santri', $idSantri)
->whereBetween('tanggal', [$startDate, $endDate])
->select(
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
DB::raw('SUM(CASE WHEN status = "Izin" THEN 1 ELSE 0 END) as izin'),
DB::raw('SUM(CASE WHEN status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
DB::raw('SUM(CASE WHEN status = "Alpa" THEN 1 ELSE 0 END) as alpa')
)
->first();
$percentage = $summary->total > 0
? round(($summary->hadir / $summary->total) * 100, 1)
: 0;
// Riwayat per hari (grouped)
$riwayat = AbsensiKegiatan::with(['kegiatan.kategori'])
->where('id_santri', $idSantri)
->whereBetween('tanggal', [$startDate, $endDate])
->orderByDesc('tanggal')
->orderBy('waktu_absen')
->get()
->groupBy(function($item) {
return Carbon::parse($item->tanggal)->format('Y-m-d');
})
->map(function($items, $date) {
$hadir = $items->where('status', 'Hadir')->count();
$total = $items->count();
return [
'tanggal' => Carbon::parse($date)->locale('id')->isoFormat('dddd, D MMMM Y'),
'tanggal_raw' => $date,
'total' => $total,
'hadir' => $hadir,
'percentage' => $total > 0 ? round(($hadir / $total) * 100, 1) : 0,
'items' => $items->map(function($absensi) {
return [
'kegiatan' => $absensi->kegiatan->nama_kegiatan,
'kategori' => $absensi->kegiatan->kategori->nama_kategori,
'status' => $absensi->status,
'waktu_absen' => $absensi->waktu_absen ? date('H:i', strtotime($absensi->waktu_absen)) : null,
];
})->values(),
];
})
->values();
// Heatmap Calendar (30 hari)
$heatmap = $this->generateHeatmap($idSantri, $startDate, $endDate);
return response()->json([
'success' => true,
'data' => [
'periode' => $date->locale('id')->isoFormat('MMMM YYYY'),
'bulan_raw' => $date->format('Y-m'),
'summary' => [
'total' => $summary->total,
'hadir' => $summary->hadir,
'izin' => $summary->izin,
'sakit' => $summary->sakit,
'alpa' => $summary->alpa,
'percentage' => $percentage,
],
'heatmap' => $heatmap,
'riwayat' => $riwayat,
],
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
/**
* ==========================================
* HELPER: Generate Heatmap Data
* ==========================================
*/
private function generateHeatmap($idSantri, $startDate, $endDate)
{
$heatmap = [];
$current = $startDate->copy();
while ($current->lte($endDate)) {
$dayData = AbsensiKegiatan::where('id_santri', $idSantri)
->whereDate('tanggal', $current)
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status = "Hadir" THEN 1 ELSE 0 END) as hadir')
->first();
$percentage = $dayData->total > 0
? round(($dayData->hadir / $dayData->total) * 100, 1)
: 0;
$level = $this->getHeatmapLevel($percentage);
$heatmap[] = [
'date' => $current->format('Y-m-d'),
'day' => $current->format('j'),
'day_name' => $current->locale('id')->isoFormat('dd'),
'percentage' => $percentage,
'level' => $level,
'is_today' => $current->isToday(),
];
$current->addDay();
}
return $heatmap;
}
/**
* Get Heatmap Level (0-4)
*/
private function getHeatmapLevel($percentage)
{
if ($percentage >= 90) return 4; // Dark green
if ($percentage >= 80) return 3; // Green
if ($percentage >= 70) return 2; // Yellow
if ($percentage > 0) return 1; // Red
return 0; // No data
}
}

View File

@ -1,235 +0,0 @@
<?php
// app/Http/Controllers/Api/ApiAuthController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\SantriAccount;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class ApiAuthController extends Controller
{
/**
* Login Wali via Mobile (Sanctum token)
*
* Request:
* - username
* - password
*
* Response:
* - token
* - user (role, id_santri)
* - santri (data lengkap)
*/
public function login(Request $request)
{
$request->validate([
'id_santri' => 'required|string',
'password' => 'required|string',
]);
// -- Cari akun di santri_accounts --
$account = SantriAccount::where('username', $request->id_santri)->first();
if (!$account || !Hash::check($request->password, $account->password)) {
throw ValidationException::withMessages([
'id_santri' => ['ID Santri atau password salah.'],
]);
}
// -- Hapus token lama --
$account->tokens()->delete();
// -- Buat token baru --
$token = $account->createToken('mobile-app')->plainTextToken;
// -- Update last_login --
$account->update(['last_login' => now()]);
// -- Response data --
$responseData = [
'success' => true,
'message' => 'Login berhasil',
'token' => $token,
'user' => [
'name' => $account->santri->nama_lengkap ?? '-',
'role' => $account->role,
'role_id' => $account->id_santri,
],
];
// -- Sertakan data santri --
$santri = Santri::with(['kelasSantri.kelas.kelompok', 'kelasPrimary.kelas'])
->where('id_santri', $account->id_santri)
->select([
'id_santri',
'nis',
'nama_lengkap',
'jenis_kelamin',
'status',
'alamat_santri',
'daerah_asal',
'nama_orang_tua',
'nomor_hp_ortu',
'foto'
])
->first();
if ($santri) {
$kelasList = $this->buildKelasListGrouped($santri);
$kelasName = 'Belum Ada Kelas';
if ($santri->kelasPrimary && $santri->kelasPrimary->kelas) {
$kelasName = $santri->kelasPrimary->kelas->nama_kelas;
} elseif ($santri->kelasSantri->isNotEmpty() && $santri->kelasSantri->first()->kelas) {
$kelasName = $santri->kelasSantri->first()->kelas->nama_kelas;
}
$responseData['santri'] = [
'id_santri' => $santri->id_santri,
'nis' => $santri->nis,
'nama_lengkap' => $santri->nama_lengkap,
'jenis_kelamin' => $santri->jenis_kelamin,
'status' => $santri->status,
'alamat_santri' => $santri->alamat_santri,
'daerah_asal' => $santri->daerah_asal,
'nama_orang_tua' => $santri->nama_orang_tua,
'nomor_hp_ortu' => $santri->nomor_hp_ortu,
'foto' => $santri->foto,
'foto_url' => $santri->foto_url,
'kelas' => $kelasName,
'kelas_list' => $kelasList,
];
} else {
$responseData['santri'] = null;
}
return response()->json($responseData, 200);
}
/**
* Logout - Hapus token
*/
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'success' => true,
'message' => 'Logout berhasil',
], 200);
}
/**
* Get Profile Santri yang sedang login
*/
public function profile(Request $request)
{
$account = $request->user();
$santri = Santri::with(['kelasSantri.kelas.kelompok', 'kelasPrimary.kelas'])
->where('id_santri', $account->id_santri)
->select([
'id_santri',
'nis',
'nama_lengkap',
'jenis_kelamin',
'status',
'alamat_santri',
'daerah_asal',
'nama_orang_tua',
'nomor_hp_ortu',
'foto',
'created_at'
])
->first();
if (!$santri) {
return response()->json([
'success' => false,
'message' => 'Data santri tidak ditemukan.',
], 404);
}
$kelasList = $this->buildKelasListGrouped($santri);
$kelasName = 'Belum Ada Kelas';
if ($santri->kelasPrimary && $santri->kelasPrimary->kelas) {
$kelasName = $santri->kelasPrimary->kelas->nama_kelas;
} elseif ($santri->kelasSantri->isNotEmpty() && $santri->kelasSantri->first()->kelas) {
$kelasName = $santri->kelasSantri->first()->kelas->nama_kelas;
}
return response()->json([
'success' => true,
'data' => [
'id_santri' => $santri->id_santri,
'nis' => $santri->nis,
'nama_lengkap' => $santri->nama_lengkap,
'jenis_kelamin' => $santri->jenis_kelamin,
'status' => $santri->status,
'alamat_santri' => $santri->alamat_santri,
'daerah_asal' => $santri->daerah_asal,
'nama_orang_tua' => $santri->nama_orang_tua,
'nomor_hp_ortu' => $santri->nomor_hp_ortu,
'foto_url' => $santri->foto_url,
'bergabung_sejak' => $santri->created_at->format('d F Y'),
'kelas' => $kelasName,
'kelas_list' => $kelasList,
]
], 200);
}
/**
* Build kelas list grouped by kelompok
*
* @param \App\Models\Santri $santri
* @return array
*/
private function buildKelasListGrouped($santri)
{
$kelasList = [];
if ($santri->kelasSantri->isEmpty()) {
return $kelasList;
}
// Group kelas by kelompok
$grouped = $santri->kelasSantri->groupBy(function ($santriKelas) {
return $santriKelas->kelas?->kelompok?->id_kelompok ?? 'unknown';
});
foreach ($grouped as $kelompokId => $santriKelasItems) {
// Skip if kelompok not found
if ($kelompokId === 'unknown') {
continue;
}
$firstItem = $santriKelasItems->first();
$kelompok = $firstItem->kelas?->kelompok;
if (!$kelompok) {
continue;
}
$kelasList[] = [
'kelompok_id' => $kelompok->id_kelompok,
'kelompok_name' => $kelompok->nama_kelompok,
'kelas' => $santriKelasItems->map(function ($santriKelas) {
$kelas = $santriKelas->kelas;
return [
'id_kelas' => $kelas->id,
'kode_kelas' => $kelas->kode_kelas,
'nama_kelas' => $kelas->nama_kelas,
'is_primary' => $santriKelas->is_primary,
];
})->sortByDesc('is_primary')->values()->toArray(),
];
}
return $kelasList;
}
}

View File

@ -1,137 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Berita;
use App\Models\Santri;
use Illuminate\Http\Request;
class ApiBeritaController extends Controller
{
/**
* Get list berita untuk santri yang login
*/
public function index(Request $request)
{
try {
$idSantri = $request->user()->id_santri;
$santri = Santri::with('kelasPrimary.kelas')->where('id_santri', $idSantri)->first();
if (!$santri) {
return response()->json([
'success' => false,
'message' => 'Data santri tidak ditemukan',
], 404);
}
$idKelasSantri = $santri->kelasPrimary?->id_kelas;
$query = Berita::where('status', 'published')
->where(function($q) use ($idKelasSantri) {
$q->where('target_berita', 'semua');
if ($idKelasSantri) {
$q->orWhere(function($subQ) use ($idKelasSantri) {
$subQ->where('target_berita', 'kelas_tertentu')
->whereJsonContains('target_kelas', $idKelasSantri);
});
}
})
->select(['id', 'id_berita', 'judul', 'konten', 'penulis', 'gambar', 'target_berita', 'created_at'])
->orderBy('created_at', 'desc');
$berita = $query->paginate(10);
$data = $berita->map(function($item) {
return [
'id' => $item->id,
'id_berita' => $item->id_berita,
'judul' => $item->judul,
'konten' => $item->konten,
'penulis' => $item->penulis,
'gambar_url' => $item->gambar ? url('storage/' . $item->gambar) : null,
'target_berita' => $item->target_berita,
'tanggal' => $item->created_at->format('d M Y'),
'tanggal_lengkap' => $item->created_at->format('d F Y, H:i'),
];
});
return response()->json([
'success' => true,
'data' => $data,
'pagination' => [
'current_page' => $berita->currentPage(),
'last_page' => $berita->lastPage(),
'total' => $berita->total(),
]
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil berita: ' . $e->getMessage(),
], 500);
}
}
/**
* Get detail berita
*/
public function show(Request $request, $idBerita)
{
try {
$idSantri = $request->user()->id_santri;
$berita = Berita::where('id_berita', $idBerita)
->where('status', 'published')
->first();
if (!$berita) {
return response()->json([
'success' => false,
'message' => 'Berita tidak ditemukan',
], 404);
}
// Cek akses
$bolehAkses = false;
if ($berita->target_berita === 'semua') {
$bolehAkses = true;
} elseif ($berita->target_berita === 'kelas_tertentu') {
$santri = Santri::with('kelasPrimary')->where('id_santri', $idSantri)->first();
$idKelasSantri = $santri?->kelasPrimary?->id_kelas;
$bolehAkses = $idKelasSantri && in_array($idKelasSantri, $berita->target_kelas ?? []);
}
if (!$bolehAkses) {
return response()->json([
'success' => false,
'message' => 'Anda tidak memiliki akses ke berita ini',
], 403);
}
return response()->json([
'success' => true,
'data' => [
'id_berita' => $berita->id_berita,
'judul' => $berita->judul,
'konten' => $berita->konten,
'penulis' => $berita->penulis,
'gambar_url' => $berita->gambar ? url('storage/' . $berita->gambar) : null,
'target_berita' => $berita->target_berita,
'tanggal' => $berita->created_at->format('d M Y'),
'tanggal_lengkap' => $berita->created_at->format('d F Y, H:i'),
]
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil detail berita: ' . $e->getMessage(),
], 500);
}
}
}

View File

@ -1,793 +0,0 @@
<?php
// app/Http/Controllers/Api/ApiCapaianController.php
// UPDATED: Support sistem kelas baru (kelompok_kelas, kelas, santri_kelas)
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Capaian;
use App\Models\Santri;
use App\Models\SantriKelas;
use App\Models\Semester;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ApiCapaianController extends Controller
{
/**
* Helper: Build kelas info dari Santri model (sistem kelas baru)
* Returns kelas_primary & all_kelas arrays
*/
private function buildKelasInfo(Santri $santri): array
{
// Eager load relasi kelas jika belum loaded
if (!$santri->relationLoaded('kelasPrimary')) {
$santri->load('kelasPrimary.kelas.kelompok');
}
if (!$santri->relationLoaded('kelasSantri')) {
$santri->load('kelasSantri.kelas.kelompok');
}
// Kelas primary
$kelasPrimary = null;
$primaryRelation = $santri->kelasPrimary;
if ($primaryRelation && $primaryRelation->kelas) {
$kelas = $primaryRelation->kelas;
$kelompok = $kelas->kelompok;
$kelasPrimary = [
'id_kelas' => $kelas->id ?? null,
'kode_kelas' => $kelas->kode_kelas ?? null,
'nama_kelas' => $kelas->nama_kelas ?? 'Belum Ada Kelas',
'kelompok' => $kelompok ? $kelompok->nama_kelompok : null,
'id_kelompok' => $kelompok ? $kelompok->id_kelompok : null,
'tahun_ajaran' => $primaryRelation->tahun_ajaran ?? null,
'is_primary' => true,
];
}
// All kelas
$allKelas = $santri->kelasSantri
->filter(fn($sk) => $sk->kelas !== null)
->map(function ($sk) {
$kelas = $sk->kelas;
$kelompok = $kelas->kelompok;
return [
'id_kelas' => $kelas->id ?? null,
'kode_kelas' => $kelas->kode_kelas ?? null,
'nama_kelas' => $kelas->nama_kelas ?? '-',
'kelompok' => $kelompok ? $kelompok->nama_kelompok : null,
'id_kelompok' => $kelompok ? $kelompok->id_kelompok : null,
'tahun_ajaran' => $sk->tahun_ajaran ?? null,
'is_primary' => (bool) $sk->is_primary,
];
})->values()->toArray();
return [
'kelas_primary' => $kelasPrimary,
'all_kelas' => $allKelas,
];
}
/**
* Helper: Build santri info array with kelas baru
*/
private function buildSantriInfo(Santri $santri): array
{
$kelasData = $this->buildKelasInfo($santri);
return [
'id_santri' => $santri->id_santri,
'nama_lengkap' => $santri->nama_lengkap,
'kelas' => $santri->kelas_name, // backward compatible string
'kelas_primary' => $kelasData['kelas_primary'],
'all_kelas' => $kelasData['all_kelas'],
];
}
/**
* Helper: Get peer santri IDs yang sekelas (via santri_kelas pivot)
*/
private function getPeerSantriIds(Santri $santri, ?string $idSemester = null): array
{
$primaryKelasId = $santri->primary_kelas_id;
if (!$primaryKelasId) {
return [$santri->id_santri]; // hanya diri sendiri jika tidak punya kelas
}
return SantriKelas::where('id_kelas', $primaryKelasId)
->pluck('id_santri')
->unique()
->toArray();
}
/**
* GET OVERVIEW CAPAIAN SANTRI
* Endpoint: GET /api/v1/capaian/overview
*/
public function overview(Request $request)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
$santri = Santri::with(['kelasPrimary.kelas.kelompok', 'kelasSantri.kelas.kelompok'])
->where('id_santri', $idSantri)
->first();
if (!$santri) {
return response()->json([
'success' => false,
'message' => 'Data santri tidak ditemukan. ID: ' . $idSantri,
], 404);
}
$semesterAktif = Semester::aktif()->first();
$idSemester = $request->input('id_semester', $semesterAktif?->id_semester);
$query = Capaian::where('id_santri', $idSantri)
->with(['materi', 'semester']);
if ($idSemester) {
$query->where('id_semester', $idSemester);
}
$capaians = $query->get();
$capaiansBerisi = $capaians->where('persentase', '>', 0);
$totalMateri = $capaiansBerisi->count();
$rataRataProgress = $capaiansBerisi->isEmpty() ? 0 : $capaiansBerisi->avg('persentase');
$materiSelesai = $capaians->where('persentase', '>=', 100)->count();
$perKategori = [];
$kategoriList = ['Al-Qur\'an', 'Hadist', 'Materi Tambahan'];
foreach ($kategoriList as $kategori) {
$capaianKategori = $capaians->filter(function($c) use ($kategori) {
return $c->materi && $c->materi->kategori === $kategori;
});
$capaianKategoriBerisi = $capaianKategori->where('persentase', '>', 0);
$perKategori[] = [
'kategori' => $kategori,
'icon' => $this->getKategoriIcon($kategori),
'color' => $this->getKategoriColor($kategori),
'total_materi' => $capaianKategoriBerisi->count(),
'rata_rata_progress' => round($capaianKategoriBerisi->isEmpty() ? 0 : $capaianKategoriBerisi->avg('persentase'), 1),
'materi_selesai' => $capaianKategori->where('persentase', '>=', 100)->count(),
];
}
$semesters = Semester::select('id_semester', 'nama_semester', 'tahun_ajaran', 'periode', 'is_active')
->orderBy('tahun_ajaran', 'desc')
->orderBy('periode', 'desc')
->get()
->map(function($s) {
return [
'id_semester' => $s->id_semester,
'nama_semester' => $s->nama_semester,
'is_aktif' => $s->is_active == 1,
];
});
$response = [
'success' => true,
'data' => [
'santri' => $this->buildSantriInfo($santri),
'semester' => [
'id_semester' => $idSemester,
'nama_semester' => $semesterAktif?->nama_semester ?? 'Semua Semester',
'list_semester' => $semesters,
],
'statistik_umum' => [
'total_materi' => $totalMateri,
'rata_rata_progress' => round($rataRataProgress, 1),
'materi_selesai' => $materiSelesai,
],
'per_kategori' => $perKategori,
],
];
return response()->json($response, 200);
} catch (\Exception $e) {
Log::error('Error di Capaian Overview', [
'message' => $e->getMessage(),
'line' => $e->getLine(),
'file' => $e->getFile(),
]);
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* GET LIST MATERI PER KATEGORI
* Endpoint: GET /api/v1/capaian/kategori/{kategori}
*/
public function listMateriByKategori(Request $request, $kategori)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
$validKategori = ['Al-Qur\'an', 'Hadist', 'Materi Tambahan'];
if (!in_array($kategori, $validKategori)) {
return response()->json([
'success' => false,
'message' => 'Kategori tidak valid: ' . $kategori,
], 400);
}
$santri = Santri::with(['kelasPrimary.kelas.kelompok'])->where('id_santri', $idSantri)->first();
if (!$santri) {
return response()->json(['success' => false, 'message' => 'Data santri tidak ditemukan'], 404);
}
$semesterAktif = Semester::aktif()->first();
$idSemester = $request->input('id_semester', $semesterAktif?->id_semester);
$query = Capaian::where('id_santri', $idSantri)
->whereHas('materi', function($q) use ($kategori) {
$q->where('kategori', $kategori);
})
->with(['materi', 'semester']);
if ($idSemester) {
$query->where('id_semester', $idSemester);
}
$capaians = $query->get();
$materiList = $capaians->map(function($capaian) {
return [
'id_capaian' => $capaian->id_capaian,
'materi' => [
'id_materi' => $capaian->materi->id_materi,
'nama_kitab' => $capaian->materi->nama_kitab,
'total_halaman' => $capaian->materi->total_halaman,
'halaman_mulai' => $capaian->materi->halaman_mulai,
'halaman_akhir' => $capaian->materi->halaman_akhir,
],
'progress' => [
'halaman_selesai' => $capaian->jumlah_halaman_selesai,
'persentase' => round($capaian->persentase, 1),
'status' => $this->getStatusCapaian($capaian->persentase),
'status_color' => $this->getStatusColor($capaian->persentase),
],
'tanggal_input' => $capaian->tanggal_input->format('d M Y'),
];
});
return response()->json([
'success' => true,
'data' => [
'kategori' => $kategori,
'icon' => $this->getKategoriIcon($kategori),
'color' => $this->getKategoriColor($kategori),
'total_materi' => $materiList->count(),
'materi_list' => $materiList,
],
], 200);
} catch (\Exception $e) {
Log::error('Error di List Materi by Kategori', [
'message' => $e->getMessage(),
'kategori' => $kategori,
]);
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* GET DETAIL CAPAIAN PER MATERI
* Endpoint: GET /api/v1/capaian/detail/{idCapaian}
* Now includes kelas_primary in santri info
*/
public function detailCapaian(Request $request, $idCapaian)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
$capaian = Capaian::where('id_capaian', $idCapaian)
->where('id_santri', $idSantri)
->with(['materi', 'semester', 'santri.kelasPrimary.kelas.kelompok'])
->first();
if (!$capaian) {
return response()->json([
'success' => false,
'message' => 'Data capaian tidak ditemukan',
], 404);
}
$halamanArray = $capaian->pages_array;
$breakdown = [
'halaman_selesai_list' => $halamanArray,
'jumlah_halaman_selesai' => count($halamanArray),
'halaman_belum_selesai' => $capaian->materi->total_halaman - count($halamanArray),
'halaman_selesai_text' => $capaian->halaman_selesai,
];
// Build kelas_primary info
$kelasPrimary = null;
if ($capaian->santri) {
$kelasData = $this->buildKelasInfo($capaian->santri);
$kelasPrimary = $kelasData['kelas_primary'];
}
return response()->json([
'success' => true,
'data' => [
'id_capaian' => $capaian->id_capaian,
'santri_info' => $capaian->santri ? $this->buildSantriInfo($capaian->santri) : null,
'materi' => [
'id_materi' => $capaian->materi->id_materi,
'kategori' => $capaian->materi->kategori,
'nama_kitab' => $capaian->materi->nama_kitab,
'kelas' => $capaian->materi->kelas,
'total_halaman' => $capaian->materi->total_halaman,
'halaman_mulai' => $capaian->materi->halaman_mulai,
'halaman_akhir' => $capaian->materi->halaman_akhir,
'deskripsi' => $capaian->materi->deskripsi,
],
'semester' => [
'id_semester' => $capaian->semester->id_semester,
'nama_semester' => $capaian->semester->nama_semester,
],
'progress' => [
'persentase' => round($capaian->persentase, 1),
'status' => $this->getStatusCapaian($capaian->persentase),
'status_color' => $this->getStatusColor($capaian->persentase),
],
'breakdown' => $breakdown,
'catatan' => $capaian->catatan,
'tanggal_input' => $capaian->tanggal_input->format('d F Y'),
'last_updated' => $capaian->updated_at->diffForHumans(),
],
], 200);
} catch (\Exception $e) {
Log::error('Error di Detail Capaian', [
'message' => $e->getMessage(),
'id_capaian' => $idCapaian,
]);
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* GET GRAFIK PROGRESS HISTORIS
* Endpoint: GET /api/v1/capaian/grafik-progress
*/
public function grafikProgress(Request $request)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
$semesters = Semester::orderBy('tahun_ajaran')
->orderBy('periode')
->get();
$dataGrafik = [];
foreach ($semesters as $semester) {
$capaians = Capaian::where('id_santri', $idSantri)
->where('id_semester', $semester->id_semester)
->where('persentase', '>', 0)
->get();
if ($capaians->count() > 0) {
$dataGrafik[] = [
'semester' => $semester->nama_semester,
'id_semester' => $semester->id_semester,
'rata_rata_progress' => round($capaians->avg('persentase'), 1),
'total_materi' => $capaians->count(),
'materi_selesai' => $capaians->where('persentase', '>=', 100)->count(),
];
}
}
return response()->json([
'success' => true,
'data' => $dataGrafik,
], 200);
} catch (\Exception $e) {
Log::error('Error di Grafik Progress', ['message' => $e->getMessage()]);
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* GET TREND SEMESTER
* Endpoint: GET /api/v1/capaian/trend-semester
* Returns progress per semester for line chart visualization
*/
public function trendSemester(Request $request)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
$santri = Santri::with(['kelasPrimary.kelas.kelompok'])->where('id_santri', $idSantri)->first();
if (!$santri) {
return response()->json(['success' => false, 'message' => 'Data santri tidak ditemukan'], 404);
}
// Load all capaian grouped by semester
$allCapaian = Capaian::where('id_santri', $idSantri)
->with(['materi', 'semester'])
->where('persentase', '>', 0)
->get();
$semesters = Semester::orderBy('tahun_ajaran')->orderBy('periode')->get();
$kategoriList = ['Al-Qur\'an', 'Hadist', 'Materi Tambahan'];
$trendData = [];
foreach ($semesters as $sem) {
$semCapaian = $allCapaian->where('id_semester', $sem->id_semester);
if ($semCapaian->isEmpty()) continue;
$perKat = [];
foreach ($kategoriList as $kat) {
$katCapaian = $semCapaian->filter(fn($c) => $c->materi && $c->materi->kategori === $kat);
if ($katCapaian->isNotEmpty()) {
$perKat[] = [
'kategori' => $kat,
'rata_rata' => round($katCapaian->avg('persentase'), 1),
'total_materi' => $katCapaian->count(),
'materi_selesai' => $katCapaian->where('persentase', '>=', 100)->count(),
];
}
}
$trendData[] = [
'id_semester' => $sem->id_semester,
'nama_semester' => $sem->nama_semester,
'tahun_ajaran' => $sem->tahun_ajaran,
'rata_rata_progress' => round($semCapaian->avg('persentase'), 1),
'total_materi' => $semCapaian->count(),
'materi_selesai' => $semCapaian->where('persentase', '>=', 100)->count(),
'per_kategori' => $perKat,
'is_aktif' => $sem->is_active == 1,
];
}
return response()->json([
'success' => true,
'data' => [
'santri' => $this->buildSantriInfo($santri),
'trend' => $trendData,
],
], 200);
} catch (\Exception $e) {
Log::error('Error di Trend Semester', ['message' => $e->getMessage()]);
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* GET DASHBOARD CAPAIAN (COMPREHENSIVE)
* Endpoint: GET /api/v1/capaian/dashboard
* Single endpoint returning all data for enhanced mobile capaian page
* UPDATED: Uses new kelas system (santri_kelas pivot table)
*/
public function dashboard(Request $request)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
$santri = Santri::with(['kelasPrimary.kelas.kelompok', 'kelasSantri.kelas.kelompok'])
->where('id_santri', $idSantri)
->first();
if (!$santri) {
return response()->json(['success' => false, 'message' => 'Data santri tidak ditemukan'], 404);
}
$semesterAktif = Semester::aktif()->first();
$idSemester = $request->input('id_semester', $semesterAktif?->id_semester);
$selectedSemester = $idSemester
? Semester::where('id_semester', $idSemester)->first()
: $semesterAktif;
$allSemesters = Semester::orderBy('tahun_ajaran')->orderBy('periode')->get();
$listSemester = $allSemesters->map(fn($s) => [
'id_semester' => $s->id_semester,
'nama_semester' => $s->nama_semester,
'tahun_ajaran' => $s->tahun_ajaran,
'periode' => $s->periode,
'is_aktif' => $s->is_active == 1,
])->values();
// ===== Load ALL capaian santri in one query =====
$allCapaianSantri = Capaian::where('id_santri', $idSantri)
->with(['materi', 'semester'])
->get();
// Current semester capaians
$currentCapaians = $allCapaianSantri->where('id_semester', $idSemester);
$currentBerisi = $currentCapaians->where('persentase', '>', 0);
$totalProgress = $currentBerisi->isEmpty() ? 0 : round($currentBerisi->avg('persentase'), 1);
$materiSelesaiSemIni = $currentCapaians->where('persentase', '>=', 100)->count();
// ===== Per Kategori =====
$kategoriList = ['Al-Qur\'an', 'Hadist', 'Materi Tambahan'];
$perKategori = [];
foreach ($kategoriList as $kategori) {
$capKat = $currentCapaians->filter(fn($c) => $c->materi && $c->materi->kategori === $kategori);
$capKatBerisi = $capKat->where('persentase', '>', 0);
$perKategori[] = [
'kategori' => $kategori,
'icon' => $this->getKategoriIcon($kategori),
'color' => $this->getKategoriColor($kategori),
'total_materi' => $capKatBerisi->count(),
'rata_rata_progress' => round($capKatBerisi->isEmpty() ? 0 : $capKatBerisi->avg('persentase'), 1),
'materi_selesai' => $capKat->where('persentase', '>=', 100)->count(),
];
}
// ===== Semester History =====
$bySemester = $allCapaianSantri->where('persentase', '>', 0)->groupBy('id_semester');
$semesterHistory = [];
foreach ($allSemesters as $sem) {
if ($bySemester->has($sem->id_semester)) {
$semCaps = $bySemester[$sem->id_semester];
$semesterHistory[] = [
'id_semester' => $sem->id_semester,
'nama_semester' => $sem->nama_semester,
'rata_rata_progress' => round($semCaps->avg('persentase'), 1),
'total_materi' => $semCaps->count(),
'materi_selesai' => $semCaps->where('persentase', '>=', 100)->count(),
'is_current' => $sem->id_semester === $idSemester,
];
}
}
// ===== Achievements =====
$achievements = [];
if ($materiSelesaiSemIni > 0) {
$achievements[] = ['icon' => 'trophy', 'text' => "Khatam $materiSelesaiSemIni Materi Semester Ini", 'type' => 'khatam'];
}
$currentIdx = -1;
for ($i = 0; $i < count($semesterHistory); $i++) {
if ($semesterHistory[$i]['id_semester'] === $idSemester) {
$currentIdx = $i;
break;
}
}
if ($currentIdx > 0) {
$prevProgress = $semesterHistory[$currentIdx - 1]['rata_rata_progress'];
$curProgress = $semesterHistory[$currentIdx]['rata_rata_progress'];
$change = round($curProgress - $prevProgress, 1);
if ($change > 0) {
$achievements[] = ['icon' => 'trending_up', 'text' => "Kenaikan {$change}% dari Semester Lalu", 'type' => 'growth'];
} elseif ($change < 0) {
$achievements[] = ['icon' => 'trending_down', 'text' => "Penurunan " . abs($change) . "% dari Semester Lalu", 'type' => 'decline'];
}
}
// ===== Ranking & Peer Comparison (NEW: via santri_kelas pivot) =====
$peerSantriIds = $this->getPeerSantriIds($santri, $idSemester);
$rankings = collect();
if ($idSemester && count($peerSantriIds) > 1) {
$rankings = Capaian::whereIn('id_santri', $peerSantriIds)
->where('id_semester', $idSemester)
->where('persentase', '>', 0)
->select('id_santri', DB::raw('AVG(persentase) as avg_progress'))
->groupBy('id_santri')
->orderByDesc('avg_progress')
->get();
}
$rank = 0;
$totalRanked = $rankings->count();
foreach ($rankings as $i => $r) {
if ($r->id_santri === $idSantri) {
$rank = $i + 1;
break;
}
}
if ($rank > 0 && $totalRanked > 1) {
$achievements[] = [
'icon' => $rank <= 3 ? 'star' : 'emoji_events',
'text' => "Peringkat $rank dari $totalRanked di Kelas",
'type' => 'rank',
];
}
// Peer comparison per kategori (NEW: via santri_kelas pivot)
$peerComparison = [];
if ($idSemester && count($peerSantriIds) > 1) {
$peerData = Capaian::whereIn('id_santri', $peerSantriIds)
->join('materi', 'capaian.id_materi', '=', 'materi.id_materi')
->where('capaian.id_semester', $idSemester)
->where('capaian.persentase', '>', 0)
->groupBy('materi.kategori')
->select('materi.kategori', DB::raw('AVG(capaian.persentase) as kelas_avg'))
->get()
->keyBy('kategori');
foreach ($kategoriList as $kategori) {
$santriKatBerisi = $currentCapaians->filter(fn($c) => $c->materi && $c->materi->kategori === $kategori && $c->persentase > 0);
$santriAvg = $santriKatBerisi->isEmpty() ? 0 : round($santriKatBerisi->avg('persentase'), 1);
$kelasAvg = isset($peerData[$kategori]) ? round($peerData[$kategori]->kelas_avg, 1) : 0;
$peerComparison[] = [
'kategori' => $kategori,
'icon' => $this->getKategoriIcon($kategori),
'color' => $this->getKategoriColor($kategori),
'santri_progress' => $santriAvg,
'kelas_avg' => $kelasAvg,
];
}
} else {
// No peers, just show santri data
foreach ($kategoriList as $kategori) {
$santriKatBerisi = $currentCapaians->filter(fn($c) => $c->materi && $c->materi->kategori === $kategori && $c->persentase > 0);
$santriAvg = $santriKatBerisi->isEmpty() ? 0 : round($santriKatBerisi->avg('persentase'), 1);
$peerComparison[] = [
'kategori' => $kategori,
'icon' => $this->getKategoriIcon($kategori),
'color' => $this->getKategoriColor($kategori),
'santri_progress' => $santriAvg,
'kelas_avg' => 0,
];
}
}
// ===== Materi Status =====
$materiStatus = $currentCapaians->map(function ($c) {
$status = 'belum_mulai';
if ($c->persentase >= 100) $status = 'selesai';
elseif ($c->persentase > 0) $status = 'progres';
return [
'id_capaian' => $c->id_capaian,
'nama_kitab' => $c->materi->nama_kitab ?? '-',
'kategori' => $c->materi->kategori ?? '-',
'persentase' => round($c->persentase, 1),
'status' => $status,
'status_label' => $this->getStatusCapaian($c->persentase),
'status_color' => $this->getStatusColor($c->persentase),
'icon' => $this->getKategoriIcon($c->materi->kategori ?? ''),
'color' => $this->getKategoriColor($c->materi->kategori ?? ''),
];
})->sortByDesc('persentase')->values();
// ===== Rapor Summary =====
$raporSummary = [
'total_progress' => $totalProgress,
'total_materi' => $currentBerisi->count(),
'materi_selesai' => $materiSelesaiSemIni,
'perubahan' => 0,
'trend' => 'tetap',
'predikat' => $this->getPredikat($totalProgress),
];
if ($currentIdx > 0) {
$prevProg = $semesterHistory[$currentIdx - 1]['rata_rata_progress'];
$curProg = $semesterHistory[$currentIdx]['rata_rata_progress'];
$raporSummary['perubahan'] = round($curProg - $prevProg, 1);
$raporSummary['trend'] = $curProg > $prevProg ? 'naik' : ($curProg < $prevProg ? 'turun' : 'tetap');
}
return response()->json([
'success' => true,
'data' => [
'role' => $user->role,
'santri' => $this->buildSantriInfo($santri),
'semester' => [
'id_semester' => $selectedSemester?->id_semester,
'nama_semester' => $selectedSemester?->nama_semester ?? 'Tidak Diketahui',
'tahun_ajaran' => $selectedSemester?->tahun_ajaran,
],
'list_semester' => $listSemester,
'current_progress' => [
'total_progress' => $totalProgress,
'total_materi' => $currentBerisi->count(),
'materi_selesai' => $materiSelesaiSemIni,
'per_kategori' => $perKategori,
],
'semester_history' => array_values($semesterHistory),
'achievements' => $achievements,
'materi_status' => $materiStatus,
'peer_comparison' => $peerComparison,
'rapor_summary' => $raporSummary,
'rank' => $rank > 0 ? ['position' => $rank, 'total' => $totalRanked] : null,
],
], 200);
} catch (\Exception $e) {
Log::error('Error di Capaian Dashboard', [
'message' => $e->getMessage(),
'line' => $e->getLine(),
'file' => $e->getFile(),
]);
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
// ==================== HELPER METHODS ====================
private function getPredikat($progress)
{
if ($progress >= 90) return 'Baik Sekali';
if ($progress >= 75) return 'Baik';
if ($progress >= 50) return 'Cukup';
return 'Perlu Perhatian';
}
private function getKategoriIcon($kategori)
{
$icons = [
'Al-Qur\'an' => 'book_quran',
'Hadist' => 'scroll',
'Materi Tambahan' => 'book',
];
return $icons[$kategori] ?? 'book';
}
private function getKategoriColor($kategori)
{
$colors = [
'Al-Qur\'an' => '#6FBAA5',
'Hadist' => '#81C6E8',
'Materi Tambahan' => '#FFD56B',
];
return $colors[$kategori] ?? '#6B7280';
}
private function getStatusCapaian($persentase)
{
if ($persentase >= 100) return 'Selesai';
if ($persentase >= 75) return 'Hampir Selesai';
if ($persentase >= 50) return 'Dalam Progress';
if ($persentase >= 25) return 'Baru Mulai';
if ($persentase > 0) return 'Mulai';
return 'Belum Mulai';
}
private function getStatusColor($persentase)
{
if ($persentase >= 100) return '#10B981';
if ($persentase >= 75) return '#3B82F6';
if ($persentase >= 50) return '#F59E0B';
if ($persentase >= 25) return '#EF4444';
return '#6B7280';
}
}

View File

@ -1,325 +0,0 @@
<?php
// app/Http/Controllers/Api/ApiKepulanganController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Kepulangan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class ApiKepulanganController extends Controller
{
/**
* Get list kepulangan santri (untuk wali santri)
* GET /api/v1/kepulangan
*/
public function index(Request $request)
{
try {
$user = Auth::user();
// Ambil id_santri dari akun yang login
$idSantri = $user->id_santri;
if (!$idSantri) {
return response()->json([
'success' => false,
'message' => 'Data santri tidak ditemukan.',
], 404);
}
// Build query dengan pagination
$page = $request->input('page', 1);
$perPage = 15;
$query = Kepulangan::with('santri')
->where('id_santri', $idSantri);
// Filter berdasarkan status (optional)
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Filter berdasarkan tahun (optional)
if ($request->filled('tahun')) {
$query->whereYear('tanggal_pulang', $request->tahun);
}
// Order by terbaru
$query->orderBy('created_at', 'desc');
// Get data dengan pagination
$kepulangan = $query->paginate($perPage, ['*'], 'page', $page);
// Get info kuota santri
$kuotaInfo = Kepulangan::getSisaKuotaSantri($idSantri);
$settings = Kepulangan::getSettings();
// Format response
$data = [
'success' => true,
'message' => 'Data kepulangan berhasil diambil.',
'data' => [
'kepulangan' => collect($kepulangan->items())->map(function($item) {
return [
'id_kepulangan' => $item->id_kepulangan,
'tanggal_izin' => $item->tanggal_izin->format('Y-m-d'),
'tanggal_izin_formatted' => $item->tanggal_izin->format('d M Y'),
'tanggal_pulang' => $item->tanggal_pulang->format('Y-m-d'),
'tanggal_pulang_formatted' => $item->tanggal_pulang->format('d M Y'),
'tanggal_kembali' => $item->tanggal_kembali->format('Y-m-d'),
'tanggal_kembali_formatted' => $item->tanggal_kembali->format('d M Y'),
'durasi_izin' => $item->durasi_izin,
'alasan' => $item->alasan,
'status' => $item->status,
'catatan' => $item->catatan,
'approved_at' => $item->approved_at ? $item->approved_at->format('Y-m-d H:i:s') : null,
'approved_at_formatted' => $item->approved_at ? $item->approved_at->format('d M Y H:i') : null,
'is_aktif' => $item->is_aktif,
'is_terlambat' => $item->is_terlambat,
];
}),
'kuota' => [
'kuota_maksimal' => $kuotaInfo['kuota_maksimal'],
'total_terpakai' => $kuotaInfo['total_terpakai'],
'sisa_kuota' => $kuotaInfo['sisa_kuota'],
'persentase' => $kuotaInfo['persentase'],
'status' => $kuotaInfo['status'], // aman, hampir_habis, melebihi
'badge_color' => $kuotaInfo['badge_color'], // success, warning, danger
'periode_mulai' => $settings->periode_mulai,
'periode_akhir' => $settings->periode_akhir,
],
'pagination' => [
'current_page' => $kepulangan->currentPage(),
'last_page' => $kepulangan->lastPage(),
'per_page' => $kepulangan->perPage(),
'total' => $kepulangan->total(),
'from' => $kepulangan->firstItem(),
'to' => $kepulangan->lastItem(),
],
],
];
return response()->json($data, 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* Get detail kepulangan
* GET /api/v1/kepulangan/{id_kepulangan}
*/
public function show($idKepulangan)
{
try {
$user = Auth::user();
$idSantri = $user->id_santri;
// Get kepulangan dengan validasi kepemilikan
$kepulangan = Kepulangan::with('santri')
->where('id_kepulangan', $idKepulangan)
->where('id_santri', $idSantri) // Pastikan milik santri yang login
->first();
if (!$kepulangan) {
return response()->json([
'success' => false,
'message' => 'Data kepulangan tidak ditemukan.',
], 404);
}
// Get info kuota
$kuotaInfo = Kepulangan::getSisaKuotaSantri($idSantri);
$settings = Kepulangan::getSettings();
$data = [
'success' => true,
'message' => 'Detail kepulangan berhasil diambil.',
'data' => [
'kepulangan' => [
'id_kepulangan' => $kepulangan->id_kepulangan,
'tanggal_izin' => $kepulangan->tanggal_izin->format('Y-m-d'),
'tanggal_izin_formatted' => $kepulangan->tanggal_izin->format('d M Y'),
'tanggal_pulang' => $kepulangan->tanggal_pulang->format('Y-m-d'),
'tanggal_pulang_formatted' => $kepulangan->tanggal_pulang->format('d M Y'),
'tanggal_kembali' => $kepulangan->tanggal_kembali->format('Y-m-d'),
'tanggal_kembali_formatted' => $kepulangan->tanggal_kembali->format('d M Y'),
'durasi_izin' => $kepulangan->durasi_izin,
'alasan' => $kepulangan->alasan,
'status' => $kepulangan->status,
'catatan' => $kepulangan->catatan,
'approved_at' => $kepulangan->approved_at ? $kepulangan->approved_at->format('Y-m-d H:i:s') : null,
'approved_at_formatted' => $kepulangan->approved_at ? $kepulangan->approved_at->format('d M Y H:i') : null,
'is_aktif' => $kepulangan->is_aktif,
'is_terlambat' => $kepulangan->is_terlambat,
'santri' => [
'nama_lengkap' => $kepulangan->santri->nama_lengkap,
'nis' => $kepulangan->santri->nis,
],
],
'kuota' => [
'kuota_maksimal' => $kuotaInfo['kuota_maksimal'],
'total_terpakai' => $kuotaInfo['total_terpakai'],
'sisa_kuota' => $kuotaInfo['sisa_kuota'],
'persentase' => $kuotaInfo['persentase'],
'status' => $kuotaInfo['status'],
'badge_color' => $kuotaInfo['badge_color'],
'periode_mulai' => $settings->periode_mulai,
'periode_akhir' => $settings->periode_akhir,
],
],
];
return response()->json($data, 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* Get info kuota santri
* GET /api/v1/kepulangan/kuota
*/
public function kuota(Request $request)
{
try {
$user = Auth::user();
if (!in_array($user->role, ['santri', 'wali'])) {
return response()->json([
'success' => false,
'message' => 'Akses ditolak.',
], 403);
}
$idSantri = $user->id_santri;
if (!$idSantri) {
return response()->json([
'success' => false,
'message' => 'Data santri tidak ditemukan.',
], 404);
}
$kuotaInfo = Kepulangan::getSisaKuotaSantri($idSantri);
$settings = Kepulangan::getSettings();
// Get detail izin dalam periode aktif
$detailIzin = Kepulangan::where('id_santri', $idSantri)
->whereIn('status', ['Disetujui', 'Selesai'])
->whereBetween('tanggal_pulang', [$settings->periode_mulai, $settings->periode_akhir])
->orderBy('tanggal_pulang', 'desc')
->get()
->map(function($item) {
return [
'id_kepulangan' => $item->id_kepulangan,
'tanggal_pulang' => $item->tanggal_pulang->format('Y-m-d'),
'tanggal_pulang_formatted' => $item->tanggal_pulang->format('d M Y'),
'tanggal_kembali' => $item->tanggal_kembali->format('Y-m-d'),
'tanggal_kembali_formatted' => $item->tanggal_kembali->format('d M Y'),
'durasi_izin' => $item->durasi_izin,
'status' => $item->status,
];
});
$data = [
'success' => true,
'message' => 'Info kuota berhasil diambil.',
'data' => [
'kuota_maksimal' => $kuotaInfo['kuota_maksimal'],
'total_terpakai' => $kuotaInfo['total_terpakai'],
'sisa_kuota' => $kuotaInfo['sisa_kuota'],
'persentase' => $kuotaInfo['persentase'],
'status' => $kuotaInfo['status'],
'badge_color' => $kuotaInfo['badge_color'],
'periode_mulai' => $settings->periode_mulai,
'periode_akhir' => $settings->periode_akhir,
'detail_izin' => $detailIzin,
],
];
return response()->json($data, 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* Notifikasi status kepulangan santri saat ini
* GET /api/v1/kepulangan/notifikasi
*/
public function notifikasiKepulangan(Request $request)
{
try {
$user = Auth::user();
if (!in_array($user->role, ['santri', 'wali'])) {
return response()->json([
'success' => false,
'message' => 'Akses ditolak.',
], 403);
}
$idSantri = $user->id_santri;
$today = Carbon::today();
// Cari kepulangan yang sedang aktif (tanggal hari ini ada di antara tanggal_pulang dan tanggal_kembali)
$kepulangan = Kepulangan::where('id_santri', $idSantri)
->where('status', 'Disetujui')
->where('tanggal_pulang', '<=', $today)
->where('tanggal_kembali', '>=', $today)
->orderBy('tanggal_kembali', 'desc')
->first();
if (!$kepulangan) {
return response()->json([
'success' => true,
'data' => [
'sedang_pulang' => false,
'tanggal_kembali' => null,
'sisa_hari' => 0,
'status' => null,
],
]);
}
$tanggalKembali = Carbon::parse($kepulangan->tanggal_kembali);
$sisaHari = $today->diffInDays($tanggalKembali, false); // negatif jika sudah lewat
$statusKepulangan = $sisaHari < 0 ? 'terlambat' : 'aktif';
return response()->json([
'success' => true,
'data' => [
'sedang_pulang' => true,
'tanggal_kembali' => $tanggalKembali->format('Y-m-d'),
'tanggal_kembali_formatted' => $tanggalKembali->locale('id')->isoFormat('D MMMM Y'),
'sisa_hari' => (int) $sisaHari,
'status' => $statusKepulangan,
'alasan' => $kepulangan->alasan,
],
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
}

View File

@ -1,184 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\KesehatanSantri;
use App\Models\Santri;
use Illuminate\Http\Request;
class ApiKesehatanController extends Controller
{
/**
* Get riwayat kesehatan santri yang login
*/
public function index(Request $request)
{
try {
// Ambil id_santri dari user yang login (wali)
$idSantri = $request->user()->id_santri;
// Cek santri exist
$santri = Santri::where('id_santri', $idSantri)->first();
if (!$santri) {
return response()->json([
'success' => false,
'message' => 'Data santri tidak ditemukan',
], 404);
}
// Query riwayat kesehatan
$query = KesehatanSantri::where('id_santri', $idSantri)
->select([
'id',
'id_kesehatan',
'id_santri',
'tanggal_masuk',
'tanggal_keluar',
'keluhan',
'catatan',
'status',
'created_at'
])
->orderBy('tanggal_masuk', 'desc');
// Filter status (optional)
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Pagination
$kesehatan = $query->paginate(20);
// Format data
$data = $kesehatan->map(function($item) {
return [
'id' => $item->id,
'id_kesehatan' => $item->id_kesehatan,
'tanggal_masuk' => $item->tanggal_masuk->format('Y-m-d'),
'tanggal_masuk_formatted' => $item->tanggal_masuk->format('d M Y'),
'tanggal_keluar' => $item->tanggal_keluar ? $item->tanggal_keluar->format('Y-m-d') : null,
'tanggal_keluar_formatted' => $item->tanggal_keluar ? $item->tanggal_keluar->format('d M Y') : null,
'keluhan' => $item->keluhan,
'catatan' => $item->catatan,
'status' => $item->status,
'lama_dirawat' => $item->lama_dirawat . ' hari',
];
});
return response()->json([
'success' => true,
'data' => $data,
'pagination' => [
'current_page' => $kesehatan->currentPage(),
'last_page' => $kesehatan->lastPage(),
'total' => $kesehatan->total(),
]
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil riwayat kesehatan: ' . $e->getMessage(),
], 500);
}
}
/**
* Get detail kesehatan
*/
public function show(Request $request, $idKesehatan)
{
try {
$idSantri = $request->user()->id_santri;
// Cari data kesehatan
$kesehatan = KesehatanSantri::where('id_kesehatan', $idKesehatan)
->where('id_santri', $idSantri) // Pastikan milik santri yang login
->first();
if (!$kesehatan) {
return response()->json([
'success' => false,
'message' => 'Data kesehatan tidak ditemukan',
], 404);
}
return response()->json([
'success' => true,
'data' => [
'id_kesehatan' => $kesehatan->id_kesehatan,
'tanggal_masuk' => $kesehatan->tanggal_masuk->format('Y-m-d'),
'tanggal_masuk_formatted' => $kesehatan->tanggal_masuk->format('d F Y'),
'tanggal_keluar' => $kesehatan->tanggal_keluar ? $kesehatan->tanggal_keluar->format('Y-m-d') : null,
'tanggal_keluar_formatted' => $kesehatan->tanggal_keluar ? $kesehatan->tanggal_keluar->format('d F Y') : null,
'keluhan' => $kesehatan->keluhan,
'catatan' => $kesehatan->catatan,
'status' => $kesehatan->status,
'lama_dirawat' => $kesehatan->lama_dirawat,
]
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil detail kesehatan: ' . $e->getMessage(),
], 500);
}
}
/**
* Get statistik kesehatan santri
*/
public function statistik(Request $request)
{
try {
$idSantri = $request->user()->id_santri;
// Hitung total per status
$totalDirawat = KesehatanSantri::where('id_santri', $idSantri)
->where('status', 'dirawat')
->count();
$totalSembuh = KesehatanSantri::where('id_santri', $idSantri)
->where('status', 'sembuh')
->count();
$totalIzin = KesehatanSantri::where('id_santri', $idSantri)
->where('status', 'izin')
->count();
$totalRiwayat = KesehatanSantri::where('id_santri', $idSantri)
->count();
// Riwayat terbaru yang sedang dirawat
$sedangDirawat = KesehatanSantri::where('id_santri', $idSantri)
->where('status', 'dirawat')
->orderBy('tanggal_masuk', 'desc')
->first();
return response()->json([
'success' => true,
'data' => [
'total_riwayat' => $totalRiwayat,
'total_dirawat' => $totalDirawat,
'total_sembuh' => $totalSembuh,
'total_izin' => $totalIzin,
'sedang_dirawat' => $sedangDirawat ? [
'id_kesehatan' => $sedangDirawat->id_kesehatan,
'tanggal_masuk' => $sedangDirawat->tanggal_masuk->format('d M Y'),
'keluhan' => $sedangDirawat->keluhan,
'lama_dirawat' => $sedangDirawat->lama_dirawat . ' hari',
] : null,
]
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil statistik: ' . $e->getMessage(),
], 500);
}
}
}

View File

@ -1,207 +0,0 @@
<?php
// app/Http/Controllers/Api/ApiPengajuanKepulanganController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\PengajuanKepulangan;
use App\Models\Kepulangan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class ApiPengajuanKepulanganController extends Controller
{
/**
* POST: Submit pengajuan kepulangan baru
* Endpoint: /api/v1/kepulangan/pengajuan
*/
public function store(Request $request)
{
try {
$user = Auth::user();
$idSantri = $user->id_santri;
// Validasi input
$validated = $request->validate([
'tanggal_pulang' => 'required|date|after_or_equal:today',
'tanggal_kembali' => 'required|date|after:tanggal_pulang',
'alasan' => 'required|string|max:500',
], [
'tanggal_pulang.required' => 'Tanggal pulang wajib diisi.',
'tanggal_pulang.after_or_equal' => 'Tanggal pulang minimal hari ini.',
'tanggal_kembali.required' => 'Tanggal kembali wajib diisi.',
'tanggal_kembali.after' => 'Tanggal kembali harus setelah tanggal pulang.',
'alasan.required' => 'Alasan kepulangan wajib diisi.',
'alasan.max' => 'Alasan maksimal 500 karakter.',
]);
// Hitung durasi izin
$tanggalPulang = Carbon::parse($validated['tanggal_pulang']);
$tanggalKembali = Carbon::parse($validated['tanggal_kembali']);
$durasiIzin = $tanggalPulang->diffInDays($tanggalKembali) + 1;
// Create pengajuan
$pengajuan = PengajuanKepulangan::create([
'id_santri' => $idSantri,
'tanggal_pulang' => $validated['tanggal_pulang'],
'tanggal_kembali' => $validated['tanggal_kembali'],
'durasi_izin' => $durasiIzin,
'alasan' => $validated['alasan'],
'status' => 'Menunggu',
]);
// Get info kuota untuk notifikasi
$kuotaInfo = Kepulangan::getSisaKuotaSantri($idSantri);
return response()->json([
'success' => true,
'message' => 'Pengajuan berhasil dikirim. Menunggu persetujuan admin.',
'data' => [
'id_pengajuan' => $pengajuan->id_pengajuan,
'tanggal_pulang' => $pengajuan->tanggal_pulang->format('Y-m-d'),
'tanggal_kembali' => $pengajuan->tanggal_kembali->format('Y-m-d'),
'durasi_izin' => $pengajuan->durasi_izin,
'status' => $pengajuan->status,
'kuota_info' => $kuotaInfo,
],
], 201);
} catch (\Illuminate\Validation\ValidationException $e) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $e->errors(),
], 422);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* GET: List pengajuan kepulangan santri
* Endpoint: /api/v1/kepulangan/pengajuan
*/
public function index(Request $request)
{
try {
$user = Auth::user();
$idSantri = $user->id_santri;
// Build query
$page = $request->input('page', 1);
$perPage = 15;
$query = PengajuanKepulangan::where('id_santri', $idSantri);
// Filter status (optional)
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Order by terbaru
$query->orderBy('created_at', 'desc');
// Paginate
$pengajuan = $query->paginate($perPage, ['*'], 'page', $page);
// Format response
$data = [
'success' => true,
'message' => 'Data pengajuan berhasil diambil.',
'data' => [
'pengajuan' => $pengajuan->map(function($item) {
return [
'id_pengajuan' => $item->id_pengajuan,
'tanggal_pulang' => $item->tanggal_pulang->format('Y-m-d'),
'tanggal_pulang_formatted' => $item->tanggal_pulang->format('d M Y'),
'tanggal_kembali' => $item->tanggal_kembali->format('Y-m-d'),
'tanggal_kembali_formatted' => $item->tanggal_kembali->format('d M Y'),
'durasi_izin' => $item->durasi_izin,
'alasan' => $item->alasan,
'status' => $item->status,
'catatan_review' => $item->catatan_review,
'reviewed_at' => $item->reviewed_at ? $item->reviewed_at->format('Y-m-d H:i:s') : null,
'reviewed_at_formatted' => $item->reviewed_at ? $item->reviewed_at->format('d M Y H:i') : null,
'created_at' => $item->created_at->format('Y-m-d H:i:s'),
'created_at_formatted' => $item->created_at->format('d M Y H:i'),
];
}),
'pagination' => [
'current_page' => $pengajuan->currentPage(),
'last_page' => $pengajuan->lastPage(),
'per_page' => $pengajuan->perPage(),
'total' => $pengajuan->total(),
'from' => $pengajuan->firstItem(),
'to' => $pengajuan->lastItem(),
],
],
];
return response()->json($data, 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* POST: Preview durasi & validasi kuota (sebelum submit)
* Endpoint: /api/v1/kepulangan/pengajuan/preview
*/
public function preview(Request $request)
{
try {
$user = Auth::user();
$idSantri = $user->id_santri;
$validated = $request->validate([
'tanggal_pulang' => 'required|date',
'tanggal_kembali' => 'required|date|after:tanggal_pulang',
]);
// Hitung durasi
$tanggalPulang = Carbon::parse($validated['tanggal_pulang']);
$tanggalKembali = Carbon::parse($validated['tanggal_kembali']);
$durasiIzin = $tanggalPulang->diffInDays($tanggalKembali) + 1;
// Get kuota info
$kuotaInfo = Kepulangan::getSisaKuotaSantri($idSantri);
$totalSetelahIzin = $kuotaInfo['total_terpakai'] + $durasiIzin;
$sisaSetelahIzin = $kuotaInfo['kuota_maksimal'] - $totalSetelahIzin;
$overLimit = $totalSetelahIzin > $kuotaInfo['kuota_maksimal'];
$warningMessage = '';
if ($overLimit) {
$kelebihan = $totalSetelahIzin - $kuotaInfo['kuota_maksimal'];
$warningMessage = "Izin ini akan melebihi batas {$kuotaInfo['kuota_maksimal']} hari per tahun. Kelebihan: {$kelebihan} hari.";
} elseif ($totalSetelahIzin >= $kuotaInfo['kuota_maksimal'] * 0.8) {
$warningMessage = "Kuota hampir habis! Sisa kuota setelah izin ini hanya " . max(0, $sisaSetelahIzin) . " hari.";
}
return response()->json([
'success' => true,
'data' => [
'durasi_izin' => $durasiIzin,
'total_setelah_izin' => $totalSetelahIzin,
'sisa_setelah_izin' => max(0, $sisaSetelahIzin),
'over_limit' => $overLimit,
'warning_message' => $warningMessage,
'kuota_maksimal' => $kuotaInfo['kuota_maksimal'],
],
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
}

View File

@ -1,260 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\PembayaranSpp;
use App\Models\Santri;
use Illuminate\Http\Request;
use Carbon\Carbon;
class ApiSppController extends Controller
{
/**
* Get status SPP bulan berjalan
*/
public function statusBulanIni(Request $request)
{
try {
$idSantri = $request->user()->id_santri;
$bulanIni = date('n');
$tahunIni = date('Y');
$spp = PembayaranSpp::where('id_santri', $idSantri)
->where('bulan', $bulanIni)
->where('tahun', $tahunIni)
->first();
if (!$spp) {
return response()->json([
'success' => true,
'data' => [
'ada_tagihan' => false,
'status' => 'Belum Ada Tagihan',
'periode' => $this->getNamaBulan($bulanIni) . ' ' . $tahunIni,
]
]);
}
// ── TAMBAHAN: data cicilan ──────────────────────────────
$isCicilan = $spp->isCicilan();
$nominalTerbayar = (int) $spp->nominal_terbayar; // accessor dari Model
$nominalSisa = (int) $spp->nominal_sisa; // accessor dari Model
$porsentase = $spp->porsentase_cicilan; // accessor dari Model
// ───────────────────────────────────────────────────────
return response()->json([
'success' => true,
'data' => [
'ada_tagihan' => true,
'id_pembayaran' => $spp->id_pembayaran,
'periode' => $this->getNamaBulan($spp->bulan) . ' ' . $spp->tahun,
'nominal' => (int) $spp->nominal,
'status' => $spp->status,
'tanggal_bayar' => $spp->tanggal_bayar?->format('Y-m-d'),
'tanggal_bayar_formatted' => $spp->tanggal_bayar?->format('d M Y'),
'batas_bayar' => $spp->batas_bayar->format('Y-m-d'),
'batas_bayar_formatted' => $spp->batas_bayar->format('d M Y'),
'is_telat' => $spp->isTelat(),
// ── field baru ──
'is_cicilan' => $isCicilan,
'nominal_terbayar' => $nominalTerbayar,
'nominal_sisa' => $nominalSisa,
'porsentase_cicilan' => $porsentase,
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil status SPP: ' . $e->getMessage(),
], 500);
}
}
/**
* Get info tunggakan
*/
public function tunggakan(Request $request)
{
try {
$idSantri = $request->user()->id_santri;
$tunggakanList = PembayaranSpp::where('id_santri', $idSantri)
->where('status', 'Belum Lunas')
->orderBy('tahun', 'asc')
->orderBy('bulan', 'asc')
->get();
$totalTunggakan = $tunggakanList->sum('nominal');
$jumlahBulan = $tunggakanList->count();
$adaTelat = $tunggakanList->filter(fn($spp) => $spp->isTelat())->count() > 0;
return response()->json([
'success' => true,
'data' => [
'ada_tunggakan' => $jumlahBulan > 0,
'total_tunggakan' => (int) $totalTunggakan,
'jumlah_bulan' => $jumlahBulan,
'ada_telat' => $adaTelat,
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil tunggakan: ' . $e->getMessage(),
], 500);
}
}
/**
* Get riwayat pembayaran SPP
*
* Query param ?status= bisa berisi:
* semua | Lunas | Belum Lunas | Cicilan
*/
public function riwayat(Request $request)
{
try {
$idSantri = $request->user()->id_santri;
$query = PembayaranSpp::where('id_santri', $idSantri)
->select([
'id', 'id_pembayaran', 'bulan', 'tahun',
'nominal', 'status', 'tanggal_bayar',
'batas_bayar', 'keterangan',
])
->orderBy('tahun', 'desc')
->orderBy('bulan', 'desc');
// ── REVISI: filter status termasuk "Cicilan" ───────────
if ($request->filled('status') && $request->status !== 'semua') {
if ($request->status === 'Cicilan') {
// Cicilan = Belum Lunas + keterangan JSON punya field "terbayar" > 0
$query->where('status', 'Belum Lunas')
->where(function ($q) {
// JSON valid & mengandung "terbayar"
$q->whereRaw("JSON_VALID(keterangan) = 1")
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(keterangan, '$.terbayar')) > 0");
});
} else {
$query->where('status', $request->status);
}
}
// ───────────────────────────────────────────────────────
$riwayat = $query->paginate(20);
$data = $riwayat->map(function ($item) {
// ── TAMBAHAN: data cicilan per item ─────────────────
$isCicilan = $item->isCicilan();
$nominalTerbayar = (int) $item->nominal_terbayar;
$nominalSisa = (int) $item->nominal_sisa;
$porsentase = $item->porsentase_cicilan;
// ────────────────────────────────────────────────────
return [
'id' => $item->id,
'id_pembayaran' => $item->id_pembayaran,
'periode' => $this->getNamaBulan($item->bulan) . ' ' . $item->tahun,
'bulan' => $item->bulan,
'tahun' => $item->tahun,
'bulan_nama' => $this->getNamaBulan($item->bulan),
'nominal' => (int) $item->nominal,
'status' => $item->status,
'tanggal_bayar' => $item->tanggal_bayar?->format('Y-m-d'),
'tanggal_bayar_formatted' => $item->tanggal_bayar?->format('d M Y'),
'batas_bayar' => $item->batas_bayar->format('Y-m-d'),
'batas_bayar_formatted' => $item->batas_bayar->format('d M Y'),
'is_telat' => $item->isTelat(),
'keterangan' => $item->keterangan,
// ── field baru ──
'is_cicilan' => $isCicilan,
'nominal_terbayar' => $nominalTerbayar,
'nominal_sisa' => $nominalSisa,
'porsentase_cicilan' => $porsentase,
];
});
return response()->json([
'success' => true,
'data' => $data,
'pagination' => [
'current_page' => $riwayat->currentPage(),
'last_page' => $riwayat->lastPage(),
'total' => $riwayat->total(),
],
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil riwayat: ' . $e->getMessage(),
], 500);
}
}
/**
* Get statistik pembayaran SPP
*/
public function statistik(Request $request)
{
try {
$idSantri = $request->user()->id_santri;
$semuaBelumLunas = PembayaranSpp::where('id_santri', $idSantri)
->where('status', 'Belum Lunas')
->get();
$totalLunas = PembayaranSpp::where('id_santri', $idSantri)
->where('status', 'Lunas')
->count();
// ── TAMBAHAN: pisahkan cicilan dari belum lunas ─────────
$totalCicilan = $semuaBelumLunas->filter(fn($s) => $s->isCicilan())->count();
$totalBelumLunas = $semuaBelumLunas->filter(fn($s) => !$s->isCicilan())->count();
// ────────────────────────────────────────────────────────
$totalNominalLunas = PembayaranSpp::where('id_santri', $idSantri)
->where('status', 'Lunas')
->sum('nominal');
$totalNominalBelumLunas = PembayaranSpp::where('id_santri', $idSantri)
->where('status', 'Belum Lunas')
->sum('nominal');
return response()->json([
'success' => true,
'data' => [
'total_lunas' => $totalLunas,
'total_cicilan' => $totalCicilan, // ← baru
'total_belum_lunas' => $totalBelumLunas, // ← sekarang exclude cicilan
'total_nominal_lunas' => (int) $totalNominalLunas,
'total_nominal_belum_lunas' => (int) $totalNominalBelumLunas,
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil statistik: ' . $e->getMessage(),
], 500);
}
}
/**
* Helper: Get nama bulan
*/
private function getNamaBulan($bulan)
{
$namaBulan = [
1 => 'Januari', 2 => 'Februari', 3 => 'Maret',
4 => 'April', 5 => 'Mei', 6 => 'Juni',
7 => 'Juli', 8 => 'Agustus', 9 => 'September',
10 => 'Oktober', 11 => 'November', 12 => 'Desember',
];
return $namaBulan[$bulan] ?? '';
}
}

View File

@ -1,141 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\UangSaku;
use App\Models\Santri;
use Illuminate\Http\Request;
class ApiUangSakuController extends Controller
{
/**
* Get saldo uang saku santri berdasarkan token wali
*/
public function saldo(Request $request)
{
try {
// Ambil id_santri dari user yang login (wali)
$idSantri = $request->user()->id_santri;
// Ambil data santri
$santri = Santri::where('id_santri', $idSantri)->first();
if (!$santri) {
return response()->json([
'success' => false,
'message' => 'Data santri tidak ditemukan',
], 404);
}
// Query untuk filter
$query = UangSaku::where('id_santri', $idSantri);
// Filter berdasarkan tanggal
if ($request->filled('tanggal_dari')) {
$query->where('tanggal_transaksi', '>=', $request->tanggal_dari);
}
if ($request->filled('tanggal_sampai')) {
$query->where('tanggal_transaksi', '<=', $request->tanggal_sampai);
}
// Hitung total pemasukan dan pengeluaran sesuai filter
$totalPemasukan = (clone $query)
->where('jenis_transaksi', 'pemasukan')
->sum('nominal');
$totalPengeluaran = (clone $query)
->where('jenis_transaksi', 'pengeluaran')
->sum('nominal');
// Saldo tetap keseluruhan (tidak terfilter)
$saldo = $santri->saldo_uang_saku;
return response()->json([
'success' => true,
'data' => [
'saldo' => (int) $saldo,
'id_santri' => $santri->id_santri,
'nama_santri' => $santri->nama_lengkap,
'total_pemasukan' => (int) $totalPemasukan,
'total_pengeluaran' => (int) $totalPengeluaran,
]
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil saldo: ' . $e->getMessage(),
], 500);
}
}
/**
* Get riwayat transaksi uang saku
*/
public function index(Request $request)
{
try {
// Ambil id_santri dari user yang login (wali)
$idSantri = $request->user()->id_santri;
// Query transaksi uang saku
$query = UangSaku::where('id_santri', $idSantri)
->select([
'id',
'tanggal_transaksi',
'jenis_transaksi',
'nominal',
'keterangan',
'saldo_sebelum',
'saldo_sesudah'
]);
// Filter berdasarkan jenis transaksi
if ($request->filled('jenis_transaksi') && $request->jenis_transaksi !== 'semua') {
$query->where('jenis_transaksi', $request->jenis_transaksi);
}
// Filter berdasarkan tanggal
if ($request->filled('tanggal_dari')) {
$query->where('tanggal_transaksi', '>=', $request->tanggal_dari);
}
if ($request->filled('tanggal_sampai')) {
$query->where('tanggal_transaksi', '<=', $request->tanggal_sampai);
}
$transaksi = $query->orderBy('tanggal_transaksi', 'desc')
->orderBy('created_at', 'desc')
->paginate(20);
// Format data
$data = $transaksi->map(function($item) {
return [
'id' => $item->id,
'tanggal_transaksi' => $item->tanggal_transaksi->format('Y-m-d'),
'jenis_transaksi' => $item->jenis_transaksi,
'nominal' => (int) $item->nominal,
'keterangan' => $item->keterangan,
'saldo_sebelum' => (int) $item->saldo_sebelum,
'saldo_sesudah' => (int) $item->saldo_sesudah,
];
});
return response()->json([
'success' => true,
'data' => $data,
'pagination' => [
'current_page' => $transaksi->currentPage(),
'last_page' => $transaksi->lastPage(),
'total' => $transaksi->total(),
]
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil riwayat: ' . $e->getMessage(),
], 500);
}
}
}

View File

@ -1,266 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\KlasifikasiPelanggaran;
use App\Models\KategoriPelanggaran;
use App\Models\PembinaanSanksi;
use App\Models\RiwayatPelanggaran;
use Illuminate\Http\Request;
class PelanggaranApiController extends Controller
{
/**
* GET KLASIFIKASI PELANGGARAN (Public - Untuk Semua)
*/
public function getKlasifikasi()
{
try {
$data = KlasifikasiPelanggaran::aktif()
->byUrutan()
->get(['id_klasifikasi', 'nama_klasifikasi', 'deskripsi', 'urutan']);
return response()->json([
'success' => true,
'data' => $data,
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
/**
* GET KATEGORI PELANGGARAN (Public - Untuk Semua)
* Bisa difilter berdasarkan klasifikasi
*/
public function getKategoriPelanggaran(Request $request)
{
try {
$query = KategoriPelanggaran::with('klasifikasi:id_klasifikasi,nama_klasifikasi')
->aktif()
->orderBy('id_klasifikasi')
->orderBy('nama_pelanggaran');
// Filter by klasifikasi (optional)
if ($request->filled('id_klasifikasi')) {
$query->where('id_klasifikasi', $request->id_klasifikasi);
}
$data = $query->get([
'id_kategori',
'id_klasifikasi',
'nama_pelanggaran',
'poin',
'kafaroh',
]);
return response()->json([
'success' => true,
'data' => $data,
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
/**
* GET PEMBINAAN & SANKSI (Public - Untuk Semua)
*/
public function getPembinaanSanksi()
{
try {
$data = PembinaanSanksi::aktif()
->byUrutan()
->get(['id_pembinaan', 'judul', 'konten', 'urutan']);
return response()->json([
'success' => true,
'data' => $data,
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
/**
* GET RIWAYAT PELANGGARAN SANTRI (Private - Hanya yang Published)
* HANYA menampilkan pelanggaran yang is_published_to_parent = true
*/
public function getRiwayatPelanggaran(Request $request)
{
try {
// Ambil id_santri dari user yang login
$user = $request->user();
$idSantri = $user->id_santri; // id_santri dari santri_accounts
// Query dengan pagination
$perPage = $request->input('per_page', 10);
$page = $request->input('page', 1);
$query = RiwayatPelanggaran::with([
'kategori:id_kategori,nama_pelanggaran,poin,kafaroh,id_klasifikasi',
'kategori.klasifikasi:id_klasifikasi,nama_klasifikasi'
])
->where('id_santri', $idSantri)
->where('is_published_to_parent', true) // HANYA yang sudah dipublish
->orderBy('tanggal', 'desc')
->orderBy('created_at', 'desc');
// Filter by status kafaroh (optional)
if ($request->filled('status_kafaroh')) {
if ($request->status_kafaroh == 'selesai') {
$query->where('is_kafaroh_selesai', true);
} else {
$query->where('is_kafaroh_selesai', false);
}
}
// Filter by tanggal (optional)
if ($request->filled('tanggal_dari')) {
$query->whereDate('tanggal', '>=', $request->tanggal_dari);
}
if ($request->filled('tanggal_sampai')) {
$query->whereDate('tanggal', '<=', $request->tanggal_sampai);
}
$data = $query->paginate($perPage, [
'id_riwayat',
'id_kategori',
'tanggal',
'poin',
'poin_asli',
'keterangan',
'is_kafaroh_selesai',
'tanggal_kafaroh_selesai',
'catatan_kafaroh',
]);
return response()->json([
'success' => true,
'data' => $data->items(),
'current_page' => $data->currentPage(),
'last_page' => $data->lastPage(),
'per_page' => $data->perPage(),
'total' => $data->total(),
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
/**
* GET STATISTIK PELANGGARAN SANTRI
*/
public function getStatistik(Request $request)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
// Hanya hitung yang sudah dipublish
$totalPelanggaran = RiwayatPelanggaran::where('id_santri', $idSantri)
->where('is_published_to_parent', true)
->count();
$totalPoin = RiwayatPelanggaran::where('id_santri', $idSantri)
->where('is_published_to_parent', true)
->sum('poin');
$totalKafarohSelesai = RiwayatPelanggaran::where('id_santri', $idSantri)
->where('is_published_to_parent', true)
->where('is_kafaroh_selesai', true)
->count();
$totalKafarohBelum = RiwayatPelanggaran::where('id_santri', $idSantri)
->where('is_published_to_parent', true)
->where('is_kafaroh_selesai', false)
->count();
// Pelanggaran bulan ini
$pelanggaranBulanIni = RiwayatPelanggaran::where('id_santri', $idSantri)
->where('is_published_to_parent', true)
->whereMonth('tanggal', now()->month)
->whereYear('tanggal', now()->year)
->count();
return response()->json([
'success' => true,
'data' => [
'total_pelanggaran' => $totalPelanggaran,
'total_poin' => $totalPoin,
'total_kafaroh_selesai' => $totalKafarohSelesai,
'total_kafaroh_belum' => $totalKafarohBelum,
'pelanggaran_bulan_ini' => $pelanggaranBulanIni,
],
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
/**
* GET DETAIL RIWAYAT PELANGGARAN
*/
public function getDetailRiwayat(Request $request, $idRiwayat)
{
try {
$user = $request->user();
$idSantri = $user->id_santri;
$riwayat = RiwayatPelanggaran::with([
'kategori:id_kategori,nama_pelanggaran,poin,kafaroh,id_klasifikasi',
'kategori.klasifikasi:id_klasifikasi,nama_klasifikasi',
'adminKafaroh:id,name',
])
->where('id_riwayat', $idRiwayat)
->where('id_santri', $idSantri)
->where('is_published_to_parent', true) // HANYA yang sudah dipublish
->first([
'id_riwayat',
'id_kategori',
'tanggal',
'poin',
'poin_asli',
'keterangan',
'is_kafaroh_selesai',
'tanggal_kafaroh_selesai',
'admin_kafaroh_id',
'catatan_kafaroh',
'tanggal_published',
]);
if (!$riwayat) {
return response()->json([
'success' => false,
'message' => 'Data tidak ditemukan atau belum dipublikasikan.',
], 404);
}
return response()->json([
'success' => true,
'data' => $riwayat,
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage(),
], 500);
}
}
}

View File

@ -1,130 +0,0 @@
<?php
// app/Http/Controllers/Auth/AdminAuthController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Events\Registered;
use Illuminate\Validation\Rules;
class AdminAuthController extends Controller
{
/**
* Tampilkan halaman login admin
*/
public function login()
{
return view('admin.auth.login');
}
/**
* Proses login admin dengan auto-clear session on failed
*/
public function authenticate(Request $request)
{
// Validasi input
$credentials = $request->validate([
'username' => ['required', 'string'],
'password' => ['required', 'string'],
]);
// -- Coba login dengan username --
if (Auth::attempt([
'username' => $credentials['username'],
'password' => $credentials['password'],
], $request->boolean('remember'))) {
$user = Auth::user();
$adminRoles = ['super_admin', 'akademik', 'pamong'];
// -- Pastikan hanya role admin yang bisa login via form admin --
if (!in_array($user->role, $adminRoles)) {
Auth::logout();
$request->session()->invalidate();
throw ValidationException::withMessages([
'username' => 'Akun ini bukan akun admin.',
]);
}
// -- Regenerate session untuk keamanan --
$request->session()->regenerate();
return redirect()->intended(route('admin.dashboard'));
}
// Track failed attempts
$attempts = $request->session()->get('login_attempts', 0) + 1;
$request->session()->put('login_attempts', $attempts);
// Auto-flush setelah 3x gagal
if ($attempts >= 3) {
$request->session()->flush();
$request->session()->regenerate();
return redirect()->back()->withErrors([
'username' => 'Terlalu banyak percobaan login gagal. Session telah direset. Silakan coba lagi.'
])->withInput($request->except('password'));
}
throw ValidationException::withMessages([
'username' => "Login gagal (Percobaan ke-{$attempts}/3). Username/Password salah atau bukan akun Admin.",
]);
}
/**
* Logout admin
*/
public function logout(Request $request)
{
Auth::logout();
$request->session()->flush();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('admin.login')
->with('success', 'Anda berhasil logout.');
}
/**
* Tampilkan halaman register admin
*/
public function register()
{
return view('admin.auth.register');
}
/**
* Proses register admin baru
*/
public function storeRegister(Request $request)
{
// Validasi hanya Email, Password, dan Konfirmasi Password
$request->validate([
'email' => 'required|string|email|max:255|unique:users,email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
], [
'email.unique' => 'Email ini sudah terdaftar sebagai Admin.',
'password.confirmed' => 'Konfirmasi password tidak cocok.'
]);
// Gunakan email sebagai username dan berikan nama default
$user = User::create([
'name' => 'Administrator',
'email' => $request->email,
'username' => $request->email,
'role' => 'super_admin',
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect()->route('admin.dashboard')
->with('success', 'Akun admin berhasil dibuat!');
}
}

View File

@ -1,242 +0,0 @@
<?php
// app/Http/Controllers/Auth/AdminForgotPasswordController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\PasswordResetOtp;
use App\Models\User;
use App\Mail\OtpMail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
class AdminForgotPasswordController extends Controller
{
// ══════════════════ STEP 1 : FORM EMAIL ══════════════════
/**
* Tampilkan form input email
*/
public function showEmailForm()
{
return view('admin.auth.forgot_password');
}
/**
* Kirim OTP ke email super admin
*/
public function sendOtp(Request $request)
{
$request->validate([
'email' => 'required|email',
], [
'email.required' => 'Email wajib diisi.',
'email.email' => 'Format email tidak valid.',
]);
// Cek apakah email terdaftar sebagai super_admin
$user = User::where('email', $request->email)
->where('role', 'super_admin')
->first();
if (!$user) {
return back()->withErrors([
'email' => 'Email tidak ditemukan atau bukan akun Super Admin.',
])->withInput();
}
// Hapus OTP lama untuk email ini
PasswordResetOtp::where('email', $request->email)->delete();
// Generate OTP 6 digit
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
// Simpan ke database dengan expired 10 menit
PasswordResetOtp::create([
'email' => $request->email,
'otp' => $otp,
'expired_at' => now()->addMinutes(10),
]);
// Kirim email OTP
Mail::to($request->email)->send(new OtpMail($otp, $user->name));
// Redirect ke form verifikasi OTP
return redirect()
->route('admin.forgot.verify_form', ['email' => $request->email])
->with('success', 'Kode OTP telah dikirim ke email Anda. Berlaku 10 menit.');
}
// ══════════════════ STEP 2 : VERIFIKASI OTP ══════════════════
/**
* Tampilkan form input OTP
*/
public function showVerifyForm(Request $request)
{
$email = $request->query('email');
if (!$email) {
return redirect()->route('admin.forgot.email_form');
}
return view('admin.auth.verify_otp', compact('email'));
}
/**
* Proses verifikasi OTP
*/
public function verifyOtp(Request $request)
{
$request->validate([
'email' => 'required|email',
'otp' => 'required|string|size:6',
], [
'otp.required' => 'Kode OTP wajib diisi.',
'otp.size' => 'Kode OTP harus 6 digit.',
]);
$record = PasswordResetOtp::where('email', $request->email)
->where('otp', $request->otp)
->where('is_verified', false)
->first();
if (!$record) {
return back()->withErrors([
'otp' => 'Kode OTP tidak valid.',
])->withInput();
}
if ($record->isExpired()) {
$record->delete();
return back()->withErrors([
'otp' => 'Kode OTP sudah expired. Silakan kirim ulang.',
])->withInput();
}
// Tandai OTP sebagai terverifikasi
$record->update(['is_verified' => true]);
// Redirect ke form reset password
return redirect()
->route('admin.forgot.reset_form', ['email' => $request->email])
->with('success', 'Kode OTP valid. Silakan buat password baru.');
}
/**
* Kirim ulang OTP
*/
public function resendOtp(Request $request)
{
$request->validate([
'email' => 'required|email',
]);
$user = User::where('email', $request->email)
->where('role', 'super_admin')
->first();
if (!$user) {
return back()->withErrors([
'email' => 'Email tidak ditemukan.',
]);
}
// Hapus OTP lama
PasswordResetOtp::where('email', $request->email)->delete();
// Generate OTP baru
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
PasswordResetOtp::create([
'email' => $request->email,
'otp' => $otp,
'expired_at' => now()->addMinutes(10),
]);
Mail::to($request->email)->send(new OtpMail($otp, $user->name));
return back()->with('success', 'Kode OTP baru telah dikirim ke email Anda.');
}
// ══════════════════ STEP 3 : RESET PASSWORD ══════════════════
/**
* Tampilkan form reset password (hanya jika OTP sudah diverifikasi)
*/
public function showResetForm(Request $request)
{
$email = $request->query('email');
if (!$email) {
return redirect()->route('admin.forgot.email_form');
}
// Pastikan OTP sudah diverifikasi
$verified = PasswordResetOtp::where('email', $email)
->where('is_verified', true)
->exists();
if (!$verified) {
return redirect()->route('admin.forgot.email_form')
->withErrors(['email' => 'Silakan verifikasi OTP terlebih dahulu.']);
}
return view('admin.auth.reset_password', compact('email'));
}
/**
* Proses reset password
*/
public function resetPassword(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => [
'required',
'string',
'min:8',
'confirmed',
'regex:/[A-Z]/', // minimal 1 huruf besar
'regex:/[a-z]/', // minimal 1 huruf kecil
'regex:/[0-9]/', // minimal 1 angka
'regex:/[^A-Za-z0-9]/', // minimal 1 simbol
],
], [
'password.required' => 'Password baru wajib diisi.',
'password.min' => 'Password minimal 8 karakter.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
'password.regex' => 'Password harus mengandung huruf besar, huruf kecil, angka, dan simbol.',
]);
// Cek ulang apakah OTP sudah terverifikasi
$verified = PasswordResetOtp::where('email', $request->email)
->where('is_verified', true)
->exists();
if (!$verified) {
return redirect()->route('admin.forgot.email_form')
->withErrors(['email' => 'Sesi tidak valid. Silakan ulangi proses.']);
}
// Update password user
$user = User::where('email', $request->email)
->where('role', 'super_admin')
->first();
if (!$user) {
return redirect()->route('admin.forgot.email_form')
->withErrors(['email' => 'Akun tidak ditemukan.']);
}
$user->password = Hash::make($request->password);
$user->save();
// Hapus semua record OTP untuk email ini
PasswordResetOtp::where('email', $request->email)->delete();
return redirect()->route('admin.login')
->with('success', 'Password berhasil diubah! Silakan login dengan password baru.');
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
return view('auth.login');
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME);
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(RouteServiceProvider::HOME);
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(RouteServiceProvider::HOME);
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(RouteServiceProvider::HOME)
: view('auth.verify-email');
}
}

View File

@ -1,61 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): View
{
return view('auth.register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(RouteServiceProvider::HOME);
}
}

View File

@ -1,78 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class SantriAuthController extends Controller
{
public function login()
{
if (Auth::guard('santri')->check()) {
return redirect()->route('santri.dashboard');
}
return view('santri.auth.login');
}
public function authenticate(Request $request)
{
$credentials = $request->validate([
'username' => 'required|string',
'password' => 'required|string',
], [
'username.required' => 'Username wajib diisi.',
'password.required' => 'Password wajib diisi.',
]);
$request->session()->forget(['login_attempts']);
if (Auth::guard('santri')->attempt($credentials, $request->boolean('remember'))) {
$request->session()->regenerate();
// Gunakan DB::table langsung — hindari masalah model cast/mutator
$account = Auth::guard('santri')->user();
DB::table('santri_accounts')
->where('id', $account->id)
->update(['last_login' => now()]);
$nama = $account->santri
? $account->santri->nama_lengkap
: $account->username;
return redirect()->route('santri.dashboard')
->with('success', 'Selamat datang, ' . $nama . '!');
}
$attempts = $request->session()->get('login_attempts', 0) + 1;
$request->session()->put('login_attempts', $attempts);
if ($attempts >= 3) {
$request->session()->flush();
$request->session()->regenerate();
return redirect()->back()->withErrors([
'username' => 'Terlalu banyak percobaan. Session direset, silakan coba lagi.',
])->withInput($request->except('password'));
}
throw ValidationException::withMessages([
'username' => 'Login gagal (Percobaan ke-' . $attempts . '/3). Username atau password salah.',
]);
}
public function logout(Request $request)
{
Auth::guard('santri')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('santri.login')
->with('success', 'Berhasil logout.');
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
}
}

View File

@ -1,12 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}

View File

@ -1,682 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use App\Models\Santri;
use App\Models\SantriKelas;
use App\Models\User;
use App\Models\Kegiatan;
use App\Models\AbsensiKegiatan;
use App\Models\KategoriKegiatan;
use App\Models\RiwayatPelanggaran;
use App\Models\Berita;
use App\Models\KesehatanSantri;
use App\Models\Kepulangan;
use App\Models\PengajuanKepulangan;
use App\Models\PembayaranSpp;
use App\Models\Keuangan;
use App\Models\UangSaku;
use App\Models\Capaian;
use App\Models\Semester;
use Carbon\Carbon;
class DashboardController extends Controller
{
/**
* Mapping hari Carbon (English) -> DB enum (Indonesia)
*/
private function hariIndonesia(): array
{
return [
'Monday' => 'Senin',
'Tuesday' => 'Selasa',
'Wednesday' => 'Rabu',
'Thursday' => 'Kamis',
'Friday' => 'Jumat',
'Saturday' => 'Sabtu',
'Sunday' => 'Ahad',
];
}
// ══════════════════════════════════════════════════════════════════
// DASHBOARD ADMIN — tidak ada perubahan
// ══════════════════════════════════════════════════════════════════
public function admin()
{
try {
$today = Carbon::today();
$now = Carbon::now();
$hariIni = $this->hariIndonesia()[$today->format('l')];
$bulanIni = (int) $today->format('m');
$tahunIni = (int) $today->format('Y');
// KPI CARDS
$user = Auth::user();
$totalSantriAktif = Cache::remember('dash_santri_aktif', 300, function () {
return Santri::aktif()->count();
});
$kegiatanHariIni = Kegiatan::with(['kategori', 'absensis' => function ($q) use ($today) {
$q->whereDate('tanggal', $today);
}])
->where('hari', $hariIni)
->orderBy('waktu_mulai')
->get();
$totalKegiatan = $kegiatanHariIni->count();
$sudahAbsensi = $kegiatanHariIni->filter(fn($k) => $k->absensis->isNotEmpty())->count();
$belumAbsensi = $totalKegiatan - $sudahAbsensi;
$santriSakit = KesehatanSantri::dirawat()->count();
$kepulanganMenunggu = PengajuanKepulangan::where('status', 'Menunggu')->count();
$santriTanpaWali = 0;
if ($user->role === 'super_admin') {
$santriTanpaWali = Santri::aktif()->whereDoesntHave('waliUser')->count();
}
$kpiCards = compact(
'totalSantriAktif', 'totalKegiatan', 'sudahAbsensi',
'belumAbsensi', 'santriSakit', 'kepulanganMenunggu', 'santriTanpaWali'
);
// JADWAL KEGIATAN HARI INI
$kegiatanHariIni->each(function ($kegiatan) use ($now, $today, $totalSantriAktif) {
$waktuMulaiStr = is_string($kegiatan->waktu_mulai) ? $kegiatan->waktu_mulai : $kegiatan->waktu_mulai->format('H:i');
$waktuSelesaiStr = is_string($kegiatan->waktu_selesai) ? $kegiatan->waktu_selesai : $kegiatan->waktu_selesai->format('H:i');
$mulai = Carbon::parse($today->format('Y-m-d') . ' ' . $waktuMulaiStr);
$selesai = Carbon::parse($today->format('Y-m-d') . ' ' . $waktuSelesaiStr);
$kegiatan->status_kegiatan = $now->lt($mulai) ? 'belum'
: ($now->between($mulai, $selesai) ? 'berlangsung' : 'selesai');
$totalAbsen = $kegiatan->absensis->count();
$hadir = $kegiatan->absensis->where('status', 'Hadir')->count();
$kegiatan->persen_kehadiran = $totalAbsen > 0 ? round(($hadir / $totalAbsen) * 100) : 0;
$kegiatan->total_absensi = $totalAbsen;
$kegiatan->belum_input = $kegiatan->status_kegiatan === 'selesai' && $totalAbsen === 0;
});
// ALERT PANEL
$santriAlpaBeruntun = $this->getSantriAlpaBeruntun();
$sppJatuhTempo = collect([]);
if ($user->role === 'super_admin') {
$sppJatuhTempo = PembayaranSpp::telat()
->with('santri:id_santri,nama_lengkap')
->select('id_pembayaran', 'id_santri', 'bulan', 'tahun', 'nominal', 'batas_bayar')
->orderBy('batas_bayar')
->limit(10)
->get();
}
$kepulanganPending = PengajuanKepulangan::where('status', 'Menunggu')
->with('santri:id_santri,nama_lengkap')
->select('id_pengajuan', 'id_santri', 'tanggal_pulang', 'tanggal_kembali', 'alasan')
->orderBy('created_at')
->limit(5)
->get();
$alerts = compact('santriAlpaBeruntun', 'sppJatuhTempo', 'kepulanganPending');
// GRAFIK TREN KEHADIRAN (4 MINGGU)
$trenKehadiran = $this->getTrenKehadiran($today);
// RINGKASAN SPP + KEUANGAN BULAN INI
$sppBulanIni = [
'lunas' => 0,
'belum' => 0,
'terkumpul' => 0,
'totalTagihan' => 0,
'pemasukanLain' => 0,
'pengeluaran' => 0,
];
if ($user->role === 'super_admin') {
$sppBulanIni = Cache::remember("dash_spp_full_{$bulanIni}_{$tahunIni}", 300, function () use ($bulanIni, $tahunIni) {
$lunas = PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->lunas()->count();
$belum = PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->belumLunas()->count();
$terkumpul = (float) PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->lunas()->sum('nominal');
$totalTagihan = (float) PembayaranSpp::where('bulan', $bulanIni)->where('tahun', $tahunIni)->sum('nominal');
$pemasukanLain = (float) Keuangan::pemasukan()
->whereMonth('tanggal', $bulanIni)
->whereYear('tanggal', $tahunIni)
->sum('nominal');
$pengeluaran = (float) Keuangan::pengeluaran()
->whereMonth('tanggal', $bulanIni)
->whereYear('tanggal', $tahunIni)
->sum('nominal');
return compact('lunas', 'belum', 'terkumpul', 'totalTagihan', 'pemasukanLain', 'pengeluaran');
});
}
return view('admin.dashboardAdmin', compact(
'kpiCards', 'kegiatanHariIni', 'alerts',
'trenKehadiran', 'sppBulanIni',
'hariIni', 'today'
));
} catch (\Exception $e) {
Log::error('Error di Dashboard Admin: ' . $e->getMessage() . ' | ' . $e->getFile() . ':' . $e->getLine());
if (config('app.debug')) {
abort(500, 'Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
}
abort(500, 'Terjadi kesalahan saat memuat dashboard Admin.');
}
}
// ══════════════════════════════════════════════════════════════════
// HELPER METHODS (dipakai oleh admin)
// ══════════════════════════════════════════════════════════════════
private function getSantriAlpaBeruntun(int $threshold = 3): \Illuminate\Support\Collection
{
$weekAgo = Carbon::today()->subDays(7);
$alpaData = AbsensiKegiatan::where('status', 'Alpa')
->whereDate('tanggal', '>=', $weekAgo)
->select('id_santri')
->selectRaw('COUNT(*) as total_alpa')
->groupBy('id_santri')
->having('total_alpa', '>=', $threshold)
->pluck('total_alpa', 'id_santri');
if ($alpaData->isEmpty()) {
return collect([]);
}
return Santri::aktif()
->whereIn('id_santri', $alpaData->keys())
->select('id_santri', 'nama_lengkap')
->get()
->map(fn($s) => (object) [
'nama' => $s->nama_lengkap,
'id_santri' => $s->id_santri,
'total_alpa' => $alpaData[$s->id_santri],
]);
}
private function getTrenKehadiran(Carbon $today): array
{
$labels = [];
$series = [];
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
for ($i = 3; $i >= 0; $i--) {
$start = $today->copy()->subWeeks($i)->startOfWeek(Carbon::MONDAY);
$end = $start->copy()->endOfWeek(Carbon::SUNDAY);
$labels[] = 'Mg ' . (4 - $i);
foreach ($kategoris as $kat) {
$kegiatanIds = Kegiatan::where('kategori_id', $kat->kategori_id)
->pluck('kegiatan_id');
$totalAbsen = AbsensiKegiatan::whereIn('kegiatan_id', $kegiatanIds)
->dateRange($start, $end)
->count();
$hadir = AbsensiKegiatan::whereIn('kegiatan_id', $kegiatanIds)
->dateRange($start, $end)
->where('status', 'Hadir')
->count();
$series[$kat->nama_kategori][] = $totalAbsen > 0
? round(($hadir / $totalAbsen) * 100, 1)
: 0;
}
}
return compact('labels', 'series');
}
private function getFeedAktivitas(Carbon $today): \Illuminate\Support\Collection
{
$items = collect();
AbsensiKegiatan::with(['santri:id_santri,nama_lengkap', 'kegiatan:kegiatan_id,nama_kegiatan'])
->whereDate('tanggal', $today)
->orderByDesc('created_at')
->limit(5)
->get()
->each(fn($a) => $items->push((object) [
'icon' => 'fa-clipboard-check',
'color' => 'success',
'text' => ($a->santri->nama_lengkap ?? '-') . ' — ' . $a->status . ' di ' . ($a->kegiatan->nama_kegiatan ?? '-'),
'time' => $a->created_at,
]));
RiwayatPelanggaran::with(['santri:id_santri,nama_lengkap', 'kategori:id_kategori,nama_pelanggaran'])
->whereDate('tanggal', '>=', $today->copy()->subDays(7))
->terbaru()
->limit(5)
->get()
->each(fn($p) => $items->push((object) [
'icon' => 'fa-exclamation-triangle',
'color' => 'danger',
'text' => ($p->santri->nama_lengkap ?? '-') . ' — ' . ($p->kategori->nama_pelanggaran ?? '-') . ' (' . $p->poin . ' poin)',
'time' => $p->created_at,
]));
PembayaranSpp::with('santri:id_santri,nama_lengkap')
->lunas()
->whereNotNull('tanggal_bayar')
->whereDate('tanggal_bayar', '>=', $today->copy()->subDays(7))
->orderByDesc('tanggal_bayar')
->limit(5)
->get()
->each(fn($s) => $items->push((object) [
'icon' => 'fa-money-bill-wave',
'color' => 'info',
'text' => ($s->santri->nama_lengkap ?? '-') . ' — SPP ' . $s->bulan_nama . '/' . $s->tahun . ' (Rp ' . number_format($s->nominal, 0, ',', '.') . ')',
'time' => $s->created_at,
]));
return $items->sortByDesc('time')->take(10)->values();
}
// ══════════════════════════════════════════════════════════════════
// HELPER: Absensi per kategori (dipakai santri())
// ══════════════════════════════════════════════════════════════════
/**
* Ambil statistik absensi per kategori kegiatan untuk 1 santri
* dalam rentang tanggal tertentu.
*
* @param int $idSantri
* @param string $dateStart format Y-m-d
* @param string $dateEnd format Y-m-d
* @return array ['labels'=>[], 'hadir'=>[], 'alpa'=>[], 'izin'=>[], 'sakit'=>[]]
*/
private function getAbsensiPerKategori(string|int $idSantri, string $dateStart, string $dateEnd): array
{
$result = ['labels' => [], 'hadir' => [], 'alpa' => [], 'izin' => [], 'sakit' => []];
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')
->orderBy('nama_kategori')
->get();
foreach ($kategoris as $kat) {
$kegIds = Kegiatan::where('kategori_id', $kat->kategori_id)
->pluck('kegiatan_id');
if ($kegIds->isEmpty()) {
continue;
}
$abs = AbsensiKegiatan::where('id_santri', $idSantri)
->whereIn('kegiatan_id', $kegIds)
->whereBetween('tanggal', [$dateStart, $dateEnd])
->get();
// Skip kategori yang tidak punya record sama sekali di periode ini
if ($abs->isEmpty()) {
continue;
}
$result['labels'][] = $kat->nama_kategori;
$result['hadir'][] = $abs->whereIn('status', ['Hadir', 'Terlambat'])->count();
$result['alpa'][] = $abs->where('status', 'Alpa')->count();
$result['izin'][] = $abs->where('status', 'Izin')->count();
$result['sakit'][] = $abs->where('status', 'Sakit')->count();
}
return $result;
}
// ══════════════════════════════════════════════════════════════════
// DASHBOARD SANTRI
// ══════════════════════════════════════════════════════════════════
public function santri()
{
try {
$account = auth('santri')->user();
Log::info('=== DASHBOARD SANTRI START ===');
Log::info('Account ID: ' . $account->id);
Log::info('Role: ' . $account->role);
Log::info('ID Santri: ' . $account->id_santri);
$santri = Santri::with(['kelasPrimary.kelas.kelompok'])
->where('id_santri', $account->id_santri)
->select('id_santri', 'nama_lengkap')
->first();
if (!$santri) {
Log::error('Santri tidak ditemukan dengan id_santri: ' . $account->id_santri);
abort(404, 'Data santri tidak ditemukan.');
}
Log::info('Santri ditemukan: ' . $santri->nama_lengkap);
$namaKelas = $santri->kelas;
$idSantri = $santri->id_santri;
$today = Carbon::today();
// ─── Semester aktif ───────────────────────────────────────
$semesterAktif = null;
try {
$semesterAktif = Semester::aktif()
->select('id_semester', 'nama_semester', 'tahun_ajaran')
->first();
if (!$semesterAktif) {
$semesterAktif = Semester::select('id_semester', 'nama_semester', 'tahun_ajaran')
->orderBy('tahun_ajaran', 'desc')
->orderBy('periode', 'desc')
->first();
}
Log::info('Semester aktif: ' . ($semesterAktif ? $semesterAktif->nama_semester : 'Tidak ada'));
} catch (\Exception $e) {
Log::warning('Error mengambil semester: ' . $e->getMessage());
}
// ─── Progres Al-Qur'an ────────────────────────────────────
$progresAlquran = 0;
try {
$query = Capaian::where('id_santri', $idSantri);
if ($semesterAktif) {
$query->where('id_semester', $semesterAktif->id_semester);
}
$progresAlquran = $query->whereHas('materi', fn($q) => $q->where('kategori', "Al-Qur'an"))
->avg('persentase') ?? 0;
} catch (\Exception $e) {
Log::warning('Error progres Al-Quran: ' . $e->getMessage());
}
// ─── Progres Hadist ───────────────────────────────────────
$progresHadist = 0;
try {
$query = Capaian::where('id_santri', $idSantri);
if ($semesterAktif) {
$query->where('id_semester', $semesterAktif->id_semester);
}
$progresHadist = $query->whereHas('materi', fn($q) => $q->where('kategori', 'Hadist'))
->avg('persentase') ?? 0;
} catch (\Exception $e) {
Log::warning('Error progres Hadist: ' . $e->getMessage());
}
// ─── Progres Materi Tambahan ──────────────────────────────
$progresMateriTambahan = 0;
try {
$query = Capaian::where('id_santri', $idSantri);
if ($semesterAktif) {
$query->where('id_semester', $semesterAktif->id_semester);
}
$progresMateriTambahan = $query->whereHas('materi', fn($q) => $q->where('kategori', 'Materi Tambahan'))
->avg('persentase') ?? 0;
} catch (\Exception $e) {
Log::warning('Error progres Materi Tambahan: ' . $e->getMessage());
}
// ─── Capaian per Materi ───────────────────────────────────
$capaianPerMateri = collect([]);
try {
$query = Capaian::with(['materi' => fn($q) => $q->select('id_materi', 'nama_kitab', 'kategori', 'total_halaman')])
->where('id_santri', $idSantri);
if ($semesterAktif) {
$query->where('id_semester', $semesterAktif->id_semester);
}
$capaianPerMateri = $query->select('id', 'id_materi', 'persentase', 'halaman_selesai')
->orderBy('persentase', 'desc')
->limit(10)
->get();
} catch (\Exception $e) {
Log::warning('Error capaian per materi: ' . $e->getMessage());
}
// ─── Distribusi Status ────────────────────────────────────
$distribusiStatus = ['selesai' => 0, 'hampir_selesai' => 0, 'sedang_berjalan' => 0, 'baru_dimulai' => 0];
try {
$baseQuery = Capaian::where('id_santri', $idSantri);
if ($semesterAktif) {
$baseQuery->where('id_semester', $semesterAktif->id_semester);
}
$distribusiStatus = [
'selesai' => (clone $baseQuery)->where('persentase', '>=', 100)->count(),
'hampir_selesai' => (clone $baseQuery)->whereBetween('persentase', [75, 99.99])->count(),
'sedang_berjalan' => (clone $baseQuery)->whereBetween('persentase', [25, 74.99])->count(),
'baru_dimulai' => (clone $baseQuery)->whereBetween('persentase', [0, 24.99])->count(),
];
} catch (\Exception $e) {
Log::warning('Error distribusi status: ' . $e->getMessage());
}
// ─── Status Kesehatan ─────────────────────────────────────
$statusKesehatan = null;
try {
$statusKesehatan = KesehatanSantri::where('id_santri', $idSantri)
->where('status', 'dirawat')
->select('id', 'keluhan', 'tanggal_masuk')
->orderBy('tanggal_masuk', 'desc')
->first();
} catch (\Exception $e) {
Log::warning('Error status kesehatan: ' . $e->getMessage());
}
// ─── Kepulangan Aktif ─────────────────────────────────────
$kepulanganAktif = null;
try {
$kepulanganAktif = Kepulangan::where('id_santri', $idSantri)
->where('status', 'Disetujui')
->whereDate('tanggal_pulang', '<=', $today)
->whereDate('tanggal_kembali', '>=', $today)
->select('id_kepulangan', 'tanggal_pulang', 'tanggal_kembali', 'alasan')
->first();
} catch (\Exception $e) {
Log::warning('Error kepulangan aktif: ' . $e->getMessage());
}
// ─── Berita Terbaru ───────────────────────────────────────
// Tanpa filter tanggal agar semua berita relevan muncul, limit 5
$beritaTerbaru = collect([]);
try {
$beritaTerbaru = Berita::select('id_berita', 'judul', 'created_at')
->where('status', 'published')
->where(function ($query) use ($namaKelas) {
$query->where('target_berita', 'semua')
->orWhere(function ($q) use ($namaKelas) {
$q->where('target_berita', 'kelas_tertentu')
->whereJsonContains('target_kelas', $namaKelas);
});
})
->orderBy('created_at', 'desc')
->limit(5)
->get();
} catch (\Exception $e) {
Log::warning('Error berita terbaru: ' . $e->getMessage());
}
// ─── Statistik Kepulangan Tahun Ini ──────────────────────
$statistikKepulangan = [
'total_hari' => 0,
'sisa_kuota' => 12,
'persen_kuota' => 0,
'disetujui' => 0,
'menunggu' => 0,
'over_limit' => false,
];
try {
$kepulanganTahunIni = Kepulangan::where('id_santri', $idSantri)
->whereYear('tanggal_pulang', $today->year)
->get();
$totalHariKepulangan = $kepulanganTahunIni
->whereIn('status', ['Disetujui', 'Selesai'])
->sum('durasi_izin');
$statistikKepulangan = [
'total_hari' => $totalHariKepulangan,
'sisa_kuota' => max(0, 12 - $totalHariKepulangan),
'persen_kuota' => min(100, round(($totalHariKepulangan / 12) * 100)),
'disetujui' => $kepulanganTahunIni->whereIn('status', ['Disetujui', 'Selesai'])->count(),
'menunggu' => $kepulanganTahunIni->where('status', 'Menunggu')->count(),
'over_limit' => $totalHariKepulangan > 12,
];
Log::info('Statistik kepulangan: ' . json_encode($statistikKepulangan));
} catch (\Exception $e) {
Log::warning('Error statistik kepulangan: ' . $e->getMessage());
}
// ─── Statistik Kesehatan Bulan Ini ───────────────────────
$statistikKesehatan = [
'total_kunjungan' => 0,
'sembuh' => 0,
'dirawat' => 0,
'izin' => 0,
];
try {
$kesehatanBulanIni = KesehatanSantri::where('id_santri', $idSantri)
->whereMonth('tanggal_masuk', $today->month)
->whereYear('tanggal_masuk', $today->year)
->get();
$statistikKesehatan = [
'total_kunjungan' => $kesehatanBulanIni->count(),
'sembuh' => $kesehatanBulanIni->where('status', 'sembuh')->count(),
'dirawat' => $kesehatanBulanIni->where('status', 'dirawat')->count(),
'izin' => $kesehatanBulanIni->where('status', 'izin')->count(),
];
} catch (\Exception $e) {
Log::warning('Error statistik kesehatan: ' . $e->getMessage());
}
// ─── 5 Pelanggaran Terbaru ────────────────────────────────
$pelanggaranTerbaru = collect([]);
try {
$pelanggaranTerbaru = RiwayatPelanggaran::with('kategori:id,id_kategori,nama_pelanggaran')
->where('id_santri', $idSantri)
->select('id', 'id_riwayat', 'id_kategori', 'tanggal', 'poin', 'keterangan')
->orderBy('tanggal', 'desc')
->limit(5)
->get();
Log::info('Pelanggaran terbaru: ' . $pelanggaranTerbaru->count() . ' items');
} catch (\Exception $e) {
Log::warning('Error pelanggaran terbaru: ' . $e->getMessage());
}
// ─── [BARU] Absensi per Kategori — Bulan Ini ─────────────
$absensiPerKategori = ['labels' => [], 'hadir' => [], 'alpa' => [], 'izin' => [], 'sakit' => []];
try {
$startBulan = $today->copy()->startOfMonth()->format('Y-m-d');
$endBulan = $today->format('Y-m-d');
$absensiPerKategori = $this->getAbsensiPerKategori($idSantri, $startBulan, $endBulan);
Log::info('Absensi per kategori bulan ini: ' . count($absensiPerKategori['labels']) . ' kategori');
} catch (\Exception $e) {
Log::warning('Error absensi per kategori bulan: ' . $e->getMessage());
}
// ─── [BARU] Absensi per Kategori — Minggu Ini ────────────
$absensiPerKategoriMinggu = ['labels' => [], 'hadir' => [], 'alpa' => [], 'izin' => [], 'sakit' => []];
try {
$startMinggu = $today->copy()->startOfWeek(Carbon::MONDAY)->format('Y-m-d');
$endMinggu = $today->format('Y-m-d');
$absensiPerKategoriMinggu = $this->getAbsensiPerKategori($idSantri, $startMinggu, $endMinggu);
Log::info('Absensi per kategori minggu ini: ' . count($absensiPerKategoriMinggu['labels']) . ' kategori');
} catch (\Exception $e) {
Log::warning('Error absensi per kategori minggu: ' . $e->getMessage());
}
// ─── [BARU] Status Input Capaian ──────────────────────────
$statusInputCapaian = [
'is_open' => false,
'deadline' => null,
'sudah_input' => 0,
'total_materi' => 0,
];
try {
if ($semesterAktif) {
// Sesuaikan nama kolom jika berbeda di tabel semesters
$bukaSemester = $semesterAktif->tanggal_buka_input ?? null;
$tutupSemester = $semesterAktif->tanggal_tutup_input ?? null;
$isOpen = false;
if ($bukaSemester && $tutupSemester) {
$now = Carbon::now();
$isOpen = $now->gte(Carbon::parse($bukaSemester))
&& $now->lte(Carbon::parse($tutupSemester));
}
$sudahInput = Capaian::where('id_santri', $idSantri)
->where('id_semester', $semesterAktif->id_semester)
->where('persentase', '>', 0)
->count();
$totalMateri = Capaian::where('id_santri', $idSantri)
->where('id_semester', $semesterAktif->id_semester)
->count();
$statusInputCapaian = [
'is_open' => $isOpen,
'deadline' => $tutupSemester,
'sudah_input' => $sudahInput,
'total_materi' => $totalMateri,
];
}
} catch (\Exception $e) {
Log::warning('Error status input capaian: ' . $e->getMessage());
}
// ─── Data array untuk view ────────────────────────────────
$data = [
'nama_santri' => $santri->nama_lengkap,
'kelas' => $namaKelas,
'progres_quran' => round($progresAlquran, 1),
'progres_hadist' => round($progresHadist, 1),
'progres_materi_tambahan' => round($progresMateriTambahan, 1),
'saldo_uang_saku' => $santri->saldo_uang_saku ?? 0,
'poin_pelanggaran' => RiwayatPelanggaran::where('id_santri', $idSantri)->sum('poin') ?? 0,
];
Log::info('=== DASHBOARD SANTRI SUCCESS ===');
return view('santri.dashboardSantri', compact(
'data',
'santri',
'account',
'beritaTerbaru',
'statusKesehatan',
'kepulanganAktif',
'capaianPerMateri',
'distribusiStatus',
'semesterAktif',
'statistikKepulangan',
'statistikKesehatan',
'pelanggaranTerbaru',
// ─── variabel baru ───
'absensiPerKategori',
'absensiPerKategoriMinggu',
'statusInputCapaian'
));
} catch (\Exception $e) {
Log::error('=== FATAL ERROR DI DASHBOARD SANTRI ===');
Log::error('Message: ' . $e->getMessage());
Log::error('File: ' . $e->getFile());
Log::error('Line: ' . $e->getLine());
Log::error('Trace: ' . $e->getTraceAsString());
if (config('app.debug')) {
abort(500, 'Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
}
abort(500, 'Terjadi kesalahan saat memuat dashboard. Silakan hubungi administrator.');
}
}
}

View File

@ -1,62 +0,0 @@
<?php
// app/Http/Controllers/ProfileController.php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): View
{
return view('profile.edit', [
'user' => $request->user(),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit')->with('status', 'profile-updated');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@ -1,418 +0,0 @@
<?php
// app/Http/Controllers/Santri/RiwayatKegiatanSantriController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\AbsensiKegiatan;
use App\Models\Kegiatan;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class RiwayatKegiatanSantriController extends Controller
{
private function getSantriId()
{
return auth('santri')->user()->id_santri;
}
private function resolveDateRange(Request $request, string $defaultPreset = 'today'): array
{
$preset = $request->input('preset', $defaultPreset);
$now = Carbon::now();
switch ($preset) {
case 'today':
return [$now->copy()->startOfDay(), $now->copy()->endOfDay(), 'today'];
case 'this_week':
return [$now->copy()->startOfWeek(), $now->copy()->endOfWeek(), 'this_week'];
case 'last_30':
return [$now->copy()->subDays(29)->startOfDay(), $now->copy()->endOfDay(), 'last_30'];
case 'this_month':
return [$now->copy()->startOfMonth(), $now->copy()->endOfMonth(), 'this_month'];
case 'last_month':
$lm = $now->copy()->subMonth();
return [$lm->copy()->startOfMonth(), $lm->copy()->endOfMonth(), 'last_month'];
default:
$from = $request->filled('date_from')
? Carbon::parse($request->date_from)->startOfDay()
: $now->copy()->startOfDay();
$to = $request->filled('date_to')
? Carbon::parse($request->date_to)->endOfDay()
: $now->copy()->endOfDay();
if ($from->gt($to)) [$from, $to] = [$to, $from];
return [$from, $to, 'custom'];
}
}
// ================================================================
// INDEX
// ================================================================
public function index(Request $request)
{
$idSantri = $this->getSantriId();
$santri = Santri::where('id_santri', $idSantri)
->with(['kelasPrimary.kelas'])
->select('id_santri', 'nama_lengkap', 'nis', 'status')
->firstOrFail();
$namaKelas = optional(optional($santri->kelasPrimary)->kelas)->nama_kelas ?? '-';
$activeTab = $request->input('tab', 'statistik');
// ── Statistik range ───────────────────────────────────
$statPresetReq = $request->input('preset_stat', $request->input('preset', 'this_week'));
[$statFrom, $statTo, $statPreset] = $this->resolveDateRange(
$request->merge([
'preset' => $statPresetReq,
'date_from' => $request->input('stat_date_from'),
'date_to' => $request->input('stat_date_to'),
]),
'this_week'
);
if ($statPreset === 'custom') {
$statFrom = $request->filled('stat_date_from') ? Carbon::parse($request->stat_date_from)->startOfDay() : $statFrom;
$statTo = $request->filled('stat_date_to') ? Carbon::parse($request->stat_date_to)->endOfDay() : $statTo;
}
// ── Jadwal range ──────────────────────────────────────
$jadPresetReq = $request->input('preset_jad', $request->input('preset', 'today'));
[$jadFrom, $jadTo, $jadPreset] = $this->resolveDateRange(
$request->merge([
'preset' => $jadPresetReq,
'date_from' => $request->input('jad_date_from'),
'date_to' => $request->input('jad_date_to'),
]),
'today'
);
// ── Mapping hari Carbon → nama hari di DB ─────────────
$hariMapDb = [
'Senin' => 'Senin',
'Selasa' => 'Selasa',
'Rabu' => 'Rabu',
'Kamis' => 'Kamis',
'Jumat' => 'Jumat',
'Sabtu' => 'Sabtu',
'Minggu' => 'Ahad',
];
$hariCarbon = Carbon::now()->locale('id')->dayName;
$hariIni = $hariMapDb[$hariCarbon] ?? $hariCarbon;
// ── KPI stats (stat range) ────────────────────────────
$statFromStr = $statFrom->format('Y-m-d');
$statToStr = $statTo->format('Y-m-d');
$statsRange = AbsensiKegiatan::where('id_santri', $idSantri)
->whereBetween('tanggal', [$statFromStr, $statToStr])
->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->pluck('total', 'status')
->toArray();
$totalRange = array_sum($statsRange);
$hadirRange = $statsRange['Hadir'] ?? 0;
$terlambatRange = $statsRange['Terlambat'] ?? 0;
$izinRange = $statsRange['Izin'] ?? 0;
$sakitRange = $statsRange['Sakit'] ?? 0;
$alpaRange = $statsRange['Alpa'] ?? 0;
$pulangRange = $statsRange['Pulang'] ?? 0;
// ── Expected total: semua kegiatan di hari itu, tanpa filter kelas ──
$expectedTotal = 0;
$curStat = $statFrom->copy();
while ($curStat->lte($statTo)) {
$hariDb = $hariMapDb[$curStat->locale('id')->dayName] ?? $curStat->locale('id')->dayName;
$expectedTotal += Kegiatan::where('hari', $hariDb)->count();
$curStat->addDay();
}
$belumAbsenRange = max(0, $expectedTotal - $totalRange);
$hadirEfektif = $hadirRange + $terlambatRange;
$persentaseKehadiran = $expectedTotal > 0 ? round($hadirEfektif / $expectedTotal * 100, 1) : 0;
// ── Jadwal dalam range: semua kegiatan, tanpa filter kelas ───
$hariDalamRange = [];
$cursor = $jadFrom->copy();
while ($cursor->lte($jadTo)) {
$hariDb = $hariMapDb[$cursor->locale('id')->dayName] ?? $cursor->locale('id')->dayName;
$hariDalamRange[$hariDb] = true;
$cursor->addDay();
}
$hariDalamRange = array_keys($hariDalamRange);
$jadwalDalamRange = Kegiatan::with('kategori')
->whereIn('hari', $hariDalamRange)
->select('kegiatan_id', 'kategori_id', 'nama_kegiatan', 'waktu_mulai', 'waktu_selesai', 'hari', 'materi')
->orderByRaw("FIELD(hari, 'Senin','Selasa','Rabu','Kamis','Jumat','Sabtu','Ahad')")
->orderBy('waktu_mulai')
->get();
// ── Status absensi santri dalam range jadwal ──────────
$absensiDalamRange = AbsensiKegiatan::where('id_santri', $idSantri)
->whereBetween('tanggal', [$jadFrom->format('Y-m-d'), $jadTo->format('Y-m-d')])
->pluck('status', 'kegiatan_id')
->toArray();
$absensiHariIni = AbsensiKegiatan::where('id_santri', $idSantri)
->whereDate('tanggal', Carbon::today())
->pluck('status', 'kegiatan_id')
->toArray();
// ── Streak ───────────────────────────────────────────
$streak = 0;
AbsensiKegiatan::where('id_santri', $idSantri)
->orderByDesc('tanggal')
->orderByDesc('waktu_absen')
->select('status')
->limit(60)
->each(function ($a) use (&$streak) {
if (in_array($a->status, ['Hadir', 'Terlambat'])) $streak++;
else return false;
});
// ── Grafik tren ───────────────────────────────────────
$diffDays = $statFrom->diffInDays($statTo);
$dataGrafik = [];
if ($diffDays <= 31) {
$cur = $statFrom->copy();
while ($cur->lte($statTo)) {
$d = $cur->format('Y-m-d');
$hadir = AbsensiKegiatan::where('id_santri', $idSantri)
->whereDate('tanggal', $d)
->whereIn('status', ['Hadir', 'Terlambat'])
->count();
$total = AbsensiKegiatan::where('id_santri', $idSantri)
->whereDate('tanggal', $d)
->count();
$dataGrafik[] = ['label' => $cur->format('d/m'), 'hadir' => $hadir, 'total' => $total];
$cur->addDay();
}
} else {
$cur = $statFrom->copy()->startOfWeek();
while ($cur->lte($statTo)) {
$wStart = $cur->copy()->max($statFrom);
$wEnd = $cur->copy()->endOfWeek()->min($statTo);
$hadir = AbsensiKegiatan::where('id_santri', $idSantri)
->whereBetween('tanggal', [$wStart->format('Y-m-d'), $wEnd->format('Y-m-d')])
->whereIn('status', ['Hadir', 'Terlambat'])
->count();
$total = AbsensiKegiatan::where('id_santri', $idSantri)
->whereBetween('tanggal', [$wStart->format('Y-m-d'), $wEnd->format('Y-m-d')])
->count();
$dataGrafik[] = [
'label' => $wStart->format('d/m') . '' . $wEnd->format('d/m'),
'hadir' => $hadir,
'total' => $total,
];
$cur->addWeek();
}
}
// ── Recent Absensi (8 terbaru dalam stat range) ───────
$recentAbsensi = AbsensiKegiatan::with('kegiatan.kategori')
->where('id_santri', $idSantri)
->whereBetween('tanggal', [$statFromStr, $statToStr])
->orderBy('tanggal', 'desc')
->orderBy('waktu_absen', 'desc')
->limit(8)
->get();
// ── Heatmap kalender ──────────────────────────────────
$heatmapMonths = [];
$cur = $statFrom->copy()->startOfMonth();
while ($cur->lte($statTo)) {
$daysInMonth = $cur->daysInMonth;
$firstDayOfWeek = $cur->copy()->startOfMonth()->dayOfWeekIso;
$days = [];
for ($d = 1; $d <= $daysInMonth; $d++) {
$date = $cur->format('Y-m') . '-' . str_pad($d, 2, '0', STR_PAD_LEFT);
$rows = AbsensiKegiatan::where('id_santri', $idSantri)->whereDate('tanggal', $date)->get();
$level = 0;
if ($rows->count() > 0) {
$hadirCount = $rows->whereIn('status', ['Hadir', 'Terlambat'])->count();
$pct = round($hadirCount / $rows->count() * 100);
$level = $pct >= 90 ? 4 : ($pct >= 70 ? 3 : ($pct >= 50 ? 2 : 1));
}
$days[] = [
'day' => $d,
'date' => $date,
'level' => $level,
'count' => $rows->whereIn('status', ['Hadir', 'Terlambat'])->count(),
'total' => $rows->count(),
'is_today' => $date === Carbon::today()->format('Y-m-d'),
'in_range' => $date >= $statFromStr && $date <= $statToStr,
];
}
$heatmapMonths[] = [
'label' => $cur->locale('id')->isoFormat('MMMM YYYY'),
'firstDayOfWeek' => $firstDayOfWeek,
'days' => $days,
];
$cur->addMonth();
}
return view('santri.kegiatan.index', compact(
'santri', 'namaKelas',
'jadwalDalamRange', 'absensiDalamRange', 'absensiHariIni', 'hariIni',
'jadPreset', 'jadFrom', 'jadTo',
'statsRange', 'totalRange',
'hadirRange', 'terlambatRange', 'izinRange', 'sakitRange', 'alpaRange', 'pulangRange',
'hadirEfektif',
'persentaseKehadiran', 'streak', 'expectedTotal', 'belumAbsenRange',
'dataGrafik', 'statPreset', 'statFrom', 'statTo', 'statFromStr', 'statToStr', 'diffDays',
'recentAbsensi',
'heatmapMonths',
'activeTab', 'hariIni'
));
}
// ================================================================
// SHOW — support filter tanggal, semua data ikut filter
// ================================================================
public function show($kegiatan_id, Request $request)
{
$idSantri = $this->getSantriId();
$santri = Santri::where('id_santri', $idSantri)
->select('id_santri', 'nama_lengkap', 'nis', 'status')
->firstOrFail();
$kegiatan = Kegiatan::with('kategori')
->where('kegiatan_id', $kegiatan_id)
->firstOrFail();
// ── Resolve date range ────────────────────────────────
$preset = $request->input('preset', 'this_week');
$now = Carbon::now();
switch ($preset) {
case 'this_week':
$dateFrom = $now->copy()->startOfWeek();
$dateTo = $now->copy()->endOfWeek();
break;
case 'this_month':
$dateFrom = $now->copy()->startOfMonth();
$dateTo = $now->copy()->endOfMonth();
break;
case 'last_month':
$dateFrom = $now->copy()->subMonth()->startOfMonth();
$dateTo = $now->copy()->subMonth()->endOfMonth();
break;
case 'last_3m':
$dateFrom = $now->copy()->subMonths(3)->startOfDay();
$dateTo = $now->copy()->endOfDay();
break;
case 'all':
$oldest = AbsensiKegiatan::where('id_santri', $idSantri)
->where('kegiatan_id', $kegiatan_id)
->min('tanggal');
$dateFrom = $oldest
? Carbon::parse($oldest)->startOfDay()
: $now->copy()->startOfWeek();
$dateTo = $now->copy()->endOfDay();
break;
default:
$dateFrom = $request->filled('date_from')
? Carbon::parse($request->date_from)->startOfDay()
: $now->copy()->startOfWeek();
$dateTo = $request->filled('date_to')
? Carbon::parse($request->date_to)->endOfDay()
: $now->copy()->endOfWeek();
if ($dateFrom->gt($dateTo)) [$dateFrom, $dateTo] = [$dateTo, $dateFrom];
$preset = 'custom';
}
$fromStr = $dateFrom->format('Y-m-d');
$toStr = $dateTo->format('Y-m-d');
// ── Stats dalam range ─────────────────────────────────
$stats = AbsensiKegiatan::where('id_santri', $idSantri)
->where('kegiatan_id', $kegiatan_id)
->whereBetween('tanggal', [$fromStr, $toStr])
->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->pluck('total', 'status')
->toArray();
$totalAbsensi = array_sum($stats);
$hadirEfektif = ($stats['Hadir'] ?? 0) + ($stats['Terlambat'] ?? 0);
$persentaseHadir = $totalAbsensi > 0
? round($hadirEfektif / $totalAbsensi * 100, 1) : 0;
// ── Riwayat tabel (paginated, ikut range) ─────────────
$riwayats = AbsensiKegiatan::where('id_santri', $idSantri)
->where('kegiatan_id', $kegiatan_id)
->whereBetween('tanggal', [$fromStr, $toStr])
->orderBy('tanggal', 'desc')
->paginate(20)
->appends($request->query());
// ── Lookup tanggal => status untuk kalender visual ────
// Query terpisah agar tidak terbatas oleh pagination $riwayats
$absensiByDate = AbsensiKegiatan::where('id_santri', $idSantri)
->where('kegiatan_id', $kegiatan_id)
->whereBetween('tanggal', [$fromStr, $toStr])
->select('tanggal', 'status')
->get()
->mapWithKeys(fn($a) => [Carbon::parse($a->tanggal)->format('Y-m-d') => $a->status])
->toArray();
// ── Tren data ─────────────────────────────────────────
$diffDays = $dateFrom->diffInDays($dateTo);
$trendData = [];
if ($diffDays <= 31) {
$cur = $dateFrom->copy();
while ($cur->lte($dateTo)) {
$d = $cur->format('Y-m-d');
$data = AbsensiKegiatan::where('id_santri', $idSantri)
->where('kegiatan_id', $kegiatan_id)
->whereDate('tanggal', $d)
->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->pluck('total', 'status')
->toArray();
$trendData[] = [
'label' => $cur->format('d/m'),
'hadir' => ($data['Hadir'] ?? 0) + ($data['Terlambat'] ?? 0),
'total' => array_sum($data),
];
$cur->addDay();
}
$trendLabel = 'Harian';
} else {
$cur = $dateFrom->copy()->startOfWeek();
while ($cur->lte($dateTo)) {
$wStart = $cur->copy()->max($dateFrom);
$wEnd = $cur->copy()->endOfWeek()->min($dateTo);
$data = AbsensiKegiatan::where('id_santri', $idSantri)
->where('kegiatan_id', $kegiatan_id)
->whereBetween('tanggal', [$wStart->format('Y-m-d'), $wEnd->format('Y-m-d')])
->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->pluck('total', 'status')
->toArray();
$trendData[] = [
'label' => $wStart->format('d/m') . '' . $wEnd->format('d/m'),
'hadir' => ($data['Hadir'] ?? 0) + ($data['Terlambat'] ?? 0),
'total' => array_sum($data),
];
$cur->addWeek();
}
$trendLabel = 'Mingguan';
}
$fromTab = $request->input('from_tab', 'jadwal');
return view('santri.kegiatan.show', compact(
'santri', 'kegiatan', 'riwayats',
'stats', 'totalAbsensi', 'hadirEfektif', 'persentaseHadir',
'trendData', 'trendLabel',
'dateFrom', 'dateTo', 'fromStr', 'toStr', 'preset', 'fromTab',
'absensiByDate'
));
}
}

View File

@ -1,88 +0,0 @@
<?php
// app/Http/Controllers/Santri/SantriBeritaController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\Berita;
use App\Models\Santri;
use App\Models\SantriKelas;
use Illuminate\Http\Request;
class SantriBeritaController extends Controller
{
// -- Helper: Ambil id_santri dari akun yang login --
private function getSantriId()
{
return auth('santri')->user()->id_santri;
}
/**
* Tampilkan daftar berita yang bisa diakses santri
*/
public function index(Request $request)
{
$idSantri = $this->getSantriId();
$santri = Santri::where('id_santri', $idSantri)
->select('id_santri')
->firstOrFail();
// -- Ambil id kelas santri --
$kelasIds = SantriKelas::where('id_santri', $santri->id_santri)
->pluck('id_kelas')->toArray();
$berita = Berita::query()
->select(['id', 'id_berita', 'judul', 'konten', 'penulis', 'gambar', 'created_at'])
->where('status', 'published')
->where(function($query) use ($kelasIds) {
$query->where('target_berita', 'semua');
if (!empty($kelasIds)) {
$query->orWhere(function($q) use ($kelasIds) {
$q->where('target_berita', 'kelas_tertentu');
foreach ($kelasIds as $kelasId) {
$q->orWhereJsonContains('target_kelas', $kelasId);
}
});
}
})
->orderBy('created_at', 'desc')
->paginate(12);
return view('santri.berita.index', compact('berita', 'santri'));
}
/**
* Tampilkan detail berita
*/
public function show($id_berita)
{
$idSantri = $this->getSantriId();
$santri = Santri::where('id_santri', $idSantri)
->select('id_santri')
->firstOrFail();
$kelasIds = SantriKelas::where('id_santri', $santri->id_santri)
->pluck('id_kelas')->toArray();
$berita = Berita::where('id_berita', $id_berita)
->where('status', 'published')
->where(function($query) use ($kelasIds) {
$query->where('target_berita', 'semua');
if (!empty($kelasIds)) {
$query->orWhere(function($q) use ($kelasIds) {
$q->where('target_berita', 'kelas_tertentu');
foreach ($kelasIds as $kelasId) {
$q->orWhereJsonContains('target_kelas', $kelasId);
}
});
}
})
->firstOrFail();
return view('santri.berita.show', compact('berita', 'santri'));
}
}

View File

@ -1,171 +0,0 @@
<?php
// app/Http/Controllers/Santri/SantriCapaianController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\Capaian;
use App\Models\Santri;
use App\Models\Semester;
use App\Services\CapaianAccessService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class SantriCapaianController extends Controller
{
private function getSantriId()
{
return auth('santri')->user()->id_santri;
}
public function index(Request $request)
{
$idSantri = $this->getSantriId();
// Ambil data santri
$santri = Cache::remember("santri_{$idSantri}_profile", 600, function () use ($idSantri) {
return Santri::where('id_santri', $idSantri)
->with(['kelasPrimary.kelas'])
->select('id_santri', 'nama_lengkap', 'nis', 'status')
->firstOrFail();
});
$semesterAktif = Semester::aktif()->first();
$selectedSemester = $request->input('id_semester',
$semesterAktif ? $semesterAktif->id_semester : null
);
// Capaian untuk tab Ringkasan / Daftar / Grafik (filter semester)
$query = Capaian::with([
'materi:id_materi,nama_kitab,kategori,total_halaman,halaman_mulai,halaman_akhir',
'semester:id_semester,nama_semester',
])
->where('id_santri', $idSantri)
->select('id', 'id_capaian', 'id_santri', 'id_materi',
'id_semester', 'halaman_selesai', 'persentase', 'tanggal_input');
if ($selectedSemester) {
$query->where('id_semester', $selectedSemester);
}
$capaians = $query->orderBy('tanggal_input', 'desc')->get();
// Statistik umum
$totalCapaian = $capaians->count();
$rataRataPersentase = $capaians->avg('persentase') ?? 0;
$materiSelesai = $capaians->where('persentase', '>=', 100)->count();
// Statistik per kategori
$statistikKategori = [
"Al-Qur'an" => ['count' => 0, 'avg' => 0, 'selesai' => 0],
'Hadist' => ['count' => 0, 'avg' => 0, 'selesai' => 0],
'Materi Tambahan' => ['count' => 0, 'avg' => 0, 'selesai' => 0],
];
foreach ($capaians as $capaian) {
$kat = $capaian->materi->kategori ?? 'Materi Tambahan';
if (!isset($statistikKategori[$kat])) continue;
$statistikKategori[$kat]['count']++;
$statistikKategori[$kat]['avg'] += $capaian->persentase;
if ($capaian->persentase >= 100) $statistikKategori[$kat]['selesai']++;
}
foreach ($statistikKategori as $kat => $data) {
if ($data['count'] > 0) {
$statistikKategori[$kat]['avg'] = round($data['avg'] / $data['count'], 2);
}
}
// Distribusi persentase
$distribusiPersentase = [
'0-25%' => $capaians->filter(fn($c) => $c->persentase >= 0 && $c->persentase <= 25)->count(),
'26-50%' => $capaians->filter(fn($c) => $c->persentase > 25 && $c->persentase <= 50)->count(),
'51-75%' => $capaians->filter(fn($c) => $c->persentase > 50 && $c->persentase <= 75)->count(),
'76-99%' => $capaians->filter(fn($c) => $c->persentase > 75 && $c->persentase < 100)->count(),
'100%' => $capaians->where('persentase', '>=', 100)->count(),
];
// PREDIKSI: ambil SEMUA capaian tanpa filter semester
$allCapaians = Capaian::with([
'materi:id_materi,nama_kitab,kategori',
'semester:id_semester,nama_semester,tahun_ajaran,periode',
])
->where('id_santri', $idSantri)
->select('id', 'id_santri', 'id_materi', 'id_semester', 'persentase')
->get();
// Susun history per semester (urut cronologis)
$allSemesters = Semester::orderBy('tahun_ajaran')->orderBy('periode')->get();
$historyData = [];
foreach ($allSemesters as $sem) {
$semCap = $allCapaians->where('id_semester', $sem->id_semester);
if ($semCap->isNotEmpty()) {
$historyData[] = [
'sem' => $sem->nama_semester,
'avg' => round($semCap->avg('persentase'), 2),
];
}
}
// Hitung growth rate (rata-rata kenaikan antar semester)
$growthRate = 0;
if (count($historyData) >= 2) {
$diffs = [];
for ($i = 1; $i < count($historyData); $i++) {
$diffs[] = $historyData[$i]['avg'] - $historyData[$i - 1]['avg'];
}
$growthRate = round(array_sum($diffs) / count($diffs), 2);
} elseif (count($historyData) === 1) {
$growthRate = round($historyData[0]['avg'], 2);
}
$progressHistory = [
'history' => $historyData,
'growth_rate' => $growthRate,
'all_capaians' => $allCapaians,
];
// Semester dropdown
$semesters = Semester::select('id_semester', 'nama_semester', 'tahun_ajaran')
->orderBy('tahun_ajaran', 'desc')
->orderBy('periode', 'desc')
->get();
// Status akses input capaian mandiri
$capaianAccessOpen = CapaianAccessService::isOpen();
$capaianAccessConfig = CapaianAccessService::getConfig();
$capaianSisaWaktu = CapaianAccessService::getSisaWaktu();
return view('santri.capaian.index', compact(
'santri',
'capaians',
'totalCapaian',
'rataRataPersentase',
'materiSelesai',
'statistikKategori',
'distribusiPersentase',
'progressHistory',
'semesters',
'selectedSemester',
'semesterAktif',
'capaianAccessOpen',
'capaianAccessConfig',
'capaianSisaWaktu'
));
}
public function show($id)
{
$idSantri = $this->getSantriId();
$capaian = Capaian::with([
'materi:id_materi,nama_kitab,kategori,halaman_mulai,halaman_akhir,total_halaman',
'semester:id_semester,nama_semester,tahun_ajaran',
'santri:id_santri,nama_lengkap,nis',
])
->where('id_santri', $idSantri)
->findOrFail($id);
return view('santri.capaian.show', compact('capaian'));
}
}

View File

@ -1,180 +0,0 @@
<?php
// app/Http/Controllers/Santri/SantriCapaianInputController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\Capaian;
use App\Models\Materi;
use App\Models\Semester;
use App\Models\Santri;
use App\Services\CapaianAccessService;
use Illuminate\Http\Request;
class SantriCapaianInputController extends Controller
{
private function getSantri(): Santri
{
$idSantri = auth('santri')->user()->id_santri;
return Santri::where('id_santri', $idSantri)
->with(['kelasSantri.kelas'])
->firstOrFail();
}
/**
* Form input capaian untuk santri.
* GET /santri/capaian/input
*/
public function create(Request $request)
{
// Cek apakah akses sedang dibuka
if (!CapaianAccessService::isOpen()) {
return redirect()->route('santri.capaian.index')
->with('error', 'Saat ini belum ada jadwal input capaian. Silakan tunggu informasi dari admin.');
}
$santri = $this->getSantri();
$accessConfig = CapaianAccessService::getConfig();
$sisaWaktu = CapaianAccessService::getSisaWaktu();
// Ambil semester yang berlaku
$idSemesterConfig = $accessConfig['id_semester'] ?? null;
if ($idSemesterConfig) {
$semesterAktif = Semester::where('id_semester', $idSemesterConfig)->first();
} else {
$semesterAktif = Semester::aktif()->first();
}
// Materi sesuai kelas santri
$kelasNames = $santri->kelasSantri->map(fn($sk) => $sk->kelas?->nama_kelas)->filter()->unique()->toArray();
$materiOptions = Materi::whereIn('kelas', $kelasNames ?: [''])
->orderBy('kategori')->orderBy('nama_kitab')->get();
// Capaian yang sudah ada di semester ini
$existingCapaians = [];
if ($semesterAktif) {
$existingCapaians = Capaian::where('id_santri', $santri->id_santri)
->where('id_semester', $semesterAktif->id_semester)
->pluck('persentase', 'id_materi')
->toArray();
}
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
return view('santri.capaian.input', compact(
'santri', 'semesterAktif', 'semesters', 'materiOptions',
'existingCapaians', 'accessConfig', 'sisaWaktu'
));
}
/**
* Simpan/update capaian oleh santri.
* POST /santri/capaian/input
*/
public function store(Request $request)
{
// Double-check akses masih terbuka
if (!CapaianAccessService::isOpen()) {
return redirect()->route('santri.capaian.index')
->with('error', 'Waktu input capaian telah berakhir.');
}
$santri = $this->getSantri();
$validated = $request->validate([
'id_materi' => 'required|exists:materi,id_materi',
'id_semester' => 'required|exists:semester,id_semester',
'halaman_selesai'=> 'required|string',
'catatan' => 'nullable|string|max:500',
'tanggal_input' => 'required|date',
]);
// Pastikan semester yang dikirim sesuai dengan yang diizinkan
$accessConfig = CapaianAccessService::getConfig();
if (!empty($accessConfig['id_semester']) && $accessConfig['id_semester'] !== $validated['id_semester']) {
return back()->with('error', 'Semester tidak sesuai dengan jadwal input yang dibuka admin.');
}
// Validasi materi sesuai kelas santri
$kelasNames = $santri->kelasSantri->map(fn($sk) => $sk->kelas?->nama_kelas)->filter()->unique()->toArray();
$materi = Materi::where('id_materi', $validated['id_materi'])
->whereIn('kelas', $kelasNames ?: [''])->first();
if (!$materi) {
return back()->with('error', 'Materi tidak sesuai dengan kelas Anda.');
}
// Upsert capaian (create or update)
$existing = Capaian::where('id_santri', $santri->id_santri)
->where('id_materi', $validated['id_materi'])
->where('id_semester', $validated['id_semester'])
->first();
if ($existing) {
$existing->update([
'halaman_selesai' => $validated['halaman_selesai'],
'catatan' => $validated['catatan'],
'tanggal_input' => $validated['tanggal_input'],
]);
$msg = "Capaian {$materi->nama_kitab} berhasil diperbarui.";
} else {
Capaian::create([
'id_santri' => $santri->id_santri,
'id_materi' => $validated['id_materi'],
'id_semester' => $validated['id_semester'],
'halaman_selesai'=> $validated['halaman_selesai'],
'catatan' => $validated['catatan'],
'tanggal_input' => $validated['tanggal_input'],
]);
$msg = "Capaian {$materi->nama_kitab} berhasil disimpan.";
}
return redirect()->route('santri.capaian.input.create')
->with('success', $msg);
}
/**
* AJAX: Ambil detail materi + existing capaian santri ini.
* POST /santri/capaian/input/ajax/detail-materi
*/
public function ajaxDetailMateri(Request $request)
{
$santri = $this->getSantri();
$materi = Materi::where('id_materi', $request->id_materi)->first();
if (!$materi) return response()->json(['error' => 'Materi tidak ditemukan'], 404);
$existing = null;
if ($request->filled('id_semester')) {
$existing = Capaian::where('id_santri', $santri->id_santri)
->where('id_materi', $request->id_materi)
->where('id_semester', $request->id_semester)
->first();
}
return response()->json([
'materi' => $materi,
'existing_capaian' => $existing,
]);
}
/**
* AJAX: Hitung persentase preview.
*/
public function ajaxHitungPersentase(Request $request)
{
if (empty($request->halaman_selesai) || empty($request->id_materi)) {
return response()->json(['persentase' => 0, 'jumlah' => 0]);
}
try {
$persentase = Capaian::calculatePersentase($request->halaman_selesai, $request->id_materi);
$pages = Capaian::parseHalamanSelesai($request->halaman_selesai);
return response()->json([
'persentase' => number_format($persentase, 2),
'jumlah' => count($pages),
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}

View File

@ -1,144 +0,0 @@
<?php
// app/Http/Controllers/Santri/SantriKepulanganController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\Kepulangan;
use App\Models\Santri;
use Illuminate\Http\Request;
use Carbon\Carbon;
class SantriKepulanganController extends Controller
{
// -- Helper: Ambil id_santri dari akun yang login --
private function getSantriId()
{
return auth('santri')->user()->id_santri;
}
/**
* Tampilkan riwayat kepulangan santri yang sedang login
*/
public function index(Request $request)
{
$idSantri = $this->getSantriId();
// -- Ambil data santri (tanpa kolom 'kelas' yang mungkin tidak ada) --
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
// -- Tahun untuk filter --
$tahunSekarang = $request->filled('tahun') ? $request->tahun : Carbon::now()->year;
// -- Query riwayat kepulangan --
$query = Kepulangan::query()
->where('id_santri', $santri->id_santri)
->whereYear('tanggal_pulang', $tahunSekarang);
// -- Filter status jika ada --
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// -- Urutkan terbaru dan paginate --
$riwayatKepulangan = $query->orderBy('tanggal_pulang', 'desc')
->paginate(10)
->appends($request->all());
// -- Hitung statistik tahun ini --
$allKepulanganTahunIni = Kepulangan::where('id_santri', $santri->id_santri)
->whereYear('tanggal_pulang', $tahunSekarang)
->get();
$statistik = [
'total_izin' => $allKepulanganTahunIni->count(),
'disetujui' => $allKepulanganTahunIni->where('status', 'Disetujui')->count(),
'ditolak' => $allKepulanganTahunIni->where('status', 'Ditolak')->count(),
'menunggu' => $allKepulanganTahunIni->where('status', 'Menunggu')->count(),
'selesai' => $allKepulanganTahunIni->where('status', 'Selesai')->count(),
'total_hari' => $allKepulanganTahunIni->whereIn('status', ['Disetujui', 'Selesai'])->sum('durasi_izin'),
];
$statistik['sisa_kuota'] = max(0, 12 - $statistik['total_hari']);
$statistik['over_limit'] = $statistik['total_hari'] > 12;
$statistik['persen_kuota'] = min(100, round(($statistik['total_hari'] / 12) * 100));
// -- Cek apakah sedang aktif pulang --
$sedangPulang = Kepulangan::where('id_santri', $santri->id_santri)
->where('status', 'Disetujui')
->whereDate('tanggal_pulang', '<=', Carbon::today())
->whereDate('tanggal_kembali', '>=', Carbon::today())
->first();
// -- Cek apakah ada yang terlambat --
$terlambat = Kepulangan::where('id_santri', $santri->id_santri)
->where('status', 'Disetujui')
->whereDate('tanggal_kembali', '<', Carbon::today())
->first();
// -- Data untuk filter --
$statusOptions = [
'Menunggu' => 'Menunggu Approval',
'Disetujui' => 'Disetujui',
'Ditolak' => 'Ditolak',
'Selesai' => 'Selesai'
];
// -- Tahun options (5 tahun terakhir) --
$tahunOptions = range(Carbon::now()->year, Carbon::now()->year - 4);
return view('santri.kepulangan.index', compact(
'riwayatKepulangan',
'santri',
'statistik',
'statusOptions',
'tahunOptions',
'tahunSekarang',
'sedangPulang',
'terlambat'
));
}
/**
* Tampilkan detail kepulangan
*/
public function show($id_kepulangan)
{
$idSantri = $this->getSantriId();
$santri = Santri::where('id_santri', $idSantri)->firstOrFail();
// -- Ambil data kepulangan dengan validasi kepemilikan --
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)
->where('id_santri', $santri->id_santri)
->firstOrFail();
// -- Hitung total hari izin tahun ini --
$tahunSekarang = Carbon::now()->year;
$totalHariTahunIni = Kepulangan::where('id_santri', $santri->id_santri)
->whereIn('status', ['Disetujui', 'Selesai'])
->whereYear('tanggal_pulang', $tahunSekarang)
->sum('durasi_izin');
$sisaKuota = max(0, 12 - $totalHariTahunIni);
$persenKuota = min(100, round(($totalHariTahunIni / 12) * 100));
// -- Riwayat kepulangan lain tahun ini --
$riwayatLain = Kepulangan::where('id_santri', $santri->id_santri)
->where('id_kepulangan', '!=', $id_kepulangan)
->whereYear('tanggal_pulang', $tahunSekarang)
->whereIn('status', ['Disetujui', 'Selesai'])
->orderBy('tanggal_pulang', 'desc')
->limit(5)
->get();
return view('santri.kepulangan.show', compact(
'kepulangan',
'santri',
'totalHariTahunIni',
'sisaKuota',
'persenKuota',
'riwayatLain'
));
}
}

View File

@ -1,163 +0,0 @@
<?php
// app/Http/Controllers/Santri/SantriKesehatanController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\KesehatanSantri;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class SantriKesehatanController extends Controller
{
private function getSantriId()
{
return auth('santri')->user()->id_santri;
}
/**
* Tampilkan riwayat kesehatan santri yang sedang login
*/
public function index(Request $request)
{
$idSantri = $this->getSantriId();
// ✅ Fix: hapus 'kelas' dari select, tambah eager load kelasPrimary
$santri = Santri::with('kelasPrimary.kelas')
->where('id_santri', $idSantri)
->select('id_santri', 'nama_lengkap', 'jenis_kelamin', 'status')
->firstOrFail();
// -- Tentukan range tanggal --
$tanggalDari = $request->filled('tanggal_dari')
? Carbon::parse($request->tanggal_dari)
: Carbon::now()->startOfMonth();
$tanggalSampai = $request->filled('tanggal_sampai')
? Carbon::parse($request->tanggal_sampai)
: Carbon::now()->endOfMonth();
// -- Validasi tanggal --
if ($tanggalSampai->lt($tanggalDari)) {
return back()->withErrors([
'tanggal_sampai' => 'Tanggal sampai harus lebih besar dari tanggal dari.'
])->withInput();
}
// -- Statistik berdasarkan filter tanggal --
$baseQuery = KesehatanSantri::where('id_santri', $idSantri)
->whereBetween('tanggal_masuk', [
$tanggalDari->format('Y-m-d'),
$tanggalSampai->format('Y-m-d'),
]);
$statistik = [
'total_kunjungan' => (clone $baseQuery)->count(),
'sedang_dirawat' => (clone $baseQuery)->where('status', 'dirawat')->count(),
'sembuh' => (clone $baseQuery)->where('status', 'sembuh')->count(),
'izin' => (clone $baseQuery)->where('status', 'izin')->count(),
];
// -- Cek apakah SAAT INI sedang dirawat (semua waktu, bukan filter) --
$sedangDirawatSekarang = KesehatanSantri::where('id_santri', $idSantri)
->where('status', 'dirawat')
->latest('tanggal_masuk')
->first();
// -- Data grafik: kunjungan per bulan (6 bulan terakhir) --
$dataGrafik = KesehatanSantri::where('id_santri', $idSantri)
->where('tanggal_masuk', '>=', Carbon::now()->subMonths(6)->startOfMonth())
->select(
DB::raw('YEAR(tanggal_masuk) as tahun'),
DB::raw('MONTH(tanggal_masuk) as bulan'),
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN status = "sembuh" THEN 1 ELSE 0 END) as sembuh'),
DB::raw('SUM(CASE WHEN status = "dirawat" THEN 1 ELSE 0 END) as dirawat'),
DB::raw('SUM(CASE WHEN status = "izin" THEN 1 ELSE 0 END) as izin')
)
->groupBy('tahun', 'bulan')
->orderBy('tahun')
->orderBy('bulan')
->get()
->map(fn($item) => [
'label' => Carbon::createFromDate($item->tahun, $item->bulan, 1)
->locale('id')->isoFormat('MMM YY'),
'total' => $item->total,
'sembuh' => $item->sembuh,
'dirawat' => $item->dirawat,
'izin' => $item->izin,
]);
// -- Statistik total keseluruhan (all time) --
$totalAllTime = KesehatanSantri::where('id_santri', $idSantri)->count();
$totalHariDirawat = KesehatanSantri::where('id_santri', $idSantri)->get()
->sum('lama_dirawat');
// -- Query riwayat dengan filter --
$query = KesehatanSantri::where('id_santri', $idSantri)
->whereBetween('tanggal_masuk', [
$tanggalDari->format('Y-m-d'),
$tanggalSampai->format('Y-m-d'),
]);
if ($request->filled('status')) {
$query->where('status', $request->status);
}
$riwayatKesehatan = $query->orderBy('tanggal_masuk', 'desc')
->paginate(10)
->appends($request->all());
$statusOptions = [
'dirawat' => 'Sedang Dirawat',
'sembuh' => 'Sembuh',
'izin' => 'Izin Sakit',
];
return view('santri.kesehatan.index', compact(
'riwayatKesehatan',
'santri',
'statistik',
'statusOptions',
'tanggalDari',
'tanggalSampai',
'sedangDirawatSekarang',
'dataGrafik',
'totalAllTime',
'totalHariDirawat'
));
}
/**
* Tampilkan detail riwayat kesehatan
*/
public function show($id)
{
$idSantri = $this->getSantriId();
// ✅ Fix: hapus 'kelas' dari select
$santri = Santri::with('kelasPrimary.kelas')
->where('id_santri', $idSantri)
->select('id_santri', 'nama_lengkap', 'jenis_kelamin', 'status')
->firstOrFail();
$kesehatanSantri = KesehatanSantri::where('id', $id)
->where('id_santri', $idSantri)
->firstOrFail();
// -- Riwayat lain santri ini (untuk konteks) --
$riwayatLain = KesehatanSantri::where('id_santri', $idSantri)
->where('id', '!=', $id)
->orderBy('tanggal_masuk', 'desc')
->take(3)
->get();
return view('santri.kesehatan.show', compact(
'kesehatanSantri',
'santri',
'riwayatLain'
));
}
}

View File

@ -1,109 +0,0 @@
<?php
// app/Http/Controllers/Santri/SantriPelanggaranController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\RiwayatPelanggaran;
use App\Models\KategoriPelanggaran;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class SantriPelanggaranController extends Controller
{
// -- Helper: Ambil id_santri dari akun yang login --
private function getSantriId()
{
return auth('santri')->user()->id_santri;
}
/**
* Tampilkan daftar riwayat pelanggaran santri yang sedang login
*/
public function index(Request $request)
{
$idSantri = $this->getSantriId();
// -- Query riwayat pelanggaran dengan relasi --
$query = RiwayatPelanggaran::with(['kategori:id,id_kategori,nama_pelanggaran,poin'])
->where('id_santri', $idSantri)
->select([
'id',
'id_riwayat',
'id_santri',
'id_kategori',
'tanggal',
'poin',
'keterangan',
'created_at'
]);
// -- Filter berdasarkan tanggal --
if ($request->filled('tanggal_mulai')) {
$query->whereDate('tanggal', '>=', $request->tanggal_mulai);
}
if ($request->filled('tanggal_selesai')) {
$query->whereDate('tanggal', '<=', $request->tanggal_selesai);
}
// -- Filter bulan ini --
if ($request->has('bulan_ini') && $request->bulan_ini == '1') {
$query->bulanIni();
}
// -- Urutkan dari terbaru --
$riwayat = $query->terbaru()->paginate(15);
// -- Statistik pelanggaran santri --
$totalPelanggaran = RiwayatPelanggaran::where('id_santri', $idSantri)->count();
$totalPoin = RiwayatPelanggaran::where('id_santri', $idSantri)->sum('poin');
$pelanggaranBulanIni = RiwayatPelanggaran::where('id_santri', $idSantri)
->bulanIni()
->count();
return view('santri.pelanggaran.index', compact(
'riwayat',
'totalPelanggaran',
'totalPoin',
'pelanggaranBulanIni'
));
}
/**
* Tampilkan detail satu riwayat pelanggaran
*/
public function show(RiwayatPelanggaran $riwayatPelanggaran)
{
// -- Validasi: pastikan pelanggaran milik santri yang login --
if ($riwayatPelanggaran->id_santri !== $this->getSantriId()) {
abort(403, 'Anda tidak memiliki akses ke data ini.');
}
// -- Load relasi kategori --
$riwayatPelanggaran->load('kategori:id,id_kategori,nama_pelanggaran,poin');
return view('santri.pelanggaran.show', compact('riwayatPelanggaran'));
}
/**
* Tampilkan daftar semua kategori pelanggaran beserta poinnya
*/
public function kategoriList()
{
// -- Cache daftar kategori selama 1 jam --
$kategoriList = Cache::remember('kategori_pelanggaran_list', 3600, function () {
return KategoriPelanggaran::select([
'id',
'id_kategori',
'nama_pelanggaran',
'poin'
])
->orderBy('poin', 'desc')
->orderBy('nama_pelanggaran', 'asc')
->get();
});
return view('santri.pelanggaran.kategori', compact('kategoriList'));
}
}

View File

@ -1,72 +0,0 @@
<?php
// app/Http/Controllers/Santri/SantriPembinaanController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\PembinaanSanksi;
use Illuminate\Support\Facades\Cache;
class SantriPembinaanController extends Controller
{
/**
* Tampilkan daftar konten pembinaan & sanksi
*/
public function index()
{
// Cache 30 menit karena konten jarang berubah
$pembinaanList = Cache::remember('pembinaan_sanksi_aktif', 1800, function () {
return PembinaanSanksi::aktif()
->byUrutan()
->select(['id', 'id_pembinaan', 'judul', 'konten', 'urutan', 'updated_at'])
->get();
});
return view('santri.pembinaan.index', compact('pembinaanList'));
}
/**
* Tampilkan detail satu konten pembinaan & sanksi
*/
public function show($id_pembinaan)
{
$pembinaan = PembinaanSanksi::aktif()
->where('id_pembinaan', $id_pembinaan)
->firstOrFail();
// Konten sebelum dan sesudah untuk navigasi
$prev = PembinaanSanksi::aktif()
->byUrutan()
->where('urutan', '<', $pembinaan->urutan)
->orWhere(function ($q) use ($pembinaan) {
$q->where('urutan', $pembinaan->urutan)
->where('id', '<', $pembinaan->id);
})
->orderBy('urutan', 'desc')
->first();
$next = PembinaanSanksi::aktif()
->byUrutan()
->where('urutan', '>', $pembinaan->urutan)
->orWhere(function ($q) use ($pembinaan) {
$q->where('urutan', $pembinaan->urutan)
->where('id', '>', $pembinaan->id);
})
->orderBy('urutan', 'asc')
->first();
// Semua konten untuk sidebar
$pembinaanList = Cache::remember('pembinaan_sanksi_aktif', 1800, function () {
return PembinaanSanksi::aktif()->byUrutan()
->select(['id', 'id_pembinaan', 'judul', 'urutan'])
->get();
});
return view('santri.pembinaan.show', compact(
'pembinaan',
'pembinaanList',
'prev',
'next'
));
}
}

View File

@ -1,52 +0,0 @@
<?php
// app/Http/Controllers/Santri/SantriProfileController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\Santri;
use Illuminate\Support\Facades\Cache;
class SantriProfileController extends Controller
{
private function getSantriId()
{
return auth('santri')->user()->id_santri;
}
/**
* Tampilkan halaman profil santri yang sedang login (READ ONLY)
*/
public function index()
{
$idSantri = $this->getSantriId();
$santri = Cache::remember(
'santri_profile_' . $idSantri,
600,
function () use ($idSantri) {
return Santri::with(['kelasSantri.kelas.kelompok', 'kelasPrimary.kelas.kelompok'])
->where('id_santri', $idSantri)
->select([
'id',
'id_santri',
'nis',
'nama_lengkap',
'jenis_kelamin',
'status',
'alamat_santri',
'daerah_asal',
'nama_orang_tua',
'nomor_hp_ortu',
'rfid_uid',
'foto',
'created_at',
'updated_at',
])
->firstOrFail();
}
);
return view('santri.profil.index', compact('santri'));
}
}

View File

@ -1,118 +0,0 @@
<?php
// app/Http/Controllers/Santri/SantriUangSakuController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Models\UangSaku;
use App\Models\Santri;
class SantriUangSakuController extends Controller
{
private function getSantriId()
{
return auth('santri')->user()->id_santri;
}
/**
* Tampilkan riwayat uang saku santri yang sedang login
*/
public function index(Request $request)
{
try {
$idSantri = $this->getSantriId();
$santri = Santri::with(['kelasPrimary.kelas'])
->where('id_santri', $idSantri)
->firstOrFail();
// -- Query uang saku --
$query = UangSaku::where('id_santri', $idSantri);
// -- Filter jenis transaksi --
if ($request->filled('jenis_transaksi')) {
$query->where('jenis_transaksi', $request->jenis_transaksi);
}
// -- Filter tanggal --
if ($request->filled('tanggal_dari')) {
$query->whereDate('tanggal_transaksi', '>=', $request->tanggal_dari);
}
if ($request->filled('tanggal_sampai')) {
$query->whereDate('tanggal_transaksi', '<=', $request->tanggal_sampai);
}
// -- Search keterangan --
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('keterangan', 'like', "%{$search}%")
->orWhere('id_uang_saku', 'like', "%{$search}%");
});
}
$riwayatUangSaku = $query->orderBy('tanggal_transaksi', 'desc')
->orderBy('created_at', 'desc')
->paginate(15)
->withQueryString();
// -- Statistik: bulan ini atau sesuai filter tanggal --
$statistikQuery = UangSaku::where('id_santri', $idSantri);
if ($request->filled('tanggal_dari') || $request->filled('tanggal_sampai')) {
if ($request->filled('tanggal_dari')) {
$statistikQuery->whereDate('tanggal_transaksi', '>=', $request->tanggal_dari);
}
if ($request->filled('tanggal_sampai')) {
$statistikQuery->whereDate('tanggal_transaksi', '<=', $request->tanggal_sampai);
}
} else {
$statistikQuery->whereMonth('tanggal_transaksi', now()->month)
->whereYear('tanggal_transaksi', now()->year);
}
$totalPemasukan = (clone $statistikQuery)->where('jenis_transaksi', 'pemasukan')->sum('nominal');
$totalPengeluaran = (clone $statistikQuery)->where('jenis_transaksi', 'pengeluaran')->sum('nominal');
$saldoTerakhir = $santri->saldo_uang_saku;
return view('santri.uang-saku.index', compact(
'riwayatUangSaku',
'santri',
'totalPemasukan',
'totalPengeluaran',
'saldoTerakhir'
));
} catch (\Exception $e) {
Log::error('Error Riwayat Uang Saku: ' . $e->getMessage());
return back()->with('error', 'Terjadi kesalahan saat memuat data uang saku.');
}
}
/**
* Tampilkan detail satu transaksi
*/
public function show($id)
{
try {
$idSantri = $this->getSantriId();
// Pastikan transaksi ini milik santri yang login
$transaksi = UangSaku::where('id', $id)
->where('id_santri', $idSantri)
->with(['santri' => function ($q) {
$q->with('kelasPrimary.kelas')
->select('id_santri', 'nama_lengkap');
}])
->firstOrFail();
return view('santri.uang-saku.show', compact('transaksi'));
} catch (\Exception $e) {
Log::error('Error Detail Uang Saku: ' . $e->getMessage());
return back()->with('error', 'Transaksi tidak ditemukan atau Anda tidak memiliki akses.');
}
}
}

View File

@ -1,75 +0,0 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// AuthenticateSession dipindah dari global ke alias 'auth.session'
// agar tidak menyebabkan redirect loop pada user tanpa remember_token
// \Illuminate\Session\Middleware\AuthenticateSession::class,
// ClearStuckSession dihapus karena menyebabkan session flush setelah login
// \App\Http\Middleware\ClearStuckSession::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'role' => \App\Http\Middleware\Role::class,
'santri.auth' => \App\Http\Middleware\CheckSantriAuth::class,
];
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}

View File

@ -1,42 +0,0 @@
<?php
// app/Http/Middleware/CheckSantriAuth.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class CheckSantriAuth
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
if (!Auth::guard('santri')->check()) {
// Jika request AJAX/API, return JSON
if ($request->expectsJson()) {
return response()->json(['message' => 'Unauthenticated.'], 401);
}
return redirect()->route('santri.login')
->with('error', 'Silakan login terlebih dahulu.');
}
$account = Auth::guard('santri')->user();
// PERBAIKAN: pastikan akun masih valid dan punya id_santri
if (!$account || !$account->id_santri) {
Auth::guard('santri')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('santri.login')
->with('error', 'Akun tidak valid. Silakan login ulang.');
}
return $next($request);
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class ClearStuckSession
{
public function handle(Request $request, Closure $next): Response
{
// Auto-clear stuck session untuk guest user
if (!Auth::check() && $request->session()->has('_token')) {
$lastActivity = $request->session()->get('last_activity', 0);
$now = time();
// Jika session idle lebih dari 5 menit, flush
if (($now - $lastActivity) > 300) {
$request->session()->flush();
$request->session()->regenerate();
}
}
// Update last activity timestamp
$request->session()->put('last_activity', time());
return $next($request);
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$path = $request->path();
if (str_starts_with($path, 'santri')) {
// Halaman guest santri → redirect hanya jika guard santri aktif
if (Auth::guard('santri')->check()) {
return redirect()->route('santri.dashboard');
}
} else {
// Halaman guest admin → redirect hanya jika guard web aktif
if (Auth::check()) {
return redirect()->route('admin.dashboard');
}
}
return $next($request);
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class Role
{
/**
* Handle an incoming request.
* Menerima daftar role yang diizinkan sebagai parameter middleware.
* Contoh: role:super_admin,akademik,pamong
*
* Laravel memecah parameter setelah ':' menjadi argumen terpisah per koma,
* sehingga kita harus gunakan variadic (...$roles) bukan string tunggal.
*/
public function handle(Request $request, Closure $next, string ...$roles): Response
{
// -- Cek apakah pengguna sudah login --
if (!Auth::check()) {
return redirect()->route('admin.login');
}
// -- Cek apakah role pengguna termasuk dalam daftar yang diizinkan --
if (!in_array(Auth::user()->role, $roles)) {
return redirect()->route('admin.dashboard')
->with('error', 'Anda tidak memiliki akses ke halaman tersebut.');
}
return $next($request);
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
// JANGAN tambahkan apa-apa disini, biarkan kosong
];
/**
* Add CSRF token to response headers
*/
protected function addCookieToResponse($request, $response)
{
$config = config('session');
if ($config['driver'] === 'array') {
return $response;
}
$response->headers->setCookie(
$this->newCookie($request, $config)
);
return $response;
}
}

View File

@ -1,85 +0,0 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)],
];
}
}

View File

@ -1,43 +0,0 @@
<?php
// app/Mail/OtpMail.php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OtpMail extends Mailable
{
use Queueable, SerializesModels;
public string $otp;
public string $nama;
public function __construct(string $otp, string $nama)
{
$this->otp = $otp;
$this->nama = $nama;
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Kode OTP Reset Password - SIM PKPPS',
);
}
public function content(): Content
{
return new Content(
view: 'emails.otp',
);
}
public function attachments(): array
{
return [];
}
}

View File

@ -1,181 +0,0 @@
<?php
// app/Models/AbsensiKegiatan.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class AbsensiKegiatan extends Model
{
use HasFactory;
protected $fillable = [
'absensi_id',
'kegiatan_id',
'id_santri',
'tanggal',
'status',
'metode_absen', // ← BARU: 'Manual' | 'RFID' | 'Import_Mesin'
'konflik_catatan', // ← BARU: catatan resolusi konflik
'waktu_absen',
];
protected $casts = [
'tanggal' => 'date',
'waktu_absen' => 'datetime:H:i',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Auto-generate absensi_id (A001, A002...)
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->absensi_id)) {
$last = self::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->absensi_id, 1)) + 1 : 1;
$model->absensi_id = 'A' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
// Default metode_absen jika tidak diset
if (empty($model->metode_absen)) {
$model->metode_absen = 'Manual';
}
});
}
// ──────────────────────────────────────────────────────────
// RELASI
// ──────────────────────────────────────────────────────────
/**
* Relasi ke Santri
*/
public function santri()
{
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
}
/**
* Relasi ke Kegiatan
*/
public function kegiatan()
{
return $this->belongsTo(Kegiatan::class, 'kegiatan_id', 'kegiatan_id');
}
// ──────────────────────────────────────────────────────────
// SCOPES
// ──────────────────────────────────────────────────────────
/**
* Scope: Filter berdasarkan tanggal
*/
public function scopeTanggal($query, $tanggal)
{
return $query->whereDate('tanggal', $tanggal);
}
/**
* Scope: Filter berdasarkan kegiatan
*/
public function scopeKegiatan($query, $kegiatan_id)
{
return $query->where('kegiatan_id', $kegiatan_id);
}
/**
* Scope: Filter by date range
*/
public function scopeDateRange($query, $start, $end)
{
return $query->whereBetween('tanggal', [$start, $end]);
}
/**
* Scope: Filter by month
*/
public function scopeByMonth($query, $month, $year)
{
return $query->whereMonth('tanggal', $month)
->whereYear('tanggal', $year);
}
/**
* Scope: Filter by metode absen
*/
public function scopeByMetode($query, $metode)
{
return $query->where('metode_absen', $metode);
}
// ──────────────────────────────────────────────────────────
// ACCESSORS
// ──────────────────────────────────────────────────────────
/**
* Accessor: Status Badge HTML (untuk admin)
*/
public function getStatusBadgeAttribute()
{
$badges = [
'Hadir' => '<span class="badge badge-success"><i class="fas fa-check"></i> Hadir</span>',
'Izin' => '<span class="badge badge-warning"><i class="fas fa-info-circle"></i> Izin</span>',
'Sakit' => '<span class="badge badge-info"><i class="fas fa-heartbeat"></i> Sakit</span>',
'Alpa' => '<span class="badge badge-danger"><i class="fas fa-times"></i> Alpa</span>',
'Terlambat' => '<span class="badge" style="background:#FF9800;color:white;"><i class="fas fa-clock"></i> Terlambat</span>',
'Pulang' => '<span class="badge" style="background:#FFF3E0;color:#E65100;"><i class="fas fa-home"></i> Pulang</span>',
];
return $badges[$this->status] ?? $this->status;
}
/**
* Accessor: Metode Badge HTML (untuk tampilan tabel absensi)
* Manual=biru, RFID=hijau, Import_Mesin=oranye
*/
public function getMetodeBadgeAttribute()
{
$badges = [
'Manual' => '<span style="background:#DBEAFE;color:#1D4ED8;border-radius:8px;padding:2px 8px;font-size:11px;font-weight:600">✋ Manual</span>',
'RFID' => '<span style="background:#DCFCE7;color:#166534;border-radius:8px;padding:2px 8px;font-size:11px;font-weight:600">💳 RFID</span>',
'Import_Mesin' => '<span style="background:#FFF7ED;color:#C05621;border-radius:8px;padding:2px 8px;font-size:11px;font-weight:600">👆 Mesin</span>',
];
return $badges[$this->metode_absen] ?? $this->metode_absen;
}
/**
* Accessor: Tanggal Formatted (untuk view santri)
*/
public function getTanggalFormattedAttribute()
{
return Carbon::parse($this->tanggal)->locale('id')->isoFormat('dddd, D MMMM YYYY');
}
/**
* Accessor: Waktu Absen Formatted
*/
public function getWaktuAbsenFormattedAttribute()
{
return $this->waktu_absen ? Carbon::parse($this->waktu_absen)->format('H:i') : '-';
}
/**
* Accessor: Status Badge Class (CSS class only - untuk view santri)
*/
public function getStatusBadgeClassAttribute()
{
return match($this->status) {
'Hadir' => 'badge-success',
'Izin' => 'badge-info',
'Sakit' => 'badge-warning',
'Alpa' => 'badge-danger',
'Terlambat' => 'badge-warning',
'Pulang' => 'badge-secondary',
default => 'badge-secondary',
};
}
}

View File

@ -1,115 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Berita extends Model
{
use HasFactory;
protected $table = 'berita';
protected $fillable = [
'id_berita',
'judul',
'konten',
'penulis',
'gambar',
'status',
'target_berita',
'target_kelas',
];
protected $casts = [
'target_kelas' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Auto-generate ID Berita (B001, B002, ...)
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_berita)) {
$last = Berita::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_berita, 1)) + 1 : 1;
$model->id_berita = 'B' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
/**
* Relasi: Kelas yang ditargetkan (via JSON target_kelas berisi id kelas)
*/
public function kelasTertentu()
{
return Kelas::whereIn('id', $this->target_kelas ?? [])->get();
}
/**
* Accessor: Tanggal Formatted
*/
public function getTanggalFormattedAttribute()
{
return $this->created_at->format('d M Y');
}
/**
* Accessor: Status Badge
*/
public function getStatusBadgeAttribute()
{
return $this->status === 'published' ? 'badge-success' : 'badge-warning';
}
/**
* Accessor: Target Audience (untuk display)
*/
public function getTargetAudienceAttribute()
{
if ($this->target_berita === 'kelas_tertentu') {
$namaKelas = Kelas::whereIn('id', $this->target_kelas ?? [])
->pluck('nama_kelas')->toArray();
return 'Kelas: ' . (count($namaKelas) ? implode(', ', $namaKelas) : '-');
}
return match($this->target_berita) {
'semua' => 'Semua Santri',
default => '-'
};
}
/**
* Scope: Filter by status
*/
public function scopeStatus($query, $status)
{
return $query->where('status', $status);
}
/**
* Scope: Filter by target
*/
public function scopeTarget($query, $target)
{
return $query->where('target_berita', $target);
}
/**
* Scope: Search berita
*/
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('judul', 'like', "%{$search}%")
->orWhere('penulis', 'like', "%{$search}%")
->orWhere('id_berita', 'like', "%{$search}%");
});
}
}

View File

@ -1,220 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Capaian extends Model
{
use HasFactory;
protected $table = 'capaian';
protected $fillable = [
'id_capaian',
'id_santri',
'id_materi',
'id_semester',
'halaman_selesai',
'persentase',
'catatan',
'tanggal_input',
];
protected $casts = [
'persentase' => 'decimal:2',
'tanggal_input' => 'date',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Generator ID Kustom (CP001, CP002, ...)
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_capaian)) {
$last = Capaian::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_capaian, 2)) + 1 : 1;
$model->id_capaian = 'CP' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
// Auto-calculate persentase
$model->persentase = self::calculatePersentase($model->halaman_selesai, $model->id_materi);
});
static::updating(function ($model) {
// Recalculate persentase saat update
$model->persentase = self::calculatePersentase($model->halaman_selesai, $model->id_materi);
});
}
/**
* Relasi: Capaian belongs to Santri
*/
public function santri()
{
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
}
/**
* Relasi: Capaian belongs to Materi
*/
public function materi()
{
return $this->belongsTo(Materi::class, 'id_materi', 'id_materi');
}
/**
* Relasi: Capaian belongs to Semester
*/
public function semester()
{
return $this->belongsTo(Semester::class, 'id_semester', 'id_semester');
}
/**
* Parse halaman_selesai string menjadi array halaman
* Input: "1-10,16-21,40"
* Output: [1,2,3,...,10,16,17,...,21,40]
*/
public static function parseHalamanSelesai($rangeString)
{
// Handle empty string
if (empty($rangeString) || trim($rangeString) === '') {
return [];
}
$pages = [];
$ranges = explode(',', $rangeString);
foreach ($ranges as $range) {
$range = trim($range);
// Skip empty ranges
if (empty($range)) {
continue;
}
if (strpos($range, '-') !== false) {
// Range format: "1-10"
list($start, $end) = explode('-', $range);
$start = intval(trim($start));
$end = intval(trim($end));
for ($i = $start; $i <= $end; $i++) {
$pages[] = $i;
}
} else {
// Single page: "40"
$pageNum = intval($range);
if ($pageNum > 0) {
$pages[] = $pageNum;
}
}
}
return array_unique($pages);
}
/**
* Calculate persentase dari halaman_selesai
*/
public static function calculatePersentase($halamanSelesai, $idMateri)
{
if (empty($halamanSelesai)) {
return 0;
}
$materi = Materi::where('id_materi', $idMateri)->first();
if (!$materi || $materi->total_halaman == 0) {
return 0;
}
$pages = self::parseHalamanSelesai($halamanSelesai);
$jumlahHalamanSelesai = count($pages);
$persentase = ($jumlahHalamanSelesai / $materi->total_halaman) * 100;
// Batasi max 100%
return min($persentase, 100);
}
/**
* Get array halaman yang sudah selesai
*/
public function getPagesArrayAttribute()
{
return self::parseHalamanSelesai($this->halaman_selesai);
}
/**
* Get jumlah halaman yang sudah selesai
*/
public function getJumlahHalamanSelesaiAttribute()
{
return count($this->pages_array);
}
/**
* Accessor: Badge persentase dengan warna
*/
public function getPersentaseBadgeAttribute()
{
$persentase = $this->persentase;
if ($persentase >= 100) {
$class = 'badge-success';
$icon = 'fa-check-circle';
} elseif ($persentase >= 75) {
$class = 'badge-primary';
$icon = 'fa-battery-three-quarters';
} elseif ($persentase >= 50) {
$class = 'badge-warning';
$icon = 'fa-battery-half';
} elseif ($persentase >= 25) {
$class = 'badge-danger';
$icon = 'fa-battery-quarter';
} else {
$class = 'badge-secondary';
$icon = 'fa-battery-empty';
}
return sprintf(
'<span class="badge %s"><i class="fas %s"></i> %.2f%%</span>',
$class,
$icon,
$persentase
);
}
/**
* Scope: Filter by santri
*/
public function scopeBySantri($query, $idSantri)
{
return $query->where('id_santri', $idSantri);
}
/**
* Scope: Filter by semester
*/
public function scopeBySemester($query, $idSemester)
{
return $query->where('id_semester', $idSemester);
}
/**
* Scope: Filter by kategori materi
*/
public function scopeByKategori($query, $kategori)
{
return $query->whereHas('materi', function($q) use ($kategori) {
$q->where('kategori', $kategori);
});
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ImportMesinLog extends Model
{
protected $fillable = [
'user_id', 'jumlah_scan', 'berhasil',
'konflik_selesai', 'dilewati', 'no_santri',
];
public function user(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class);
}
}

View File

@ -1,66 +0,0 @@
<?php
// Models/KategoriKegiatan
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class KategoriKegiatan extends Model
{
use HasFactory;
protected $fillable = [
'kategori_id',
'nama_kategori',
'keterangan',
];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Auto-generate kategori_id (KT001, KT002...)
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->kategori_id)) {
$last = self::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->kategori_id, 2)) + 1 : 1;
$model->kategori_id = 'KT' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
/**
* Relasi ke Kegiatan (One to Many)
*/
public function kegiatans()
{
return $this->hasMany(Kegiatan::class, 'kategori_id', 'kategori_id');
}
/**
* Accessor: Total Kegiatan
*/
public function getTotalKegiatanAttribute()
{
return $this->kegiatans()->count();
}
/**
* Scope: Search
*/
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('nama_kategori', 'like', "%{$search}%")
->orWhere('kategori_id', 'like', "%{$search}%")
->orWhere('keterangan', 'like', "%{$search}%");
});
}
}

View File

@ -1,71 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class KategoriPelanggaran extends Model
{
use HasFactory;
protected $fillable = [
'id_kategori',
'id_klasifikasi',
'nama_pelanggaran',
'poin',
'kafaroh',
'is_active',
];
protected $casts = [
'poin' => 'integer',
'is_active' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_kategori)) {
$last = KategoriPelanggaran::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_kategori, 2)) + 1 : 1;
$model->id_kategori = 'KP' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
// Relasi: Pelanggaran belongsTo Klasifikasi
public function klasifikasi()
{
return $this->belongsTo(KlasifikasiPelanggaran::class, 'id_klasifikasi', 'id_klasifikasi');
}
// Relasi: Pelanggaran hasMany Riwayat
public function riwayatPelanggaran()
{
return $this->hasMany(RiwayatPelanggaran::class, 'id_kategori', 'id_kategori');
}
// Scope: Hanya yang aktif
public function scopeAktif($query)
{
return $query->where('is_active', true);
}
// Scope: Filter by klasifikasi
public function scopeByKlasifikasi($query, $idKlasifikasi)
{
return $query->where('id_klasifikasi', $idKlasifikasi);
}
// Accessor: Nama dengan klasifikasi
public function getNamaLengkapAttribute()
{
$klasifikasi = $this->klasifikasi ? $this->klasifikasi->nama_klasifikasi : 'Tanpa Klasifikasi';
return "[{$klasifikasi}] {$this->nama_pelanggaran}";
}
}

View File

@ -1,201 +0,0 @@
<?php
// Models/Kegiatan
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Kegiatan extends Model
{
use HasFactory;
protected $fillable = [
'kegiatan_id',
'kategori_id',
'nama_kegiatan',
'hari',
'waktu_mulai',
'waktu_selesai',
'materi',
'keterangan',
];
protected $casts = [
'waktu_mulai' => 'datetime:H:i',
'waktu_selesai' => 'datetime:H:i',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Auto-generate kegiatan_id (KG001, KG002...)
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->kegiatan_id)) {
$last = self::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->kegiatan_id, 2)) + 1 : 1;
$model->kegiatan_id = 'KG' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
/**
* Relasi ke Kategori
*/
public function kategori()
{
return $this->belongsTo(KategoriKegiatan::class, 'kategori_id', 'kategori_id');
}
/**
* Relasi ke Absensi (akan dibuat di tahap selanjutnya)
*/
public function absensis()
{
return $this->hasMany(AbsensiKegiatan::class, 'kegiatan_id', 'kegiatan_id');
}
// ==========================================
// RELASI SISTEM KELAS BARU
// ==========================================
/**
* Relasi: Kegiatan belongs to many Kelas (many-to-many through kegiatan_kelas)
*/
public function kelasKegiatan()
{
return $this->belongsToMany(Kelas::class, 'kegiatan_kelas', 'kegiatan_id', 'id_kelas', 'kegiatan_id', 'id')
->withTimestamps();
}
/**
* Relasi: Kegiatan memiliki banyak record kegiatan_kelas (hasMany)
*/
public function kegiatanKelasPivot()
{
return $this->hasMany(KegiatanKelas::class, 'kegiatan_id', 'kegiatan_id');
}
/**
* Scope: Filter berdasarkan hari
*/
public function scopeHari($query, $hari)
{
return $query->where('hari', $hari);
}
/**
* Scope: Search
*/
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('nama_kegiatan', 'like', "%{$search}%")
->orWhere('kegiatan_id', 'like', "%{$search}%")
->orWhere('materi', 'like', "%{$search}%");
});
}
/**
* Accessor: Waktu Lengkap
*/
public function getWaktuLengkapAttribute()
{
return date('H:i', strtotime($this->waktu_mulai)) . ' - ' .
date('H:i', strtotime($this->waktu_selesai));
}
// ==========================================
// HELPER METHODS SISTEM KELAS BARU
// ==========================================
/**
* Check apakah kegiatan untuk semua kelas (umum)
* Kegiatan dianggap umum jika tidak ada relasi ke kegiatan_kelas
*
* @return bool
*/
public function isForAllClasses()
{
return $this->kegiatanKelasPivot()->count() === 0;
}
/**
* Check apakah kegiatan untuk kelas tertentu
* Return true jika kegiatan umum ATAU ada relasi ke kelas tersebut
*
* @param int $id_kelas
* @return bool
*/
public function isForKelas($id_kelas)
{
// Jika kegiatan umum (tidak ada relasi kelas), semua kelas bisa
if ($this->isForAllClasses()) {
return true;
}
// Cek apakah ada relasi ke kelas tertentu
return $this->kelasKegiatan()->where('kelas.id', $id_kelas)->exists();
}
/**
* Get santri yang eligible untuk kegiatan ini
* - Jika umum: return all active santri
* - Jik a specific: return santri yang kelasnya match
*
* @param string|null $tahun_ajaran - Filter by tahun ajaran
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getEligibleSantris($tahun_ajaran = null)
{
if ($tahun_ajaran === null) {
$tahun_ajaran = SantriKelas::getCurrentAcademicYear();
}
// Jika kegiatan umum, return semua santri aktif
if ($this->isForAllClasses()) {
return Santri::where('status', 'Aktif');
}
// Jika specific, return santri yang kelasnya match
$kelasIds = $this->kelasKegiatan()->pluck('kelas.id');
return Santri::where('status', 'Aktif')
->whereHas('kelasSantri', function($q) use ($kelasIds, $tahun_ajaran) {
$q->whereIn('id_kelas', $kelasIds)
->where('tahun_ajaran', $tahun_ajaran);
});
}
/**
* Assign kegiatan ke kelas-kelas tertentu
* Akan replace semua relasi kelas existing
*
* @param array $kelas_ids - Array of kelas IDs
* @return void
*/
public function assignKelas(array $kelas_ids)
{
// Delete existing relations
$this->kegiatanKelasPivot()->delete();
// Create new relations
if (!empty($kelas_ids)) {
$data = [];
foreach ($kelas_ids as $id_kelas) {
$data[] = [
'kegiatan_id' => $this->kegiatan_id,
'id_kelas' => $id_kelas,
'created_at' => now(),
'updated_at' => now(),
];
}
KegiatanKelas::insert($data);
}
}
}

View File

@ -1,113 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* Model KegiatanKelas (Pivot Model)
*
* Mengelola relasi many-to-many antara Kegiatan dan Kelas
*
* @property int $id
* @property string $kegiatan_id - Foreign key ke kegiatans
* @property int $id_kelas - Foreign key ke kelas
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class KegiatanKelas extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'kegiatan_kelas';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'kegiatan_id',
'id_kelas',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Relasi: KegiatanKelas belongs to Kegiatan
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function kegiatan()
{
return $this->belongsTo(Kegiatan::class, 'kegiatan_id', 'kegiatan_id');
}
/**
* Relasi: KegiatanKelas belongs to Kelas
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function kelas()
{
return $this->belongsTo(Kelas::class, 'id_kelas', 'id');
}
/**
* Scope: Filter by kegiatan
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $kegiatan_id
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeByKegiatan($query, $kegiatan_id)
{
return $query->where('kegiatan_id', $kegiatan_id);
}
/**
* Scope: Filter by kelas
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $id_kelas
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeByKelas($query, $id_kelas)
{
return $query->where('id_kelas', $id_kelas);
}
/**
* Accessor: Nama kegiatan
*
* @return string
*/
public function getNamaKegiatanAttribute()
{
return $this->kegiatan ? $this->kegiatan->nama_kegiatan : '-';
}
/**
* Accessor: Nama kelas
*
* @return string
*/
public function getNamaKelasAttribute()
{
return $this->kelas ? $this->kelas->nama_kelas : '-';
}
}

View File

@ -1,180 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* Model Kelas
*
* Mengelola detail kelas per kelompok (PB, Lambatan, SD 1-6, dst)
*
* @property int $id
* @property string $kode_kelas - Kode unik kelas (KLS001, KLS002, dst)
* @property string $nama_kelas - Nama kelas (PB, Lambatan, SD 1, dst)
* @property string $id_kelompok - Foreign key ke kelompok_kelas
* @property int $urutan - Urutan tampilan dalam kelompok
* @property bool $is_active - Status aktif
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class Kelas extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'kelas';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'kode_kelas',
'nama_kelas',
'id_kelompok',
'urutan',
'is_active',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'is_active' => 'boolean',
'urutan' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Boot method untuk auto-generate kode_kelas
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->kode_kelas)) {
$last = self::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->kode_kelas, 3)) + 1 : 1;
$model->kode_kelas = 'KLS' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
/**
* Relasi: Kelas belongs to Kelompok (Many to One)
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function kelompok()
{
return $this->belongsTo(KelompokKelas::class, 'id_kelompok', 'id_kelompok');
}
/**
* Relasi: Kelas memiliki banyak santri (Many to Many through santri_kelas)
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function santris()
{
return $this->belongsToMany(Santri::class, 'santri_kelas', 'id_kelas', 'id_santri')
->withPivot('tahun_ajaran', 'is_primary')
->withTimestamps();
}
/**
* Relasi: Kelas memiliki banyak kegiatan (Many to Many through kegiatan_kelas)
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function kegiatans()
{
return $this->belongsToMany(Kegiatan::class, 'kegiatan_kelas', 'id_kelas', 'kegiatan_id', 'id', 'kegiatan_id')
->withTimestamps();
}
/**
* Relasi: Kelas memiliki banyak record santri_kelas (One to Many)
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function santriKelas()
{
return $this->hasMany(SantriKelas::class, 'id_kelas', 'id');
}
/**
* Scope: Filter kelas yang aktif
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope: Order by urutan
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeOrdered($query)
{
return $query->orderBy('urutan', 'asc');
}
/**
* Scope: Filter by kelompok
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $id_kelompok
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeByKelompok($query, $id_kelompok)
{
return $query->where('id_kelompok', $id_kelompok);
}
/**
* Accessor: Total santri dalam kelas
*
* @return int
*/
public function getTotalSantriAttribute()
{
return $this->santris()->count();
}
/**
* Accessor: Total kegiatan untuk kelas ini
*
* @return int
*/
public function getTotalKegiatanAttribute()
{
return $this->kegiatans()->count();
}
/**
* Accessor: Nama kelas lengkap dengan kelompok
*
* @return string
*/
public function getNamaLengkapAttribute()
{
return $this->kelompok->nama_kelompok . ' - ' . $this->nama_kelas;
}
}

View File

@ -1,125 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* Model KelompokKelas
*
* Mengelola kategori/kelompok kelas (Pondok, Sekolah Formal, Umum)
*
* @property int $id
* @property string $id_kelompok - Kode unik kelompok (KEL001, KEL002, dst)
* @property string $nama_kelompok - Nama kelompok kelas
* @property string|null $deskripsi - Deskripsi kelompok
* @property int $urutan - Urutan tampilan
* @property bool $is_active - Status aktif
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class KelompokKelas extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'kelompok_kelas';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'id_kelompok',
'nama_kelompok',
'deskripsi',
'urutan',
'is_active',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'is_active' => 'boolean',
'urutan' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Boot method untuk auto-generate id_kelompok
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_kelompok)) {
$last = self::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_kelompok, 3)) + 1 : 1;
$model->id_kelompok = 'KEL' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
/**
* Relasi: Kelompok memiliki banyak kelas (One to Many)
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function kelas()
{
return $this->hasMany(Kelas::class, 'id_kelompok', 'id_kelompok');
}
/**
* Scope: Filter kelompok yang aktif
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope: Order by urutan
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeOrdered($query)
{
return $query->orderBy('urutan', 'asc');
}
/**
* Accessor: Total kelas dalam kelompok
*
* @return int
*/
public function getTotalKelasAttribute()
{
return $this->kelas()->count();
}
/**
* Accessor: Total kelas aktif dalam kelompok
*
* @return int
*/
public function getTotalKelasAktifAttribute()
{
return $this->kelas()->where('is_active', true)->count();
}
}

View File

@ -1,487 +0,0 @@
<?php
// app/Models/Kepulangan.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class Kepulangan extends Model
{
use HasFactory;
protected $table = 'kepulangan';
protected $fillable = [
'id_kepulangan',
'id_santri',
'tanggal_izin',
'tanggal_pulang',
'tanggal_kembali',
'durasi_izin',
'alasan',
'status',
'approved_by',
'approved_at',
'catatan',
];
protected $casts = [
'tanggal_izin' => 'date',
'tanggal_pulang' => 'date',
'tanggal_kembali' => 'date',
'approved_at' => 'datetime',
];
/**
* Boot method - Auto generate ID & calculate durasi
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
// Generate ID Kepulangan
if (empty($model->id_kepulangan)) {
$last = Kepulangan::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_kepulangan, 2)) + 1 : 1;
$model->id_kepulangan = 'KP' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
// Hitung durasi_izin otomatis
if ($model->tanggal_pulang && $model->tanggal_kembali) {
$model->durasi_izin = $model->hitungDurasiIzin(
$model->tanggal_pulang,
$model->tanggal_kembali
);
}
// Set tanggal_izin jika kosong
if (empty($model->tanggal_izin)) {
$model->tanggal_izin = now();
}
});
static::updating(function ($model) {
// Recalculate durasi_izin saat update tanggal
if ($model->isDirty(['tanggal_pulang', 'tanggal_kembali'])) {
$model->durasi_izin = $model->hitungDurasiIzin(
$model->tanggal_pulang,
$model->tanggal_kembali
);
}
});
}
/**
* Method untuk menghitung durasi izin dalam hari
* Formula: (tanggal_kembali - tanggal_pulang) + 1
*/
private function hitungDurasiIzin($tanggalPulang, $tanggalKembali)
{
$pulang = Carbon::parse($tanggalPulang);
$kembali = Carbon::parse($tanggalKembali);
// +1 karena hari pertama juga dihitung
return $pulang->diffInDays($kembali) + 1;
}
/**
* Relasi ke Santri
*/
public function santri()
{
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
}
/**
* Accessor: Format tanggal
*/
public function getTanggalIzinFormattedAttribute()
{
return $this->tanggal_izin ? $this->tanggal_izin->format('d F Y') : '-';
}
public function getTanggalPulangFormattedAttribute()
{
return $this->tanggal_pulang ? $this->tanggal_pulang->format('d F Y') : '-';
}
public function getTanggalKembaliFormattedAttribute()
{
return $this->tanggal_kembali ? $this->tanggal_kembali->format('d F Y') : '-';
}
public function getApprovedAtFormattedAttribute()
{
return $this->approved_at ? $this->approved_at->format('d F Y H:i') : '-';
}
/**
* Accessor: Status badge
*/
public function getStatusBadgeAttribute()
{
$badges = [
'Menunggu' => 'badge-warning',
'Disetujui' => 'badge-success',
'Ditolak' => 'badge-danger',
'Selesai' => 'badge-secondary',
];
return $badges[$this->status] ?? 'badge-secondary';
}
/**
* Accessor: Apakah sedang dalam periode izin
*/
public function getIsAktifAttribute()
{
$today = Carbon::today();
return $this->status === 'Disetujui'
&& $today->between($this->tanggal_pulang, $this->tanggal_kembali);
}
/**
* Accessor: Apakah terlambat kembali
*/
public function getIsTerlambatAttribute()
{
if ($this->status !== 'Disetujui') {
return false;
}
return Carbon::today()->greaterThan($this->tanggal_kembali);
}
/**
* Scopes
*/
public function scopeStatus($query, $status)
{
return $query->where('status', $status);
}
public function scopeSantri($query, $idSantri)
{
return $query->where('id_santri', $idSantri);
}
public function scopeAktif($query)
{
$today = Carbon::today();
return $query->where('status', 'Disetujui')
->whereDate('tanggal_pulang', '<=', $today)
->whereDate('tanggal_kembali', '>=', $today);
}
public function scopeTerlambat($query)
{
return $query->where('status', 'Disetujui')
->whereDate('tanggal_kembali', '<', Carbon::today());
}
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('id_kepulangan', 'like', "%{$search}%")
->orWhere('alasan', 'like', "%{$search}%")
->orWhereHas('santri', function($sq) use ($search) {
$sq->where('nama_lengkap', 'like', "%{$search}%")
->orWhere('id_santri', 'like', "%{$search}%")
->orWhere('nis', 'like', "%{$search}%");
});
});
}
/**
* ========================================
* FITUR KUOTA TAHUNAN (DIPERBAIKI)
* ========================================
*/
/**
* Get settings kepulangan (kuota, periode)
*/
public static function getSettings()
{
$settings = DB::table('kepulangan_settings')->latest()->first();
if (!$settings) {
// Create default settings
DB::table('kepulangan_settings')->insert([
'kuota_maksimal' => 12,
'periode_mulai' => now()->startOfYear()->format('Y-m-d'),
'periode_akhir' => now()->endOfYear()->format('Y-m-d'),
'created_at' => now(),
'updated_at' => now(),
]);
$settings = DB::table('kepulangan_settings')->latest()->first();
}
return (object) [
'id' => $settings->id,
'kuota_maksimal' => $settings->kuota_maksimal,
'periode_mulai' => Carbon::parse($settings->periode_mulai),
'periode_akhir' => Carbon::parse($settings->periode_akhir),
'terakhir_reset' => $settings->terakhir_reset ? Carbon::parse($settings->terakhir_reset) : null,
'reset_by' => $settings->reset_by,
];
}
/**
* Update settings kepulangan
*/
public static function updateSettings($kuotaMaksimal, $periodeMulai, $periodeAkhir)
{
$existing = DB::table('kepulangan_settings')->latest()->first();
if ($existing) {
DB::table('kepulangan_settings')
->where('id', $existing->id)
->update([
'kuota_maksimal' => $kuotaMaksimal,
'periode_mulai' => $periodeMulai,
'periode_akhir' => $periodeAkhir,
'updated_at' => now(),
]);
} else {
DB::table('kepulangan_settings')->insert([
'kuota_maksimal' => $kuotaMaksimal,
'periode_mulai' => $periodeMulai,
'periode_akhir' => $periodeAkhir,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
/**
* PERBAIKAN UTAMA: Get total hari izin santri dalam periode tertentu
* HANYA menghitung yang Disetujui & Selesai
* AKUMULASI durasi_izin (HARI), bukan COUNT jumlah pengajuan
*/
public static function getTotalHariIzinSantri($idSantri, $periodeMulai = null, $periodeAkhir = null)
{
if (!$periodeMulai || !$periodeAkhir) {
$settings = self::getSettings();
$periodeMulai = $settings->periode_mulai;
$periodeAkhir = $settings->periode_akhir;
}
// PERBAIKAN: SUM durasi_izin (hari), bukan COUNT
return self::where('id_santri', $idSantri)
->whereIn('status', ['Disetujui', 'Selesai']) // Hanya yang approved/selesai
->whereBetween('tanggal_pulang', [$periodeMulai, $periodeAkhir])
->sum('durasi_izin'); // Akumulasi HARI
}
/**
* PERBAIKAN: Get detail kuota santri
* Status MELEBIHI tetap dihitung (tidak direset ke 0)
*/
public static function getSisaKuotaSantri($idSantri)
{
$settings = self::getSettings();
$totalTerpakai = self::getTotalHariIzinSantri(
$idSantri,
$settings->periode_mulai,
$settings->periode_akhir
);
// PERBAIKAN: Bisa negatif jika over limit
$sisaKuota = $settings->kuota_maksimal - $totalTerpakai;
$persentase = $settings->kuota_maksimal > 0 ?
($totalTerpakai / $settings->kuota_maksimal) * 100 : 0;
// Tentukan status berdasarkan persentase
$status = 'aman'; // 0-80%
if ($persentase >= 80 && $persentase < 100) {
$status = 'hampir_habis'; // 80-100%
} elseif ($persentase >= 100) {
$status = 'melebihi'; // >100%
}
// Warna badge
$badgeColor = 'success'; // Hijau
if ($persentase >= 80 && $persentase < 100) {
$badgeColor = 'warning'; // Kuning
} elseif ($persentase >= 100) {
$badgeColor = 'danger'; // Merah
}
return [
'kuota_maksimal' => $settings->kuota_maksimal,
'total_terpakai' => $totalTerpakai, // Bisa > kuota_maksimal
'sisa_kuota' => max(0, $sisaKuota), // Tampilkan 0 jika negatif (untuk UI)
'sisa_kuota_real' => $sisaKuota, // Nilai asli (bisa negatif)
'persentase' => round($persentase, 1),
'status' => $status,
'badge_color' => $badgeColor,
'periode_mulai' => $settings->periode_mulai->format('d M Y'),
'periode_akhir' => $settings->periode_akhir->format('d M Y'),
'terakhir_reset' => $settings->terakhir_reset ?
$settings->terakhir_reset->format('d M Y') : '-',
];
}
/**
* Check apakah santri over limit
*/
public static function isOverLimit($idSantri)
{
$kuota = self::getSisaKuotaSantri($idSantri);
return $kuota['status'] === 'melebihi';
}
/**
* PERBAIKAN: Get list santri yang over limit
* Return array: [id_santri => total_hari_terpakai]
*/
public static function getSantriOverLimit()
{
$settings = self::getSettings();
// Ambil semua santri aktif
$santriIds = Santri::where('status', 'Aktif')->pluck('id_santri');
$overLimitList = [];
foreach ($santriIds as $idSantri) {
$totalHari = self::getTotalHariIzinSantri(
$idSantri,
$settings->periode_mulai,
$settings->periode_akhir
);
// PERBAIKAN: Tampilkan total hari sebenarnya (tidak reset ke 0)
if ($totalHari > $settings->kuota_maksimal) {
$overLimitList[$idSantri] = $totalHari;
}
}
return $overLimitList;
}
/**
* ========================================
* FITUR RESET KUOTA
* ========================================
*/
/**
* Reset kuota untuk satu santri
*/
public static function resetKuotaSantri($idSantri, $resetBy, $catatan = null)
{
$settings = self::getSettings();
// Hitung total hari sebelum reset
$totalHariSebelumReset = self::getTotalHariIzinSantri(
$idSantri,
$settings->periode_mulai,
$settings->periode_akhir
);
// Catat log reset
DB::table('kepulangan_reset_logs')->insert([
'id_santri' => $idSantri,
'total_hari_sebelum_reset' => $totalHariSebelumReset,
'periode_mulai' => $settings->periode_mulai,
'periode_akhir' => $settings->periode_akhir,
'kuota_tahunan' => $settings->kuota_maksimal,
'jenis_reset' => 'individual',
'reset_by' => $resetBy,
'catatan' => $catatan,
'created_at' => now(),
'updated_at' => now(),
]);
// Update semua kepulangan santri yang Disetujui menjadi Selesai
self::where('id_santri', $idSantri)
->where('status', 'Disetujui')
->whereBetween('tanggal_pulang', [$settings->periode_mulai, $settings->periode_akhir])
->update(['status' => 'Selesai']);
return [
'success' => true,
'total_hari_direset' => $totalHariSebelumReset,
];
}
/**
* Reset kuota untuk semua santri
*/
public static function resetKuotaSemuaSantri($resetBy, $catatan = null)
{
$settings = self::getSettings();
// Get semua santri aktif
$santriIds = Santri::where('status', 'Aktif')->pluck('id_santri');
$totalSantri = $santriIds->count();
$totalHariDireset = 0;
foreach ($santriIds as $idSantri) {
$totalHari = self::getTotalHariIzinSantri(
$idSantri,
$settings->periode_mulai,
$settings->periode_akhir
);
$totalHariDireset += $totalHari;
}
// Catat log reset massal
DB::table('kepulangan_reset_logs')->insert([
'id_santri' => null, // Null untuk reset massal
'total_hari_sebelum_reset' => $totalHariDireset,
'periode_mulai' => $settings->periode_mulai,
'periode_akhir' => $settings->periode_akhir,
'kuota_tahunan' => $settings->kuota_maksimal,
'jenis_reset' => 'massal',
'reset_by' => $resetBy,
'catatan' => $catatan ?? "Reset massal untuk {$totalSantri} santri",
'created_at' => now(),
'updated_at' => now(),
]);
// Update semua kepulangan yang Disetujui menjadi Selesai
$jumlahDiupdate = self::whereIn('id_santri', $santriIds)
->where('status', 'Disetujui')
->whereBetween('tanggal_pulang', [$settings->periode_mulai, $settings->periode_akhir])
->update(['status' => 'Selesai']);
// Update tanggal terakhir reset di settings
DB::table('kepulangan_settings')
->where('id', $settings->id)
->update([
'terakhir_reset' => now(),
'reset_by' => $resetBy,
'updated_at' => now(),
]);
return [
'success' => true,
'total_santri' => $totalSantri,
'total_hari_direset' => $totalHariDireset,
'jumlah_izin_diupdate' => $jumlahDiupdate,
];
}
/**
* Get history reset logs
*/
public static function getResetLogs($limit = 20)
{
return DB::table('kepulangan_reset_logs')
->leftJoin('santris', 'kepulangan_reset_logs.id_santri', '=', 'santris.id_santri')
->select(
'kepulangan_reset_logs.*',
'santris.nama_lengkap'
)
->orderBy('kepulangan_reset_logs.created_at', 'desc')
->limit($limit)
->get();
}
}

View File

@ -1,157 +0,0 @@
<?php
// app/Models/KesehatanSantri.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class KesehatanSantri extends Model
{
use HasFactory;
protected $fillable = [
'id_kesehatan',
'id_santri',
'tanggal_masuk',
'tanggal_keluar',
'keluhan',
'catatan',
'status',
];
protected $casts = [
'tanggal_masuk' => 'date',
'tanggal_keluar' => 'date',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Boot method untuk auto-generate ID Kesehatan
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_kesehatan)) {
// Ambil data terakhir
$last = KesehatanSantri::orderBy('id', 'desc')->first();
// Generate nomor urut
$num = $last ? intval(substr($last->id_kesehatan, 1)) + 1 : 1;
// Format: K001, K002, dst
$model->id_kesehatan = 'K' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
/**
* Relasi ke Santri
*/
public function santri()
{
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
}
/**
* Accessor: Format tanggal masuk Indonesia
*/
public function getTanggalMasukFormattedAttribute()
{
return $this->tanggal_masuk ?
Carbon::parse($this->tanggal_masuk)->locale('id')->isoFormat('D MMM Y') :
'-';
}
/**
* Accessor: Format tanggal keluar Indonesia
*/
public function getTanggalKeluarFormattedAttribute()
{
return $this->tanggal_keluar ?
Carbon::parse($this->tanggal_keluar)->locale('id')->isoFormat('D MMM Y') :
null;
}
/**
* Accessor: Hitung lama dirawat
*/
public function getLamaDirawatAttribute()
{
$tanggalMasuk = Carbon::parse($this->tanggal_masuk);
$tanggalKeluar = $this->tanggal_keluar ?
Carbon::parse($this->tanggal_keluar) :
Carbon::now();
return $tanggalMasuk->diffInDays($tanggalKeluar);
}
/**
* Accessor: Warna badge status
*/
public function getStatusBadgeColorAttribute()
{
return [
'dirawat' => 'danger',
'sembuh' => 'success',
'izin' => 'warning',
][$this->status] ?? 'secondary';
}
/**
* Scope: Filter santri yang sedang dirawat
*/
public function scopeDirawat($query)
{
return $query->where('status', 'dirawat');
}
/**
* Scope: Filter santri yang sudah sembuh
*/
public function scopeSembuh($query)
{
return $query->where('status', 'sembuh');
}
/**
* Scope: Filter santri yang izin
*/
public function scopeIzin($query)
{
return $query->where('status', 'izin');
}
/**
* Scope: Filter berdasarkan bulan dan tahun
*/
public function scopeByMonthYear($query, $month = null, $year = null)
{
if ($month) {
$query->whereMonth('tanggal_masuk', $month);
}
if ($year) {
$query->whereYear('tanggal_masuk', $year);
}
return $query;
}
/**
* Scope: Search
*/
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('id_kesehatan', 'like', "%{$search}%")
->orWhere('id_santri', 'like', "%{$search}%")
->orWhere('keluhan', 'like', "%{$search}%")
->orWhereHas('santri', function($query) use ($search) {
$query->where('nama_lengkap', 'like', "%{$search}%");
});
});
}
}

View File

@ -1,57 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Keuangan extends Model
{
use HasFactory;
protected $table = 'keuangan';
protected $fillable = [
'id_keuangan', 'jenis', 'nominal', 'keterangan', 'tanggal',
];
protected $casts = [
'tanggal' => 'date',
'nominal' => 'decimal:2',
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_keuangan)) {
$last = static::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_keuangan, 3)) + 1 : 1;
$model->id_keuangan = 'KEU' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
// ── Scopes ──
public function scopePemasukan($query) { return $query->where('jenis', 'pemasukan'); }
public function scopePengeluaran($query) { return $query->where('jenis', 'pengeluaran'); }
public function scopeBulan($query, $bulan, $tahun)
{
return $query->whereMonth('tanggal', $bulan)->whereYear('tanggal', $tahun);
}
public function scopeSearch($query, $search)
{
return $query->where(function ($q) use ($search) {
$q->where('id_keuangan', 'like', "%{$search}%")
->orWhere('keterangan', 'like', "%{$search}%");
});
}
// ── Accessors ──
public function getNominalFormatAttribute()
{
return 'Rp ' . number_format($this->nominal, 0, ',', '.');
}
}

Some files were not shown because too many files have changed in this diff Show More