Initial commit

This commit is contained in:
HelgaFaisa 2025-11-20 16:25:03 +07:00
commit 3201648b16
339 changed files with 52223 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

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

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# TugasAkhir

18
sim-pkpps/.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

59
sim-pkpps/.env.example Normal file
View File

@ -0,0 +1,59 @@
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}"

11
sim-pkpps/.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

19
sim-pkpps/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
/.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

66
sim-pkpps/README.md Normal file
View File

@ -0,0 +1,66 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

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

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

@ -0,0 +1,184 @@
<?php
namespace App\Http\Controllers\Admin;
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;
class AbsensiKegiatanController extends Controller
{
/**
* Daftar kegiatan untuk absensi
*/
public function index(Request $request)
{
$query = Kegiatan::with('kategori');
if ($request->filled('hari')) {
$query->where('hari', $request->hari);
}
$kegiatans = $query->orderBy('hari')->orderBy('waktu_mulai')->paginate(10);
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
return view('admin.kegiatan.absensi.index', compact('kegiatans', 'hariList'));
}
/**
* Form input absensi
*/
public function inputAbsensi($kegiatan_id)
{
$kegiatan = Kegiatan::with('kategori')->where('kegiatan_id', $kegiatan_id)->firstOrFail();
$tanggal = request('tanggal', now()->format('Y-m-d'));
// Ambil semua santri aktif
$santris = Santri::where('status', 'Aktif')
->select('id', 'id_santri', 'nama_lengkap', 'kelas', 'rfid_uid')
->orderBy('nama_lengkap')
->get();
// Ambil data absensi yang sudah ada
$absensiData = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
->whereDate('tanggal', $tanggal)
->pluck('status', 'id_santri')
->toArray();
return view('admin.kegiatan.absensi.input', compact('kegiatan', 'santris', 'absensiData', 'tanggal'));
}
/**
* Simpan absensi manual
*/
public function simpanAbsensi(Request $request)
{
$validated = $request->validate([
'kegiatan_id' => 'required|exists:kegiatans,kegiatan_id',
'tanggal' => 'required|date',
'absensi' => 'required|array',
'absensi.*' => 'required|in:Hadir,Izin,Sakit,Alpa',
]);
DB::beginTransaction();
try {
foreach ($request->absensi as $id_santri => $status) {
AbsensiKegiatan::updateOrCreate(
[
'kegiatan_id' => $request->kegiatan_id,
'id_santri' => $id_santri,
'tanggal' => $request->tanggal,
],
[
'status' => $status,
'metode_absen' => 'Manual',
'waktu_absen' => now()->format('H:i:s'),
]
);
}
DB::commit();
return redirect()->route('admin.absensi-kegiatan.index')
->with('success', 'Absensi berhasil disimpan.');
} catch (\Exception $e) {
DB::rollBack();
return back()->with('error', 'Gagal menyimpan absensi: ' . $e->getMessage());
}
}
/**
* Rekap absensi kegiatan
*/
public function rekapAbsensi(Request $request, $kegiatan_id)
{
$kegiatan = Kegiatan::with('kategori')->where('kegiatan_id', $kegiatan_id)->firstOrFail();
$query = AbsensiKegiatan::with('santri')
->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)));
}
$absensis = $query->orderBy('tanggal', 'desc')
->orderBy('waktu_absen', 'desc')
->paginate(20);
// Statistik
$stats = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->pluck('total', 'status')
->toArray();
return view('admin.kegiatan.absensi.rekap', compact('kegiatan', 'absensis', 'stats'));
}
/**
* 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

@ -0,0 +1,215 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Berita;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class BeritaController extends Controller
{
/**
* Tampilkan daftar berita
*/
public function index(Request $request)
{
$query = Berita::query()->with('santriTertentu');
// Search
if ($request->filled('search')) {
$query->search($request->search);
}
// Filter status
if ($request->filled('status')) {
$query->status($request->status);
}
// Filter target
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()
{
// Ambil data santri aktif - sesuaikan dengan kolom yang ada di model Santri
$santri = Santri::aktif()
->select('id_santri', 'nama_lengkap', 'kelas')
->orderBy('nama_lengkap')
->get();
$kelasOptions = ['PB', 'Lambatan', 'Cepatan'];
return view('admin.berita.create', compact('santri', '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,santri_tertentu',
'target_kelas' => 'nullable|array',
'target_kelas.*' => 'in:PB,Lambatan,Cepatan',
'santri_tertentu' => 'nullable|array',
'santri_tertentu.*' => 'exists:santris,id_santri',
], [
'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');
}
// Buat berita
$berita = Berita::create($validated);
// Attach santri jika target santri_tertentu
if ($validated['target_berita'] === 'santri_tertentu' && $request->filled('santri_tertentu')) {
$berita->santriTertentu()->attach($request->santri_tertentu);
}
// Attach santri berdasarkan kelas jika target kelas_tertentu
if ($validated['target_berita'] === 'kelas_tertentu' && $request->filled('target_kelas')) {
$santriKelas = Santri::whereIn('kelas', $request->target_kelas)
->where('status', 'Aktif')
->pluck('id_santri');
$berita->santriTertentu()->attach($santriKelas);
}
return redirect()->route('admin.berita.index')
->with('success', 'Berita berhasil ditambahkan!');
}
/**
* Tampilkan detail berita
*/
public function show(Berita $berita)
{
$berita->load('santriTertentu');
return view('admin.berita.show', compact('berita'));
}
/**
* Tampilkan form edit
*/
public function edit(Berita $berita)
{
$berita->load('santriTertentu');
// Ambil data santri aktif - sesuaikan dengan kolom yang ada di model Santri
$santri = Santri::aktif()
->select('id_santri', 'nama_lengkap', 'kelas')
->orderBy('nama_lengkap')
->get();
$kelasOptions = ['PB', 'Lambatan', 'Cepatan'];
$selectedSantri = $berita->santriTertentu->pluck('id_santri')->toArray();
return view('admin.berita.edit', compact('berita', 'santri', 'kelasOptions', 'selectedSantri'));
}
/**
* 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,santri_tertentu',
'target_kelas' => 'nullable|array',
'target_kelas.*' => 'in:PB,Lambatan,Cepatan',
'santri_tertentu' => 'nullable|array',
'santri_tertentu.*' => 'exists:santris,id_santri',
]);
// Upload gambar baru jika ada
if ($request->hasFile('gambar')) {
// Hapus gambar lama
if ($berita->gambar) {
Storage::disk('public')->delete($berita->gambar);
}
$validated['gambar'] = $request->file('gambar')->store('berita', 'public');
}
// Update berita
$berita->update($validated);
// Sync santri
if ($validated['target_berita'] === 'santri_tertentu' && $request->filled('santri_tertentu')) {
$berita->santriTertentu()->sync($request->santri_tertentu);
} elseif ($validated['target_berita'] === 'kelas_tertentu' && $request->filled('target_kelas')) {
$santriKelas = Santri::whereIn('kelas', $request->target_kelas)
->where('status', 'Aktif')
->pluck('id_santri');
$berita->santriTertentu()->sync($santriKelas);
} else {
$berita->santriTertentu()->detach();
}
return redirect()->route('admin.berita.index')
->with('success', 'Berita berhasil diperbarui!');
}
/**
* Hapus berita
*/
public function destroy(Berita $berita)
{
// Hapus gambar jika ada
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();
$beritaTertentu = Berita::where('target_berita', 'santri_tertentu')->count();
return view('admin.berita.statistik', compact(
'totalBerita',
'totalPublished',
'totalDraft',
'beritaSemua',
'beritaTertentu'
));
}
}

View File

