From ff81f4cbf6c6c3353a79a2831ed9cedd493f0850 Mon Sep 17 00:00:00 2001 From: HelgaFaisa <158024195+HelgaFaisa@users.noreply.github.com> Date: Wed, 13 May 2026 11:22:50 +0700 Subject: [PATCH] upload project TA SIM Santri --- .../Controllers/Admin/UangSakuController.php | 300 ++++--- .../views/admin/uang-saku/create.blade.php | 595 ++++++++++++-- .../views/admin/uang-saku/edit.blade.php | 140 +++- .../views/admin/uang-saku/index.blade.php | 322 ++++---- .../views/admin/uang-saku/riwayat.blade.php | 715 +++++++++------- .../views/admin/uang-saku/show.blade.php | 38 +- sim-pkpps/routes/web.php | 16 +- .../7HNO5zAVTX7osk5DNw3Ws91IJFIdNnXEAVFmSFUe | 1 - .../98TcXy0dZyU2qen4InQ6kb0lWIhs3jDYEHoPzZ9F | 1 - .../Db26P0I3DZ0iqkMb5TTztMRBIEPV5l3UlXzfn7xY | 1 - .../MxVUB2gytfb3tIk5LNAy3n2F8l00rSxvaytMGvAN | 1 - .../Puc2TzyQCnjFG6TESHwPibLlpgxgFK7oN1dCkqUm | 1 + .../TNlMADEKucRoDeR0sBZIo1Q9GrUrqbcbnWWH34kI | 1 - .../uWhN56g6ZJd9l3WiPkDIIw825EEpZ6H5IcATHhsN | 1 + .../02f062229c85f0045f8e936b2cc6931a.php | 5 - .../0c7ce0d95747e48aa6c47d6726a7077a.php | 417 ---------- .../110d795af2097eabaa249a0a3180facc.php | 704 ++++++++++++++++ .../17cd61851063d95913ca53079e0560a4.php | 107 --- .../25e18337d272f9d0e44384a50ffc0f2e.php | 395 +++++++++ .../313421d53bce467b3badbcdb3a6a679a.php | 777 ------------------ .../3c6140ea8a742aaf2aeffd1e7082af63.php | 35 - .../4abc92fe448282e66644310b3fb9a805.php | 164 ---- .../563c872ad4366bc1346c69477cba06bb.php | 303 +++++++ .../65e2b857610af023b413a97dd9a152f2.php | 168 ---- .../7f6365c6b1f7e1f6c677fd10a0a2cecb.php | 144 ---- .../84c709f0b9be2a14b454115d6ed83ed6.php | 50 -- .../87643fe3b359872f8ce3c66a3684eab2.php | 592 ------------- .../9b58324ce92b42a8f9c43eb5d7bb3bd2.php | 456 ---------- .../a3725133d78be20f1a39c9d3a6172e8d.php | 74 -- .../a55ee0bd4bb0ff61d001bcba38d0f720.php | 173 ++++ .../b130696dd20b50a4932a7b40f32091c1.php | 465 +++++++++++ .../b189c66d92090748551bf01fbdf8d452.php | 373 --------- .../b97f1c478bb1ee2f89803d6e58654ed6.php | 28 - .../bbf90b40396229b9af5ccf996fe33770.php | 680 +++++++++++++++ .../bf740845a4b1e169ca4b9cfa2f601c79.php | 258 ------ .../c5e1a22ff4ed6201da5132bb8f82a4a3.php | 512 ++++++++++++ .../e7a3eb2c7c53a4310ad6e32050798b86.php | 387 --------- .../f40c6e82dbaae8582485c19f3028947f.php | 140 ++++ .../f5f9ca518d99a1b7d925ef13ad0bbbe1.php | 315 ------- .../f7d8d067fc835ce54b0dc2d499afcbbd.php | 426 ---------- 40 files changed, 4832 insertions(+), 5449 deletions(-) delete mode 100644 sim-pkpps/storage/framework/sessions/7HNO5zAVTX7osk5DNw3Ws91IJFIdNnXEAVFmSFUe delete mode 100644 sim-pkpps/storage/framework/sessions/98TcXy0dZyU2qen4InQ6kb0lWIhs3jDYEHoPzZ9F delete mode 100644 sim-pkpps/storage/framework/sessions/Db26P0I3DZ0iqkMb5TTztMRBIEPV5l3UlXzfn7xY delete mode 100644 sim-pkpps/storage/framework/sessions/MxVUB2gytfb3tIk5LNAy3n2F8l00rSxvaytMGvAN create mode 100644 sim-pkpps/storage/framework/sessions/Puc2TzyQCnjFG6TESHwPibLlpgxgFK7oN1dCkqUm delete mode 100644 sim-pkpps/storage/framework/sessions/TNlMADEKucRoDeR0sBZIo1Q9GrUrqbcbnWWH34kI create mode 100644 sim-pkpps/storage/framework/sessions/uWhN56g6ZJd9l3WiPkDIIw825EEpZ6H5IcATHhsN delete mode 100644 sim-pkpps/storage/framework/views/02f062229c85f0045f8e936b2cc6931a.php delete mode 100644 sim-pkpps/storage/framework/views/0c7ce0d95747e48aa6c47d6726a7077a.php create mode 100644 sim-pkpps/storage/framework/views/110d795af2097eabaa249a0a3180facc.php delete mode 100644 sim-pkpps/storage/framework/views/17cd61851063d95913ca53079e0560a4.php create mode 100644 sim-pkpps/storage/framework/views/25e18337d272f9d0e44384a50ffc0f2e.php delete mode 100644 sim-pkpps/storage/framework/views/313421d53bce467b3badbcdb3a6a679a.php delete mode 100644 sim-pkpps/storage/framework/views/3c6140ea8a742aaf2aeffd1e7082af63.php delete mode 100644 sim-pkpps/storage/framework/views/4abc92fe448282e66644310b3fb9a805.php create mode 100644 sim-pkpps/storage/framework/views/563c872ad4366bc1346c69477cba06bb.php delete mode 100644 sim-pkpps/storage/framework/views/65e2b857610af023b413a97dd9a152f2.php delete mode 100644 sim-pkpps/storage/framework/views/7f6365c6b1f7e1f6c677fd10a0a2cecb.php delete mode 100644 sim-pkpps/storage/framework/views/84c709f0b9be2a14b454115d6ed83ed6.php delete mode 100644 sim-pkpps/storage/framework/views/87643fe3b359872f8ce3c66a3684eab2.php delete mode 100644 sim-pkpps/storage/framework/views/9b58324ce92b42a8f9c43eb5d7bb3bd2.php delete mode 100644 sim-pkpps/storage/framework/views/a3725133d78be20f1a39c9d3a6172e8d.php create mode 100644 sim-pkpps/storage/framework/views/a55ee0bd4bb0ff61d001bcba38d0f720.php create mode 100644 sim-pkpps/storage/framework/views/b130696dd20b50a4932a7b40f32091c1.php delete mode 100644 sim-pkpps/storage/framework/views/b189c66d92090748551bf01fbdf8d452.php delete mode 100644 sim-pkpps/storage/framework/views/b97f1c478bb1ee2f89803d6e58654ed6.php create mode 100644 sim-pkpps/storage/framework/views/bbf90b40396229b9af5ccf996fe33770.php delete mode 100644 sim-pkpps/storage/framework/views/bf740845a4b1e169ca4b9cfa2f601c79.php create mode 100644 sim-pkpps/storage/framework/views/c5e1a22ff4ed6201da5132bb8f82a4a3.php delete mode 100644 sim-pkpps/storage/framework/views/e7a3eb2c7c53a4310ad6e32050798b86.php create mode 100644 sim-pkpps/storage/framework/views/f40c6e82dbaae8582485c19f3028947f.php delete mode 100644 sim-pkpps/storage/framework/views/f5f9ca518d99a1b7d925ef13ad0bbbe1.php delete mode 100644 sim-pkpps/storage/framework/views/f7d8d067fc835ce54b0dc2d499afcbbd.php diff --git a/sim-pkpps/app/Http/Controllers/Admin/UangSakuController.php b/sim-pkpps/app/Http/Controllers/Admin/UangSakuController.php index 57a5574..0662d46 100644 --- a/sim-pkpps/app/Http/Controllers/Admin/UangSakuController.php +++ b/sim-pkpps/app/Http/Controllers/Admin/UangSakuController.php @@ -12,6 +12,21 @@ class UangSakuController extends Controller { + // ──────────────────────────────────────────────────────────────── + // PRIVATE: cek apakah user aktif adalah pamong + // ──────────────────────────────────────────────────────────────── + private function isPamong(): bool + { + return auth()->user()->role === 'pamong'; + } + + private function requirePamong(): void + { + if (! $this->isPamong()) { + abort(403, 'Akses ditolak. Hanya Pamong yang dapat melakukan aksi ini.'); + } + } + /** * Tampilkan daftar uang saku — Grouped per Santri * Default: bulan ini @@ -20,12 +35,11 @@ public function index(Request $request) { $search = $request->get('search'); - // ── Default: bulan ini ────────────────────────────────────── $dari = $request->get('dari', now()->startOfMonth()->format('Y-m-d')); $sampai = $request->get('sampai', now()->endOfMonth()->format('Y-m-d')); - $sort = $request->get('sort', 'nama'); // nama | saldo_asc | saldo_desc | transaksi_desc | terakhir + $sort = $request->get('sort', 'nama'); - // ── KPI ringkasan periode (dipengaruhi filter tanggal) ────── + // ── KPI ringkasan periode ─────────────────────────────────── $kpiQuery = UangSaku::whereBetween('tanggal_transaksi', [$dari, $sampai]); $kpi = [ 'total_transaksi' => (clone $kpiQuery)->count(), @@ -33,23 +47,22 @@ public function index(Request $request) 'total_pengeluaran' => (float)(clone $kpiQuery)->where('jenis_transaksi', 'pengeluaran')->sum('nominal'), 'total_santri' => (clone $kpiQuery)->distinct('id_santri')->count('id_santri'), ]; - // Selisih periode: apakah dalam rentang ini uang yang masuk lebih besar dari yang keluar $kpi['selisih'] = $kpi['total_pemasukan'] - $kpi['total_pengeluaran']; - // ── KPI Real-time: Total saldo aktual seluruh santri (tidak dipengaruhi filter) ── - // Ambil saldo_sesudah dari transaksi TERAKHIR masing-masing santri - $totalSaldoSemua = DB::table('uang_saku as u1') - ->join(DB::raw('( - SELECT id_santri, MAX(id) as max_id - FROM uang_saku - GROUP BY id_santri - ) as latest'), function ($join) { - $join->on('u1.id_santri', '=', 'latest.id_santri') - ->on('u1.id', '=', 'latest.max_id'); + // ── KPI Real-time: total saldo semua santri ───────────────── + $kpi['total_saldo_realtime'] = (float) DB::table('uang_saku as a') + ->whereNotExists(function ($q) { + $q->from('uang_saku as b') + ->whereColumn('b.id_santri', 'a.id_santri') + ->where(function ($inner) { + $inner->whereColumn('b.tanggal_transaksi', '>', 'a.tanggal_transaksi') + ->orWhere(function ($tie) { + $tie->whereColumn('b.tanggal_transaksi', '=', 'a.tanggal_transaksi') + ->whereColumn('b.id', '>', 'a.id'); + }); + }); }) - ->sum('u1.saldo_sesudah'); - - $kpi['total_saldo_realtime'] = (float) $totalSaldoSemua; + ->sum('saldo_sesudah'); // ── Query santri ──────────────────────────────────────────── $santriQuery = Santri::aktif() @@ -64,71 +77,79 @@ public function index(Request $request) } $santriQuery->orderBy('nama_lengkap'); - $santriList = $santriQuery->paginate(20)->appends(request()->query()); - $ids = $santriList->pluck('id_santri'); - // ── Saldo terakhir per santri (efisien: subquery per-id) ──── - // Ambil id transaksi terakhir per santri lalu join, hindari get()->unique() yang boros - $latestIds = DB::table('uang_saku') - ->whereIn('id_santri', $ids) - ->select('id_santri', DB::raw('MAX(id) as max_id')) - ->groupBy('id_santri') - ->pluck('max_id', 'id_santri'); + // ── Saldo terakhir per santri (NOT EXISTS) ────────────────── + $latestIds = DB::table('uang_saku as a') + ->whereIn('a.id_santri', $ids) + ->whereNotExists(function ($q) { + $q->from('uang_saku as b') + ->whereColumn('b.id_santri', 'a.id_santri') + ->where(function ($inner) { + $inner->whereColumn('b.tanggal_transaksi', '>', 'a.tanggal_transaksi') + ->orWhere(function ($tie) { + $tie->whereColumn('b.tanggal_transaksi', '=', 'a.tanggal_transaksi') + ->whereColumn('b.id', '>', 'a.id'); + }); + }); + }) + ->select('a.id_santri', 'a.id') + ->pluck('a.id', 'a.id_santri'); $saldoMap = UangSaku::whereIn('id', $latestIds->values()) ->get() ->keyBy('id_santri'); - // ── Pemasukan & pengeluaran bulan ini per santri ──────────── - $bulanIniStats = UangSaku::whereIn('id_santri', $ids) - ->whereMonth('tanggal_transaksi', now()->month) - ->whereYear('tanggal_transaksi', now()->year) + // ── Statistik per santri mengikuti PERIODE filter ─────────── + $periodeStats = UangSaku::whereIn('id_santri', $ids) + ->whereBetween('tanggal_transaksi', [$dari, $sampai]) ->select( 'id_santri', - DB::raw('SUM(CASE WHEN jenis_transaksi="pemasukan" THEN nominal ELSE 0 END) as pemasukan_bulan'), - DB::raw('SUM(CASE WHEN jenis_transaksi="pengeluaran" THEN nominal ELSE 0 END) as pengeluaran_bulan'), - DB::raw('COUNT(*) as total_bulan') + DB::raw('SUM(CASE WHEN jenis_transaksi="pemasukan" THEN nominal ELSE 0 END) as pemasukan_periode'), + DB::raw('SUM(CASE WHEN jenis_transaksi="pengeluaran" THEN nominal ELSE 0 END) as pengeluaran_periode'), + DB::raw('COUNT(*) as total_periode') ) ->groupBy('id_santri') ->get() ->keyBy('id_santri'); - // ── Transaksi terbaru per santri (max 5, untuk collapsed detail) ── + // ── Transaksi terbaru per santri (max 5) ──────────────────── $transaksiMap = UangSaku::whereIn('id_santri', $ids) ->orderByDesc('tanggal_transaksi') - ->orderByDesc('created_at') + ->orderByDesc('id') ->get() ->groupBy('id_santri') ->map(fn($g) => $g->take(5)); - // ── Attach semua data ke santri objects ───────────────────── - $collection = $santriList->getCollection()->map(function ($santri) use ($saldoMap, $bulanIniStats, $transaksiMap) { - $saldoRow = $saldoMap[$santri->id_santri] ?? null; - $bulan = $bulanIniStats[$santri->id_santri] ?? null; + // ── Attach data ke santri objects ─────────────────────────── + $collection = $santriList->getCollection()->map(function ($santri) use ($saldoMap, $periodeStats, $transaksiMap) { + $saldoRow = $saldoMap[$santri->id_santri] ?? null; + $periode = $periodeStats[$santri->id_santri] ?? null; - $santri->saldo_terakhir = $saldoRow ? (float)$saldoRow->saldo_sesudah : 0; - $santri->transaksi_terakhir_tgl = $saldoRow ? $saldoRow->tanggal_transaksi : null; - $santri->pemasukan_bulan = $bulan ? (float)$bulan->pemasukan_bulan : 0; - $santri->pengeluaran_bulan = $bulan ? (float)$bulan->pengeluaran_bulan : 0; - $santri->transaksi_bulan_ini = $bulan ? (int)$bulan->total_bulan : 0; - $santri->transaksi_terbaru = $transaksiMap[$santri->id_santri] ?? collect(); + $santri->saldo_terakhir = $saldoRow ? (float)$saldoRow->saldo_sesudah : 0; + $santri->transaksi_terakhir_tgl = $saldoRow ? $saldoRow->tanggal_transaksi : null; + $santri->pemasukan_periode = $periode ? (float)$periode->pemasukan_periode : 0; + $santri->pengeluaran_periode = $periode ? (float)$periode->pengeluaran_periode : 0; + $santri->transaksi_periode = $periode ? (int)$periode->total_periode : 0; + $santri->transaksi_terbaru = $transaksiMap[$santri->id_santri] ?? collect(); return $santri; }); - // ── Re-sort collection setelah attach ─────────────────────── $sorted = match($sort) { - 'saldo_asc' => $collection->sortBy('saldo_terakhir'), - 'saldo_desc' => $collection->sortByDesc('saldo_terakhir'), - 'transaksi_desc' => $collection->sortByDesc('transaksi_bulan_ini'), - 'terakhir' => $collection->sortByDesc('transaksi_terakhir_tgl'), - default => $collection->sortBy('nama_lengkap'), + 'saldo_asc' => $collection->sortBy('saldo_terakhir'), + 'saldo_desc' => $collection->sortByDesc('saldo_terakhir'), + 'transaksi_desc' => $collection->sortByDesc('transaksi_periode'), + 'terakhir' => $collection->sortByDesc('transaksi_terakhir_tgl'), + default => $collection->sortBy('nama_lengkap'), }; $santriList->setCollection($sorted->values()); - return view('admin.uang-saku.index', compact('santriList', 'kpi', 'dari', 'sampai', 'sort')); + // Kirim flag ke view agar tampilan bisa menyesuaikan + $canCrud = $this->isPamong(); + + return view('admin.uang-saku.index', compact('santriList', 'kpi', 'dari', 'sampai', 'sort', 'canCrud')); } /** @@ -136,13 +157,12 @@ public function index(Request $request) */ public function santriInfo($id_santri) { - $santri = Santri::where('id_santri', $id_santri)->firstOrFail(); - + $santri = Santri::where('id_santri', $id_santri)->firstOrFail(); $bulanIni = now(); $lastTx = UangSaku::where('id_santri', $id_santri) ->orderByDesc('tanggal_transaksi') - ->orderByDesc('created_at') + ->orderByDesc('id') ->first(); $saldo = $lastTx ? (float)$lastTx->saldo_sesudah : 0; @@ -161,7 +181,7 @@ public function santriInfo($id_santri) $transaksiTerakhir = UangSaku::where('id_santri', $id_santri) ->orderByDesc('tanggal_transaksi') - ->orderByDesc('created_at') + ->orderByDesc('id') ->limit(3) ->get() ->map(fn($t) => [ @@ -183,6 +203,8 @@ public function santriInfo($id_santri) public function create() { + $this->requirePamong(); + $santriList = Santri::where('status', 'Aktif') ->select('id_santri', 'nama_lengkap') ->orderBy('nama_lengkap') @@ -193,6 +215,8 @@ public function create() public function store(Request $request) { + $this->requirePamong(); + $validated = $request->validate([ 'id_santri' => 'required|exists:santris,id_santri', 'jenis_transaksi' => 'required|in:pemasukan,pengeluaran', @@ -205,6 +229,17 @@ public function store(Request $request) try { UangSaku::create($validated); $this->recalculateSaldoAfter($validated['id_santri'], $validated['tanggal_transaksi']); + + if ($validated['jenis_transaksi'] === 'pengeluaran' + && $this->hasSaldoNegatif($validated['id_santri'], $validated['tanggal_transaksi'])) { + DB::rollBack(); + return back()->withInput()->with( + 'error', + 'Transaksi gagal: Saldo tidak mencukupi. ' . + 'Jumlah pengeluaran melebihi saldo yang tersedia pada tanggal tersebut.' + ); + } + DB::commit(); Cache::forget('santri_aktif_uang_saku'); return redirect()->route('admin.uang-saku.index') @@ -218,11 +253,14 @@ public function store(Request $request) public function show($id) { $transaksi = UangSaku::with('santri')->findOrFail($id); - return view('admin.uang-saku.show', compact('transaksi')); + $canCrud = $this->isPamong(); + return view('admin.uang-saku.show', compact('transaksi', 'canCrud')); } public function edit($id) { + $this->requirePamong(); + $transaksi = UangSaku::with('santri')->findOrFail($id); $santriList = Santri::where('status', 'Aktif') ->select('id_santri', 'nama_lengkap') @@ -233,6 +271,8 @@ public function edit($id) public function update(Request $request, $id) { + $this->requirePamong(); + $transaksi = UangSaku::findOrFail($id); $validated = $request->validate([ 'jenis_transaksi' => 'required|in:pemasukan,pengeluaran', @@ -241,20 +281,24 @@ public function update(Request $request, $id) 'tanggal_transaksi' => 'required|date', ]); - // Simpan tanggal lama sebelum update, agar recalculate dimulai dari yang paling awal - $tanggalLama = $transaksi->tanggal_transaksi->format('Y-m-d'); + $tanggalLama = $transaksi->tanggal_transaksi->format('Y-m-d'); + $tanggalBaru = $validated['tanggal_transaksi']; + $tanggalMulai = min($tanggalLama, $tanggalBaru); DB::beginTransaction(); try { - // Gunakan saveQuietly agar model boot (updating) tidak ikut menghitung ulang saldo - // — recalculate akan dikerjakan secara menyeluruh oleh recalculateSaldoAfter() $transaksi->fill($validated)->saveQuietly(); - - // Recalculate dari tanggal yang paling awal antara tanggal lama dan baru - $tanggalBaru = $validated['tanggal_transaksi']; - $tanggalMulai = min($tanggalLama, $tanggalBaru); - $this->recalculateSaldoAfter($transaksi->id_santri, $tanggalMulai); + + if ($this->hasSaldoNegatif($transaksi->id_santri, $tanggalMulai)) { + DB::rollBack(); + return back()->withInput()->with( + 'error', + 'Perubahan gagal: Perubahan ini menyebabkan saldo menjadi negatif. ' . + 'Pengeluaran tidak boleh melebihi saldo yang tersedia.' + ); + } + DB::commit(); Cache::forget('santri_aktif_uang_saku'); return redirect()->route('admin.uang-saku.index') @@ -267,6 +311,8 @@ public function update(Request $request, $id) public function destroy($id) { + $this->requirePamong(); + $transaksi = UangSaku::findOrFail($id); $idSantri = $transaksi->id_santri; $tanggal = $transaksi->tanggal_transaksi->format('Y-m-d'); @@ -289,17 +335,22 @@ public function riwayat(Request $request, $id_santri) { $santri = Santri::where('id_santri', $id_santri)->firstOrFail(); - $tanggalDari = $request->filled('tanggal_dari') ? $request->tanggal_dari : now()->startOfMonth()->format('Y-m-d'); - $tanggalSampai = $request->filled('tanggal_sampai') ? $request->tanggal_sampai : now()->endOfMonth()->format('Y-m-d'); + $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 = UangSaku::where('id_santri', $id_santri) - ->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai]); - - $transaksi = $query->orderBy('tanggal_transaksi', 'desc') - ->orderBy('created_at', 'desc') + // Transaksi dalam periode (paginated) + $transaksi = UangSaku::where('id_santri', $id_santri) + ->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai]) + ->orderBy('tanggal_transaksi', 'desc') + ->orderBy('id', 'desc') ->paginate(20) ->appends($request->query()); + // Total pemasukan & pengeluaran periode $totalPemasukan = UangSaku::where('id_santri', $id_santri) ->where('jenis_transaksi', 'pemasukan') ->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai]) @@ -310,50 +361,102 @@ public function riwayat(Request $request, $id_santri) ->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai]) ->sum('nominal'); - // Ambil saldo aktual dari transaksi TERAKHIR santri ini (real-time, bukan dari filter) + // Saldo aktual real-time (kumulatif semua waktu) $lastTx = UangSaku::where('id_santri', $id_santri) ->orderByDesc('tanggal_transaksi') - ->orderByDesc('created_at') + ->orderByDesc('id') ->first(); $saldoTerakhir = $lastTx ? (float)$lastTx->saldo_sesudah : 0; - $dataGrafik = UangSaku::where('id_santri', $id_santri) + // Saldo awal periode = saldo_sesudah transaksi terakhir SEBELUM tanggalDari + $txSebelumPeriode = UangSaku::where('id_santri', $id_santri) + ->where('tanggal_transaksi', '<', $tanggalDari) + ->orderByDesc('tanggal_transaksi') + ->orderByDesc('id') + ->first(); + $saldoAwalPeriode = $txSebelumPeriode ? (float)$txSebelumPeriode->saldo_sesudah : 0; + + // Saldo akhir periode = saldo_sesudah transaksi terakhir s.d. tanggalSampai + $txAkhirPeriode = UangSaku::where('id_santri', $id_santri) + ->where('tanggal_transaksi', '<=', $tanggalSampai) + ->orderByDesc('tanggal_transaksi') + ->orderByDesc('id') + ->first(); + $saldoAkhirPeriode = $txAkhirPeriode ? (float)$txAkhirPeriode->saldo_sesudah : 0; + + // ── DATA GRAFIK: PERJALANAN SALDO ──────────────────────────── + $saldoPerHari = UangSaku::where('id_santri', $id_santri) ->whereBetween('tanggal_transaksi', [$tanggalDari, $tanggalSampai]) ->select( - DB::raw('DATE(tanggal_transaksi) as 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') + DB::raw('DATE(tanggal_transaksi) as tgl'), + DB::raw('SUBSTRING_INDEX(GROUP_CONCAT(saldo_sesudah ORDER BY id DESC), ",", 1) as saldo_akhir_hari') ) - ->groupBy('tanggal') - ->orderBy('tanggal') - ->get(); - - if ($dataGrafik->isEmpty()) { - $dataGrafik = collect([(object)['tanggal' => $tanggalDari, 'pemasukan' => 0, 'pengeluaran' => 0]]); - } + ->groupBy('tgl') + ->orderBy('tgl') + ->get() + ->keyBy('tgl'); $periodeDari = Carbon::parse($tanggalDari); $periodeSampai = Carbon::parse($tanggalSampai); + $dataGrafikSaldo = []; + $saldoBerjalan = $saldoAwalPeriode; + $current = $periodeDari->copy(); + + $dataGrafikSaldo[] = [ + 'tanggal' => $periodeDari->format('Y-m-d'), + 'saldo' => $saldoAwalPeriode, + 'is_awal' => true, + ]; + + while ($current->lte($periodeSampai)) { + $tgl = $current->format('Y-m-d'); + if (isset($saldoPerHari[$tgl])) { + $saldoBerjalan = (float)$saldoPerHari[$tgl]->saldo_akhir_hari; + } + if ($current->eq($periodeDari)) { + if (isset($saldoPerHari[$tgl])) { + $dataGrafikSaldo[0]['saldo'] = (float)$saldoPerHari[$tgl]->saldo_akhir_hari; + $dataGrafikSaldo[0]['is_awal'] = false; + } + } else { + $dataGrafikSaldo[] = [ + 'tanggal' => $tgl, + 'saldo' => $saldoBerjalan, + 'is_awal' => false, + ]; + } + $current->addDay(); + } + + $canCrud = $this->isPamong(); + return view('admin.uang-saku.riwayat', compact( 'santri', 'transaksi', - 'totalPemasukan', 'totalPengeluaran', 'saldoTerakhir', - 'dataGrafik', 'tanggalDari', 'tanggalSampai', - 'periodeDari', 'periodeSampai' + 'totalPemasukan', 'totalPengeluaran', + 'saldoAwalPeriode', 'saldoAkhirPeriode', 'saldoTerakhir', + 'dataGrafikSaldo', + 'tanggalDari', 'tanggalSampai', + 'periodeDari', 'periodeSampai', + 'canCrud' )); } - /** - * Hitung ulang saldo_sebelum & saldo_sesudah untuk semua transaksi - * milik $idSantri yang tanggalnya >= $tanggal. - * - * Dipanggil setelah store / update / destroy agar urutan saldo - * tetap konsisten meski transaksi disisipkan di tengah. - */ + // ──────────────────────────────────────────────────────────────── + // PRIVATE HELPERS + // ──────────────────────────────────────────────────────────────── + + private function hasSaldoNegatif(string $idSantri, string $tanggal): bool + { + return UangSaku::where('id_santri', $idSantri) + ->where('tanggal_transaksi', '>=', $tanggal) + ->where('saldo_sesudah', '<', 0) + ->exists(); + } + private function recalculateSaldoAfter($idSantri, $tanggal) { - // Pastikan format tanggal string (bukan Carbon object) - $tanggal = $tanggal instanceof \Carbon\Carbon + $tanggal = $tanggal instanceof Carbon ? $tanggal->format('Y-m-d') : $tanggal; @@ -366,7 +469,6 @@ private function recalculateSaldoAfter($idSantri, $tanggal) foreach ($transaksiSetelah as $index => $trans) { if ($index === 0) { - // Cari saldo_sesudah transaksi tepat sebelum batch ini $prev = UangSaku::where('id_santri', $idSantri) ->where('tanggal_transaksi', '<', $tanggal) ->orderByDesc('tanggal_transaksi') diff --git a/sim-pkpps/resources/views/admin/uang-saku/create.blade.php b/sim-pkpps/resources/views/admin/uang-saku/create.blade.php index fe6c1cb..7751c89 100644 --- a/sim-pkpps/resources/views/admin/uang-saku/create.blade.php +++ b/sim-pkpps/resources/views/admin/uang-saku/create.blade.php @@ -4,6 +4,240 @@ {{-- Select2 CSS --}} + +