'date', 'batas_bayar' => 'date', 'nominal' => 'decimal:2', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; // ══════════════════════════════════════════════════════ // BOOT // ══════════════════════════════════════════════════════ protected static function boot() { parent::boot(); static::creating(function ($model) { if (empty($model->id_pembayaran)) { $last = PembayaranSpp::orderBy('id', 'desc')->first(); $num = $last ? intval(substr($last->id_pembayaran, 3)) + 1 : 1; $model->id_pembayaran = 'SPP' . str_pad($num, 3, '0', STR_PAD_LEFT); } }); } // ══════════════════════════════════════════════════════ // RELASI // ══════════════════════════════════════════════════════ public function santri() { return $this->belongsTo(Santri::class, 'id_santri', 'id_santri'); } // ══════════════════════════════════════════════════════ // CICILAN HELPERS // // Status di DB tetap "Belum Lunas" (tidak ubah enum). // Cicilan dideteksi dari keterangan berformat JSON: // {"terbayar": 150000, "catatan": "Cicilan ke-1"} // // Keterangan teks biasa (non-JSON) tetap terbaca normal. // ══════════════════════════════════════════════════════ /** * Cek apakah record ini berstatus cicilan * (status Belum Lunas + ada data terbayar di keterangan). */ public function isCicilan(): bool { if ($this->status !== 'Belum Lunas') return false; $data = $this->getCicilanData(); return $data !== null && ($data['terbayar'] ?? 0) > 0; } /** * Ambil array cicilan dari keterangan, atau null jika bukan JSON cicilan. */ public function getCicilanData(): ?array { if (!$this->keterangan) return null; $decoded = json_decode($this->keterangan, true); if (json_last_error() !== JSON_ERROR_NONE) return null; if (!array_key_exists('terbayar', $decoded)) return null; return $decoded; } /** * Nominal yang sudah dibayar. */ public function getNominalTerbayarAttribute(): float { if ($this->status === 'Lunas') return (float) $this->nominal; $data = $this->getCicilanData(); return $data ? (float) ($data['terbayar'] ?? 0) : 0; } /** * Sisa yang belum dibayar. */ public function getNominalSisaAttribute(): float { return max(0, (float) $this->nominal - $this->nominal_terbayar); } /** * Persentase cicilan (0–100). */ public function getPorsentaseCicilanAttribute(): int { if (!$this->nominal || (float) $this->nominal == 0) return 0; return (int) min(100, round(($this->nominal_terbayar / (float) $this->nominal) * 100)); } /** * Simpan progres cicilan ke keterangan (JSON). * Status DB tidak diubah — tetap "Belum Lunas". */ public function setCicilan(float $terbayar, ?string $catatan = null): void { // Jika keterangan sebelumnya teks biasa, pindahkan sebagai catatan if ($this->keterangan && !$this->getCicilanData()) { $catatan = $catatan ?? $this->keterangan; } $data = ['terbayar' => $terbayar]; if ($catatan) $data['catatan'] = $catatan; $this->keterangan = json_encode($data); } /** * Baca catatan teks (dari JSON atau teks biasa). */ public function getCatatanTeksAttribute(): ?string { if (!$this->keterangan) return null; $data = $this->getCicilanData(); if ($data) return $data['catatan'] ?? null; return $this->keterangan; } // ══════════════════════════════════════════════════════ // ACCESSORS // ══════════════════════════════════════════════════════ public function getBulanNamaAttribute(): string { $bulanIndo = [ 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 $bulanIndo[$this->bulan] ?? '-'; } public function getPeriodeLengkapAttribute(): string { return $this->bulan_nama . ' ' . $this->tahun; } public function getNominalFormatAttribute(): string { return 'Rp ' . number_format($this->nominal, 0, ',', '.'); } public function getNominalTerbayarFormatAttribute(): string { return 'Rp ' . number_format($this->nominal_terbayar, 0, ',', '.'); } public function getNominalSisaFormatAttribute(): string { return 'Rp ' . number_format($this->nominal_sisa, 0, ',', '.'); } /** * Status Badge HTML — mengenali cicilan dari keterangan JSON, * bukan dari nilai kolom status. */ public function getStatusBadgeAttribute(): string { if ($this->status === 'Lunas') { return ' Lunas'; } if ($this->isCicilan()) { return ' Cicilan ' . $this->porsentase_cicilan . '%'; } if ($this->isTelat()) { return ' Belum Lunas (Telat)'; } return ' Belum Lunas'; } // ══════════════════════════════════════════════════════ // HELPERS // ══════════════════════════════════════════════════════ public function isTelat(): bool { if ($this->status === 'Lunas') return false; return Carbon::now()->isAfter($this->batas_bayar); } // ══════════════════════════════════════════════════════ // SCOPES // ══════════════════════════════════════════════════════ public function scopeBelumLunas($query) { return $query->where('status', 'Belum Lunas'); } public function scopeLunas($query) { return $query->where('status', 'Lunas'); } public function scopeTelat($query) { return $query->where('status', 'Belum Lunas') ->where('batas_bayar', '<', Carbon::now()); } public function scopeTahun($query, $tahun) { return $query->where('tahun', $tahun); } public function scopeBulan($query, $bulan) { return $query->where('bulan', $bulan); } public function scopeSearch($query, $search) { return $query->whereHas('santri', function ($q) use ($search) { $q->where('nama_lengkap', 'like', "%{$search}%") ->orWhere('id_santri', 'like', "%{$search}%") ->orWhere('nis', 'like', "%{$search}%"); })->orWhere('id_pembayaran', 'like', "%{$search}%"); } }