@ -0,0 +1,637 @@
<?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 Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class CapaianController extends Controller
{
/**
* Display a listing of capaian
*/
public function index(Request $request)
{
$query = Capaian::with(['santri', 'materi', 'semester']);
// Filter santri
if ($request->filled('id_santri')) {
$query->bySantri($request->id_santri);
}
// Filter semester
if ($request->filled('id_semester')) {
$query->bySemester($request->id_semester);
}
// Filter kategori
if ($request->filled('kategori')) {
$query->byKategori($request->kategori);
}
$capaians = $query->orderBy('created_at', 'desc')
->paginate(20)
->appends(request()->query());
// Data untuk filter
$santris = Santri::aktif()->orderBy('nama_lengkap')->get();
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
return view('admin.capaian.index', compact('capaians', 'santris', 'semesters'));
}
/**
* Show the form for creating new capaian
*/
public function create(Request $request)
{
// Get santri list
$santris = Santri::aktif()
->select('id', 'id_santri', 'nis', 'nama_lengkap', 'kelas')
->orderBy('nama_lengkap')
->get();
// Get semester aktif
$semesterAktif = Semester::aktif()->first();
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
// Jika ada pre-selected santri
$selectedSantri = null;
$materiOptions = [];
if ($request->filled('id_santri')) {
$selectedSantri = Santri::where('id_santri', $request->id_santri)->first();
if ($selectedSantri) {
// Get materi sesuai kelas santri
$materiOptions = Materi::where('kelas', $selectedSantri->kelas)
->orderBy('kategori')
->orderBy('nama_kitab')
->get();
}
}
return view('admin.capaian.create', compact('santris', 'semesters', 'semesterAktif', 'selectedSantri', 'materiOptions'));
}
/**
* Get materi by santri kelas (AJAX)
*/
public function getMateriByKelas(Request $request)
{
$santri = Santri::where('id_santri', $request->id_santri)->first();
if (!$santri) {
return response()->json(['error' => 'Santri tidak ditemukan'], 404);
}
$materis = Materi::where('kelas', $santri->kelas)
->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
]);
}
/**
* Get detail materi (AJAX)
*/
public function getDetailMateri(Request $request)
{
$materi = Materi::where('id_materi', $request->id_materi)->first();
if (!$materi) {
return response()->json(['error' => 'Materi tidak ditemukan'], 404);
}
// Check existing capaian
$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
]);
}
/**
* Store a newly created capaian
*/
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',
], [
'id_santri.required' => 'Santri wajib dipilih.',
'id_materi.required' => 'Materi wajib dipilih.',
'id_semester.required' => 'Semester wajib dipilih.',
'halaman_selesai.required' => 'Halaman yang selesai wajib diisi.',
'tanggal_input.required' => 'Tanggal input wajib diisi.',
]);
// Check duplikasi
$existing = Capaian::where('id_santri', $validated['id_santri'])
->where('id_materi', $validated['id_materi'])
->where('id_semester', $validated['id_semester'])
->first();
if ($existing) {
return redirect()->back()
->withInput()
->with('error', 'Capaian untuk santri, materi, dan semester ini sudah ada. Silakan edit data yang ada.');
}
Capaian::create($validated);
return redirect()->route('admin.capaian.index')
->with('success', 'Capaian berhasil ditambahkan.');
}
/**
* Display the specified capaian
*/
public function show(Capaian $capaian)
{
$capaian->load(['santri', 'materi', 'semester']);
return view('admin.capaian.show', compact('capaian'));
}
/**
* Show the form for editing the specified capaian
*/
public function edit(Capaian $capaian)
{
$capaian->load(['santri', 'materi', 'semester']);
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
return view('admin.capaian.edit', compact('capaian', 'semesters'));
}
/**
* Update the specified capaian
*/
public function update(Request $request, Capaian $capaian)
{
$validated = $request->validate([
'halaman_selesai' => 'required|string',
'catatan' => 'nullable|string',
'tanggal_input' => 'required|date',
], [
'halaman_selesai.required' => 'Halaman yang selesai wajib diisi.',
'tanggal_input.required' => 'Tanggal input wajib diisi.',
]);
$capaian->update($validated);
return redirect()->route('admin.capaian.show', $capaian)
->with('success', 'Capaian berhasil diperbarui.');
}
/**
* Remove the specified capaian
*/
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.");
}
/**
* Show riwayat capaian per santri
*/
public function riwayatSantri($id_santri, Request $request)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
$query = Capaian::with(['materi', 'semester'])
->bySantri($id_santri);
// Filter semester
if ($request->filled('id_semester')) {
$query->bySemester($request->id_semester);
}
$capaians = $query->orderBy('created_at', 'desc')
->paginate(15)
->appends(request()->query());
// Statistik
$totalCapaian = $capaians->total();
$rataRataPersentase = Capaian::bySantri($id_santri)->avg('persentase') ?? 0;
// Statistik per kategori
$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 untuk preview)
*/
public function calculatePersentase(Request $request)
{
$halamanSelesai = $request->halaman_selesai;
$idMateri = $request->id_materi;
if (empty($halamanSelesai) || empty($idMateri)) {
return response()->json(['persentase' => 0, 'jumlah' => 0]);
}
try {
$persentase = Capaian::calculatePersentase($halamanSelesai, $idMateri);
$pages = Capaian::parseHalamanSelesai($halamanSelesai);
$jumlah = count($pages);
return response()->json([
'persentase' => number_format($persentase, 2),
'jumlah' => $jumlah,
'pages' => $pages
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
/**
* Dashboard capaian dengan grafik
*/
public function dashboard(Request $request)
{
// Get filter inputs
$idSantri = $request->input('id_santri');
$idSemester = $request->input('id_semester');
$kelas = $request->input('kelas');
// Get semester aktif sebagai default
$semesterAktif = Semester::aktif()->first();
$selectedSemester = $idSemester ?: ($semesterAktif ? $semesterAktif->id_semester : null);
// Data untuk filter
$santris = Santri::aktif()->orderBy('nama_lengkap')->get();
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
// Build query capaian
$query = Capaian::with(['santri', 'materi', 'semester']);
if ($idSantri) {
$query->bySantri($idSantri);
}
if ($selectedSemester) {
$query->bySemester($selectedSemester);
}
if ($kelas) {
$query->whereHas('santri', function($q) use ($kelas) {
$q->where('kelas', $kelas);
});
}
// Get data
$capaians = $query->get();
// Statistik Umum
$totalCapaian = $capaians->count();
$totalSantri = $capaians->pluck('id_santri')->unique()->count();
$rataRataPersentase = $capaians->avg('persentase') ?? 0;
$capaianSelesai = $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) {
$kategori = $capaian->materi->kategori;
$statistikKategori[$kategori]['count']++;
$statistikKategori[$kategori]['avg'] += $capaian->persentase;
if ($capaian->persentase >= 100) {
$statistikKategori[$kategori]['selesai']++;
}
}
// Calculate average
foreach ($statistikKategori as $kategori => $data) {
if ($data['count'] > 0) {
$statistikKategori[$kategori]['avg'] = $data['avg'] / $data['count'];
}
}
// Data untuk grafik distribusi persentase
$distribusiPersentase = [
'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(),
];
// Top 10 Santri dengan Progress Tertinggi
$topSantri = Capaian::select('id_santri', DB::raw('AVG(persentase) as rata_rata'))
->when($selectedSemester, function($q) use ($selectedSemester) {
return $q->where('id_semester', $selectedSemester);
})
->when($kelas, function($q) use ($kelas) {
return $q->whereHas('santri', function($query) use ($kelas) {
$query->where('kelas', $kelas);
});
})
->groupBy('id_santri')
->orderBy('rata_rata', 'desc')
->limit(10)
->with('santri')
->get();
// Materi dengan Progress Terendah
$materiTerendah = Capaian::select('id_materi', DB::raw('AVG(persentase) as rata_rata'), DB::raw('COUNT(*) as jumlah_santri'))
->when($selectedSemester, function($q) use ($selectedSemester) {
return $q->where('id_semester', $selectedSemester);
})
->groupBy('id_materi')
->having('rata_rata', '<', 50)
->orderBy('rata_rata', 'asc')
->limit(5)
->with('materi')
->get();
return view('admin.capaian.dashboard', compact(
'santris',
'semesters',
'semesterAktif',
'selectedSemester',
'idSantri',
'kelas',
'totalCapaian',
'totalSantri',
'rataRataPersentase',
'capaianSelesai',
'statistikKategori',
'distribusiPersentase',
'topSantri',
'materiTerendah'
));
}
/**
* Rekap capaian per kelas
*/
public function rekapKelas(Request $request)
{
$kelas = $request->input('kelas', 'Lambatan');
$idSemester = $request->input('id_semester');
$semesterAktif = Semester::aktif()->first();
$selectedSemester = $idSemester ?: ($semesterAktif ? $semesterAktif->id_semester : null);
// Get santri per kelas
$santris = Santri::where('kelas', $kelas)
->where('status', 'Aktif')
->orderBy('nama_lengkap')
->get();
// Get capaian per santri
$rekapData = [];
foreach ($santris as $santri) {
$capaians = Capaian::where('id_santri', $santri->id_santri)
->when($selectedSemester, function($q) use ($selectedSemester) {
return $q->where('id_semester', $selectedSemester);
})
->with('materi')
->get();
$rataRata = $capaians->avg('persentase') ?? 0;
$totalMateri = $capaians->count();
$selesai = $capaians->where('persentase', '>=', 100)->count();
// Per kategori
$alquran = $capaians->filter(function($c) {
return $c->materi->kategori == 'Al-Qur\'an';
})->avg('persentase') ?? 0;
$hadist = $capaians->filter(function($c) {
return $c->materi->kategori == 'Hadist';
})->avg('persentase') ?? 0;
$tambahan = $capaians->filter(function($c) {
return $c->materi->kategori == 'Materi Tambahan';
})->avg('persentase') ?? 0;
$rekapData[] = [
'santri' => $santri,
'rata_rata' => $rataRata,
'total_materi' => $totalMateri,
'selesai' => $selesai,
'alquran' => $alquran,
'hadist' => $hadist,
'tambahan' => $tambahan,
];
}
// Sort by rata-rata desc
usort($rekapData, function($a, $b) {
return $b['rata_rata'] <=> $a['rata_rata'];
});
$semesters = Semester::orderBy('tahun_ajaran', 'desc')->get();
return view('admin.capaian.rekap-kelas', compact('rekapData', 'kelas', 'semesters', 'selectedSemester'));
}
/**
* Detail capaian per materi (semua santri)
*/
public function detailMateri($id_materi, Request $request)
{
$materi = Materi::where('id_materi', $id_materi)->firstOrFail();
$idSemester = $request->input('id_semester');
$semesterAktif = Semester::aktif()->first();
$selectedSemester = $idSemester ?: ($semesterAktif ? $semesterAktif->id_semester : null);
// Get all capaian untuk materi ini
$capaians = Capaian::where('id_materi', $id_materi)
->when($selectedSemester, function($q) use ($selectedSemester) {
return $q->where('id_semester', $selectedSemester);
})
->with(['santri', 'semester'])
->orderBy('persentase', 'desc')
->get();
// Statistik
$totalSantri = $capaians->count();
$rataRataPersentase = $capaians->avg('persentase') ?? 0;
$santriSelesai = $capaians->where('persentase', '>=', 100)->count();
$santriMulai = $capaians->where('persentase', '>', 0)->where('persentase', '<', 100)->count();
// Distribusi persentase
$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 untuk data 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', function($q) use ($kelas) {
$q->where('kelas', $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':
// Get data per semester
$semesters = Semester::orderBy('tahun_ajaran')->orderBy('periode')->get();
$labels = [];
$dataPoints = [];
foreach ($semesters as $semester) {
$labels[] = $semester->nama_semester;
$avg = Capaian::where('id_semester', $semester->id_semester)
->when($kelas, function($q) use ($kelas) {
return $q->whereHas('santri', function($query) use ($kelas) {
$query->where('kelas', $kelas);
});
})
->avg('persentase') ?? 0;
$dataPoints[] = round($avg, 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);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Santri;
use Illuminate\Http\Request;
use Barryvdh\DomPDF\Facade\Pdf;
class KartuRfidController extends Controller
{
/**
* Halaman kelola kartu RFID
*/
public function index(Request $request)
{
$query = Santri::where('status', 'Aktif');
// Filter: Santri yang sudah/belum punya RFID
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', 'nama_lengkap', 'kelas', 'rfid_uid')
->orderBy('nama_lengkap')
->paginate(15);
return view('admin.kegiatan.kartu.index', compact('santris'));
}
/**
* Form daftarkan RFID ke santri
*/
public function daftarRfid($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
return view('admin.kegiatan.kartu.daftar', compact('santri'));
}
/**
* Simpan RFID UID ke santri
*/
public function simpanRfid(Request $request, $id_santri)
{
$validated = $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);
}
/**
* Hapus RFID dari santri
*/
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);
}
/**
* Cetak kartu RFID santri (PDF)
*/
public function cetakKartu($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
if (!$santri->rfid_uid) {
return back()->with('error', 'Santri belum memiliki RFID yang terdaftar.');
}
$pdf = Pdf::loadView('admin.kegiatan.kartu.cetak', compact('santri'));
$pdf->setPaper([0, 0, 243, 153], 'landscape'); // Ukuran kartu ID (85.6mm x 54mm)
return $pdf->stream('Kartu_RFID_' . $santri->id_santri . '.pdf');
}
}

View File

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

@ -0,0 +1,118 @@
<?php
// app/Http/Controllers/Admin/KategoriPelanggaranController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\KategoriPelanggaran;
use Illuminate\Http\Request;
class KategoriPelanggaranController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$data = KategoriPelanggaran::orderBy('created_at', 'desc')->get();
return view('admin.kategori_pelanggaran.index', compact('data'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
// Generate preview ID kategori berikutnya
$lastKategori = KategoriPelanggaran::orderBy('id', 'desc')->first();
$nextNum = $lastKategori ? intval(substr($lastKategori->id_kategori, 2)) + 1 : 1;
$nextIdKategori = 'KP' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
return view('admin.kategori_pelanggaran.create', compact('nextIdKategori'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'nama_pelanggaran' => 'required|string|max:255',
'poin' => 'required|integer|min:1|max:100',
], [
'nama_pelanggaran.required' => 'Nama pelanggaran wajib diisi.',
'poin.required' => 'Poin wajib diisi.',
'poin.min' => 'Poin minimal 1.',
'poin.max' => 'Poin maksimal 100.',
]);
KategoriPelanggaran::create($validated);
return redirect()->route('admin.kategori-pelanggaran.index')
->with('success', 'Kategori pelanggaran berhasil ditambahkan.');
}
/**
* Display the specified resource.
*/
public function show(KategoriPelanggaran $kategoriPelanggaran)
{
$kategoriPelanggaran->load('riwayatPelanggaran.santri');
return view('admin.kategori_pelanggaran.show', [
'kategori' => $kategoriPelanggaran
]);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(KategoriPelanggaran $kategoriPelanggaran)
{
return view('admin.kategori_pelanggaran.index', [
'data' => KategoriPelanggaran::orderBy('created_at', 'desc')->get(),
'kategori' => $kategoriPelanggaran
]);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, KategoriPelanggaran $kategoriPelanggaran)
{
$validated = $request->validate([
'nama_pelanggaran' => 'required|string|max:255',
'poin' => 'required|integer|min:1|max:100',
], [
'nama_pelanggaran.required' => 'Nama pelanggaran wajib diisi.',
'poin.required' => 'Poin wajib diisi.',
'poin.min' => 'Poin minimal 1.',
'poin.max' => 'Poin maksimal 100.',
]);
$kategoriPelanggaran->update($validated);
return redirect()->route('admin.kategori-pelanggaran.index')
->with('success', 'Kategori pelanggaran berhasil diperbarui.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(KategoriPelanggaran $kategoriPelanggaran)
{
$namaKategori = $kategoriPelanggaran->nama_pelanggaran;
// Cek apakah kategori masih digunakan
if ($kategoriPelanggaran->riwayatPelanggaran()->count() > 0) {
return redirect()->route('admin.kategori-pelanggaran.index')
->with('error', 'Kategori "' . $namaKategori . '" tidak dapat dihapus karena masih digunakan dalam riwayat pelanggaran.');
}
$kategoriPelanggaran->delete();
return redirect()->route('admin.kategori-pelanggaran.index')
->with('success', 'Kategori "' . $namaKategori . '" berhasil dihapus.');
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Kegiatan;
use App\Models\KategoriKegiatan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class KegiatanController extends Controller
{
/**
* Tampilkan daftar kegiatan
*/
public function index(Request $request)
{
$query = Kegiatan::with('kategori');
// Filter hari
if ($request->filled('hari')) {
$query->where('hari', $request->hari);
}
// Filter kategori
if ($request->filled('kategori_id')) {
$query->where('kategori_id', $request->kategori_id);
}
// Search
if ($request->filled('search')) {
$query->search($request->search);
}
$kegiatans = $query->select('id', 'kegiatan_id', 'kategori_id', 'nama_kegiatan', 'hari', 'waktu_mulai', 'waktu_selesai', 'materi')
->orderBy('hari')
->orderBy('waktu_mulai')
->paginate(15)
->appends(request()->query());
// Data untuk filter
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
$hariList = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Ahad'];
return view('admin.kegiatan.data.index', compact('kegiatans', 'kategoris', 'hariList'));
}
/**
* 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'];
return view('admin.kegiatan.data.create', compact('nextId', 'kategoris', 'hariList'));
}
/**
* 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|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',
], [
'kategori_id.required' => 'Kategori wajib dipilih.',
'nama_kegiatan.required' => 'Nama kegiatan wajib diisi.',
'hari.required' => 'Hari wajib dipilih.',
'waktu_mulai.required' => 'Waktu mulai wajib diisi.',
'waktu_selesai.required' => 'Waktu selesai wajib diisi.',
'waktu_selesai.after' => 'Waktu selesai harus lebih dari waktu mulai.',
]);
Kegiatan::create($validated);
Cache::forget('next_kegiatan_id');
return redirect()->route('admin.kegiatan.index')
->with('success', 'Kegiatan berhasil ditambahkan.');
}
/**
* Tampilkan detail kegiatan
*/
public function show(Kegiatan $kegiatan)
{
$kegiatan->load('kategori');
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'];
return view('admin.kegiatan.data.edit', compact('kegiatan', 'kategoris', 'hariList'));
}
/**
* Update kegiatan
*/
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|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',
], [
'kategori_id.required' => 'Kategori wajib dipilih.',
'nama_kegiatan.required' => 'Nama kegiatan wajib diisi.',
'waktu_selesai.after' => 'Waktu selesai harus lebih dari waktu mulai.',
]);
$kegiatan->update($validated);
return redirect()->route('admin.kegiatan.index')
->with('success', 'Kegiatan berhasil diperbarui.');
}
/**
* Hapus kegiatan
*/
public function destroy(Kegiatan $kegiatan)
{
$nama = $kegiatan->nama_kegiatan;
$kegiatan->delete();
Cache::forget('next_kegiatan_id');
return redirect()->route('admin.kegiatan.index')
->with('success', "Kegiatan \"$nama\" berhasil dihapus.");
}
}

View File

@ -0,0 +1,411 @@
<?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' => $this->getOverLimitSantri()->count(),
];
// 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 = $this->getOverLimitSantri()->pluck('total_hari', 'id_santri');
return view('admin.kepulangan.index', compact(
'kepulangan',
'stats',
'tahunList',
'santriOverLimit'
));
}
/**
* Show the form for creating a new kepulangan
*/
public function create()
{
$santriList = Santri::where('status', 'Aktif')
->orderBy('nama_lengkap')
->get();
return view('admin.kepulangan.create', compact('santriList'));
}
/**
* Store a newly created kepulangan
*/
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|min:10|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.min' => 'Alasan minimal 10 karakter.',
'alasan.max' => 'Alasan maksimal 500 karakter.',
]);
// Create kepulangan
Kepulangan::create($validated);
return redirect()->route('admin.kepulangan.index')
->with('success', 'Izin kepulangan berhasil diajukan.');
}
/**
* Display the specified kepulangan
*/
public function show($id_kepulangan)
{
// Cari data berdasarkan id_kepulangan (KP001, KP002, dst)
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)
->with('santri')
->firstOrFail();
// Get detail izin tahun ini
$tahunSekarang = Carbon::now()->year;
$detailIzin = $this->getDetailIzinSantri($kepulangan->id_santri, $tahunSekarang);
// 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',
'detailIzin',
'history'
));
}
/**
* Show the form for editing kepulangan
*/
public function edit($id_kepulangan)
{
// Cari data berdasarkan id_kepulangan
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
// Hanya bisa edit jika status Menunggu
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();
return view('admin.kepulangan.edit', compact('kepulangan', 'santriList'));
}
/**
* Update the specified kepulangan
*/
public function update(Request $request, $id_kepulangan)
{
// Cari data berdasarkan id_kepulangan
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
// Hanya bisa update jika status Menunggu
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|min:10|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.',
'alasan.min' => 'Alasan minimal 10 karakter.',
]);
$kepulangan->update($validated);
return redirect()->route('admin.kepulangan.index')
->with('success', 'Data kepulangan berhasil diperbarui.');
}
/**
* Remove the specified kepulangan
*/
public function destroy($id_kepulangan)
{
// Cari data berdasarkan id_kepulangan
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
// Hanya bisa hapus jika status Menunggu atau Ditolak
if (!in_array($kepulangan->status, ['Menunggu', 'Ditolak'])) {
return response()->json([
'success' => false,
'message' => 'Hanya izin dengan status "Menunggu" atau "Ditolak" 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)
{
// Cari data berdasarkan 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);
}
// Update status - catatan opsional (tidak perlu validasi)
$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)
{
// Cari data berdasarkan id_kepulangan
$kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail();
// Validasi alasan penolakan (wajib diisi minimal 10 karakter)
$validated = $request->validate([
'alasan_penolakan' => 'required|string|min:10',
], [
'alasan_penolakan.required' => 'Alasan penolakan wajib diisi.',
'alasan_penolakan.min' => 'Alasan penolakan minimal 10 karakter.',
]);
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 (mark as selesai)
*/
public function complete($id_kepulangan)
{
// Cari data berdasarkan 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);
}
$kepulangan->update([
'status' => 'Selesai',
]);
return response()->json([
'success' => true,
'message' => 'Kepulangan santri berhasil diselesaikan.'
]);
}
/**
* Print surat izin kepulangan
*/
public function print($id_kepulangan)
{
// Cari data berdasarkan 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 izin
*/
public function getSantriData($idSantri)
{
$santri = Santri::where('id_santri', $idSantri)->first();
if (!$santri) {
return response()->json([
'success' => false,
'message' => 'Santri tidak ditemukan.'
], 404);
}
$tahunSekarang = Carbon::now()->year;
$penggunaanIzin = $this->getDetailIzinSantri($idSantri, $tahunSekarang);
return response()->json([
'success' => true,
'santri' => $santri,
'penggunaan_izin' => [
'total_hari' => $penggunaanIzin['total_hari'],
'total_izin' => $penggunaanIzin['total_izin'],
'sisa_kuota' => $penggunaanIzin['sisa_kuota'],
'over_limit' => $penggunaanIzin['over_limit'],
]
]);
}
/**
* Helper: Get detail izin santri per tahun
*/
private function getDetailIzinSantri($idSantri, $tahun)
{
$kepulanganList = Kepulangan::where('id_santri', $idSantri)
->where('status', 'Disetujui')
->whereYear('tanggal_pulang', $tahun)
->orderBy('tanggal_pulang', 'desc')
->get();
$totalHari = $kepulanganList->sum('durasi_izin');
$totalIzin = $kepulanganList->count();
$sisaKuota = max(0, 12 - $totalHari);
$overLimit = $totalHari > 12;
$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,
];
});
return [
'total_hari' => $totalHari,
'total_izin' => $totalIzin,
'sisa_kuota' => $sisaKuota,
'over_limit' => $overLimit,
'details' => $details,
];
}
/**
* Helper: Get santri yang over limit
*/
private function getOverLimitSantri()
{
$tahunSekarang = Carbon::now()->year;
return Kepulangan::selectRaw('id_santri, SUM(durasi_izin) as total_hari')
->where('status', 'Disetujui')
->whereYear('tanggal_pulang', $tahunSekarang)
->groupBy('id_santri')
->having('total_hari', '>', 12)
->get();
}
}

