Files
webstatement/resources/views/statements/stmt.blade.php
Daeng Deni Mardaeni 5752427297 refactor(report): konversi query raw SQL ke pure Eloquent ORM
Melakukan refactor besar pada job GenerateClosingBalanceReport untuk mengganti penggunaan raw SQL dan left join dengan implementasi Eloquent ORM penuh, guna meningkatkan maintainability, akurasi data, dan performa sistem.

🧱 Refactor Arsitektur Query:
- Menghilangkan semua penggunaan `leftJoin` yang menyebabkan duplikasi data
- Mengganti query menjadi pure Eloquent dengan relasi dan `with()` (eager loading)
- Menghindari N+1 problem melalui optimasi relasi `ft` (TempFundsTransfer) dan `dc` (DataCapture)

🧩 Integrasi Model:
- Menggunakan model: `StmtEntry`, `StmtEntryDetail`, `TempFundsTransfer`, `DataCapture`
- Seleksi model berdasarkan `groupName`:
  - `QRIS` → `StmtEntry`
  - selainnya → `StmtEntryDetail`
- Relasi dimanfaatkan langsung via properti model, dengan fallback logic

⚙️ Peningkatan Proses Data:
- Menyederhanakan metode `processTransactionData()` untuk memanfaatkan relasi langsung
- Tambahan null safety menggunakan null coalescing operator (`??`)
- Tetap mempertahankan chunk processing untuk efisiensi memori

🔒 Konsistensi & Logging:
- Tambahan DB transaction (`beginTransaction`, `commit`, `rollback`) untuk data integrity
- Logging komprehensif di tiap tahap proses: pemilihan model, query, pemrosesan, fallback
- Error handling dengan informasi error yang lebih informatif

🚀 Optimasi Performa:
- Selective field loading untuk minimalisasi beban memori
- Chunking data untuk skala besar
- Eager loading efisien tanpa join kompleks

 Tujuan & Manfaat:
- Meningkatkan maintainability & keterbacaan kode
- Menghilangkan duplikasi data akibat `leftJoin`
- Meningkatkan akurasi dan konsistensi data laporan
- Mengikuti Laravel best practices dalam penulisan query & relasi
2025-07-26 08:42:42 +07:00

