feat(webstatement): tambahkan fitur pembuatan, penyimpanan, dan pengunduhan PDF statement

- **Fitur Baru:**
  - Menambahkan kemampuan untuk membuat PDF statement secara dinamis menggunakan Browsershot.
  - Fitur penyimpanan otomatis PDF ke dalam local storage dengan struktur direktori berdasarkan periode dan account number.
  - Menyediakan fitur unduhan langsung dari storage atau melalui preview di browser.
  - Mendukung penghapusan PDF dari storage dengan log terintegrasi.

- **Perubahan pada Controller:**
  - Ditambah method baru `generated` untuk membangun PDF atau tampilan HTML statement.
  - Integrasi penghitungan periode saldo (`calculateSaldoPeriod`) untuk menghasilkan data laporan yang lebih akurat.
  - Perubahan pada `printStatementRekening` untuk mendukung pengiriman objek statement secara penuh.
  - Menambahkan method tambahan untuk preview, download, dan delete PDF langsung dari storage.

- **Validasi Permintaan:**
  - Menambah validasi wajib pada `branch_code` untuk memastikan data cabang sesuai.
  - Menyesuaikan logika validasi di `PrintStatementRequest` untuk mendukung input cabang dan parameter lain.

- **Cakupan Logging:**
  - Meningkatkan logging pada setiap proses penting:
    - Mulai dari validasi data, proses pembuatan PDF, hingga penyimpanan.
    - Deteksi error secara spesifik pada setiap tahapan proses.
  - Menambah log debugging untuk nama file, ukuran file, dan path penyimpanan.

- **Peningkatan pada UI:**
  - Penyesuaian field `branch_code` pada form UI untuk menggantikan `branch_id` dengan validasi error yang lebih eksplisit.
  - Membuat halaman baru untuk tampilan preview PDF di interface.

- **Optimisasi dan Refaktor:**
  - Grouping import library untuk meningkatkan keterbacaan.
  - Manajemen direktori penyimpanan PDF dipastikan berjalan dinamis dan fleksibel.
  - Penghilangan redundansi logika pada proses pencarian saldo awal bulan.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
This commit is contained in:
Daeng Deni Mardaeni
2025-07-09 14:54:48 +07:00
parent e2c9f3480d
commit 51697f017e
5 changed files with 1069 additions and 18 deletions

View File