View File

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

@ -0,0 +1,168 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Materi;
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());
return view('admin.materi.index', compact('materis'));
}
/**
* 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);
});
return view('admin.materi.create', compact('nextIdMateri'));
}
/**
* Store a newly created materi in storage
*/
public function store(Request $request)
{
$validated = $request->validate([
'kategori' => 'required|in:Al-Qur\'an,Hadist,Materi Tambahan',
'kelas' => 'required|in:Lambatan,Cepatan,PB',
'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.',
'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::create($validated);
// Clear cache
Cache::forget('next_materi_id');
return redirect()->route('admin.materi.index')
->with('success', 'Data materi berhasil ditambahkan.');
}
/**
* 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)
{
return view('admin.materi.edit', compact('materi'));
}
/**
* Update the specified materi in storage
*/
public function update(Request $request, Materi $materi)
{
$validated = $request->validate([
'kategori' => 'required|in:Al-Qur\'an,Hadist,Materi Tambahan',
'kelas' => 'required|in:Lambatan,Cepatan,PB',
'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.',
'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

@ -0,0 +1,368 @@
<?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
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = PembayaranSpp::with('santri');
// Search
if ($request->filled('search')) {
$query->search($request->search);
}
// Filter status
if ($request->filled('status')) {
if ($request->status === 'Telat') {
$query->telat();
} else {
$query->where('status', $request->status);
}
}
// Filter tahun
if ($request->filled('tahun')) {
$query->tahun($request->tahun);
}
// Filter bulan
if ($request->filled('bulan')) {
$query->bulan($request->bulan);
}
$pembayaranSpp = $query->orderBy('tahun', 'desc')
->orderBy('bulan', 'desc')
->orderBy('created_at', 'desc')
->paginate(20)
->appends(request()->query());
// Data untuk filter
$tahunList = PembayaranSpp::selectRaw('DISTINCT tahun')
->orderBy('tahun', 'desc')
->pluck('tahun');
return view('admin.pembayaran-spp.index', compact('pembayaranSpp', 'tahunList'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
// Ambil santri yang aktif
$santris = Santri::where('status', 'Aktif')
->orderBy('nama_lengkap', 'asc')
->get();
// Generate preview ID
$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'));
}
/**
* Store a newly created resource in storage.
*/
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
$exists = PembayaranSpp::where('id_santri', $validated['id_santri'])
->where('bulan', $validated['bulan'])
->where('tahun', $validated['tahun'])
->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');
}
PembayaranSpp::create($validated);
return redirect()->route('admin.pembayaran-spp.index')
->with('success', 'Data pembayaran SPP berhasil ditambahkan.');
}
/**
* Display the specified resource.
*/
public function show(PembayaranSpp $pembayaranSpp)
{
$pembayaranSpp->load('santri');
return view('admin.pembayaran-spp.show', compact('pembayaranSpp'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(PembayaranSpp $pembayaranSpp)
{
$santris = Santri::orderBy('nama_lengkap', 'asc')->get();
return view('admin.pembayaran-spp.edit', compact('pembayaranSpp', 'santris'));
}
/**
* Update the specified resource in storage.
*/
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');
}
$pembayaranSpp->update($validated);
return redirect()->route('admin.pembayaran-spp.index')
->with('success', 'Data pembayaran SPP berhasil diperbarui.');
}
/**
* Remove the specified resource from storage.
*/
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.");
}
/**
* Tampilkan riwayat pembayaran 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 untuk semua santri aktif dalam periode tertentu
*/
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) {
// Cek apakah sudah ada
$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');
}
/**
* Halaman pilihan laporan
*/
public function laporan()
{
return view('admin.pembayaran-spp.laporan');
}
/**
* Cetak laporan SPP (semua data atau filter)
*/
public function cetakLaporan(Request $request)
{
$query = PembayaranSpp::with('santri');
// Filter
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();
// Statistik
$totalLunas = $pembayaranSpp->where('status', 'Lunas')->sum('nominal');
$totalTunggakan = $pembayaranSpp->where('status', 'Belum Lunas')->sum('nominal');
$jumlahTelat = $pembayaranSpp->filter(function($spp) {
return $spp->isTelat();
})->count();
return view('admin.pembayaran-spp.cetak-laporan', compact(
'pembayaranSpp',
'totalLunas',
'totalTunggakan',
'jumlahTelat'
));
}
/**
* Cetak laporan SPP per santri
*/
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();
// Statistik
$totalLunas = $pembayaranSpp->where('status', 'Lunas')->sum('nominal');
$totalTunggakan = $pembayaranSpp->where('status', 'Belum Lunas')->sum('nominal');
$jumlahTelat = $pembayaranSpp->filter(function($spp) {
return $spp->isTelat();
})->count();
return view('admin.pembayaran-spp.cetak-laporan-santri', compact(
'santri',
'pembayaranSpp',
'totalLunas',
'totalTunggakan',
'jumlahTelat'
));
}
/**
* Cetak bukti pembayaran
*/
public function cetakBukti(PembayaranSpp $pembayaranSpp)
{
$pembayaranSpp->load('santri');
return view('admin.pembayaran-spp.cetak-bukti', compact('pembayaranSpp'));
}
}

View File

@ -0,0 +1,222 @@
<?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 Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class RiwayatKegiatanController extends Controller
{
/**
* Halaman utama riwayat kegiatan & absensi
*/
public function index(Request $request)
{
$query = AbsensiKegiatan::with(['santri', 'kegiatan.kategori']);
// Filter Santri
if ($request->filled('id_santri')) {
$query->where('id_santri', $request->id_santri);
}
// Filter Kategori
if ($request->filled('kategori_id')) {
$query->whereHas('kegiatan', function($q) use ($request) {
$q->where('kategori_id', $request->kategori_id);
});
}
// Filter Kegiatan
if ($request->filled('kegiatan_id')) {
$query->where('kegiatan_id', $request->kegiatan_id);
}
// Filter Status
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Filter Tanggal
if ($request->filled('tanggal_dari')) {
$query->whereDate('tanggal', '>=', $request->tanggal_dari);
}
if ($request->filled('tanggal_sampai')) {
$query->whereDate('tanggal', '<=', $request->tanggal_sampai);
}
// Filter Bulan
if ($request->filled('bulan')) {
$query->whereMonth('tanggal', date('m', strtotime($request->bulan)))
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
}
$riwayats = $query->orderBy('tanggal', 'desc')
->orderBy('waktu_absen', 'desc')
->paginate(20)
->appends(request()->query());
// Data untuk filter
$santris = Santri::where('status', 'Aktif')
->select('id_santri', 'nama_lengkap')
->orderBy('nama_lengkap')
->get();
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
$kegiatans = Kegiatan::select('kegiatan_id', 'nama_kegiatan')
->orderBy('nama_kegiatan')
->get();
// Statistik Global
$statsQuery = AbsensiKegiatan::query();
// Apply same filters to stats
if ($request->filled('id_santri')) {
$statsQuery->where('id_santri', $request->id_santri);
}
if ($request->filled('kategori_id')) {
$statsQuery->whereHas('kegiatan', function($q) use ($request) {
$q->where('kategori_id', $request->kategori_id);
});
}
if ($request->filled('kegiatan_id')) {
$statsQuery->where('kegiatan_id', $request->kegiatan_id);
}
if ($request->filled('tanggal_dari')) {
$statsQuery->whereDate('tanggal', '>=', $request->tanggal_dari);
}
if ($request->filled('tanggal_sampai')) {
$statsQuery->whereDate('tanggal', '<=', $request->tanggal_sampai);
}
if ($request->filled('bulan')) {
$statsQuery->whereMonth('tanggal', date('m', strtotime($request->bulan)))
->whereYear('tanggal', date('Y', strtotime($request->bulan)));
}
$stats = $statsQuery->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->pluck('total', 'status')
->toArray();
return view('admin.kegiatan.riwayat.index', compact(
'riwayats',
'santris',
'kategoris',
'kegiatans',
'stats'
));
}
/**
* Riwayat kehadiran per santri (detail)
*/
public function detailSantri($id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
// Statistik per santri
$stats = AbsensiKegiatan::where('id_santri', $id_santri)
->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->pluck('total', 'status')
->toArray();
// Total kehadiran per kategori
$statsByKategori = 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.nama_kategori',
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
DB::raw('COUNT(*) as total')
)
->groupBy('kategori_kegiatans.nama_kategori')
->get();
// Riwayat 30 hari terakhir
$riwayat30Hari = AbsensiKegiatan::where('id_santri', $id_santri)
->whereDate('tanggal', '>=', now()->subDays(30))
->select(
DB::raw('DATE(tanggal) as tanggal'),
DB::raw('SUM(CASE WHEN status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
DB::raw('COUNT(*) as total')
)
->groupBy('tanggal')
->orderBy('tanggal', 'asc')
->get();
// Riwayat lengkap
$riwayats = AbsensiKegiatan::with('kegiatan.kategori')
->where('id_santri', $id_santri)
->orderBy('tanggal', 'desc')
->paginate(15);
return view('admin.kegiatan.riwayat.detail-santri', compact(
'santri',
'stats',
'statsByKategori',
'riwayat30Hari',
'riwayats'
));
}
/**
* Show detail riwayat
*/
public function show(AbsensiKegiatan $riwayat)
{
$riwayat->load(['santri', 'kegiatan.kategori']);
return view('admin.kegiatan.riwayat.show', compact('riwayat'));
}
/**
* Edit riwayat absensi
*/
public function edit(AbsensiKegiatan $riwayat)
{
$riwayat->load(['santri', 'kegiatan']);
return view('admin.kegiatan.riwayat.edit', compact('riwayat'));
}
/**
* Update riwayat absensi
*/
public function update(Request $request, AbsensiKegiatan $riwayat)
{
$validated = $request->validate([
'status' => 'required|in:Hadir,Izin,Sakit,Alpa',
'waktu_absen' => 'nullable|date_format:H:i',
]);
$riwayat->update($validated);
return redirect()->route('admin.riwayat-kegiatan.index')
->with('success', 'Riwayat absensi berhasil diperbarui.');
}
/**
* Hapus riwayat absensi
*/
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.");
}
/**
* Export/Cetak laporan (opsional - bisa dikembangkan)
*/
public function exportPdf(Request $request)
{
// Implementasi export PDF jika diperlukan
return response()->json(['message' => 'Fitur export sedang dikembangkan']);
}
}

View File

