504 lines
23 KiB
PHP
504 lines
23 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('content')
|
||
<div class="page-header">
|
||
<h2><i class="fas fa-history"></i> Riwayat Uang Saku — {{ $santri->nama_lengkap }}</h2>
|
||
</div>
|
||
|
||
{{-- Filter Periode --}}
|
||
<div class="content-box" style="margin-bottom:14px;">
|
||
<form method="GET" action="{{ route('admin.uang-saku.riwayat', $santri->id_santri) }}" id="filterPeriode">
|
||
<div style="display:flex;align-items:flex-end;gap:11px;flex-wrap:wrap;">
|
||
<div class="form-group" style="margin-bottom:0;flex:1;min-width:200px;">
|
||
<label for="tanggal_dari" style="display:block;margin-bottom:5px;font-weight:600;">
|
||
<i class="fas fa-calendar-alt"></i> Dari Tanggal
|
||
</label>
|
||
<input type="date" name="tanggal_dari" id="tanggal_dari"
|
||
class="form-control" value="{{ $tanggalDari }}" max="{{ date('Y-m-d') }}">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:0;flex:1;min-width:200px;">
|
||
<label for="tanggal_sampai" style="display:block;margin-bottom:5px;font-weight:600;">
|
||
<i class="fas fa-calendar-check"></i> Sampai Tanggal
|
||
</label>
|
||
<input type="date" name="tanggal_sampai" id="tanggal_sampai"
|
||
class="form-control" value="{{ $tanggalSampai }}" max="{{ date('Y-m-d') }}">
|
||
</div>
|
||
<div style="display:flex;gap:10px;">
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-filter"></i> Terapkan
|
||
</button>
|
||
<button type="button" class="btn btn-success" onclick="setBulanIni()">
|
||
<i class="fas fa-calendar-day"></i> Bulan Ini
|
||
</button>
|
||
<a href="{{ route('admin.uang-saku.riwayat', $santri->id_santri) }}" class="btn btn-secondary">
|
||
<i class="fas fa-redo"></i> Reset
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{{-- ═══════════════════════════════════════════════════
|
||
PERSAMAAN SALDO PERIODE
|
||
════════════════════════════════════════════════════ --}}
|
||
|
||
<div style="margin-bottom:10px;">
|
||
<div style="font-size:.78rem;font-weight:600;color:var(--text-light);
|
||
text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;">
|
||
<i class="fas fa-calculator" style="color:var(--primary-color);"></i>
|
||
Ringkasan Periode:
|
||
<strong style="color:var(--text-color);">
|
||
{{ $periodeDari->format('d M Y') }} – {{ $periodeSampai->format('d M Y') }}
|
||
</strong>
|
||
</div>
|
||
|
||
{{-- Persamaan saldo --}}
|
||
<div style="display:grid;
|
||
grid-template-columns: 1fr 32px 1fr 32px 1fr 32px 1fr;
|
||
gap:0;align-items:stretch;
|
||
border-radius:12px;overflow:hidden;
|
||
border:1.5px solid #e2e8f0;background:#fff;
|
||
margin-bottom:10px;">
|
||
|
||
{{-- Saldo Awal --}}
|
||
<div style="padding:14px 16px;border-right:1px solid #e2e8f0;">
|
||
<div style="font-size:.66rem;color:#aaa;text-transform:uppercase;letter-spacing:.4px;margin-bottom:5px;">
|
||
Saldo Awal Periode
|
||
</div>
|
||
<div style="font-size:1rem;font-weight:800;color:#4a5568;">
|
||
Rp {{ number_format($saldoAwalPeriode, 0, ',', '.') }}
|
||
</div>
|
||
<div style="font-size:.66rem;color:#bbb;margin-top:3px;">
|
||
per {{ $periodeDari->format('d M Y') }}
|
||
@if($saldoAwalPeriode == 0)
|
||
<span style="color:#f5a623;">(belum ada saldo sebelumnya)</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
{{-- + --}}
|
||
<div style="display:flex;align-items:center;justify-content:center;
|
||
font-size:1.3rem;font-weight:300;color:#6FBA9D;
|
||
border-right:1px solid #e2e8f0;">+</div>
|
||
|
||
{{-- Pemasukan --}}
|
||
<div style="padding:14px 16px;border-right:1px solid #e2e8f0;">
|
||
<div style="font-size:.66rem;color:#aaa;text-transform:uppercase;letter-spacing:.4px;margin-bottom:5px;">
|
||
Pemasukan Periode
|
||
</div>
|
||
<div style="font-size:1rem;font-weight:800;color:#6FBA9D;">
|
||
Rp {{ number_format($totalPemasukan, 0, ',', '.') }}
|
||
</div>
|
||
<div style="font-size:.66rem;color:#bbb;margin-top:3px;">uang masuk di periode ini</div>
|
||
</div>
|
||
|
||
{{-- − --}}
|
||
<div style="display:flex;align-items:center;justify-content:center;
|
||
font-size:1.3rem;font-weight:300;color:#FF8B94;
|
||
border-right:1px solid #e2e8f0;">−</div>
|
||
|
||
{{-- Pengeluaran --}}
|
||
<div style="padding:14px 16px;border-right:1px solid #e2e8f0;">
|
||
<div style="font-size:.66rem;color:#aaa;text-transform:uppercase;letter-spacing:.4px;margin-bottom:5px;">
|
||
Pengeluaran Periode
|
||
</div>
|
||
<div style="font-size:1rem;font-weight:800;color:#FF8B94;">
|
||
Rp {{ number_format($totalPengeluaran, 0, ',', '.') }}
|
||
</div>
|
||
<div style="font-size:.66rem;color:#bbb;margin-top:3px;">uang keluar di periode ini</div>
|
||
</div>
|
||
|
||
{{-- = --}}
|
||
<div style="display:flex;align-items:center;justify-content:center;
|
||
font-size:1.3rem;font-weight:300;color:#718096;
|
||
border-right:1px solid #e2e8f0;">=</div>
|
||
|
||
{{-- Saldo Akhir --}}
|
||
<div style="padding:14px 16px;background:linear-gradient(135deg,#f0fdf7 0%,#e6f9f2 100%);">
|
||
<div style="font-size:.66rem;color:#aaa;text-transform:uppercase;letter-spacing:.4px;margin-bottom:5px;">
|
||
Saldo Akhir Periode
|
||
</div>
|
||
<div style="font-size:1rem;font-weight:800;
|
||
color:{{ $saldoAkhirPeriode >= 0 ? '#38a169' : '#e53e3e' }};">
|
||
Rp {{ number_format($saldoAkhirPeriode, 0, ',', '.') }}
|
||
</div>
|
||
<div style="font-size:.66rem;color:#bbb;margin-top:3px;">
|
||
per {{ $periodeSampai->format('d M Y') }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Notif jika saldo akhir periode ≠ saldo saat ini --}}
|
||
@if(round($saldoAkhirPeriode) !== round($saldoTerakhir))
|
||
<div style="background:#fffbeb;border:1px solid #fbd38d;border-radius:9px;
|
||
padding:10px 14px;font-size:.8rem;color:#744210;margin-bottom:10px;
|
||
display:flex;align-items:flex-start;gap:8px;">
|
||
<i class="fas fa-exclamation-triangle" style="color:#f5a623;margin-top:2px;flex-shrink:0;"></i>
|
||
<div>
|
||
<strong>Catatan:</strong>
|
||
Saldo akhir periode (Rp {{ number_format($saldoAkhirPeriode, 0, ',', '.') }})
|
||
berbeda dengan <strong>Saldo Saat Ini (Rp {{ number_format($saldoTerakhir, 0, ',', '.') }})</strong>
|
||
karena ada transaksi di luar rentang
|
||
{{ $periodeDari->format('d M') }}–{{ $periodeSampai->format('d M Y') }}.
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Saldo saat ini (real-time) --}}
|
||
<div style="background:linear-gradient(135deg,#ebf8ff 0%,#bee3f8 40%,#ebf8ff 100%);
|
||
border:1.5px solid #90cdf4;border-radius:12px;
|
||
padding:14px 18px;display:flex;justify-content:space-between;
|
||
align-items:center;flex-wrap:wrap;gap:10px;">
|
||
<div>
|
||
<div style="font-size:.66rem;color:#2b6cb0;text-transform:uppercase;
|
||
letter-spacing:.5px;margin-bottom:4px;font-weight:600;">
|
||
<i class="fas fa-wallet"></i> Saldo Saat Ini (Akumulasi Semua Waktu)
|
||
</div>
|
||
<div style="font-size:1.4rem;font-weight:900;
|
||
color:{{ $saldoTerakhir >= 0 ? '#2b6cb0' : '#e53e3e' }};">
|
||
Rp {{ number_format($saldoTerakhir, 0, ',', '.') }}
|
||
</div>
|
||
</div>
|
||
<div style="font-size:.78rem;color:#2c5282;text-align:right;max-width:300px;line-height:1.5;">
|
||
<i class="fas fa-info-circle"></i>
|
||
Total uang santri saat ini, dihitung dari <strong>seluruh transaksi sejak awal</strong>,
|
||
bukan hanya periode yang ditampilkan.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ═══════════════════════════════════════════════════
|
||
GRAFIK PERJALANAN SALDO
|
||
════════════════════════════════════════════════════ --}}
|
||
<div class="content-box" style="margin-bottom:22px;">
|
||
<div style="display:flex;justify-content:space-between;align-items:flex-start;
|
||
flex-wrap:wrap;gap:8px;margin-bottom:16px;">
|
||
<div>
|
||
<h3 style="margin:0 0 4px;color:var(--primary-color);">
|
||
<i class="fas fa-chart-area"></i> Perjalanan Saldo
|
||
</h3>
|
||
<div style="font-size:.78rem;color:var(--text-light);">
|
||
{{ $periodeDari->format('d M Y') }} – {{ $periodeSampai->format('d M Y') }}
|
||
• Grafik menunjukkan nilai saldo aktual setiap hari
|
||
</div>
|
||
</div>
|
||
{{-- Mini legend --}}
|
||
<div style="display:flex;gap:14px;align-items:center;font-size:.78rem;color:var(--text-light);">
|
||
<span>
|
||
<span style="display:inline-block;width:24px;height:3px;background:#6FBA9D;
|
||
vertical-align:middle;border-radius:2px;margin-right:5px;"></span>
|
||
Saldo naik (pemasukan)
|
||
</span>
|
||
<span>
|
||
<span style="display:inline-block;width:24px;height:3px;background:#FF8B94;
|
||
vertical-align:middle;border-radius:2px;margin-right:5px;"></span>
|
||
Saldo turun (pengeluaran)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<canvas id="chartSaldo" style="max-height:380px;"></canvas>
|
||
</div>
|
||
|
||
{{-- Action Buttons --}}
|
||
<div class="content-box" style="margin-bottom:14px;">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:11px;">
|
||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||
<a href="{{ route('admin.uang-saku.index') }}" class="btn btn-secondary">
|
||
<i class="fas fa-arrow-left"></i> Kembali
|
||
</a>
|
||
<a href="{{ route('admin.santri.show', $santri->id) }}" class="btn btn-primary">
|
||
<i class="fas fa-user"></i> Profil Santri
|
||
</a>
|
||
</div>
|
||
{{-- Tambah Transaksi: hanya pamong --}}
|
||
@if($canCrud)
|
||
<a href="{{ route('admin.uang-saku.create') }}?id_santri={{ $santri->id_santri }}"
|
||
class="btn btn-success">
|
||
<i class="fas fa-plus"></i> Tambah Transaksi
|
||
</a>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Tabel Riwayat --}}
|
||
<div class="content-box">
|
||
<h3 style="margin-bottom:12px;color:var(--text-color);">
|
||
<i class="fas fa-list"></i> Daftar Transaksi
|
||
@if($transaksi->total() > 0)
|
||
<span style="color:var(--text-light);font-weight:400;">
|
||
({{ $transaksi->total() }} transaksi)
|
||
</span>
|
||
@endif
|
||
</h3>
|
||
|
||
@if($transaksi->count() > 0)
|
||
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;
|
||
padding:9px 14px;margin-bottom:12px;font-size:.78rem;color:var(--text-light);">
|
||
<i class="fas fa-info-circle" style="color:var(--primary-color);"></i>
|
||
<strong>Saldo Sebelum</strong> & <strong>Saldo Sesudah</strong>
|
||
menunjukkan saldo kumulatif santri sebelum dan setelah tiap transaksi.
|
||
</div>
|
||
|
||
<div class="table-wrapper">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:4%;">No</th>
|
||
<th style="width:11%;">ID</th>
|
||
<th style="width:10%;">Tanggal</th>
|
||
<th style="width:10%;">Jenis</th>
|
||
<th style="width:13%;">Nominal</th>
|
||
<th style="width:13%;">Saldo Sebelum</th>
|
||
<th style="width:13%;">Saldo Sesudah</th>
|
||
<th style="width:20%;">Keterangan</th>
|
||
<th style="width:6%;" class="text-center">Aksi</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach($transaksi as $index => $item)
|
||
<tr>
|
||
<td>{{ $transaksi->firstItem() + $index }}</td>
|
||
<td><strong>{{ $item->id_uang_saku }}</strong></td>
|
||
<td>{{ $item->tanggal_transaksi->format('d/m/Y') }}</td>
|
||
<td>
|
||
@if($item->jenis_transaksi === 'pemasukan')
|
||
<span class="badge badge-success">
|
||
<i class="fas fa-arrow-down"></i> Masuk
|
||
</span>
|
||
@else
|
||
<span class="badge badge-danger">
|
||
<i class="fas fa-arrow-up"></i> Keluar
|
||
</span>
|
||
@endif
|
||
</td>
|
||
<td class="nominal-highlight">{{ $item->nominal_format }}</td>
|
||
<td style="color:#718096;">
|
||
Rp {{ number_format($item->saldo_sebelum, 0, ',', '.') }}
|
||
</td>
|
||
<td>
|
||
<strong style="color:{{ $item->saldo_sesudah >= 0 ? '#38a169' : '#e53e3e' }};">
|
||
{{ $item->saldo_sesudah_format }}
|
||
</strong>
|
||
</td>
|
||
<td><div class="content-preview">{{ $item->keterangan ?? '-' }}</div></td>
|
||
<td class="text-center">
|
||
<div style="display:flex;gap:4px;justify-content:center;">
|
||
<a href="{{ route('admin.uang-saku.show', $item->id) }}"
|
||
class="btn btn-primary btn-sm" title="Detail">
|
||
<i class="fas fa-eye"></i>
|
||
</a>
|
||
{{-- Edit: hanya pamong --}}
|
||
@if($canCrud)
|
||
<a href="{{ route('admin.uang-saku.edit', $item->id) }}"
|
||
class="btn btn-warning btn-sm" title="Edit">
|
||
<i class="fas fa-edit"></i>
|
||
</a>
|
||
@endif
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div style="margin-top:14px;">{{ $transaksi->links() }}</div>
|
||
|
||
@else
|
||
<div class="empty-state">
|
||
<i class="fas fa-calendar-times"></i>
|
||
<h3>Tidak Ada Transaksi</h3>
|
||
<p>
|
||
Tidak ada transaksi pada periode
|
||
{{ $periodeDari->format('d F Y') }} – {{ $periodeSampai->format('d F Y') }}.
|
||
</p>
|
||
@if(round($saldoTerakhir) > 0)
|
||
<p style="font-size:.85rem;color:var(--text-light);">
|
||
Santri memiliki saldo
|
||
<strong>Rp {{ number_format($saldoTerakhir, 0, ',', '.') }}</strong>
|
||
dari transaksi di periode lain.
|
||
</p>
|
||
@endif
|
||
@if($canCrud)
|
||
<a href="{{ route('admin.uang-saku.create') }}?id_santri={{ $santri->id_santri }}"
|
||
class="btn btn-success">
|
||
<i class="fas fa-plus"></i> Tambah Transaksi
|
||
</a>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||
<script>
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// DATA dari Laravel
|
||
// ═══════════════════════════════════════════════════════════════
|
||
const rawData = @json($dataGrafikSaldo);
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// Bangun dataset perjalanan saldo
|
||
// ═══════════════════════════════════════════════════════════════
|
||
const labels = [];
|
||
const saldoArr = [];
|
||
|
||
rawData.forEach((d, i) => {
|
||
const dt = new Date(d.tanggal + 'T00:00:00');
|
||
const opt = { day: 'numeric', month: 'short' };
|
||
labels.push(
|
||
d.is_awal
|
||
? dt.toLocaleDateString('id-ID', opt) + ' (Awal)'
|
||
: dt.toLocaleDateString('id-ID', opt)
|
||
);
|
||
saldoArr.push(d.saldo);
|
||
});
|
||
|
||
function segmentColor(ctx, naik, turun, flat) {
|
||
const i = ctx.p1DataIndex;
|
||
const cur = saldoArr[i];
|
||
const prv = saldoArr[i - 1] ?? cur;
|
||
if (cur > prv) return naik;
|
||
if (cur < prv) return turun;
|
||
return flat;
|
||
}
|
||
|
||
function buildGradient(ctx) {
|
||
const chart = ctx.chart;
|
||
const { chartArea } = chart;
|
||
if (!chartArea) return 'rgba(111,186,157,0.15)';
|
||
const gradient = chart.ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
|
||
gradient.addColorStop(0, 'rgba(111,186,157,0.25)');
|
||
gradient.addColorStop(0.6, 'rgba(111,186,157,0.06)');
|
||
gradient.addColorStop(1, 'rgba(111,186,157,0.0)');
|
||
return gradient;
|
||
}
|
||
|
||
const ctx = document.getElementById('chartSaldo').getContext('2d');
|
||
|
||
new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels,
|
||
datasets: [{
|
||
label: 'Saldo',
|
||
data: saldoArr,
|
||
borderWidth: 3,
|
||
tension: 0.35,
|
||
fill: true,
|
||
backgroundColor: buildGradient,
|
||
pointRadius: function(ctx) {
|
||
const i = ctx.dataIndex;
|
||
const cur = saldoArr[i];
|
||
const prv = saldoArr[i - 1] ?? cur;
|
||
return cur !== prv || i === 0 || i === saldoArr.length - 1 ? 6 : 3;
|
||
},
|
||
pointHoverRadius: 8,
|
||
pointBackgroundColor: function(ctx) {
|
||
const i = ctx.dataIndex;
|
||
const cur = saldoArr[i];
|
||
const prv = saldoArr[i - 1] ?? cur;
|
||
if (i === 0) return '#718096';
|
||
if (cur > prv) return '#6FBA9D';
|
||
if (cur < prv) return '#FF8B94';
|
||
return '#d1d9e0';
|
||
},
|
||
pointBorderColor: '#fff',
|
||
pointBorderWidth: 2,
|
||
segment: {
|
||
borderColor: ctx => segmentColor(ctx, '#6FBA9D', '#FF8B94', '#d1d9e0'),
|
||
},
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: true,
|
||
interaction: { intersect: false, mode: 'index' },
|
||
plugins: {
|
||
legend: { display: false },
|
||
tooltip: {
|
||
backgroundColor: 'rgba(30,41,59,0.92)',
|
||
padding: 13,
|
||
cornerRadius: 10,
|
||
titleFont: { size: 13, weight: 'bold' },
|
||
bodyFont: { size: 13 },
|
||
callbacks: {
|
||
title: items => items[0].label,
|
||
label: item => {
|
||
const i = item.dataIndex;
|
||
const cur = saldoArr[i];
|
||
const prv = saldoArr[i - 1] ?? cur;
|
||
const diff = cur - prv;
|
||
const fmt = v => 'Rp\u00a0' + new Intl.NumberFormat('id-ID').format(v);
|
||
|
||
let lines = [' Saldo: ' + fmt(cur)];
|
||
if (i > 0 && diff !== 0) {
|
||
const sign = diff > 0 ? '▲ +' : '▼ ';
|
||
lines.push(' ' + sign + fmt(Math.abs(diff))
|
||
+ (diff > 0 ? ' (pemasukan)' : ' (pengeluaran)'));
|
||
} else if (i > 0) {
|
||
lines.push(' — tidak ada transaksi');
|
||
}
|
||
return lines;
|
||
},
|
||
labelColor: item => {
|
||
const i = item.dataIndex;
|
||
const cur = saldoArr[i];
|
||
const prv = saldoArr[i - 1] ?? cur;
|
||
const c = i === 0 ? '#718096'
|
||
: cur > prv ? '#6FBA9D'
|
||
: cur < prv ? '#FF8B94' : '#d1d9e0';
|
||
return { borderColor: c, backgroundColor: c, borderRadius: 3 };
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
min: function() {
|
||
const minVal = Math.min(...saldoArr);
|
||
return Math.max(0, Math.floor(minVal * 0.8 / 10000) * 10000);
|
||
}(),
|
||
ticks: {
|
||
callback: v => 'Rp\u00a0' + new Intl.NumberFormat('id-ID', {
|
||
notation: 'compact', compactDisplay: 'short'
|
||
}).format(v),
|
||
font: { size: 12 },
|
||
maxTicksLimit: 8,
|
||
},
|
||
grid: { color: 'rgba(0,0,0,.05)', drawBorder: false }
|
||
},
|
||
x: {
|
||
grid: { display: false, drawBorder: false },
|
||
ticks: {
|
||
font: { size: 11 },
|
||
maxRotation: 45,
|
||
minRotation: 45,
|
||
maxTicksLimit: rawData.length > 20 ? 12 : rawData.length,
|
||
autoSkip: true,
|
||
}
|
||
}
|
||
},
|
||
animation: { duration: 1200, easing: 'easeInOutQuart' }
|
||
}
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// Helpers
|
||
// ═══════════════════════════════════════════════════════════════
|
||
function setBulanIni() {
|
||
const today = new Date();
|
||
const first = new Date(today.getFullYear(), today.getMonth(), 1);
|
||
const last = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||
document.getElementById('tanggal_dari').value = first.toISOString().split('T')[0];
|
||
document.getElementById('tanggal_sampai').value = last.toISOString().split('T')[0];
|
||
document.getElementById('filterPeriode').submit();
|
||
}
|
||
|
||
document.getElementById('tanggal_sampai').addEventListener('change', function () {
|
||
const dari = document.getElementById('tanggal_dari').value;
|
||
if (dari && this.value && this.value < dari) {
|
||
alert('Tanggal sampai tidak boleh lebih kecil dari tanggal dari!');
|
||
this.value = dari;
|
||
}
|
||
});
|
||
</script>
|
||
@endsection |