@@ -1,6 +1,6 @@
<?php
namespace Modules\Webstatement\Http\Controllers;
namespace Modules\Webstatement\Http\Controllers;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
@@ -17,7 +17,9 @@ use Modules\Webstatement\{
Models\AccountBalance,
Jobs\ExportStatementPeriodJob
};
use Modules\Webstatement\Models\ProcessedStatement;
use ZipArchive;
use Spatie\Browsershot\Browsershot;
class PrintStatementController extends Controller
{
@@ -80,12 +82,17 @@ use ZipArchive;
try {
$validated = $request->validated();
$validated['request_type'] = 'single_account'; // Default untuk request manual
if($validated['branch_code'] && $validated['stmt_sent_type']){
$validated['request_type'] = 'multi_account'; // Default untuk request manual
}
// Add user tracking data dan field baru untuk single account request
$validated['user_id'] = Auth::id();
$validated['created_by'] = Auth::id();
$validated['ip_address'] = $request->ip();
$validated['user_agent'] = $request->userAgent();
$validated['request_type'] = 'single_account'; // Default untuk request manual
$validated['status'] = 'pending'; // Status awal
$validated['authorization_status'] = 'approved'; // Status otorisasi awal
$validated['total_accounts'] = 1; // Untuk single account
@@ -93,7 +100,7 @@ use ZipArchive;
$validated['success_count'] = 0;
$validated['failed_count'] = 0;
$validated['stmt_sent_type'] = $request->input('stmt_sent_type') ? implode(',', $request->input('stmt_sent_type')) : '';
$validated['branch_code'] = $branch_code; // Awal tidak tersedia
$validated['branch_code'] = $validated['branch_code'] ?? $branch_code; // Awal tidak tersedia
// Create the statement log
$statement = PrintStatementLog::create($validated);
@@ -109,7 +116,7 @@ use ZipArchive;
// Process statement availability check
$this->checkStatementAvailability($statement);
if(!$statement->is_available){
$this->printStatementRekening($statement->account_number,$statement->period_from,$statement->period_to,$statement->stmt_sent_type);
$this->printStatementRekening($statement);
}
$statement = PrintStatementLog::find($statement->id);
@@ -784,9 +791,493 @@ use ZipArchive;
return "statement_{$accountNumber}_{$statement->period_from}.pdf";
}
/**
* Generate statement view atau PDF berdasarkan parameter
*
* @param string $norek Nomor rekening
* @param string $period Periode dalam format YYYYMM
* @param string|null $format Format output: 'html' atau 'pdf'
* @return \Illuminate\View\View|\Illuminate\Http\Response
*/
public function generated($norek, $period='202505', $format = 'html'){
try {
DB::beginTransaction();
function printStatementRekening($accountNumber, $period, $periodTo = null, $stmtSentType = null) {
$period = $period ?? date('Ym');
Log::info('Generating statement', [
'account_number' => $norek,
'period' => $period,
'format' => $format,
'user_id' => Auth::id()
]);
$stmtEntries = ProcessedStatement::where(['account_number' => $norek, 'period' => $period])->orderBy('sequence_no')->get();
$account = Account::with('customer')->where('account_number', $norek)->first();
if (!$account) {
throw new Exception("Account not found: {$norek}");
}
$customer = $account->customer;
$branch = Branch::where('code', $account->branch_code)->first();
// Cek apakah file gambar ada
$headerImagePath = public_path('assets/media/images/bg-header-table.png');
$headerTableBg = file_exists($headerImagePath)
? base64_encode(file_get_contents($headerImagePath))
: null;
// Logika untuk menentukan period saldo berdasarkan aturan baru
$saldoPeriod = $this->calculateSaldoPeriod($period);
Log::info('Calculated saldo period', [
'original_period' => $period,
'saldo_period' => $saldoPeriod
]);
$saldoAwalBulan = AccountBalance::where(['account_number' => $norek, 'period' => $saldoPeriod])->first();
if (!$saldoAwalBulan) {
Log::warning('Saldo awal bulan not found', [
'account_number' => $norek,
'saldo_period' => $saldoPeriod
]);
$saldoAwalBulan = (object) ['actual_balance' => 0];
}
DB::commit();
Log::info('Statement data prepared successfully', [
'account_number' => $norek,
'period' => $period,
'saldo_period' => $saldoPeriod,
'saldo_awal' => $saldoAwalBulan->actual_balance ?? 0,
'entries_count' => $stmtEntries->count()
]);
$periodDates = calculatePeriodDates($period);
// Jika format adalah PDF, generate PDF
if ($format === 'pdf') {
return $this->generateStatementPdf($norek, $period, $stmtEntries, $account, $customer, $headerTableBg, $branch, $saldoAwalBulan);
}
// Default return HTML view
return view('webstatement::statements.stmt', compact('stmtEntries', 'account', 'customer', 'headerTableBg', 'branch', 'period', 'saldoAwalBulan'));
} catch (Exception $e) {
DB::rollBack();
Log::error('Failed to generate statement', [
'error' => $e->getMessage(),
'account_number' => $norek,
'period' => $period,
'format' => $format,
'trace' => $e->getTraceAsString()
]);
if ($format === 'pdf') {
return response()->json([
'success' => false,
'message' => 'Failed to generate PDF statement',
'error' => $e->getMessage()
], 500);
}
throw $e;
}
}
/**
* Generate PDF dari statement HTML dan simpan ke storage
*
* @param string $norek Nomor rekening
* @param string $period Periode
* @param \Illuminate\Database\Eloquent\Collection $stmtEntries Data transaksi
* @param object $account Data akun
* @param object $customer Data customer
* @param string|null $headerTableBg Base64 encoded header image
* @param object $branch Data cabang
* @param object $saldoAwalBulan Data saldo awal
* @return \Illuminate\Http\Response
*/
protected function generateStatementPdf($norek, $period, $stmtEntries, $account, $customer, $headerTableBg, $branch, $saldoAwalBulan)
{
try {
DB::beginTransaction();
Log::info('Starting PDF generation with storage', [
'account_number' => $norek,
'period' => $period,
'user_id' => Auth::id()
]);
// Render HTML view
$html = view('webstatement::statements.stmt', compact(
'stmtEntries',
'account',
'customer',
'headerTableBg',
'branch',
'period',
'saldoAwalBulan'
))->render();
// Generate nama file PDF
$filename = $this->generatePdfFileName($norek, $period);
// Tentukan path storage
$storagePath = "statements/{$period}/{$norek}";
$fullStoragePath = "{$storagePath}/{$filename}";
// Pastikan direktori storage ada
Storage::disk('local')->makeDirectory($storagePath);
// Path temporary untuk Browsershot
$tempPath = storage_path("app/{$fullStoragePath}");
// Generate PDF menggunakan Browsershot dan simpan langsung ke storage
Browsershot::html($html)
->showBackground()
->setOption('addStyleTag', json_encode(['content' => '@page { margin: 0; }']))
->format('A4')
->margins(0, 0, 0, 0)
->waitUntilNetworkIdle()
->timeout(60)
->save($tempPath);
// Verifikasi file berhasil dibuat
if (!file_exists($tempPath)) {
throw new Exception('PDF file was not created successfully');
}
$fileSize = filesize($tempPath);
Log::info('PDF generated and saved to storage', [
'account_number' => $norek,
'period' => $period,
'storage_path' => $fullStoragePath,
'file_size' => $fileSize,
'temp_path' => $tempPath
]);
DB::commit();
// Return download response
return response()->download($tempPath, $filename, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="' . $filename . '"'
])->deleteFileAfterSend(false); // Keep file in storage
} catch (Exception $e) {
DB::rollBack();
Log::error('Failed to generate PDF with storage', [
'error' => $e->getMessage(),
'account_number' => $norek,
'period' => $period,
'trace' => $e->getTraceAsString()
]);
throw new Exception('Failed to generate PDF: ' . $e->getMessage());
}
}
/**
* Generate nama file PDF berdasarkan nomor rekening dan periode
*
* @param string $norek Nomor rekening
* @param string $period Periode
* @return string
*/
protected function generatePdfFileName($norek, $period)
{
try {
$filename = "statement_{$norek}_{$period}.pdf";
Log::info('Generated PDF filename', [
'account_number' => $norek,
'period' => $period,
'filename' => $filename
]);
return $filename;
} catch (Exception $e) {
Log::error('Error generating PDF filename', [
'error' => $e->getMessage(),
'account_number' => $norek,
'period' => $period
]);
// Fallback filename
return "statement_{$norek}_{$period}.pdf";
}
}
/**
* Simpan PDF ke storage dengan validasi dan logging
*
* @param string $tempPath Path temporary file
* @param string $storagePath Path di storage
* @param string $norek Nomor rekening
* @param string $period Periode
* @return string|null Path file yang disimpan
*/
protected function savePdfToStorage($tempPath, $storagePath, $norek, $period)
{
try {
// Validasi file temporary ada
if (!file_exists($tempPath)) {
throw new Exception('Temporary PDF file not found');
}
// Validasi ukuran file
$fileSize = filesize($tempPath);
if ($fileSize === 0) {
throw new Exception('PDF file is empty');
}
// Baca konten file
$pdfContent = file_get_contents($tempPath);
if ($pdfContent === false) {
throw new Exception('Failed to read PDF content');
}
// Simpan ke storage
$saved = Storage::disk('local')->put($storagePath, $pdfContent);
if (!$saved) {
throw new Exception('Failed to save PDF to storage');
}
// Verifikasi file tersimpan
if (!Storage::disk('local')->exists($storagePath)) {
throw new Exception('PDF file not found in storage after save');
}
$savedSize = Storage::disk('local')->size($storagePath);
Log::info('PDF successfully saved to storage', [
'account_number' => $norek,
'period' => $period,
'storage_path' => $storagePath,
'original_size' => $fileSize,
'saved_size' => $savedSize,
'temp_path' => $tempPath
]);
return $storagePath;
} catch (Exception $e) {
Log::error('Failed to save PDF to storage', [
'error' => $e->getMessage(),
'account_number' => $norek,
'period' => $period,
'storage_path' => $storagePath,
'temp_path' => $tempPath
]);
return null;
}
}
/**
* Ambil PDF dari storage untuk download
*
* @param string $norek Nomor rekening
* @param string $period Periode
* @param string $filename Nama file (optional)
* @return \Illuminate\Http\Response
*/
public function downloadFromStorage($norek, $period, $filename = null)
{
try {
// Generate filename jika tidak disediakan
if (!$filename) {
$filename = $this->generatePdfFileName($norek, $period);
}
$storagePath = "statements/{$period}/{$norek}/{$filename}";
// Cek apakah file ada di storage
if (!Storage::disk('local')->exists($storagePath)) {
Log::warning('PDF not found in storage', [
'account_number' => $norek,
'period' => $period,
'storage_path' => $storagePath
]);
return response()->json([
'success' => false,
'message' => 'PDF file not found in storage'
], 404);
}
$fullPath = Storage::disk('local')->path($storagePath);
Log::info('PDF downloaded from storage', [
'account_number' => $norek,
'period' => $period,
'storage_path' => $storagePath
]);
return response()->download($fullPath, $filename, [
'Content-Type' => 'application/pdf'
]);
} catch (Exception $e) {
Log::error('Failed to download PDF from storage', [
'error' => $e->getMessage(),
'account_number' => $norek,
'period' => $period,
'filename' => $filename
]);
return response()->json([
'success' => false,
'message' => 'Failed to download PDF',
'error' => $e->getMessage()
], 500);
}
}
/**
* Hapus PDF dari storage
*
* @param string $norek Nomor rekening
* @param string $period Periode
* @param string $filename Nama file (optional)
* @return bool
*/
public function deleteFromStorage($norek, $period, $filename = null)
{
try {
if (!$filename) {
$filename = $this->generatePdfFileName($norek, $period);
}
$storagePath = "statements/{$period}/{$norek}/{$filename}";
if (Storage::disk('local')->exists($storagePath)) {
$deleted = Storage::disk('local')->delete($storagePath);
Log::info('PDF deleted from storage', [
'account_number' => $norek,
'period' => $period,
'storage_path' => $storagePath,
'success' => $deleted
]);
return $deleted;
}
Log::warning('PDF not found for deletion', [
'account_number' => $norek,
'period' => $period,
'storage_path' => $storagePath
]);
return false;
} catch (Exception $e) {
Log::error('Failed to delete PDF from storage', [
'error' => $e->getMessage(),
'account_number' => $norek,
'period' => $period,
'filename' => $filename
]);
return false;
}
}
/**
* Generate PDF dan return sebagai stream untuk preview
*
* @param string $norek Nomor rekening
* @param string $period Periode
* @return \Illuminate\Http\Response
*/
public function previewPdf($norek, $period = '202505')
{
try {
Log::info('Generating PDF preview', [
'account_number' => $norek,
'period' => $period,
'user_id' => Auth::id()
]);
// Generate PDF dengan format parameter
$response = $this->generated($norek, $period, 'pdf');
// Ubah header untuk preview di browser
return $response->header('Content-Disposition', 'inline; filename="statement_preview.pdf"');
} catch (Exception $e) {
Log::error('Failed to generate PDF preview', [
'error' => $e->getMessage(),
'account_number' => $norek,
'period' => $period
]);
return response()->json([
'success' => false,
'message' => 'Failed to generate PDF preview',
'error' => $e->getMessage()
], 500);
}
}
/**
* Menghitung period untuk pengambilan saldo berdasarkan aturan bisnis
* - Jika period = 202505, gunakan 20250510
* - Jika period > 202505, ambil tanggal akhir bulan sebelumnya
*
* @param string $period Format YYYYMM
* @return string Format YYYYMMDD
*/
private function calculateSaldoPeriod($period)
{
try {
// Jika period adalah 202505, gunakan tanggal 10 Mei 2025
if ($period === '202505') {
return '20250510';
}
// Jika period lebih dari 202505, ambil tanggal akhir bulan sebelumnya
if ($period > '202505') {
$year = substr($period, 0, 4);
$month = substr($period, 4, 2);
// Buat tanggal pertama bulan ini
$firstDayOfMonth = Carbon::createFromDate($year, $month, 1);
// Ambil tanggal terakhir bulan sebelumnya
$lastDayPrevMonth = $firstDayOfMonth->subDay();
return $lastDayPrevMonth->format('Ymd');
}
// Untuk period sebelum 202505, gunakan logika default (tanggal 10)
$year = substr($period, 0, 4);
$month = substr($period, 4, 2);
return $year . $month . '10';
} catch (Exception $e) {
Log::error('Error calculating saldo period', [
'period' => $period,
'error' => $e->getMessage()
]);
// Fallback ke format default
return $period . '10';
}
}
function printStatementRekening($statement) {
$accountNumber = $statement->account_number;
$period = $statement->period_from ?? date('Ym');
$balance = AccountBalance::where('account_number', $accountNumber)
->when($period === '202505', function($query) {
return $query->where('period', '>=', '20250512')
@@ -810,7 +1301,7 @@ use ZipArchive;
}
// Dispatch the job
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName);
$job = ExportStatementPeriodJob::dispatch($statement, $accountNumber, $period, $balance, $clientName);
Log::info("Statement export job dispatched successfully", [
'job_id' => $job->job_id ?? null,

View File

@@ -21,6 +21,7 @@ class PrintStatementRequest extends FormRequest
public function rules(): array
{
$rules = [
'branch_code' => ['required', 'string'],
// account_number required jika stmt_sent_type tidak diisi atau kosong
'account_number' => [
function ($attribute, $value, $fail) {
@@ -99,6 +100,8 @@ class PrintStatementRequest extends FormRequest
public function messages(): array
{
return [
'branch_code.required' => 'Branch code is required',
'branch_code.string' => 'Branch code must be a string',
'account_number.required' => 'Account number is required when statement type is not specified',
'stmt_sent_type.*.in' => 'Invalid statement type selected',
'period_from.required' => 'Period is required',

View File

@@ -33,6 +33,7 @@ class ExportStatementPeriodJob implements ShouldQueue
protected $startDate;
protected $endDate;
protected $toCsv;
protected $statement;
/**
* Create a new job instance.
@@ -43,8 +44,9 @@ class ExportStatementPeriodJob implements ShouldQueue
* @param string $client
* @param string $disk
*/
public function __construct(string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local', bool $toCsv = true)
public function __construct($statement, string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local', bool $toCsv = true)
{
$this->statement = $statement;
$this->account_number = $account_number;
$this->period = $period;
$this->saldo = $saldo;
@@ -170,10 +172,7 @@ class ExportStatementPeriodJob implements ShouldQueue
});
if($entry){
$printLog = PrintStatementLog::where('account_number', $this->account_number)
->where('period_from', $this->period)
->latest()
->first();
$printLog = PrintStatementLog::find($this->statement->id);
if($printLog){
$printLog->update(['is_generated' => true]);
}

View File

@@ -22,19 +22,19 @@
<div class="grid grid-cols-1 gap-5">
@if ($multiBranch)
<div class="form-group">
<label class="form-label required" for="branch_id">Branch/Cabang</label>
<label class="form-label required" for="branch_code">Branch/Cabang</label>
<select
class="input form-control tomselect @error('branch_id') border-danger bg-danger-light @enderror"
id="branch_id" name="branch_id" required>
class="input form-control tomselect @error('branch_code') border-danger bg-danger-light @enderror"
id="branch_code" name="branch_code" required>
<option value="">Pilih Branch/Cabang</option>
@foreach ($branches as $branchOption)
<option value="{{ $branchOption->code }}"
{{ old('branch_id', $statement->branch_id ?? ($branch->code ?? '')) == $branchOption->code ? 'selected' : '' }}>
{{ old('branch_code', $statement->branch_code ?? ($branch->code ?? '')) == $branchOption->code ? 'selected' : '' }}>
{{ $branchOption->code }} - {{ $branchOption->name }}
</option>
@endforeach
</select>
@error('branch_id')
@error('branch_code')
<div class="text-sm alert text-danger">{{ $message }}</div>
@enderror
</div>
@@ -43,7 +43,10 @@
<label class="form-label" for="branch_display">Branch/Cabang</label>
<input type="text" class="input form-control" id="branch_display"
value="{{ $branch->code ?? '' }} - {{ $branch->name ?? '' }}" readonly>
<input type="hidden" name="branch_id" value="{{ $branch->code ?? '' }}">
<input type="hidden" name="branch_code" value="{{ $branch->code ?? '' }}">
@error('branch_code')
<div class="text-sm alert text-danger">{{ $message }}</div>
@enderror
</div>
@endif

View File

@@ -0,0 +1,555 @@
<!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;
}
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;
}
/* Column width classes */
.col-date {
width: 10%;
text-align: center;
}
.col-desc {
width: 25%;
}
.col-valuta {
width: 10%;
text-align: center;
}
.col-referensi {
width: 20%;
}
.col-debet,
.col-kredit {
width: 10%;
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;
@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) > 35) {
$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 (19 lines per page)
$totalPages = ceil($totalLines / 19);
$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 }}</p>
<p>{{ ($account->customer->city ? $account->customer->city . ' ' : '') . ($account->customer->province ? $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="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($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) > 35) {
$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">{{ date('d/m/Y', strtotime($row->actual_date)) }}</td>
<td>{{ $narrativeLines[0] ?? '' }}</td>
<td>{{ $row->reference_number }}</td>
<td class="text-right">{{ $debit > 0 ? number_format($debit, 2, ',', '.') : '' }}</td>
<td class="text-right">{{ $kredit > 0 ? number_format($kredit, 2, ',', '.') : '' }}
</td>
<td class="text-right">{{ number_format($saldo, 2, ',', '.') }}</td>
</tr>
@for ($i = 1; $i < count($narrativeLines); $i++)
<tr>
<td class="text-center"></td>
<td class="text-center"></td>
<td>{{ $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 >= 19 && !$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 }}</p>
<p>{{ ($account->customer->city ? $account->customer->city . ' ' : '') . ($account->customer->province ? $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 < 19 - $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>