@ -0,0 +1,223 @@
<?php
// app/Http/Controllers/Admin/RiwayatPelanggaranController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\RiwayatPelanggaran;
use App\Models\KategoriPelanggaran;
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']);
// 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 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::orderBy('nama_pelanggaran')->get();
// Statistik
$totalPelanggaran = RiwayatPelanggaran::count();
$pelanggaranBulanIni = RiwayatPelanggaran::bulanIni()->count();
$totalPoin = RiwayatPelanggaran::sum('poin');
return view('admin.riwayat_pelanggaran.index', compact(
'data',
'santriList',
'kategoriList',
'totalPelanggaran',
'pelanggaranBulanIni',
'totalPoin'
));
}
/**
* 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();
$kategoriList = KategoriPelanggaran::orderBy('nama_pelanggaran')->get();
return view('admin.riwayat_pelanggaran.create', compact(
'nextIdRiwayat',
'santriList',
'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;
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']);
// 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::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();
$validated['poin'] = $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();
return view('admin.riwayat_pelanggaran.riwayat_santri', compact(
'santri',
'riwayat',
'totalPoin',
'totalPelanggaran'
));
}
}

View File

@ -0,0 +1,174 @@
<?php
// app/Http/Controllers/Admin/SantriController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class SantriController extends Controller
{
/**
* Tampilkan daftar data santri dengan fitur search.
*/
public function index(Request $request)
{
$query = Santri::query();
// 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
if ($request->filled('kelas')) {
$query->where('kelas', $request->kelas);
}
// Select kolom yang diperlukan saja
$santris = $query->select(
'id',
'id_santri',
'nis',
'nama_lengkap',
'jenis_kelamin',
'kelas',
'status',
'created_at'
)
->orderBy('created_at', 'desc')
->paginate(20)
->appends(request()->query());
return view('admin.santri.index', compact('santris'));
}
/**
* 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);
});
return view('admin.santri.create', compact('nextIdSantri'));
}
/**
* 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' => 'required|in:PB,Lambatan,Cepatan',
'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',
], [
'nis.unique' => 'NIS sudah digunakan oleh santri lain.',
'nama_lengkap.required' => 'Nama lengkap wajib diisi.',
'jenis_kelamin.required' => 'Jenis kelamin wajib dipilih.',
'kelas.required' => 'Kelas wajib dipilih.',
'status.required' => 'Status wajib dipilih.',
]);
Santri::create($validated);
// 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)
{
return view('admin.santri.show', compact('santri'));
}
/**
* Tampilkan form untuk mengedit santri.
*/
public function edit(Santri $santri)
{
return view('admin.santri.edit', compact('santri'));
}
/**
* 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' => 'required|in:PB,Lambatan,Cepatan',
'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',
], [
'nis.unique' => 'NIS sudah digunakan oleh santri lain.',
'nama_lengkap.required' => 'Nama lengkap wajib diisi.',
'jenis_kelamin.required' => 'Jenis kelamin wajib dipilih.',
'kelas.required' => 'Kelas wajib dipilih.',
'status.required' => 'Status wajib dipilih.',
]);
$santri->update($validated);
// 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;
$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

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

@ -0,0 +1,317 @@
<?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;
class UangSakuController extends Controller
{
/**
* Tampilkan daftar transaksi uang saku
*/
public function index(Request $request)
{
$query = UangSaku::with('santri:id_santri,nama_lengkap');
// Search
if ($request->filled('search')) {
$query->search($request->search);
}
// Filter berdasarkan santri
if ($request->filled('id_santri')) {
$query->bySantri($request->id_santri);
}
// Filter berdasarkan jenis transaksi
if ($request->filled('jenis_transaksi')) {
$query->byJenis($request->jenis_transaksi);
}
// Filter berdasarkan tanggal
if ($request->filled('tanggal_dari') && $request->filled('tanggal_sampai')) {
$query->byDateRange($request->tanggal_dari, $request->tanggal_sampai);
}
$transaksi = $query->orderBy('tanggal_transaksi', 'desc')
->orderBy('created_at', 'desc')
->paginate(20)
->appends(request()->query());
// Cache santri list untuk dropdown
$santriList = Cache::remember('santri_aktif_uang_saku', 300, function () {
return Santri::where('status', 'Aktif')
->select('id_santri', 'nama_lengkap')
->orderBy('nama_lengkap')
->get();
});
return view('admin.uang-saku.index', compact('transaksi', 'santriList'));
}
/**
* Form tambah transaksi
*/
public function create()
{
$santriList = Santri::where('status', 'Aktif')
->select('id_santri', 'nama_lengkap')
->orderBy('nama_lengkap')
->get();
return view('admin.uang-saku.create', compact('santriList'));
}
/**
* Simpan transaksi baru
*/
public function store(Request $request)
{
$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',
], [
'id_santri.required' => 'Santri wajib dipilih.',
'id_santri.exists' => 'Santri tidak ditemukan.',
'jenis_transaksi.required' => 'Jenis transaksi wajib dipilih.',
'nominal.required' => 'Nominal wajib diisi.',
'nominal.numeric' => 'Nominal harus berupa angka.',
'nominal.min' => 'Nominal minimal Rp 1.',
'tanggal_transaksi.required' => 'Tanggal transaksi wajib diisi.',
]);
DB::beginTransaction();
try {
UangSaku::create($validated);
// Update saldo transaksi berikutnya jika ada
$this->recalculateSaldoAfter($validated['id_santri'], $validated['tanggal_transaksi']);
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());
}
}
/**
* Tampilkan detail transaksi
*/
public function show($id)
{
$transaksi = UangSaku::with('santri')->findOrFail($id);
return view('admin.uang-saku.show', compact('transaksi'));
}
/**
* Form edit transaksi
*/
public function edit($id)
{
$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'));
}
/**
* Update transaksi
*/
public function update(Request $request, $id)
{
$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',
], [
'jenis_transaksi.required' => 'Jenis transaksi wajib dipilih.',
'nominal.required' => 'Nominal wajib diisi.',
'nominal.numeric' => 'Nominal harus berupa angka.',
'nominal.min' => 'Nominal minimal Rp 1.',
'tanggal_transaksi.required' => 'Tanggal transaksi wajib diisi.',
]);
DB::beginTransaction();
try {
$transaksi->update($validated);
// Recalculate semua saldo setelah transaksi ini
$this->recalculateSaldoAfter($transaksi->id_santri, $transaksi->tanggal_transaksi);
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());
}
}
/**
* Hapus transaksi
*/
public function destroy($id)
{
$transaksi = UangSaku::findOrFail($id);
$idSantri = $transaksi->id_santri;
$tanggal = $transaksi->tanggal_transaksi;
DB::beginTransaction();
try {
$transaksi->delete();
// Recalculate saldo setelah transaksi dihapus
$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());
}
}
/**
* Tampilkan riwayat uang saku per santri dengan filter tanggal
*/
public function riwayat(Request $request, $id_santri)
{
$santri = Santri::where('id_santri', $id_santri)->firstOrFail();
// Default: bulan ini
$tanggalDari = $request->filled('tanggal_dari')
? $request->tanggal_dari
: now()->startOfMonth()->format('Y-m-d');
$tanggalSampai = $request->filled('tanggal_sampai')
? $request->tanggal_sampai
: now()->endOfMonth()->format('Y-m-d');
// Query transaksi dengan filter tanggal
$query = UangSaku::where('id_santri', $id_santri);
if ($tanggalDari && $tanggalSampai) {
$query->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai]);
}
$transaksi = $query->orderBy('tanggal_transaksi', 'desc')
->orderBy('created_at', 'desc')
->paginate(20)
->appends($request->query());
// Statistik dengan filter tanggal
$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 terakhir tetap dari keseluruhan transaksi
$saldoTerakhir = $santri->saldo_uang_saku;
// Data untuk grafik dengan filter tanggal
$dataGrafik = UangSaku::where('id_santri', $id_santri)
->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai])
->select(
DB::raw('DATE(tanggal_transaksi) as tanggal'),
DB::raw('SUM(CASE WHEN jenis_transaksi = "pemasukan" THEN nominal ELSE 0 END) as pemasukan'),
DB::raw('SUM(CASE WHEN jenis_transaksi = "pengeluaran" THEN nominal ELSE 0 END) as pengeluaran')
)
->groupBy('tanggal')
->orderBy('tanggal')
->get();
// Jika tidak ada transaksi di rentang tanggal, buat data kosong
if ($dataGrafik->isEmpty()) {
$dataGrafik = collect([
(object)[
'tanggal' => $tanggalDari,
'pemasukan' => 0,
'pengeluaran' => 0
]
]);
}
// Info periode
$periodeDari = \Carbon\Carbon::parse($tanggalDari);
$periodeSampai = \Carbon\Carbon::parse($tanggalSampai);
return view('admin.uang-saku.riwayat', compact(
'santri',
'transaksi',
'totalPemasukan',
'totalPengeluaran',
'saldoTerakhir',
'dataGrafik',
'tanggalDari',
'tanggalSampai',
'periodeDari',
'periodeSampai'
));
}
/**
* Helper: Recalculate saldo untuk transaksi setelah tanggal tertentu
*/
private function recalculateSaldoAfter($idSantri, $tanggal)
{
$transaksiSetelah = UangSaku::where('id_santri', $idSantri)
->where('tanggal_transaksi', '>=', $tanggal)
->orderBy('tanggal_transaksi')
->orderBy('created_at')
->get();
foreach ($transaksiSetelah as $index => $trans) {
if ($index === 0) {
// Transaksi pertama: ambil saldo dari transaksi sebelumnya
$saldoSebelumnya = UangSaku::where('id_santri', $idSantri)
->where('id', '<', $trans->id)
->orderBy('tanggal_transaksi', 'desc')
->orderBy('created_at', 'desc')
->first();
$trans->saldo_sebelum = $saldoSebelumnya ? $saldoSebelumnya->saldo_sesudah : 0;
} else {
$trans->saldo_sebelum = $transaksiSetelah[$index - 1]->saldo_sesudah;
}
if ($trans->jenis_transaksi === 'pemasukan') {
$trans->saldo_sesudah = $trans->saldo_sebelum + $trans->nominal;
} else {
$trans->saldo_sesudah = $trans->saldo_sebelum - $trans->nominal;
}
$trans->saveQuietly(); // Save tanpa trigger event
}
}
}

View File

@ -0,0 +1,113 @@
<?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\Wali;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
class UserController extends Controller
{
/**
* Tampilkan daftar akun Santri.
*/
public function santriAccounts()
{
// Ambil akun user dengan role 'santri'
$users = User::where('role', 'santri')->get();
// Ambil data santri yang belum memiliki akun
$santris_tanpa_akun = Santri::whereDoesntHave('user')->get();
return view('admin.users.santri_accounts', compact('users', 'santris_tanpa_akun'));
}
/**
* Tampilkan daftar akun Wali Santri.
*/
public function waliAccounts()
{
// Ambil akun user dengan role 'wali'
$users = User::where('role', 'wali')->get();
// Asumsi: Wali tidak punya tabel biodata terpisah untuk langkah 3 ini,
// jadi kita ambil dari data Santri.
// Jika Wali memiliki tabel biodata Walis, kita bisa tambahkan logika Wali::whereDoesntHave('user')
$walis = Wali::all();
return view('admin.users.wali_accounts', compact('users', 'walis'));
}
/**
* Tampilkan form untuk membuat akun baru (digunakan untuk santri dan wali).
*/
public function createAccount(string $role)
{
if (!in_array($role, ['santri', 'wali'])) {
abort(404);
}
$list_data = [];
if ($role === 'santri') {
// Ambil santri yang BELUM punya akun
$list_data = Santri::whereDoesntHave('user')->get();
} elseif ($role === 'wali') {
// Ambil semua data wali (kita asumsikan Wali adalah individu terpisah yang didata admin)
$list_data = Wali::all();
}
return view('admin.users.create_account', compact('role', 'list_data'));
}
/**
* Simpan akun baru.
*/
public function storeAccount(Request $request, string $role)
{
if (!in_array($role, ['santri', 'wali'])) {
abort(404);
}
// Validasi
$validated = $request->validate([
'role_id' => [
'required',
Rule::unique('users', 'role_id')->where(function ($query) use ($role) {
return $query->where('role', $role);
})
],
'username' => 'required|string|max:255|unique:users,username',
'password' => 'required|string|min:8|confirmed',
], [
'role_id.unique' => 'Akun untuk data ini sudah ada.',
'role_id.required' => 'Wajib memilih data Santri/Wali yang akan dibuatkan akun.',
'username.unique' => 'Username ini sudah digunakan.',
]);
// Dapatkan nama berdasarkan role_id
if ($role === 'santri') {
$data_induk = Santri::where('id_santri', $request->role_id)->firstOrFail();
$name = $data_induk->nama_lengkap;
} elseif ($role === 'wali') {
$data_induk = Wali::where('id_wali', $request->role_id)->firstOrFail();
$name = $data_induk->nama_wali;
}
// Simpan User
User::create([
'name' => $name,
'username' => $validated['username'],
'password' => Hash::make($validated['password']),
'role' => $role,
'role_id' => $validated['role_id'],
]);
return redirect()->route('admin.users.'.$role.'_accounts')->with('success', 'Akun '.$role.' berhasil dibuat.');
}
// Tambahkan method edit/update/destroy untuk akun di langkah berikutnya
}

View File

@ -0,0 +1,126 @@
<?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'],
]);
// Clear session lama sebelum login
$request->session()->invalidate();
$request->session()->regenerateToken();
// Start session baru
$request->session()->start();
// Coba login dengan username DAN role harus 'admin'
if (Auth::attempt([
'username' => $credentials['username'],
'password' => $credentials['password'],
'role' => 'admin'
], $request->boolean('remember'))) {
// 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, // WAJIB: Gunakan email sebagai username untuk login
'role' => '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

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

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

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

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

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

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

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

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

@ -0,0 +1,93 @@
<?php
// app/Http/Controllers/Auth/SantriAuthController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class SantriAuthController extends Controller
{
/**
* Tampilkan halaman login santri/wali
*/
public function login()
{
return view('santri.auth.login');
}
/**
* Proses login santri/wali dengan auto-clear session on failed
*/
public function authenticate(Request $request)
{
$credentials = $request->validate([
'username' => 'required|string',
'password' => 'required|string',
], [
'username.required' => 'Username wajib diisi.',
'password.required' => 'Password wajib diisi.',
]);
// ✅ TAMBAHAN 1: Clear old session data
$request->session()->forget(['login_attempts', 'last_attempt_time']);
// Coba login dengan guard default
if (Auth::attempt($credentials, $request->boolean('remember'))) {
$user = Auth::user();
// Cek apakah user adalah santri atau wali
if ($user->role === 'santri' || $user->role === 'wali') {
// ✅ TAMBAHAN 2: Regenerate & clear
$request->session()->regenerate();
$request->session()->forget(['login_attempts', 'last_attempt_time']);
return redirect()->intended(route('santri.dashboard'))
->with('success', 'Selamat datang, ' . $user->name . '!');
}
// ✅ TAMBAHAN 3: Role tidak sesuai - clear session
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerate();
return redirect()->back()->withErrors([
'username' => 'Akun Anda tidak memiliki akses ke halaman ini. Gunakan login Admin jika Anda admin.'
])->withInput($request->except('password'));
}
// ✅ TAMBAHAN 4: Track & auto-flush
$attempts = $request->session()->get('login_attempts', 0) + 1;
$request->session()->put('login_attempts', $attempts);
$request->session()->put('last_attempt_time', now());
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 akun tidak terdaftar.",
]);
}
/**
* Logout santri/wali
*/
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('santri.login')
->with('success', 'Anda berhasil logout.');
}
}