581 lines
22 KiB
PHP

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rekening Tabungan</title>
<style>
@page {
size: A4;
margin: 0;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #fff;
width: 210mm;
height: 297mm;
display: flex;
flex-direction: column;
}
.container {
width: 190mm;
min-height: 277mm;
margin: 10mm auto;
background: #ffffff;
border: 1px solid #ccc;
padding: 10mm;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
}
.watermark {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 1;
z-index: 0;
pointer-events: none;
width: 100%;
height: auto;
}
.watermark img {
margin: 0px 50px;
width: 85%;
height: auto;
}
.content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
/* Ensure content is above watermark */
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding-bottom: 10px;
height: 45px;
}
.header .title {
text-align: left;
font-size: 12px;
color: #0056b3;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.header .title h1 {
padding: 0;
margin: 0;
}
.header .logo {
text-align: center;
display: flex;
align-items: flex-end;
justify-content: center;
}
.header .logo img {
max-height: 50px;
margin-right: 10px
}
.info-section {
text-transform: uppercase;
padding: 10px 0;
font-size: 10px;
font-weight: bold;
}
.info-section .column {
width: 48%;
display: inline-block;
vertical-align: top;
}
.info-section .column p {
margin: 5px 0;
}
.table-section {
padding-top: 15px;
flex: 1;
/* Allow table section to grow */
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
table th,
table td {
padding: 5px;
text-align: left;
font-size: 10px;
word-wrap: break-word;
word-break: break-word;
white-space: normal;
overflow-wrap: break-word;
hyphens: auto;
}
table th {
text-transform: uppercase;
font-weight: bold;
color: white;
}
table td.text-right {
text-align: right !important;
}
table td.text-center {
text-align: center !important;
}
table td.text-left {
text-align: left !important;
}
.footer {
font-size: 10px;
color: #666;
margin-top: auto;
position: relative;
bottom: 0;
width: 100%;
z-index: 1;
/* Ensure footer is above watermark */
}
.footer p {
margin: 0px;
}
.footer .highlight {
font-weight: bold;
}
.left-25 {
margin-left: 25px !important;
}
.same-size {
display: inline-block;
width: 100px;
}
.sponsor {
border-top: 1.5px solid black;
padding: 10px;
text-align: center;
}
.highlight {
margin: 10px 0px 0px 0px;
padding: 0;
width: 100%;
}
tbody td {
border-bottom: none;
border-top: none;
vertical-align: top;
}
/* Menghilangkan padding dan margin untuk baris narrative tambahan */
tbody tr td {
padding: 5px;
}
/* Khusus untuk baris narrative tambahan - tanpa padding/margin */
tbody tr.narrative-line td {
padding: 2px 5px;
margin: 0;
line-height: 1;
}
/* Column width classes */
.col-date {
width: 10%;
text-align: center;
}
.col-desc {
width: 25%;
min-width: 150px;
max-width: 150px;
}
.col-valuta {
width: 10%;
text-align: center;
}
.col-referensi {
width: 15%;
}
.col-debet,
.col-kredit {
width: 12.5%;
text-align: right;
}
.col-saldo {
width: 15%;
text-align: right;
}
.page-number {
position: absolute;
bottom: 5mm;
right: 10mm;
font-size: 10px;
color: #666;
}
@media print {
body {
width: 210mm;
height: 297mm;
}
.container {
margin: 0;
border: initial;
border-radius: initial;
width: initial;
min-height: initial;
box-shadow: initial;
background: initial;
page-break-after: always;
}
.container:last-of-type {
page-break-after: auto;
}
}
</style>
</head>
<body>
@php
$saldo = $saldoAwalBulan->actual_balance ?? 0;
$totalDebit = 0;
$totalKredit = 0;
$line = 1;
$linePerPage = 23;
@endphp
@php
// Hitung tanggal periode berdasarkan $period
$periodDates = calculatePeriodDates($period);
$startDate = $periodDates['start'];
$endDate = $periodDates['end'];
// Log hasil perhitungan
\Log::info('Period dates calculated', [
'period' => $period,
'start_date' => $startDate->format('d/m/Y'),
'end_date' => $endDate->format('d/m/Y'),
]);
@endphp
@php
// Calculate total pages based on actual line count
$totalLines = 0;
foreach ($stmtEntries as $entry) {
// Split narrative into multiple lines of approximately 35 characters, breaking at word boundaries
$narrative = $entry->description ?? '';
$words = explode(' ', $narrative);
$narrativeLineCount = 0;
$currentLine = '';
foreach ($words as $word) {
if (strlen($currentLine . ' ' . $word) > 30) {
$narrativeLineCount++;
$currentLine = $word;
} else {
$currentLine .= ($currentLine ? ' ' : '') . $word;
}
}
if ($currentLine) {
$narrativeLineCount++;
}
// Each entry takes at least one line for the main data + narrative lines + gap row
$totalLines += $narrativeLineCount; // +1 for gap row
}
// Add 1 for the "Saldo Awal Bulan" row
$totalLines += 1;
// Calculate total pages ($linePerPage lines per page)
$totalPages = ceil($totalLines / $linePerPage);
$pageNumber = 0;
$footerContent =
'
<div class="footer">
<p class="sponsor">Belanja puas di Electronic City! Dapatkan cashback hingga 250 ribu dan nikmati makan enak dengan cashback hingga 25 ribu. Bayar pakai QRIS AGI. S&amp;K berlaku. Info lengkap: www.arthagraha.com</p>
<p class="sponsor">Waspada dalam bertransaksi QRIS! Periksa kembali identitas penjual & nominal pembayaran sebelum melanjutkan transaksi. Info terkait Bank Artha Graha Internasional, kunjungi website www.arthagraha.com</p>
<div class="highlight">
<img src="' .
public_path('assets/media/images/banner-footer.png') .
'" alt="Logo Bank" style="width: 100%; height: auto;">
</div>
</div>
';
@endphp
<div class="container">
<div class="watermark">
<img src="{{ public_path('assets/media/images/watermark.png') }}" alt="Watermark">
</div>
<div class="content-wrapper">
<!-- Header Section -->
<div class="header">
<div class="logo">
<img src="{{ public_path('assets/media/images/logo-arthagraha.png') }}" alt="Logo Bank">
<img src="{{ public_path('assets/media/images/logo-agi.png') }}" alt="Logo Bank">
</div>
</div>
<!-- Bank Information Section -->
<div class="info-section">
<div class="column">
<p>{{ $branch->name }}</p>
<p style="text-transform: capitalize">Kepada</p>
<p>{{ $account->customer->name }}</p>
<p>{{ $account->customer->address }}</p>
<p>{{ $account->customer->district }}
{{ ($account->customer->ktp_rt ?: $account->customer->home_rt) ? 'RT ' . ($account->customer->ktp_rt ?: $account->customer->home_rt) : '' }}
{{ ($account->customer->ktp_rw ?: $account->customer->home_rw) ? 'RW ' . ($account->customer->ktp_rw ?: $account->customer->home_rw) : '' }}
</p>
<p>{{ trim($account->customer->city . ' ' . ($account->customer->province ? getProvinceCoreName($account->customer->province) . ' ' : '') . ($account->customer->postal_code ?? '')) }}
</p>
</div>
<div style="text-transform: capitalize;" class="column">
<p style="padding-left:50px"><span class="same-size">Periode Statement </span>:
{{ dateFormat($startDate) }} <span style="text-transform:lowercase !important">s/d</span>
{{ dateFormat($endDate) }}</p>
<p style="padding-left:50px"><span class="same-size">Nomor Rekening</span>:
{{ $account->account_number }}</p>
</div>
</div>
<!-- Table Section -->
<div class="table-section">
<table>
<thead>
<tr
style="@if ($headerTableBg) background-image: url('data:image/png;base64,{{ $headerTableBg }}'); background-repeat: no-repeat; background-size: cover; background-position: center; @else background-color: #0056b3; @endif height: 30px;">
<th class="col-date">Tanggal</th>
<th class="col-valuta">Tanggal<br>Valuta</th>
<th class="text-left col-desc">Keterangan</th>
<th class="text-left col-referensi">Referensi</th>
<th class="col-debet">Debet</th>
<th class="col-kredit">Kredit</th>
<th class="col-saldo">Saldo</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"></td>
<td class="text-center">&nbsp;</td>
<td><strong>Saldo Awal Bulan</strong></td>
<td class="text-center">&nbsp;</td>
<td class="text-right">&nbsp;</td>
<td class="text-right">&nbsp;</td>
<td class="text-right">
<strong>{{ number_format((float)$saldoAwalBulan->actual_balance, 2, ',', '.') }}</strong>
</td>
</tr>
@foreach ($stmtEntries as $row)
@php
$debit = $row->transaction_amount < 0 ? abs($row->transaction_amount) : 0;
$kredit = $row->transaction_amount > 0 ? $row->transaction_amount : 0;
$saldo += $kredit - $debit;
$totalDebit += $debit;
$totalKredit += $kredit;
// Split narrative into multiple lines of approximately 35 characters, breaking at word boundaries
$narrative = $row->description ?? '';
$words = explode(' ', $narrative);
$narrativeLines = [];
$currentLine = '';
foreach ($words as $word) {
if (strlen($currentLine . ' ' . $word) > 30) {
$narrativeLines[] = trim($currentLine);
$currentLine = $word;
} else {
$currentLine .= ($currentLine ? ' ' : '') . $word;
}
}
if ($currentLine) {
$narrativeLines[] = trim($currentLine);
}
@endphp
<tr>
<td class="text-center">{{ date('d/m/Y', strtotime($row->transaction_date)) }}</td>
<td class="text-center">{{ substr($row->actual_date, 0, 10) }}</td>
<td>{{ str_replace(['[', ']'], ' ', $narrativeLines[0] ?? '') }}</td>
<td>{{ $row->reference_number }}</td>
<td class="text-right">{{ $debit > 0 ? number_format((float)$debit, 2, ',', '.') : '' }}</td>
<td class="text-right">{{ $kredit > 0 ? number_format((float)$kredit, 2, ',', '.') : '' }}
</td>
<td class="text-right">{{ number_format((float)$saldo, 2, ',', '.') }}</td>
</tr>
@for ($i = 1; $i < count($narrativeLines); $i++)
<tr class="narrative-line">
<td class="text-center"></td>
<td class="text-center"></td>
<td>{{ str_replace(['[', ']'], ' ', $narrativeLines[$i] ?? '') }}</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right"></td>
<td class="text-right"></td>
</tr>
@endfor
@php $line += count($narrativeLines); @endphp
<!-- Add a gap row -->
<tr class="gap-row">
<td class="text-center"></td>
<td class="text-center"></td>
<td class="text-center"></td>
<td></td>
<td class="text-right"></td>
<td class="text-right"></td>
<td class="text-right"></td>
</tr>
@if ($line >= $linePerPage && !$loop->last)
@php
$line = 0;
$pageNumber++;
@endphp
<tr>
<td class="text-center"></td>
<td class="text-center"></td>
<td><strong>Pindah ke Halaman Berikutnya</strong></td>
<td class="text-center"></td>
<td class="text-right">&nbsp;</td>
<td class="text-right">&nbsp;</td>
<td class="text-right"></td>
</tr>
</tbody>
</table>
</div>
{!! $footerContent !!}
</div>
<div class="page-number">Halaman {{ $pageNumber }} dari {{ $totalPages }}</div>
</div>
<div class="container">
<div class="watermark">
<img src="{{ public_path('assets/media/images/watermark.png') }}" alt="Watermark">
</div>
<div class="content-wrapper">
<!-- Header Section for continuation page -->
<div class="header">
<div class="logo">
<img src="{{ public_path('assets/media/images/logo-arthagraha.png') }}" alt="Logo Bank">
<img src="{{ public_path('assets/media/images/logo-agi.png') }}" alt="Logo Bank">
</div>
</div>
<!-- Bank Information Section for continuation page -->
<div class="info-section">
<div class="column">
<p>{{ $branch->name }}</p>
<p style="text-transform: capitalize">Kepada</p>
<p>{{ $account->customer->name }}</p>
<p>{{ $account->customer->address }}</p>
<p>{{ $account->customer->district }}
{{ ($account->customer->ktp_rt ?: $account->customer->home_rt) ? 'RT ' . ($account->customer->ktp_rt ?: $account->customer->home_rt) : '' }}
{{ ($account->customer->ktp_rw ?: $account->customer->home_rw) ? 'RW ' . ($account->customer->ktp_rw ?: $account->customer->home_rw) : '' }}
</p>
<p>{{ trim($account->customer->city . ' ' . ($account->customer->province ? getProvinceCoreName($account->customer->province) . ' ' : '') . ($account->customer->postal_code ?? '')) }}
</p>
</div>
<div style="text-transform: capitalize;" class="column">
<p style="padding-left:50px"><span class="same-size">Periode Statement </span>:
{{ dateFormat($startDate) }} <span style="text-transform:lowercase !important">s/d</span>
{{ dateFormat($endDate) }}</p>
<p style="padding-left:50px"><span class="same-size">Nomor Rekening</span>:
{{ $account->account_number }}</p>
</div>
</div>
<div class="table-section">
<table>
<thead>
<tr
style="@if ($headerTableBg) background-image: url('data:image/png;base64,{{ $headerTableBg }}'); background-repeat: no-repeat; background-size: cover; background-position: center; @else background-color: #0056b3; @endif height: 30px;">
<th class="col-date">Tanggal</th>
<th class="col-valuta">Tanggal<br>Valuta</th>
<th class="text-left col-desc">Keterangan</th>
<th class="col-referensi">Referensi</th>
<th class="col-debet">Debet</th>
<th class="col-kredit">Kredit</th>
<th class="col-saldo">Saldo</th>
</tr>
</thead>
<tbody>
@endif
@endforeach
@for ($i = 0; $i < $linePerPage - $line; $i++)
<tr>
<td class="text-center"></td>
<td class="text-center"></td>
<td></td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right"></td>
<td class="text-right"></td>
</tr>
@endfor
<tr>
<td class="text-center"></td>
<td class="text-center"></td>
<td><strong>Total Akhir</strong></td>
<td class="text-center"></td>
<td class="text-right"><strong>{{ number_format($totalDebit, 2, ',', '.') }}</strong></td>
<td class="text-right"><strong>{{ number_format($totalKredit, 2, ',', '.') }}</strong>
</td>
<td class="text-right"><strong>{{ number_format($saldo, 2, ',', '.') }}</strong></td>
</tr>
</tbody>
</table>
</div>
<!-- Footer Section -->
{!! $footerContent !!}
</div>
<div class="page-number">Halaman {{ $pageNumber + 1 }} dari {{ $totalPages }}</div>
</div>
</body>
</html>