'datetime', 'updated_at' => 'datetime', ]; /** * Generator ID Kustom (S001, S002, ...) */ protected static function boot() { parent::boot(); static::creating(function ($model) { if (empty($model->id_santri)) { $last = Santri::orderBy('id', 'desc')->first(); $num = $last ? intval(substr($last->id_santri, 1)) + 1 : 1; $model->id_santri = 'S' . str_pad($num, 3, '0', STR_PAD_LEFT); } }); } /** * Relasi: Santri memiliki satu User Account (hasOne) */ public function user() { return $this->hasOne(User::class, 'role_id', 'id_santri') ->where('role', 'santri'); } /** * Relasi: Santri memiliki satu akun Wali (orang tua) */ public function waliUser() { return $this->hasOne(User::class, 'role_id', 'id_santri') ->where('role', 'wali'); } /** * Relasi: Santri memiliki banyak data kesehatan */ public function kesehatanSantri() { return $this->hasMany(KesehatanSantri::class, 'id_santri', 'id_santri'); } /** * Relasi: Kesehatan santri yang masih dirawat */ public function kesehatanAktif() { return $this->hasMany(KesehatanSantri::class, 'id_santri', 'id_santri') ->where('status', 'dirawat'); } /** * Relasi: Santri memiliki banyak data kepulangan */ public function kepulangan() { return $this->hasMany(Kepulangan::class, 'id_santri', 'id_santri'); } /** * Relasi: Kepulangan yang sedang aktif */ public function kepulanganAktif() { return $this->hasMany(Kepulangan::class, 'id_santri', 'id_santri') ->where('status', 'Disetujui') ->whereDate('tanggal_pulang', '<=', now()) ->whereDate('tanggal_kembali', '>=', now()); } /** * Relasi: Santri memiliki banyak riwayat pelanggaran */ public function riwayatPelanggaran() { return $this->hasMany(RiwayatPelanggaran::class, 'id_santri', 'id_santri'); } /** * Relasi: Santri memiliki banyak pembayaran SPP */ public function pembayaranSpp() { return $this->hasMany(PembayaranSpp::class, 'id_santri', 'id_santri'); } /** * Relasi: SPP yang belum lunas */ public function sppBelumLunas() { return $this->hasMany(PembayaranSpp::class, 'id_santri', 'id_santri') ->where('status', 'Belum Lunas'); } /** * Relasi: SPP yang telat */ public function sppTelat() { return $this->hasMany(PembayaranSpp::class, 'id_santri', 'id_santri') ->where('status', 'Belum Lunas') ->where('batas_bayar', '<', now()); } /** * Relasi: Santri memiliki banyak transaksi uang saku */ public function uangSaku() { return $this->hasMany(UangSaku::class, 'id_santri', 'id_santri'); } /** * Relasi: Santri memiliki banyak absensi kegiatan (BARU) */ public function absensiKegiatans() { return $this->hasMany(AbsensiKegiatan::class, 'id_santri', 'id_santri'); } /** * Accessor: Nama kelompok kelas */ public function getKelompokNameAttribute() { return $this->kelasPrimary?->kelas?->kelompok?->nama_kelompok ?? '-'; } /** * Accessor untuk mendapatkan badge HTML status */ public function getStatusBadgeAttribute() { $badges = [ 'Aktif' => ' Aktif', 'Lulus' => ' Lulus', 'Khatam' => ' Khatam', 'Tidak Aktif' => ' Tidak Aktif', ]; return $badges[$this->status] ?? $this->status; } /** * Accessor: Total poin pelanggaran */ public function getTotalPoinPelanggaranAttribute() { return $this->riwayatPelanggaran()->sum('poin'); } /** * Accessor: Total tunggakan SPP */ public function getTotalTunggakanAttribute() { return $this->sppBelumLunas()->sum('nominal'); } /** * Accessor: Saldo uang saku terakhir */ public function getSaldoUangSakuAttribute() { $transaksiTerakhir = $this->uangSaku() ->orderBy('tanggal_transaksi', 'desc') ->orderBy('created_at', 'desc') ->first(); return $transaksiTerakhir ? $transaksiTerakhir->saldo_sesudah : 0; } /** * Accessor: Total pemasukan uang saku */ public function getTotalPemasukanUangSakuAttribute() { return $this->uangSaku()->where('jenis_transaksi', 'pemasukan')->sum('nominal'); } /** * Accessor: Total pengeluaran uang saku */ public function getTotalPengeluaranUangSakuAttribute() { return $this->uangSaku()->where('jenis_transaksi', 'pengeluaran')->sum('nominal'); } /** * Accessor: Status RFID (BARU) */ public function getHasRfidAttribute() { return !empty($this->rfid_uid); } /** * Accessor: Total kehadiran kegiatan (BARU) */ public function getTotalKehadiranAttribute() { return $this->absensiKegiatans()->where('status', 'Hadir')->count(); } /** * Accessor: URL Foto Santri (BARU) */ public function getFotoUrlAttribute() { if ($this->foto && file_exists(storage_path('app/public/' . $this->foto))) { return asset('storage/' . $this->foto); } // Fallback ke gambar default berdasarkan jenis kelamin if ($this->jenis_kelamin === 'Perempuan') { return asset('images/default-female.png'); } return asset('images/default-male.png'); } /** * Scope untuk filter santri aktif */ public function scopeAktif($query) { return $query->where('status', 'Aktif'); } /** * Scope untuk filter santri lulus */ public function scopeLulus($query) { return $query->where('status', 'Lulus'); } /** * Scope untuk filter santri tidak aktif */ public function scopeTidakAktif($query) { return $query->where('status', 'Tidak Aktif'); } /** * Scope untuk filter berdasarkan kelas (santri yang punya kelas ini) */ public function scopeKelas($query, $idKelas) { return $query->whereHas('kelasSantri', function($q) use ($idKelas) { $q->where('id_kelas', $idKelas); }); } /** * Scope untuk filter berdasarkan kelompok kelas */ public function scopeKelompok($query, $idKelompok) { return $query->whereHas('kelasSantri', function($q) use ($idKelompok) { $q->whereHas('kelas', function($q2) use ($idKelompok) { $q2->where('id_kelompok', $idKelompok); }); }); } /** * Scope: Filter santri by kelas name (via relational system) * Replaces old Santri::where('kelas', $name) queries * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $namaKelas - Nama kelas (e.g., 'PB', 'Lambatan', 'Cepatan') * @return \Illuminate\Database\Eloquent\Builder */ public function scopeKelasByName($query, $namaKelas) { return $query->whereHas('kelasSantri', function($q) use ($namaKelas) { $q->whereHas('kelas', function($q2) use ($namaKelas) { $q2->where('nama_kelas', $namaKelas); }); }); } /** * Scope: Filter santri by PRIMARY kelas name only * Used in dashboard/capaian where only primary class matters * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $namaKelas - Nama kelas (e.g., 'PB', 'Lambatan', 'SMA 12') * @return \Illuminate\Database\Eloquent\Builder */ public function scopePrimaryKelasByName($query, $namaKelas) { return $query->whereHas('kelasSantri', function($q) use ($namaKelas) { $q->where('is_primary', true) ->whereHas('kelas', function($q2) use ($namaKelas) { $q2->where('nama_kelas', $namaKelas); }); }); } /** * Scope untuk search santri */ public function scopeSearch($query, $search) { return $query->where(function($q) use ($search) { $q->where('nama_lengkap', 'like', "%{$search}%") ->orWhere('nis', 'like', "%{$search}%") ->orWhere('id_santri', 'like', "%{$search}%"); }); } /** * Relasi: Santri memiliki banyak capaian */ public function capaian() { return $this->hasMany(Capaian::class, 'id_santri', 'id_santri'); } // ========================================== // RELASI SISTEM KELAS BARU // ========================================== /** * Relasi: Santri memiliki banyak record kelas (hasMany ke santri_kelas) */ public function kelasSantri() { return $this->hasMany(SantriKelas::class, 'id_santri', 'id_santri'); } /** * Relasi: Santri memiliki satu kelas primary (hasOne ke santri_kelas dengan is_primary = true) */ public function kelasPrimary() { return $this->hasOne(SantriKelas::class, 'id_santri', 'id_santri') ->where('is_primary', true) ->with('kelas'); } /** * Relasi: Santri belongs to many Kelas (many-to-many through santri_kelas) */ public function kelasMany() { return $this->belongsToMany(Kelas::class, 'santri_kelas', 'id_santri', 'id_kelas', 'id_santri', 'id') ->withPivot('tahun_ajaran', 'is_primary') ->withTimestamps(); } /** * Get rata-rata capaian per semester */ public function getRataRataCapaianAttribute() { return $this->capaian()->avg('persentase') ?? 0; } // ========================================== // ACCESSOR SISTEM KELAS BARU // ========================================== /** * Accessor: Get kelas name (primary atau pertama) * * @return string */ public function getKelasNameAttribute() { $primary = $this->kelasPrimary; if ($primary && $primary->kelas) { return $primary->kelas->nama_kelas; } // Fallback ke kelas pertama jika tidak ada primary $first = $this->kelasSantri->first(); return $first && $first->kelas ? $first->kelas->nama_kelas : 'Belum Ada Kelas'; } /** * Accessor: Backward compatible kelas accessor (replaces dropped column) * Returns primary kelas name for seamless migration from old system * * @return string */ public function getKelasAttribute() { return $this->kelas_name; } /** * Accessor: Get semua kelas sebagai string (untuk display ringkas) * * @return string */ public function getKelasListStringAttribute() { $items = $this->kelasSantri ->filter(fn($sk) => $sk->kelas && $sk->kelas->kelompok) ->map(fn($sk) => $sk->kelas->kelompok->nama_kelompok . ': ' . $sk->kelas->nama_kelas); return $items->isNotEmpty() ? $items->implode(', ') : 'Belum Ada Kelas'; } /** * Accessor: Get kelas ID dari sistem baru (primary class ID) * * @return int|null */ public function getPrimaryKelasIdAttribute() { $kelasPrimary = $this->kelasPrimary; return $kelasPrimary ? $kelasPrimary->id_kelas : null; } // ========================================== // HELPER METHODS SISTEM KELAS BARU // ========================================== /** * Check apakah santri ada di kelas tertentu * * @param int $id_kelas * @return bool */ public function hasKelas($id_kelas) { return $this->kelasMany()->where('kelas.id', $id_kelas)->exists(); } /** * Get all kelas santri untuk tahun ajaran tertentu * * @param string|null $tahun_ajaran - Format: 2024/2025, null untuk tahun ajaran saat ini * @return \Illuminate\Database\Eloquent\Collection */ public function getKelasByTahun($tahun_ajaran = null) { if ($tahun_ajaran === null) { $tahun_ajaran = SantriKelas::getCurrentAcademicYear(); } return $this->kelasSantri() ->with('kelas.kelompok') ->where('tahun_ajaran', $tahun_ajaran) ->get(); } /** * Assign santri ke kelas baru * * @param int $id_kelas * @param string|null $tahun_ajaran - Format: 2024/2025, null untuk tahun ajaran saat ini * @param bool $is_primary - Set sebagai kelas utama * @return \App\Models\SantriKelas */ public function assignKelas($id_kelas, $tahun_ajaran = null, $is_primary = false) { if ($tahun_ajaran === null) { $tahun_ajaran = SantriKelas::getCurrentAcademicYear(); } // Jika set as primary, unset kelas primary lainnya di tahun ajaran yang sama if ($is_primary) { $this->kelasSantri() ->where('tahun_ajaran', $tahun_ajaran) ->update(['is_primary' => false]); } // Create or update santri_kelas return SantriKelas::updateOrCreate( [ 'id_santri' => $this->id_santri, 'id_kelas' => $id_kelas, 'tahun_ajaran' => $tahun_ajaran, ], [ 'is_primary' => $is_primary, ] ); } }