View File

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

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

@ -0,0 +1,134 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use App\Models\Santri;
use App\Models\User;
use App\Models\RiwayatPelanggaran;
use App\Models\Berita;
use App\Models\KesehatanSantri;
use App\Models\Kepulangan;
use Carbon\Carbon;
class DashboardController extends Controller
{
/**
* Dashboard Admin
*/
public function admin()
{
try {
$data = [
'total_santri' => Santri::count(),
'total_wali' => User::where('role', 'wali')->count(),
'kegiatan_hari_ini' => 0,
];
return view('admin.dashboardAdmin', compact('data'));
} catch (\Exception $e) {
Log::error('Error di Dashboard Admin: ' . $e->getMessage());
return response()->view('errors.500', [
'error' => 'Terjadi kesalahan saat memuat dashboard',
'message' => $e->getMessage()
], 500);
}
}
/**
* Dashboard Santri/Wali - OPTIMIZED
*/
public function santri()
{
try {
$user = Auth::user();
// Validasi role
if (!in_array($user->role, ['santri', 'wali'])) {
Log::error('Role tidak sesuai: ' . $user->role);
abort(403, 'Akses ditolak. Role Anda: ' . $user->role);
}
// ✅ QUERY OPTIMIZED - Ambil data santri dengan kolom minimal
$santri = Santri::where('id_santri', $user->role_id)
->select('id_santri', 'nama_lengkap', 'kelas')
->firstOrFail();
$idSantri = $santri->id_santri;
$today = Carbon::today();
$weekAgo = Carbon::now()->subDays(7);
// ✅ PARALLEL QUERIES - Eksekusi query secara bersamaan untuk performa
$data = [
'nama_santri' => $santri->nama_lengkap,
'kelas' => $santri->kelas,
'progres_quran' => 0, // Nanti diisi dari database capaian
'progres_hadist' => 0, // Nanti diisi dari database capaian
// Query langsung untuk saldo uang saku (dari accessor model)
'saldo_uang_saku' => $santri->saldo_uang_saku,
// Query optimized untuk poin pelanggaran
'poin_pelanggaran' => RiwayatPelanggaran::where('id_santri', $idSantri)
->sum('poin'),
];
// ✅ Query status kesehatan (hanya jika sedang dirawat)
$statusKesehatan = KesehatanSantri::where('id_santri', $idSantri)
->where('status', 'dirawat')
->select('id', 'keluhan', 'tanggal_masuk')
->orderBy('tanggal_masuk', 'desc')
->first();
// ✅ Query kepulangan aktif (hanya jika sedang dalam periode pulang)
$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();
// ✅ Query berita terbaru (7 hari terakhir) - OPTIMIZED
$beritaTerbaru = Berita::select('id_berita', 'judul', 'created_at')
->where('status', 'published')
->where('created_at', '>=', $weekAgo)
->where(function($query) use ($santri) {
$query->where('target_berita', 'semua')
->orWhere(function($q) use ($santri) {
$q->where('target_berita', 'kelas_tertentu')
->whereJsonContains('target_kelas', $santri->kelas);
})
->orWhereHas('santriTertentu', function($q) use ($santri) {
$q->where('santris.id_santri', $santri->id_santri);
});
})
->orderBy('created_at', 'desc')
->limit(5)
->get();
// Return view dengan data yang sudah dioptimasi
return view('santri.dashboardSantri', compact(
'data',
'santri',
'user',
'beritaTerbaru',
'statusKesehatan',
'kepulanganAktif'
));
} catch (\Exception $e) {
Log::error('=== ERROR DI DASHBOARD SANTRI ===');
Log::error('Message: ' . $e->getMessage());
Log::error('File: ' . $e->getFile());
Log::error('Line: ' . $e->getLine());
return response()->view('errors.500', [
'error' => $e->getMessage(),
], 500);
}
}
}

View File

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

@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\Berita;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class SantriBeritaController extends Controller
{
/**
* Tampilkan daftar berita yang bisa diakses santri
*/
public function index(Request $request)
{
$user = Auth::user();
// Ambil data santri sekali saja
$santri = Santri::where('id_santri', $user->role_id)
->select('id_santri', 'kelas')
->firstOrFail();
// Query berita yang published dan sesuai target
$berita = Berita::query()
->select([
'id',
'id_berita',
'judul',
'konten',
'penulis',
'gambar',
'created_at'
])
->where('status', 'published')
->where(function($query) use ($santri) {
// Berita untuk semua
$query->where('target_berita', 'semua')
// Atau berita untuk kelas santri ini
->orWhere(function($q) use ($santri) {
$q->where('target_berita', 'kelas_tertentu')
->whereJsonContains('target_kelas', $santri->kelas);
})
// Atau berita khusus untuk santri ini
->orWhereHas('santriTertentu', function($q) use ($santri) {
$q->where('santris.id_santri', $santri->id_santri);
});
})
->orderBy('created_at', 'desc')
->paginate(12);
// Ambil status baca santri untuk setiap berita (efficient query)
$beritaIds = $berita->pluck('id_berita')->toArray();
$statusBaca = DB::table('berita_santri')
->where('id_santri', $santri->id_santri)
->whereIn('id_berita', $beritaIds)
->pluck('sudah_dibaca', 'id_berita')
->toArray();
// Attach status baca ke collection
$berita->getCollection()->transform(function($item) use ($statusBaca) {
$item->sudah_dibaca = $statusBaca[$item->id_berita] ?? false;
return $item;
});
return view('santri.berita.index', compact('berita', 'santri'));
}
/**
* Tampilkan detail berita dan tandai sebagai sudah dibaca
*/
public function show($id_berita)
{
$user = Auth::user();
$santri = Santri::where('id_santri', $user->role_id)
->select('id_santri', 'kelas')
->firstOrFail();
// Ambil berita dengan validasi akses
$berita = Berita::where('id_berita', $id_berita)
->where('status', 'published')
->where(function($query) use ($santri) {
$query->where('target_berita', 'semua')
->orWhere(function($q) use ($santri) {
$q->where('target_berita', 'kelas_tertentu')
->whereJsonContains('target_kelas', $santri->kelas);
})
->orWhereHas('santriTertentu', function($q) use ($santri) {
$q->where('santris.id_santri', $santri->id_santri);
});
})
->firstOrFail();
// Tandai sebagai sudah dibaca (insert or update)
DB::table('berita_santri')->updateOrInsert(
[
'id_berita' => $berita->id_berita,
'id_santri' => $santri->id_santri
],
[
'sudah_dibaca' => true,
'tanggal_baca' => now(),
'updated_at' => now()
]
);
return view('santri.berita.show', compact('berita', 'santri'));
}
}

View File

@ -0,0 +1,214 @@
<?php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\Capaian;
use App\Models\Santri;
use App\Models\Semester;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class SantriCapaianController extends Controller
{
/**
* Tampilkan daftar capaian santri yang sedang login
*/
public function index(Request $request)
{
$user = Auth::user();
// Validasi role
if (!in_array($user->role, ['santri', 'wali'])) {
abort(403, 'Unauthorized access');
}
$idSantri = $user->role_id;
// Cache data santri selama 10 menit
$santri = Cache::remember("santri_capaian_{$idSantri}", 600, function () use ($idSantri) {
return Santri::where('id_santri', $idSantri)
->select('id_santri', 'nama_lengkap', 'kelas', 'nis')
->firstOrFail();
});
// Get semester aktif
$semesterAktif = Semester::aktif()->first();
$selectedSemester = $request->input('id_semester', $semesterAktif?->id_semester);
// Query capaian dengan relasi
$query = Capaian::with(['materi:id_materi,nama_kitab,kategori,total_halaman', 'semester:id_semester,nama_semester'])
->where('id_santri', $idSantri)
->select('id', 'id_capaian', 'id_santri', 'id_materi', 'id_semester', 'halaman_selesai', 'persentase', 'tanggal_input');
// Filter semester
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) {
$kategori = $capaian->materi->kategori;
$statistikKategori[$kategori]['count']++;
$statistikKategori[$kategori]['avg'] += $capaian->persentase;
if ($capaian->persentase >= 100) {
$statistikKategori[$kategori]['selesai']++;
}
}
// Hitung rata-rata
foreach ($statistikKategori as $kategori => $data) {
if ($data['count'] > 0) {
$statistikKategori[$kategori]['avg'] = $data['avg'] / $data['count'];
}
}
// Distribusi persentase untuk chart
$distribusiPersentase = [
'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(),
];
// Data untuk semester dropdown
$semesters = Semester::select('id_semester', 'nama_semester', 'tahun_ajaran')
->orderBy('tahun_ajaran', 'desc')
->orderBy('periode', 'desc')
->get();
return view('santri.capaian.index', compact(
'santri',
'capaians',
'totalCapaian',
'rataRataPersentase',
'materiSelesai',
'statistikKategori',
'distribusiPersentase',
'semesters',
'selectedSemester',
'semesterAktif'
));
}
/**
* Tampilkan detail capaian tertentu
*/
public function show($id)
{
$user = Auth::user();
if (!in_array($user->role, ['santri', 'wali'])) {
abort(403, 'Unauthorized access');
}
$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,kelas'
])
->where('id_santri', $user->role_id)
->findOrFail($id);
return view('santri.capaian.show', compact('capaian'));
}
/**
* API untuk data grafik (AJAX)
*/
public function apiGrafikData(Request $request)
{
$user = Auth::user();
$type = $request->input('type', 'kategori');
$idSemester = $request->input('id_semester');
$query = Capaian::with('materi:id_materi,kategori')
->where('id_santri', $user->role_id)
->select('id', 'id_materi', 'persentase', 'id_semester');
if ($idSemester) {
$query->where('id_semester', $idSemester);
}
$capaians = $query->get();
$data = [];
switch ($type) {
case 'kategori':
$avgAlquran = $capaians->filter(fn($c) => $c->materi->kategori == 'Al-Qur\'an')->avg('persentase') ?? 0;
$avgHadist = $capaians->filter(fn($c) => $c->materi->kategori == 'Hadist')->avg('persentase') ?? 0;
$avgTambahan = $capaians->filter(fn($c) => $c->materi->kategori == 'Materi Tambahan')->avg('persentase') ?? 0;
$data = [
'labels' => ['Al-Qur\'an', 'Hadist', 'Materi Tambahan'],
'datasets' => [[
'label' => 'Rata-rata Progress (%)',
'data' => [
round($avgAlquran, 2),
round($avgHadist, 2),
round($avgTambahan, 2)
],
'backgroundColor' => [
'rgba(111, 186, 157, 0.8)',
'rgba(129, 198, 232, 0.8)',
'rgba(255, 213, 107, 0.8)',
],
]]
];
break;
case 'distribusi':
$data = [
'labels' => ['0-25%', '26-50%', '51-75%', '76-99%', '100%'],
'datasets' => [[
'label' => 'Jumlah Materi',
'data' => [
$capaians->whereBetween('persentase', [0, 25])->count(),
$capaians->whereBetween('persentase', [26, 50])->count(),
$capaians->whereBetween('persentase', [51, 75])->count(),
$capaians->whereBetween('persentase', [76, 99])->count(),
$capaians->where('persentase', '>=', 100)->count(),
],
'backgroundColor' => [
'rgba(255, 139, 148, 0.8)',
'rgba(255, 171, 145, 0.8)',
'rgba(255, 213, 107, 0.8)',
'rgba(129, 198, 232, 0.8)',
'rgba(111, 186, 157, 0.8)',
],
]]
];
break;
}
return response()->json($data);
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace App\Http\Controllers\Santri;
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;
class SantriKepulanganController extends Controller
{
/**
* Tampilkan riwayat kepulangan santri yang sedang login
*/
public function index(Request $request)
{
$user = Auth::user();
// Ambil data santri
$santri = Santri::where('id_santri', $user->role_id)
->select('id_santri', 'nama_lengkap', 'kelas')
->firstOrFail();
// Tahun untuk filter
$tahunSekarang = $request->filled('tahun') ? $request->tahun : Carbon::now()->year;
// Query riwayat kepulangan
$query = Kepulangan::query()
->select([
'id',
'id_kepulangan',
'id_santri',
'tanggal_izin',
'tanggal_pulang',
'tanggal_kembali',
'durasi_izin',
'alasan',
'status',
'approved_at',
'created_at'
])
->where('id_santri', $santri->id_santri)
->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
$statistik = [
'total_izin' => Kepulangan::where('id_santri', $santri->id_santri)
->whereYear('tanggal_pulang', $tahunSekarang)
->count(),
'disetujui' => Kepulangan::where('id_santri', $santri->id_santri)
->where('status', 'Disetujui')
->whereYear('tanggal_pulang', $tahunSekarang)
->count(),
'total_hari' => Kepulangan::where('id_santri', $santri->id_santri)
->where('status', 'Disetujui')
->whereYear('tanggal_pulang', $tahunSekarang)
->sum('durasi_izin'),
'menunggu' => Kepulangan::where('id_santri', $santri->id_santri)
->where('status', 'Menunggu')
->whereYear('tanggal_pulang', $tahunSekarang)
->count(),
];
// Hitung sisa kuota (maksimal 12 hari/tahun)
$statistik['sisa_kuota'] = max(0, 12 - $statistik['total_hari']);
$statistik['over_limit'] = $statistik['total_hari'] > 12;
// 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'
));
}
/**
* Tampilkan detail kepulangan
*/
public function show($id_kepulangan)
{
$user = Auth::user();
$santri = Santri::where('id_santri', $user->role_id)
->select('id_santri', 'nama_lengkap', 'kelas')
->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)
->where('status', 'Disetujui')
->whereYear('tanggal_pulang', $tahunSekarang)
->sum('durasi_izin');
$sisaKuota = max(0, 12 - $totalHariTahunIni);
return view('santri.kepulangan.show', compact('kepulangan', 'santri', 'totalHariTahunIni', 'sisaKuota'));
}
}

View File

