288 lines
11 KiB
Plaintext
288 lines
11 KiB
Plaintext
@{
|
|
ViewData["Title"] = "Laporan Keuangan";
|
|
}
|
|
|
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:gap-0">
|
|
<div class="prose">
|
|
<span class="text-xl font-semibold text-black">
|
|
Laporan Keuangan
|
|
</span>
|
|
<p class="text-sm text-gray-500">Rekap otomatis pembelian, penjualan, dan pengeluaran BSU per bulan.</p>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2 md:flex-row items-center">
|
|
<select id="filter_tahun" class="select select-bordered w-full md:w-48">
|
|
</select>
|
|
|
|
<button class="btn btn-sm bg-green-800 max-w-full rounded-full text-white hover:bg-green-900" onclick="modal_pengeluaran.showModal()">
|
|
Tambah Pengeluaran Lain-lain
|
|
<i class="ph ph-plus"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="h-6"></div>
|
|
|
|
<div class="card bg-white">
|
|
<div class="card-body p-2">
|
|
<div class="w-full overflow-x-auto">
|
|
<table class="table-zebra table" id="laporanTable">
|
|
<thead>
|
|
<tr>
|
|
<th class="w-[5%]">No</th>
|
|
<th class="w-[20%]">Bulan</th>
|
|
<th class="w-[15%]">Pembelian (Rp)</th>
|
|
<th class="w-[15%]">Penjualan (Rp)</th>
|
|
<th class="w-[15%]">Pengeluaran Lain-lain (Rp)</th>
|
|
<th class="w-[15%]">Laba/Rugi Bulanan (Rp)</th>
|
|
<th class="w-[15%]">Saldo Akumulatif (Rp)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 rounded-2xl bg-gray-50 p-4 flex flex-col md:flex-row md:items-center md:justify-between gap-2">
|
|
<div>
|
|
<p class="text-sm text-gray-500">Saldo akumulatif tahun berjalan</p>
|
|
<p class="text-xl font-semibold text-gray-900" id="totalSaldoLabel">Rp 0</p>
|
|
</div>
|
|
<div class="text-sm text-gray-500">
|
|
Data dihitung otomatis dari transaksi pembelian, penjualan, dan pengeluaran manual.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Pengeluaran -->
|
|
<dialog id="modal_pengeluaran" class="modal modal-bottom sm:modal-middle">
|
|
<div class="modal-box w-full max-w-2xl p-6 bg-white rounded-2xl">
|
|
<h3 class="text-gray-900 text-xl font-semibold leading-8 mb-4">Tambah Pengeluaran Lain-lain</h3>
|
|
<form id="formPengeluaran">
|
|
<div class="flex flex-col gap-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<fieldset class="fieldset">
|
|
<legend class="fieldset-legend">Tanggal<span class="text-red-500">*</span></legend>
|
|
<input type="date" name="Tanggal" class="input w-full" required />
|
|
</fieldset>
|
|
</div>
|
|
<div>
|
|
<fieldset class="fieldset">
|
|
<legend class="fieldset-legend">Nama Pengeluaran<span class="text-red-500">*</span></legend>
|
|
<input type="text" name="NamaPengeluaran" class="input w-full" placeholder="Contoh: Biaya Operasional" required />
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<fieldset class="fieldset">
|
|
<legend class="fieldset-legend">Nominal (Rp)<span class="text-red-500">*</span></legend>
|
|
<input type="number" min="0" step="1000" name="Nominal" class="input w-full" placeholder="0" required />
|
|
</fieldset>
|
|
</div>
|
|
<div>
|
|
<fieldset class="fieldset">
|
|
<legend class="fieldset-legend">Keterangan</legend>
|
|
<input type="text" name="Keterangan" class="input w-full" placeholder="Opsional" />
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3">
|
|
<button type="button" class="btn bg-white rounded-full" onclick="closePengeluaranModal()">Batal</button>
|
|
<button type="submit" class="btn bg-green-800 text-white rounded-full">Simpan</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
<!-- /modal -->
|
|
|
|
@section Scripts {
|
|
<script type="text/javascript">
|
|
const monthlyTransactions = [
|
|
{ year: 2025, month: 1, pembelian: 12500000, penjualan: 18200000 },
|
|
{ year: 2025, month: 2, pembelian: 10350000, penjualan: 16750000 },
|
|
{ year: 2025, month: 3, pembelian: 9800000, penjualan: 17150000 },
|
|
{ year: 2025, month: 4, pembelian: 11200000, penjualan: 15600000 },
|
|
{ year: 2025, month: 5, pembelian: 12200000, penjualan: 18900000 },
|
|
{ year: 2025, month: 6, pembelian: 10450000, penjualan: 14800000 },
|
|
{ year: 2025, month: 7, pembelian: 9900000, penjualan: 19750000 },
|
|
{ year: 2025, month: 8, pembelian: 11650000, penjualan: 17500000 },
|
|
{ year: 2025, month: 9, pembelian: 12400000, penjualan: 18350000 },
|
|
{ year: 2024, month: 10, pembelian: 10050000, penjualan: 15500000 },
|
|
{ year: 2024, month: 11, pembelian: 11250000, penjualan: 16200000 },
|
|
{ year: 2024, month: 12, pembelian: 12800000, penjualan: 18600000 }
|
|
];
|
|
|
|
let manualExpenses = [
|
|
{ id: 1, tanggal: '2025-01-12', title: 'Transport & Operasional', amount: 1200000, notes: 'Pengiriman sampah ke offtaker' },
|
|
{ id: 2, tanggal: '2025-03-08', title: 'Perawatan Timbangan', amount: 800000, notes: 'Kalibrasi timbangan BSU' },
|
|
{ id: 3, tanggal: '2025-05-20', title: 'Konsumsi Sosialisasi', amount: 450000, notes: 'Kegiatan edukasi warga' },
|
|
{ id: 4, tanggal: '2024-11-14', title: 'Biaya Listrik Gudang', amount: 600000, notes: 'Tagihan bulan berjalan' }
|
|
];
|
|
|
|
let latestExpenseId = manualExpenses.length;
|
|
let currentYear = getLatestYear();
|
|
let laporanTable;
|
|
|
|
$(document).ready(function () {
|
|
populateYearFilter();
|
|
|
|
laporanTable = new DataTable('#laporanTable', {
|
|
data: [],
|
|
scrollX: true,
|
|
autoWidth: false,
|
|
columns: [
|
|
{ data: 'no' },
|
|
{ data: 'bulan' },
|
|
{ data: 'pembelian', render: (d) => formatCurrency(d) },
|
|
{ data: 'penjualan', render: (d) => formatCurrency(d) },
|
|
{ data: 'pengeluaran', render: (d) => formatCurrency(d) },
|
|
{
|
|
data: 'labaRugi',
|
|
render: (d, t, row) => {
|
|
const color = row.isProfit ? 'text-green-600' : 'text-red-600';
|
|
return `<span class="${color} font-semibold">${formatCurrency(d)}</span>`;
|
|
}
|
|
},
|
|
{ data: 'saldo', render: (d) => formatCurrency(d) }
|
|
]
|
|
});
|
|
|
|
refreshTable();
|
|
|
|
$('#filter_tahun').on('change', function () {
|
|
currentYear = parseInt($(this).val(), 10) || getLatestYear();
|
|
refreshTable();
|
|
});
|
|
|
|
$('#formPengeluaran').on('submit', function (e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(this);
|
|
const tanggal = formData.get('Tanggal');
|
|
const nama = formData.get('NamaPengeluaran')?.trim();
|
|
const nominal = Number(formData.get('Nominal'));
|
|
const keterangan = formData.get('Keterangan')?.trim() || '';
|
|
|
|
if (!tanggal || !nama || isNaN(nominal) || nominal <= 0) {
|
|
Swal.fire('Gagal', 'Mohon lengkapi semua data pengeluaran.', 'error');
|
|
return;
|
|
}
|
|
|
|
manualExpenses.push({
|
|
id: ++latestExpenseId,
|
|
tanggal,
|
|
title: nama,
|
|
amount: nominal,
|
|
notes: keterangan
|
|
});
|
|
|
|
populateYearFilter();
|
|
refreshTable();
|
|
|
|
Swal.fire({
|
|
title: 'Berhasil',
|
|
text: 'Pengeluaran berhasil ditambahkan',
|
|
icon: 'success',
|
|
confirmButtonText: 'OK',
|
|
buttonsStyling: false,
|
|
customClass: {
|
|
confirmButton: 'btn bg-green-800 text-white hover:bg-green-900 rounded-full'
|
|
}
|
|
}).then(() => {
|
|
document.getElementById('formPengeluaran').reset();
|
|
closePengeluaranModal();
|
|
});
|
|
});
|
|
});
|
|
|
|
function refreshTable() {
|
|
const rows = buildRows(currentYear);
|
|
laporanTable.clear();
|
|
laporanTable.rows.add(rows);
|
|
laporanTable.draw();
|
|
updateTotalSaldo(rows.length ? rows[rows.length - 1].saldo : 0);
|
|
}
|
|
|
|
function buildRows(year) {
|
|
const formatter = new Intl.DateTimeFormat('id-ID', { month: 'long' });
|
|
let saldoAkumulatif = 0;
|
|
const rows = [];
|
|
|
|
for (let month = 1; month <= 12; month++) {
|
|
const baseData = monthlyTransactions.find((t) => t.year === year && t.month === month);
|
|
const pembelian = baseData ? baseData.pembelian : 0;
|
|
const penjualan = baseData ? baseData.penjualan : 0;
|
|
const pengeluaran = manualExpenses
|
|
.filter((e) => new Date(e.tanggal).getFullYear() === year && new Date(e.tanggal).getMonth() + 1 === month)
|
|
.reduce((sum, e) => sum + e.amount, 0);
|
|
|
|
const labaRugi = penjualan - (pembelian + pengeluaran);
|
|
saldoAkumulatif += labaRugi;
|
|
|
|
rows.push({
|
|
no: month,
|
|
bulan: `${formatter.format(new Date(year, month - 1, 1))} ${year}`,
|
|
pembelian,
|
|
penjualan,
|
|
pengeluaran,
|
|
labaRugi,
|
|
saldo: saldoAkumulatif,
|
|
isProfit: labaRugi >= 0
|
|
});
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
function populateYearFilter() {
|
|
const select = $('#filter_tahun');
|
|
const years = Array.from(new Set([
|
|
...monthlyTransactions.map((t) => t.year),
|
|
...manualExpenses.map((e) => new Date(e.tanggal).getFullYear())
|
|
])).sort((a, b) => b - a);
|
|
|
|
if (!years.includes(currentYear)) {
|
|
currentYear = years[0] || new Date().getFullYear();
|
|
}
|
|
|
|
select.empty();
|
|
years.forEach((year) => {
|
|
const option = $('<option></option>').attr('value', year).text(year);
|
|
if (year === currentYear) {
|
|
option.attr('selected', 'selected');
|
|
}
|
|
select.append(option);
|
|
});
|
|
}
|
|
|
|
function getLatestYear() {
|
|
const allYears = [
|
|
...monthlyTransactions.map((t) => t.year),
|
|
...manualExpenses.map((e) => new Date(e.tanggal).getFullYear())
|
|
];
|
|
return allYears.length ? Math.max(...allYears) : new Date().getFullYear();
|
|
}
|
|
|
|
function formatCurrency(value) {
|
|
const number = Number(value) || 0;
|
|
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(number);
|
|
}
|
|
|
|
function updateTotalSaldo(value) {
|
|
$('#totalSaldoLabel').text(formatCurrency(value));
|
|
}
|
|
|
|
function closePengeluaranModal() {
|
|
document.getElementById('formPengeluaran').reset();
|
|
modal_pengeluaran.close();
|
|
}
|
|
</script>
|
|
}
|