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,