@ -0,0 +1,121 @@
<?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\Auth;
use Carbon\Carbon;
class SantriKesehatanController extends Controller
{
/**
* Tampilkan riwayat kesehatan santri yang sedang login dengan filter tanggal
*/
public function index(Request $request)
{
$user = Auth::user();
// Ambil data santri
$santri = Santri::where('id_santri', $user->role_id)
->select('id_santri', 'nama_lengkap', 'kelas')
->firstOrFail();
// ✅ TENTUKAN RANGE TANGGAL
// Jika tidak ada filter, default bulan ini
$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_sampai tidak boleh lebih kecil dari tanggal_dari
if ($tanggalSampai->lt($tanggalDari)) {
return back()->withErrors([
'tanggal_sampai' => 'Tanggal sampai harus lebih besar atau sama dengan tanggal dari.'
])->withInput();
}
// ✅ QUERY DASAR DENGAN FILTER TANGGAL
$baseQuery = KesehatanSantri::where('id_santri', $santri->id_santri)
->whereBetween('tanggal_masuk', [
$tanggalDari->format('Y-m-d'),
$tanggalSampai->format('Y-m-d')
]);
// ✅ HITUNG STATISTIK BERDASARKAN FILTER TANGGAL
$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(),
];
// ✅ QUERY RIWAYAT KESEHATAN UNTUK TABEL
$query = KesehatanSantri::query()
->select([
'id',
'id_kesehatan',
'id_santri',
'tanggal_masuk',
'tanggal_keluar',
'keluhan',
'status',
'created_at'
])
->where('id_santri', $santri->id_santri)
->whereBetween('tanggal_masuk', [
$tanggalDari->format('Y-m-d'),
$tanggalSampai->format('Y-m-d')
]);
// Filter status jika ada
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Urutkan terbaru dan paginate
$riwayatKesehatan = $query->orderBy('tanggal_masuk', 'desc')
->paginate(10)
->appends($request->all()); // Append query string untuk pagination
// Data untuk filter
$statusOptions = [
'dirawat' => 'Sedang Dirawat',
'sembuh' => 'Sembuh',
'izin' => 'Izin Sakit'
];
return view('santri.kesehatan.index', compact(
'riwayatKesehatan',
'santri',
'statistik',
'statusOptions',
'tanggalDari',
'tanggalSampai'
));
}
/**
* Tampilkan detail riwayat kesehatan
*/
public function show($id)
{
$user = Auth::user();
$santri = Santri::where('id_santri', $user->role_id)
->select('id_santri', 'nama_lengkap', 'kelas')
->firstOrFail();
// Ambil data kesehatan dengan validasi kepemilikan
$kesehatanSantri = KesehatanSantri::where('id', $id)
->where('id_santri', $santri->id_santri)
->firstOrFail();
return view('santri.kesehatan.show', compact('kesehatanSantri', 'santri'));
}
}

View File

@ -0,0 +1,111 @@
<?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\Auth;
use Illuminate\Support\Facades\Cache;
class SantriPelanggaranController extends Controller
{
/**
* Tampilkan daftar riwayat pelanggaran santri yang sedang login
*/
public function index(Request $request)
{
$user = Auth::user();
// Validasi role
if (!in_array($user->role, ['santri', 'wali'])) {
abort(403, 'Akses ditolak.');
}
// Query riwayat pelanggaran dengan relasi
$query = RiwayatPelanggaran::with(['kategori:id,id_kategori,nama_pelanggaran,poin'])
->where('id_santri', $user->role_id)
->select([
'id',
'id_riwayat',
'id_santri',
'id_kategori',
'tanggal',
'poin',
'keterangan',
'created_at'
]);
// Filter berdasarkan tanggal (opsional)
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 (jika ada parameter)
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', $user->role_id)->count();
$totalPoin = RiwayatPelanggaran::where('id_santri', $user->role_id)->sum('poin');
$pelanggaranBulanIni = RiwayatPelanggaran::where('id_santri', $user->role_id)
->bulanIni()
->count();
return view('santri.pelanggaran.index', compact(
'riwayat',
'totalPelanggaran',
'totalPoin',
'pelanggaranBulanIni'
));
}
/**
* Tampilkan detail satu riwayat pelanggaran
*/
public function show(RiwayatPelanggaran $riwayatPelanggaran)
{
$user = Auth::user();
// Validasi: pastikan pelanggaran milik santri yang login
if ($riwayatPelanggaran->id_santri !== $user->role_id) {
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

@ -0,0 +1,107 @@
<?php
// app/Http/Controllers/Santri/SantriProfileController.php
namespace App\Http\Controllers\Santri;
use App\Http\Controllers\Controller;
use App\Models\Santri;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
class SantriProfileController extends Controller
{
/**
* Tampilkan halaman profil santri yang sedang login
*/
public function index()
{
// Ambil data user yang sedang login
$user = Auth::guard('web')->user();
// Pastikan user adalah santri
if ($user->role !== 'santri') {
abort(403, 'Unauthorized access');
}
// Cache data santri selama 10 menit untuk mengurangi query database
$santri = Cache::remember(
'santri_profile_' . $user->role_id,
600, // 10 menit
function () use ($user) {
return Santri::where('id_santri', $user->role_id)
->select([
'id',
'id_santri',
'nis',
'nama_lengkap',
'jenis_kelamin',
'kelas',
'status',
'alamat_santri',
'daerah_asal',
'nama_orang_tua',
'nomor_hp_ortu',
'rfid_uid',
'created_at'
])
->firstOrFail();
}
);
return view('santri.profil.index', compact('santri'));
}
/**
* Tampilkan form edit profil (data terbatas yang bisa diedit santri)
*/
public function edit()
{
$user = Auth::guard('web')->user();
if ($user->role !== 'santri') {
abort(403, 'Unauthorized access');
}
$santri = Santri::where('id_santri', $user->role_id)
->select([
'id',
'id_santri',
'nama_lengkap',
'alamat_santri',
'nomor_hp_ortu'
])
->firstOrFail();
return view('santri.profil.edit', compact('santri'));
}
/**
* Update profil santri (hanya field tertentu yang boleh diedit)
*/
public function update(Request $request)
{
$user = Auth::guard('web')->user();
if ($user->role !== 'santri') {
abort(403, 'Unauthorized access');
}
$validated = $request->validate([
'alamat_santri' => 'nullable|string|max:500',
'nomor_hp_ortu' => 'nullable|string|max:20|regex:/^[0-9+\-\s()]+$/',
], [
'nomor_hp_ortu.regex' => 'Format nomor HP tidak valid. Hanya boleh berisi angka, +, -, spasi, dan tanda kurung.',
'alamat_santri.max' => 'Alamat maksimal 500 karakter.',
]);
$santri = Santri::where('id_santri', $user->role_id)->firstOrFail();
$santri->update($validated);
// Clear cache setelah update
Cache::forget('santri_profile_' . $user->role_id);
return redirect()->route('santri.profil.index')
->with('success', 'Profil berhasil diperbarui.');
}
}

View File

@ -0,0 +1,151 @@
<?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\Auth;
use Illuminate\Support\Facades\Log;
use App\Models\UangSaku;
use App\Models\Santri;
class SantriUangSakuController extends Controller
{
/**
* Tampilkan riwayat uang saku santri yang sedang login
*/
public function index(Request $request)
{
try {
$user = Auth::user();
// Validasi role
if (!in_array($user->role, ['santri', 'wali'])) {
abort(403, 'Akses ditolak');
}
// Ambil data santri
$santri = Santri::where('id_santri', $user->role_id)->first();
if (!$santri) {
abort(404, 'Data santri tidak ditemukan');
}
// Query uang saku dengan pagination dan filter
$query = UangSaku::where('id_santri', $santri->id_santri)
->with('santri:id_santri,nama_lengkap,kelas');
// Filter berdasarkan jenis transaksi
if ($request->filled('jenis_transaksi')) {
$query->where('jenis_transaksi', $request->jenis_transaksi);
}
// Filter berdasarkan 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
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('keterangan', 'like', "%{$search}%")
->orWhere('id_uang_saku', 'like', "%{$search}%");
});
}
// Urutkan dari yang terbaru
$query->orderBy('tanggal_transaksi', 'desc')
->orderBy('created_at', 'desc');
// Pagination
$riwayatUangSaku = $query->paginate(15)->withQueryString();
// ✅ Hitung statistik berdasarkan filter atau bulan ini
$statistikQuery = UangSaku::where('id_santri', $santri->id_santri);
// Jika ada filter tanggal, gunakan filter tersebut
if ($request->filled('tanggal_dari') || $request->filled('tanggal_sampai')) {
if ($request->filled('tanggal_dari')) {
$statistikQuery->whereDate('tanggal_transaksi', '>=', $request->tanggal_dari);
}
if ($request->filled('tanggal_sampai')) {
$statistikQuery->whereDate('tanggal_transaksi', '<=', $request->tanggal_sampai);
}
} else {
// Jika tidak ada filter, tampilkan data bulan ini saja
$statistikQuery->whereMonth('tanggal_transaksi', now()->month)
->whereYear('tanggal_transaksi', now()->year);
}
// Clone query untuk menghitung pemasukan dan pengeluaran
$totalPemasukan = (clone $statistikQuery)->where('jenis_transaksi', 'pemasukan')->sum('nominal');
$totalPengeluaran = (clone $statistikQuery)->where('jenis_transaksi', 'pengeluaran')->sum('nominal');
// Saldo terakhir tetap dari data terbaru (tidak terpengaruh filter)
$saldoTerakhir = $santri->saldo_uang_saku;
// Info periode untuk ditampilkan di view
if ($request->filled('tanggal_dari') || $request->filled('tanggal_sampai')) {
$periodeTeks = 'Periode: ';
if ($request->filled('tanggal_dari')) {
$periodeTeks .= \Carbon\Carbon::parse($request->tanggal_dari)->format('d/m/Y');
}
$periodeTeks .= ' - ';
if ($request->filled('tanggal_sampai')) {
$periodeTeks .= \Carbon\Carbon::parse($request->tanggal_sampai)->format('d/m/Y');
}
} else {
$periodeTeks = 'Bulan Ini: ' . now()->isoFormat('MMMM YYYY');
}
return view('santri.uang-saku.index', compact(
'riwayatUangSaku',
'santri',
'totalPemasukan',
'totalPengeluaran',
'saldoTerakhir'
));
} catch (\Exception $e) {
Log::error('Error di Riwayat Uang Saku Santri: ' . $e->getMessage());
return back()->with('error', 'Terjadi kesalahan saat memuat riwayat uang saku');
}
}
/**
* Tampilkan detail transaksi
*/
public function show($id)
{
try {
$user = Auth::user();
// Ambil data santri
$santri = Santri::where('id_santri', $user->role_id)->first();
if (!$santri) {
abort(404, 'Data santri tidak ditemukan');
}
// Ambil transaksi dengan validasi kepemilikan
$transaksi = UangSaku::where('id', $id)
->where('id_santri', $santri->id_santri)
->with('santri:id_santri,nama_lengkap,kelas')
->firstOrFail();
return view('santri.uang-saku.show', compact('transaksi', 'santri'));
} catch (\Exception $e) {
Log::error('Error di Detail Uang Saku: ' . $e->getMessage());
return back()->with('error', 'Transaksi tidak ditemukan atau Anda tidak memiliki akses');
}
}
}

View File

@ -0,0 +1,71 @@
<?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,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\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,
];
}

View File

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

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

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

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

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class Role
{
public function handle(Request $request, Closure $next, string $roles): Response
{
// 1. Cek apakah pengguna sudah login
if (!Auth::check()) {
// Clear session jika belum login tapi masih ada session
$request->session()->flush();
$request->session()->regenerate();
return redirect('/admin/login');
}
// Ambil role pengguna saat ini
$currentRole = Auth::user()->role;
// Pisahkan daftar role yang diizinkan
$allowedRoles = explode(',', $roles);
// 2. Cek apakah role pengguna termasuk dalam daftar yang diizinkan
if (!in_array($currentRole, $allowedRoles)) {
// ✅ TAMBAHAN: Redirect ke dashboard yang sesuai, jangan abort
if ($currentRole === 'admin') {
return redirect()->route('admin.dashboard')
->with('error', 'Anda tidak memiliki akses ke halaman tersebut.');
}
if ($currentRole === 'santri' || $currentRole === 'wali') {
return redirect()->route('santri.dashboard')
->with('error', 'Anda tidak memiliki akses ke halaman tersebut.');
}
// Jika role tidak dikenali, logout paksa
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerate();
return redirect('/admin/login')
->with('error', 'Role tidak valid. Silakan login kembali.');
}
return $next($request);
}
}

View File

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

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

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

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

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

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

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

@ -0,0 +1,91 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AbsensiKegiatan extends Model
{
use HasFactory;
protected $fillable = [
'absensi_id',
'kegiatan_id',
'id_santri',
'tanggal',
'status',
'metode_absen',
'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);
}
});
}
/**
* 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');
}
/**
* 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);
}
/**
* Accessor: Status Badge
*/
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>',
];
return $badges[$this->status] ?? $this->status;
}
}

View File

@ -0,0 +1,113 @@
<?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 Many-to-Many dengan Santri
*/
public function santriTertentu()
{
return $this->belongsToMany(Santri::class, 'berita_santri', 'id_berita', 'id_santri', 'id_berita', 'id_santri')
->withPivot('sudah_dibaca', 'tanggal_baca')
->withTimestamps();
}
/**
* 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()
{
return match($this->target_berita) {
'semua' => 'Semua Santri',
'kelas_tertentu' => 'Kelas: ' . implode(', ', $this->target_kelas ?? []),
'santri_tertentu' => $this->santriTertentu->count() . ' 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

@ -0,0 +1,207 @@
<?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)
{
$pages = [];
$ranges = explode(',', $rangeString);
foreach ($ranges as $range) {
$range = trim($range);
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"
$pages[] = intval($range);
}
}
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

@ -0,0 +1,66 @@
<?php
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

@ -0,0 +1,116 @@
<?php
// app/Models/KategoriPelanggaran.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class KategoriPelanggaran extends Model
{
use HasFactory;
/**
* Field yang boleh diisi massal (mass assignment)
*/
protected $fillable = [
'id_kategori',
'nama_pelanggaran',
'poin',
];
/**
* Cast attributes ke tipe data tertentu
*/
protected $casts = [
'poin' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Generator ID Kustom (KP001, KP002, ...)
* Metode ini akan dijalankan setiap kali model baru dibuat (insert).
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
// Pastikan ID kustom belum terisi
if (empty($model->id_kategori)) {
// Ambil data kategori terakhir berdasarkan ID default
$last = KategoriPelanggaran::orderBy('id', 'desc')->first();
// Tentukan nomor urut berikutnya
// Jika ada data terakhir, ambil angka dari ID kustom (misal KP001 -> 1) dan tambahkan 1
$num = $last ? intval(substr($last->id_kategori, 2)) + 1 : 1;
// Format ID: 'KP' + nomor urut 3 digit (dengan padding 0)
$model->id_kategori = 'KP' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
/**
* Relasi: Kategori memiliki banyak riwayat pelanggaran (hasMany).
* Satu kategori bisa digunakan untuk banyak riwayat pelanggaran.
*/
public function riwayatPelanggaran()
{
return $this->hasMany(RiwayatPelanggaran::class, 'id_kategori', 'id_kategori');
}
/**
* Accessor: Mendapatkan total penggunaan kategori
*/
public function getTotalPenggunaanAttribute()
{
return $this->riwayatPelanggaran()->count();
}
/**
* Accessor: Mendapatkan total poin terkumpul dari kategori ini
*/
public function getTotalPoinTerkumpulAttribute()
{
return $this->riwayatPelanggaran()->sum('poin');
}
/**
* Scope: Filter kategori berdasarkan rentang poin
*/
public function scopePoinRendah($query)
{
return $query->where('poin', '<', 10);
}
public function scopePoinSedang($query)
{
return $query->whereBetween('poin', [10, 20]);
}
public function scopePoinTinggi($query)
{
return $query->where('poin', '>', 20);
}
/**
* Scope: Search kategori berdasarkan nama
*/
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('nama_pelanggaran', 'like', "%{$search}%")
->orWhere('id_kategori', 'like', "%{$search}%");
});
}
/**
* Method: Cek apakah kategori masih digunakan
*/
public function isUsed()
{
return $this->riwayatPelanggaran()->exists();
}
}

View File

@ -0,0 +1,90 @@
<?php
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');
}
/**
* 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));
}
}

View File

@ -0,0 +1,247 @@
<?php
// app/Models/Kepulangan.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
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',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Boot method untuk auto-generate ID Kepulangan
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
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);
}
// Auto-calculate durasi_izin
if ($model->tanggal_pulang && $model->tanggal_kembali) {
$pulang = Carbon::parse($model->tanggal_pulang);
$kembali = Carbon::parse($model->tanggal_kembali);
$model->durasi_izin = $pulang->diffInDays($kembali) + 1;
}
// Set tanggal_izin ke hari ini jika tidak diisi
if (empty($model->tanggal_izin)) {
$model->tanggal_izin = now();
}
});
static::updating(function ($model) {
// Recalculate durasi_izin saat update
if ($model->isDirty(['tanggal_pulang', 'tanggal_kembali'])) {
$pulang = Carbon::parse($model->tanggal_pulang);
$kembali = Carbon::parse($model->tanggal_kembali);
$model->durasi_izin = $pulang->diffInDays($kembali) + 1;
}
});
}
/**
* Relasi ke Santri
*/
public function santri()
{
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
}
/**
* Accessor: Format tanggal izin
*/
public function getTanggalIzinFormattedAttribute()
{
return $this->tanggal_izin ? $this->tanggal_izin->format('d F Y') : '-';
}
/**
* Accessor: Format tanggal pulang
*/
public function getTanggalPulangFormattedAttribute()
{
return $this->tanggal_pulang ? $this->tanggal_pulang->format('d F Y') : '-';
}
/**
* Accessor: Format tanggal kembali
*/
public function getTanggalKembaliFormattedAttribute()
{
return $this->tanggal_kembali ? $this->tanggal_kembali->format('d F Y') : '-';
}
/**
* Accessor: Format approved_at
*/
public function getApprovedAtFormattedAttribute()
{
return $this->approved_at ? $this->approved_at->format('d F Y H:i') : '-';
}
/**
* Accessor: Durasi izin calculated
*/
public function getDurasiIzinCalculatedAttribute()
{
if ($this->tanggal_pulang && $this->tanggal_kembali) {
$pulang = Carbon::parse($this->tanggal_pulang);
$kembali = Carbon::parse($this->tanggal_kembali);
return $pulang->diffInDays($kembali) + 1;
}
return $this->durasi_izin ?? 0;
}
/**
* Accessor: Status badge HTML
*/
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);
}
/**
* Scope: Filter berdasarkan status
*/
public function scopeStatus($query, $status)
{
return $query->where('status', $status);
}
/**
* Scope: Filter berdasarkan santri
*/
public function scopeSantri($query, $idSantri)
{
return $query->where('id_santri', $idSantri);
}
/**
* Scope: Kepulangan yang sedang aktif
*/
public function scopeAktif($query)
{
$today = Carbon::today();
return $query->where('status', 'Disetujui')
->whereDate('tanggal_pulang', '<=', $today)
->whereDate('tanggal_kembali', '>=', $today);
}
/**
* Scope: Kepulangan yang terlambat
*/
public function scopeTerlambat($query)
{
return $query->where('status', 'Disetujui')
->whereDate('tanggal_kembali', '<', Carbon::today());
}
/**
* Scope: Search kepulangan
*/
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}%");
});
});
}
/**
* Static method: Get total hari izin per santri per tahun
*/
public static function getTotalHariIzinSantri($idSantri, $tahun = null)
{
$tahun = $tahun ?? Carbon::now()->year;
return self::where('id_santri', $idSantri)
->where('status', 'Disetujui')
->whereYear('tanggal_pulang', $tahun)
->sum('durasi_izin');
}
/**
* Static method: Check apakah santri over limit (lebih dari 12 hari/tahun)
*/
public static function isOverLimit($idSantri, $tahun = null)
{
$totalHari = self::getTotalHariIzinSantri($idSantri, $tahun);
return $totalHari > 12;
}
/**
* Static method: Get sisa kuota izin santri
*/
public static function getSisaKuota($idSantri, $tahun = null)
{
$totalHari = self::getTotalHariIzinSantri($idSantri, $tahun);
return max(0, 12 - $totalHari);
}
}

View File

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

@ -0,0 +1,138 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Materi extends Model
{
use HasFactory;
protected $table = 'materi';
/**
* Field yang boleh diisi massal
*/
protected $fillable = [
'id_materi',
'kategori',
'kelas',
'nama_kitab',
'halaman_mulai',
'halaman_akhir',
'total_halaman',
'deskripsi',
];
/**
* Cast attributes
*/
protected $casts = [
'halaman_mulai' => 'integer',
'halaman_akhir' => 'integer',
'total_halaman' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Generator ID Kustom (M001, M002, ...)
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_materi)) {
$last = Materi::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_materi, 1)) + 1 : 1;
$model->id_materi = 'M' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
// Auto-calculate total_halaman
if ($model->halaman_mulai && $model->halaman_akhir) {
$model->total_halaman = $model->halaman_akhir - $model->halaman_mulai + 1;
}
});
static::updating(function ($model) {
// Auto-calculate total_halaman saat update
if ($model->halaman_mulai && $model->halaman_akhir) {
$model->total_halaman = $model->halaman_akhir - $model->halaman_mulai + 1;
}
});
}
/**
* Scope untuk filter berdasarkan kategori
*/
public function scopeKategori($query, $kategori)
{
return $query->where('kategori', $kategori);
}
/**
* Scope untuk filter berdasarkan kelas
*/
public function scopeKelas($query, $kelas)
{
return $query->where('kelas', $kelas);
}
/**
* Scope untuk search
*/
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('nama_kitab', 'like', "%{$search}%")
->orWhere('id_materi', 'like', "%{$search}%")
->orWhere('deskripsi', 'like', "%{$search}%");
});
}
/**
* Accessor untuk badge kategori
*/
public function getKategoriBadgeAttribute()
{
$badges = [
'Al-Qur\'an' => '<span class="badge badge-primary"><i class="fas fa-book-quran"></i> Al-Qur\'an</span>',
'Hadist' => '<span class="badge badge-success"><i class="fas fa-scroll"></i> Hadist</span>',
'Materi Tambahan' => '<span class="badge badge-info"><i class="fas fa-book"></i> Materi Tambahan</span>',
];
return $badges[$this->kategori] ?? $this->kategori;
}
/**
* Accessor untuk badge kelas
*/
public function getKelasBadgeAttribute()
{
$badges = [
'Lambatan' => '<span class="badge badge-secondary">Lambatan</span>',
'Cepatan' => '<span class="badge badge-warning">Cepatan</span>',
'PB' => '<span class="badge badge-danger">PB</span>',
];
return $badges[$this->kelas] ?? $this->kelas;
}
/**
* Relasi: Materi memiliki banyak capaian
*/
public function capaian()
{
return $this->hasMany(Capaian::class, 'id_materi', 'id_materi');
}
/**
* Get jumlah santri yang sudah ada capaian
*/
public function getJumlahSantriAttribute()
{
return $this->capaian()->distinct('id_santri')->count('id_santri');
}
}

View File

@ -0,0 +1,172 @@
<?php
// app/Models/PembayaranSpp.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class PembayaranSpp extends Model
{
use HasFactory;
protected $table = 'pembayaran_spp';
protected $fillable = [
'id_pembayaran',
'id_santri',
'bulan',
'tahun',
'nominal',
'status',
'tanggal_bayar',
'batas_bayar',
'keterangan',
];
protected $casts = [
'tanggal_bayar' => 'date',
'batas_bayar' => 'date',
'nominal' => 'decimal:2',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Boot method untuk auto-generate ID
*/
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: Pembayaran SPP milik satu Santri
*/
public function santri()
{
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
}
/**
* Accessor: Nama bulan dalam bahasa Indonesia
*/
public function getBulanNamaAttribute()
{
$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] ?? '-';
}
/**
* Accessor: Periode lengkap (Januari 2024)
*/
public function getPeriodeLengkapAttribute()
{
return $this->bulan_nama . ' ' . $this->tahun;
}
/**
* Accessor: Status Badge HTML
*/
public function getStatusBadgeAttribute()
{
if ($this->status === 'Lunas') {
return '<span class="badge badge-success"><i class="fas fa-check-circle"></i> Lunas</span>';
}
// Cek apakah telat
if ($this->isTelat()) {
return '<span class="badge badge-danger"><i class="fas fa-exclamation-triangle"></i> Belum Lunas (Telat)</span>';
}
return '<span class="badge badge-warning"><i class="fas fa-clock"></i> Belum Lunas</span>';
}
/**
* Cek apakah pembayaran sudah telat
*/
public function isTelat()
{
if ($this->status === 'Lunas') {
return false;
}
return Carbon::now()->isAfter($this->batas_bayar);
}
/**
* Accessor: Nominal format Rupiah
*/
public function getNominalFormatAttribute()
{
return 'Rp ' . number_format($this->nominal, 0, ',', '.');
}
/**
* Scope: Filter pembayaran belum lunas
*/
public function scopeBelumLunas($query)
{
return $query->where('status', 'Belum Lunas');
}
/**
* Scope: Filter pembayaran lunas
*/
public function scopeLunas($query)
{
return $query->where('status', 'Lunas');
}
/**
* Scope: Filter pembayaran telat
*/
public function scopeTelat($query)
{
return $query->where('status', 'Belum Lunas')
->where('batas_bayar', '<', Carbon::now());
}
/**
* Scope: Filter by tahun
*/
public function scopeTahun($query, $tahun)
{
return $query->where('tahun', $tahun);
}
/**
* Scope: Filter by bulan
*/
public function scopeBulan($query, $bulan)
{
return $query->where('bulan', $bulan);
}
/**
* Scope: Search
*/
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}%");
}
}

View File

@ -0,0 +1,163 @@
<?php
// app/Models/RiwayatPelanggaran.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class RiwayatPelanggaran extends Model
{
use HasFactory;
/**
* Field yang boleh diisi massal (mass assignment)
*/
protected $fillable = [
'id_riwayat',
'id_santri',
'id_kategori',
'tanggal',
'poin',
'keterangan',
];
/**
* Cast attributes ke tipe data tertentu
*/
protected $casts = [
'tanggal' => 'date',
'poin' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Generator ID Kustom (P001, P002, ...)
* Metode ini akan dijalankan setiap kali model baru dibuat (insert).
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
// Pastikan ID kustom belum terisi
if (empty($model->id_riwayat)) {
// Ambil data riwayat terakhir berdasarkan ID default
$last = RiwayatPelanggaran::orderBy('id', 'desc')->first();
// Tentukan nomor urut berikutnya
// Jika ada data terakhir, ambil angka dari ID kustom (misal P001 -> 1) dan tambahkan 1
$num = $last ? intval(substr($last->id_riwayat, 1)) + 1 : 1;
// Format ID: 'P' + nomor urut 3 digit (dengan padding 0)
$model->id_riwayat = 'P' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
/**
* Relasi: Riwayat belongsTo Santri
* Setiap riwayat pelanggaran dimiliki oleh satu santri
*/
public function santri()
{
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
}
/**
* Relasi: Riwayat belongsTo Kategori
* Setiap riwayat pelanggaran memiliki satu kategori
*/
public function kategori()
{
return $this->belongsTo(KategoriPelanggaran::class, 'id_kategori', 'id_kategori');
}
/**
* Accessor: Format tanggal Indonesia
*/
public function getTanggalFormatAttribute()
{
return Carbon::parse($this->tanggal)->isoFormat('D MMMM YYYY');
}
/**
* Accessor: Get nama santri (dengan fallback)
*/
public function getNamaSantriAttribute()
{
return $this->santri ? $this->santri->nama_lengkap : 'Santri tidak ditemukan';
}
/**
* Accessor: Get nama kategori (dengan fallback)
*/
public function getNamaKategoriAttribute()
{
return $this->kategori ? $this->kategori->nama_pelanggaran : 'Kategori tidak ditemukan';
}
/**
* Scope: Filter riwayat berdasarkan santri
*/
public function scopeBySantri($query, $idSantri)
{
return $query->where('id_santri', $idSantri);
}
/**
* Scope: Filter riwayat berdasarkan kategori
*/
public function scopeByKategori($query, $idKategori)
{
return $query->where('id_kategori', $idKategori);
}
/**
* Scope: Filter riwayat berdasarkan tanggal
*/
public function scopeByTanggal($query, $tanggalMulai, $tanggalSelesai = null)
{
if ($tanggalSelesai) {
return $query->whereBetween('tanggal', [$tanggalMulai, $tanggalSelesai]);
}
return $query->whereDate('tanggal', $tanggalMulai);
}
/**
* Scope: Filter riwayat bulan ini
*/
public function scopeBulanIni($query)
{
return $query->whereMonth('tanggal', Carbon::now()->month)
->whereYear('tanggal', Carbon::now()->year);
}
/**
* Scope: Urutkan berdasarkan tanggal terbaru
*/
public function scopeTerbaru($query)
{
return $query->orderBy('tanggal', 'desc')
->orderBy('created_at', 'desc');
}
/**
* Scope: Search riwayat
*/
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('id_riwayat', 'like', "%{$search}%")
->orWhere('keterangan', 'like', "%{$search}%")
->orWhereHas('santri', function($sq) use ($search) {
$sq->where('nama_lengkap', 'like', "%{$search}%");
})
->orWhereHas('kategori', function($sq) use ($search) {
$sq->where('nama_pelanggaran', 'like', "%{$search}%");
});
});
}
}

View File

@ -0,0 +1,308 @@
<?php
// app/Models/Santri.php - Model untuk Data Santri (LENGKAP)
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Santri extends Model
{
use HasFactory;
/**
* Field yang boleh diisi massal (mass assignment)
*/
protected $fillable = [
'id_santri',
'nis',
'nama_lengkap',
'jenis_kelamin',
'kelas',
'status',
'alamat_santri',
'daerah_asal',
'nama_orang_tua',
'nomor_hp_ortu',
'rfid_uid', // TAMBAHAN BARU
];
/**
* Cast attributes ke tipe data tertentu
*/
protected $casts = [
'created_at' => '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 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 berita (Many-to-Many)
*/
public function berita()
{
return $this->belongsToMany(Berita::class, 'berita_santri', 'id_santri', 'id_berita', 'id_santri', 'id_berita')
->withPivot('sudah_dibaca', 'tanggal_baca')
->withTimestamps();
}
/**
* 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 untuk mendapatkan nama kelas lengkap
*/
public function getKelasLengkapAttribute()
{
$kelasMap = [
'PB' => 'Pembinaan (PB)',
'Lambatan' => 'Lambatan',
'Cepatan' => 'Cepatan',
];
return $kelasMap[$this->kelas] ?? $this->kelas;
}
/**
* Accessor untuk mendapatkan badge HTML status
*/
public function getStatusBadgeAttribute()
{
$badges = [
'Aktif' => '<span class="badge badge-success"><i class="fas fa-check-circle"></i> Aktif</span>',
'Lulus' => '<span class="badge badge-info"><i class="fas fa-graduation-cap"></i> Lulus</span>',
'Tidak Aktif' => '<span class="badge badge-secondary"><i class="fas fa-times-circle"></i> Tidak Aktif</span>',
];
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();
}
/**
* 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
*/
public function scopeKelas($query, $kelas)
{
return $query->where('kelas', $kelas);
}
/**
* 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');
}
/**
* Get rata-rata capaian per semester
*/
public function getRataRataCapaianAttribute()
{
return $this->capaian()->avg('persentase') ?? 0;
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Semester extends Model
{
use HasFactory;
protected $table = 'semester';
protected $fillable = [
'id_semester',
'nama_semester',
'tahun_ajaran',
'periode',
'tanggal_mulai',
'tanggal_akhir',
'is_active',
];
protected $casts = [
'tanggal_mulai' => 'date',
'tanggal_akhir' => 'date',
'is_active' => 'boolean',
'periode' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Generator ID Kustom (SEM001, SEM002, ...)
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_semester)) {
$last = Semester::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_semester, 3)) + 1 : 1;
$model->id_semester = 'SEM' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
// Auto-generate nama_semester jika kosong
if (empty($model->nama_semester)) {
$model->nama_semester = "Semester {$model->periode} {$model->tahun_ajaran}";
}
});
// Pastikan hanya 1 semester yang aktif
static::saving(function ($model) {
if ($model->is_active) {
Semester::where('id', '!=', $model->id)->update(['is_active' => 0]);
}
});
}
/**
* Relasi: Semester memiliki banyak capaian
*/
public function capaian()
{
return $this->hasMany(Capaian::class, 'id_semester', 'id_semester');
}
/**
* Scope: Semester aktif
*/
public function scopeAktif($query)
{
return $query->where('is_active', 1);
}
/**
* Scope: Tahun ajaran tertentu
*/
public function scopeTahunAjaran($query, $tahun)
{
return $query->where('tahun_ajaran', $tahun);
}
/**
* Accessor: Badge status aktif
*/
public function getStatusBadgeAttribute()
{
if ($this->is_active) {
return '<span class="badge badge-success"><i class="fas fa-check-circle"></i> Aktif</span>';
}
return '<span class="badge badge-secondary">Tidak Aktif</span>';
}
}

View File

@ -0,0 +1,145 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UangSaku extends Model
{
use HasFactory;
protected $table = 'uang_saku';
protected $fillable = [
'id_uang_saku',
'id_santri',
'jenis_transaksi',
'nominal',
'keterangan',
'tanggal_transaksi',
'saldo_sebelum',
'saldo_sesudah',
];
protected $casts = [
'tanggal_transaksi' => 'date',
'nominal' => 'decimal:2',
'saldo_sebelum' => 'decimal:2',
'saldo_sesudah' => 'decimal:2',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Auto-generate ID kustom saat create
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_uang_saku)) {
$last = UangSaku::orderBy('id', 'desc')->first();
$num = $last ? intval(substr($last->id_uang_saku, 2)) + 1 : 1;
$model->id_uang_saku = 'SK' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
// Hitung saldo otomatis
$saldoTerakhir = UangSaku::where('id_santri', $model->id_santri)
->orderBy('tanggal_transaksi', 'desc')
->orderBy('created_at', 'desc')
->first();
$model->saldo_sebelum = $saldoTerakhir ? $saldoTerakhir->saldo_sesudah : 0;
if ($model->jenis_transaksi === 'pemasukan') {
$model->saldo_sesudah = $model->saldo_sebelum + $model->nominal;
} else {
$model->saldo_sesudah = $model->saldo_sebelum - $model->nominal;
}
});
static::updating(function ($model) {
// Recalculate saldo when updating
if ($model->isDirty(['nominal', 'jenis_transaksi'])) {
$saldoTerakhir = UangSaku::where('id_santri', $model->id_santri)
->where('id', '<', $model->id)
->orderBy('tanggal_transaksi', 'desc')
->orderBy('created_at', 'desc')
->first();
$model->saldo_sebelum = $saldoTerakhir ? $saldoTerakhir->saldo_sesudah : 0;
if ($model->jenis_transaksi === 'pemasukan') {
$model->saldo_sesudah = $model->saldo_sebelum + $model->nominal;
} else {
$model->saldo_sesudah = $model->saldo_sebelum - $model->nominal;
}
}
});
}
/**
* Relasi ke Santri
*/
public function santri()
{
return $this->belongsTo(Santri::class, 'id_santri', 'id_santri');
}
/**
* Scope untuk filter berdasarkan santri
*/
public function scopeBySantri($query, $idSantri)
{
return $query->where('id_santri', $idSantri);
}
/**
* Scope untuk filter berdasarkan jenis transaksi
*/
public function scopeByJenis($query, $jenis)
{
return $query->where('jenis_transaksi', $jenis);
}
/**
* Scope untuk filter berdasarkan tanggal
*/
public function scopeByDateRange($query, $start, $end)
{
return $query->whereBetween('tanggal_transaksi', [$start, $end]);
}
/**
* Scope untuk search
*/
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('id_uang_saku', 'like', "%{$search}%")
->orWhere('keterangan', 'like', "%{$search}%")
->orWhereHas('santri', function($sq) use ($search) {
$sq->where('nama_lengkap', 'like', "%{$search}%")
->orWhere('id_santri', 'like', "%{$search}%");
});
});
}
/**
* Accessor untuk format nominal
*/
public function getNominalFormatAttribute()
{
return 'Rp ' . number_format($this->nominal, 0, ',', '.');
}
/**
* Accessor untuk format saldo
*/
public function getSaldoSesudahFormatAttribute()
{
return 'Rp ' . number_format($this->saldo_sesudah, 0, ',', '.');
}
}

View File

@ -0,0 +1,65 @@
<?php
// app/Models/User.php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'username', // Ditambahkan
'password',
'role', // Ditambahkan
'role_id', // Ditambahkan
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
// Helper method untuk cek role
public function isAdmin()
{
return $this->role === 'admin';
}
public function isSantri()
{
return $this->role === 'santri';
}
public function isWali()
{
return $this->role === 'wali';
}
}

View File

@ -0,0 +1,32 @@
<?php
// app/Models/Wali.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Wali extends Model
{
use HasFactory;
protected $guarded = ['id'];
/**
* Generator ID Kustom (WS001, WS002, ...)
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_wali)) {
$last = Wali::orderBy('id', 'desc')->first();
// Ambil nomor urut terakhir, jika ada, tambahkan 1. Jika tidak, mulai dari 1.
$num = $last ? intval(substr($last->id_wali, 2)) + 1 : 1;
// Format ID: 'WS' + nomor urut 3 digit (dengan padding 0)
$model->id_wali = 'WS' . str_pad($num, 3, '0', STR_PAD_LEFT);
}
});
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
//
];
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
//
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Broadcast::routes();
require base_path('routes/channels.php');
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
*/
public function boot(): void
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to your application's "home" route.
*
* Typically, users are redirected here after authentication.
*
* @var string
*/
public const HOME = '/dashboard';
/**
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.app');
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.guest');
}
}

53
sim-pkpps/artisan Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env php
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any of our classes manually. It's great to relax.
|
*/
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running, we will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/
$kernel->terminate($input, $status);
exit($status);

View File

@ -0,0 +1,55 @@
<?php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;

2
sim-pkpps/bootstrap/cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

68
sim-pkpps/composer.json Normal file
View File

@ -0,0 +1,68 @@
{
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.1",
"barryvdh/laravel-dompdf": "^3.1",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
"laravel/breeze": "^1.29",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1",
"spatie/laravel-ignition": "^2.0"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

8678
sim-pkpps/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

188
sim-pkpps/config/app.php Normal file
View File

@ -0,0 +1,188 @@
<?php
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\ServiceProvider;
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application. This value is used when the
| framework needs to place the application's name in a notification or
| any other location as required by the application or its packages.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
|
*/
'url' => env('APP_URL', 'http://localhost'),
'asset_url' => env('ASSET_URL'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => 'en',
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Faker Locale
|--------------------------------------------------------------------------
|
| This locale will be used by the Faker PHP library when generating fake
| data for your database seeds. For example, this will be used to get
| localized telephone numbers, street address information and more.
|
*/
'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => 'file',
// 'store' => 'redis',
],
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => ServiceProvider::defaultProviders()->merge([
/*
* Package Service Providers...
*/
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
])->toArray(),
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => Facade::defaultAliases()->merge([
// 'Example' => App\Facades\Example::class,
])->toArray(),
];

115
sim-pkpps/config/auth.php Normal file
View File

@ -0,0 +1,115 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| times out and the user is prompted to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => 10800,
];

View File

@ -0,0 +1,71 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_DRIVER', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over websockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

111
sim-pkpps/config/cache.php Normal file
View File

@ -0,0 +1,111 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache connection that gets used while
| using this caching library. This connection is used when another is
| not explicitly specified when executing a given caching function.
|
*/
'default' => env('CACHE_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "apc", "array", "database", "file",
| "memcached", "redis", "dynamodb", "octane", "null"
|
*/
'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'table' => 'cache',
'connection' => null,
'lock_connection' => null,
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, or DynamoDB cache
| stores there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
];

34
sim-pkpps/config/cors.php Normal file
View File

@ -0,0 +1,34 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];

View File

@ -0,0 +1,151 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for all database work. Of course
| you may use many connections at once using the Database library.
|
*/
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Here are each of the database connections setup for your application.
| Of course, examples of configuring each database platform that is
| supported by Laravel is shown below to make development simple.
|
|
| All database work in Laravel is done through the PHP PDO facilities
| so make sure you have the driver for your particular database of
| choice installed on your machine before you begin development.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DATABASE_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run in the database.
|
*/
'migrations' => 'migrations',
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
];

View File

@ -0,0 +1,76 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application. Just store away!
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Here you may configure as many filesystem "disks" as you wish, and you
| may even configure multiple disks of the same driver. Defaults have
| been set up for each driver as an example of the required values.
|
| Supported Drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

View File

@ -0,0 +1,54 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Hash Driver
|--------------------------------------------------------------------------
|
| This option controls the default hash driver that will be used to hash
| passwords for your application. By default, the bcrypt algorithm is
| used; however, you remain free to modify this option if you wish.
|
| Supported: "bcrypt", "argon", "argon2id"
|
*/
'driver' => 'bcrypt',
/*
|--------------------------------------------------------------------------
| Bcrypt Options
|--------------------------------------------------------------------------
|
| Here you may specify the configuration options that should be used when
| passwords are hashed using the Bcrypt algorithm. This will allow you
| to control the amount of time it takes to hash the given password.
|
*/
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12),
'verify' => true,
],
/*
|--------------------------------------------------------------------------
| Argon Options
|--------------------------------------------------------------------------
|
| Here you may specify the configuration options that should be used when
| passwords are hashed using the Argon algorithm. These will allow you
| to control the amount of time it takes to hash the given password.
|
*/
'argon' => [
'memory' => 65536,
'threads' => 1,
'time' => 4,
'verify' => true,
],
];

View File

@ -0,0 +1,131 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that gets used when writing
| messages to the logs. The name specified in this option should match
| one of the channels defined in the "channels" configuration array.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => false,
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog",
| "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => LOG_USER,
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

134
sim-pkpps/config/mail.php Normal file
View File

@ -0,0 +1,134 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send any email
| messages sent by your application. Alternative mailers may be setup
| and used as needed; however, this mailer will be used by default.
|
*/
'default' => env('MAIL_MAILER', 'smtp'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers to be used while
| sending an e-mail. You will specify which one you are using for your
| mailers below. You are free to add additional mailers as required.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "log", "array", "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => null,
// 'client' => [
// 'timeout' => 5,
// ],
],
'mailgun' => [
'transport' => 'mailgun',
// 'client' => [
// 'timeout' => 5,
// ],
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
/*
|--------------------------------------------------------------------------
| Markdown Mail Settings
|--------------------------------------------------------------------------
|
| If you are using Markdown based email rendering, you may configure your
| theme and component paths here, allowing you to customize the design
| of the emails. Or, you may simply stick with the Laravel defaults!
|
*/
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
];

109
sim-pkpps/config/queue.php Normal file
View File

@ -0,0 +1,109 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue API supports an assortment of back-ends via a single
| API, giving you convenient access to each back-end using the same
| syntax for every one. Here you may define a default connection.
|
*/
'default' => env('QUEUE_CONNECTION', 'sync'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection information for each server that
| is used by your application. A default configuration has been added
| for each back-end shipped with Laravel. You are free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => 'localhost',
'queue' => 'default',
'retry_after' => 90,
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
'after_commit' => false,
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control which database and table are used to store the jobs that
| have failed. You may change them to any database / table you wish.
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'failed_jobs',
],
];

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