feat(webstatement): tambahkan pengaturan ekspor dan optimasi fungsionalitas print statement

- **Pembaruan pada `ExportStatementPeriodJob`:**
  - Menambahkan atribut baru `toCsv` untuk mendukung validasi sebelum proses ekspor CSV.
  - Menyesuaikan method `__construct` untuk menerima parameter tambahan `toCsv`.
  - Menambahkan validasi ekspor CSV dengan conditional check pada `toCsv` sebelum menjalankan `exportToCsv`.
  - Memperbaiki logika di `getTotalEntryCount` menggunakan `booking_date` untuk query lebih akurat.
  - Menambahkan logging terperinci pada proses penghitungan jumlah entri untuk meningkatkan debugging.

- **Integrasi Log Print Statement:**
  - Mengupdate status kolom `is_generated` pada model `PrintStatementLog` setelah entri diproses.
  - Menambahkan mekanisme pembaruan data log print statement melalui validasi entry statement.

- **Peningkatan pada Controller `PrintStatementController`:**
  - Memampukan proses ekspor otomatis jika statement tidak tersedia dengan metode baru `printStatementRekening`.
  - Menambahkan parameter `stmt_sent_type` untuk log print pada proses pencatatan data.
  - Mengimplementasikan pemrosesan period statement melalui job `ExportStatementPeriodJob`.

- **Perubahan pada UI/Blade `statements/index`:**
  - Menambahkan opsi pemilihan multiple untuk tipe laporan `stmt_sent_type`.
  - Mengupdate dan merapikan komponen form untuk input branch, akun, email, dan periode laporan.
  - Menambahkan kolom baru `is_generated` pada tabel untuk menampilkan status log hasil pembuatan laporan.

- **Pembaruan pada Datatable dan Skrip Frontend:**
  - Menambahkan render visual dengan badge untuk status `is_generated`.
  - Memperbaiki dan mengoptimalkan element HTML untuk datatable termasuk pagination dan search.
  - Menambahkan konfirmasi aksi dengan Ajax untuk retry pembuatan laporan jika diperlukan.

- **Optimisasi dan Refactor:**
  - Menggunakan group import pada controller untuk meningkatkan keterbacaan.
  - Memperbaiki alignment dan indentasi pada beberapa file blade.
  - Menghapus kode yang tidak digunakan atau redundan seperti conditional unprocessed data.

Dengan perubahan ini, sistem print statement lebih fleksibel, mencatat log lebih baik, dan mendukung fitur tracking pengeluaran laporan.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
This commit is contained in:
Daeng Deni Mardaeni
2025-07-08 17:40:11 +07:00
parent a3060322f9
commit 65b846f0c7
3 changed files with 246 additions and 102 deletions

View File

@@ -2,20 +2,20 @@
namespace Modules\Webstatement\Http\Controllers;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Modules\Basicdata\Models\Branch;
use Modules\Webstatement\Http\Requests\PrintStatementRequest;
use Modules\Webstatement\Mail\StatementEmail;
use Modules\Webstatement\Models\PrintStatementLog;
use ZipArchive;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\{Auth, DB, Log, Mail, Storage};
use Modules\Basicdata\Models\Branch;
use Modules\Webstatement\{
Http\Requests\PrintStatementRequest,
Mail\StatementEmail,
Models\PrintStatementLog,
Models\AccountBalance,
Jobs\ExportStatementPeriodJob
};
use ZipArchive;
class PrintStatementController extends Controller
{
@@ -51,6 +51,7 @@
$validated['processed_accounts'] = 0;
$validated['success_count'] = 0;
$validated['failed_count'] = 0;
$validated['stmt_sent_type'] = implode(',', $request->input('stmt_sent_type'));
// Create the statement log
$statement = PrintStatementLog::create($validated);
@@ -65,7 +66,9 @@
// 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);
}
DB::commit();
return redirect()->route('statements.index')
@@ -383,6 +386,7 @@
'period_to' => $item->is_period_range ? $item->period_to : null,
'authorization_status' => $item->authorization_status,
'is_available' => $item->is_available,
'is_generated' => $item->is_generated,
'is_downloaded' => $item->is_downloaded,
'created_at' => dateFormat($item->created_at, 1, 1),
'created_by' => $item->user->name ?? 'N/A',
@@ -588,4 +592,65 @@
return "statement_{$accountNumber}_{$statement->period_from}.pdf";
}
function printStatementRekening($accountNumber, $period, $periodTo = null, $stmtSentType = null) {
$period = $period ?? date('Ym');
$balance = AccountBalance::where('account_number', $accountNumber)
->when($period === '202505', function($query) {
return $query->where('period', '>=', '20250512')
->orderBy('period', 'asc');
}, function($query) use ($period) {
// Get balance from last day of previous month
$firstDayOfMonth = Carbon::createFromFormat('Ym', $period)->startOfMonth();
$lastDayPrevMonth = $firstDayOfMonth->copy()->subDay()->format('Ymd');
return $query->where('period', $lastDayPrevMonth);
})
->first()
->actual_balance ?? '0.00';
$clientName = 'client1';
try {
\Log::info("Starting statement export for account: {$accountNumber}, period: {$period}, client: {$clientName}");
// Validate inputs
if (empty($accountNumber) || empty($period) || empty($clientName)) {
throw new \Exception('Required parameters missing');
}
// Dispatch the job
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName);
\Log::info("Statement export job dispatched successfully", [
'job_id' => $job->job_id ?? null,
'account' => $accountNumber,
'period' => $period,
'client' => $clientName
]);
return response()->json([
'success' => true,
'message' => 'Statement export job queued successfully',
'data' => [
'job_id' => $job->job_id ?? null,
'account_number' => $accountNumber,
'period' => $period,
'client_name' => $clientName
]
]);
} catch (\Exception $e) {
\Log::error("Failed to export statement", [
'error' => $e->getMessage(),
'account' => $accountNumber,
'period' => $period
]);
return response()->json([
'success' => false,
'message' => 'Failed to queue statement export job',
'error' => $e->getMessage()
]);
}
}
}

View File

@@ -12,6 +12,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Modules\Webstatement\Models\PrintStatementLog;
use Modules\Webstatement\Models\ProcessedStatement;
use Modules\Webstatement\Models\StmtEntry;
use Modules\Webstatement\Models\TempFundsTransfer;
@@ -31,6 +32,7 @@ class ExportStatementPeriodJob implements ShouldQueue
protected $chunkSize = 1000;
protected $startDate;
protected $endDate;
protected $toCsv;
/**
* Create a new job instance.
@@ -41,7 +43,7 @@ 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')
public function __construct(string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local', bool $toCsv = true)
{
$this->account_number = $account_number;
$this->period = $period;
@@ -49,6 +51,7 @@ class ExportStatementPeriodJob implements ShouldQueue
$this->disk = $disk;
$this->client = $client;
$this->fileName = "{$account_number}_{$period}.csv";
$this->toCsv = $toCsv;
// Calculate start and end dates based on period
$this->calculatePeriodDates();
@@ -84,8 +87,9 @@ class ExportStatementPeriodJob implements ShouldQueue
Log::info("Date range: {$this->startDate->format('Y-m-d')} to {$this->endDate->format('Y-m-d')}");
$this->processStatementData();
$this->exportToCsv();
if($this->toCsv){
$this->exportToCsv();
}
Log::info("Export statement period job completed successfully for account: {$this->account_number}, period: {$this->period}");
} catch (Exception $e) {
Log::error("Error in ExportStatementPeriodJob: " . $e->getMessage());
@@ -104,20 +108,28 @@ class ExportStatementPeriodJob implements ShouldQueue
$existingDataCount = $this->getExistingProcessedCount($accountQuery);
// Only process if data is not fully processed
if ($existingDataCount !== $totalCount) {
//if ($existingDataCount !== $totalCount) {
$this->deleteExistingProcessedData($accountQuery);
$this->processAndSaveStatementEntries($totalCount);
}
//}
}
private function getTotalEntryCount(): int
{
return StmtEntry::where('account_number', $this->account_number)
->whereBetween('date_time', [
$this->startDate->format('ymdHi'),
$this->endDate->format('ymdHi')
])
->count();
$query = StmtEntry::where('account_number', $this->account_number)
->whereBetween('booking_date', [
$this->startDate->format('Ymd'),
$this->endDate->format('Ymd')
]);
Log::info("Getting total entry count with query: " . $query->toSql(), [
'bindings' => $query->getBindings(),
'account' => $this->account_number,
'start_date' => $this->startDate->format('Ymd'),
'end_date' => $this->endDate->format('Ymd')
]);
return $query->count();
}
private function getExistingProcessedCount(array $criteria): int
@@ -141,11 +153,11 @@ class ExportStatementPeriodJob implements ShouldQueue
Log::info("Processing {$totalCount} statement entries for account: {$this->account_number}");
StmtEntry::with(['ft', 'transaction'])
$entry = StmtEntry::with(['ft', 'transaction'])
->where('account_number', $this->account_number)
->whereBetween('date_time', [
$this->startDate->format('ymdHi'),
$this->endDate->format('ymdHi')
->whereBetween('booking_date', [
$this->startDate->format('Ymd'),
$this->endDate->format('Ymd')
])
->orderBy('date_time', 'ASC')
->orderBy('trans_reference', 'ASC')
@@ -156,6 +168,16 @@ class ExportStatementPeriodJob implements ShouldQueue
DB::table('processed_statements')->insert($processedData);
}
});
if($entry){
$printLog = PrintStatementLog::where('account_number', $this->account_number)
->where('period_from', $this->period)
->latest()
->first();
if($printLog){
$printLog->update(['is_generated' => true]);
}
}
}
private function prepareProcessedData($entries, &$runningBalance, &$globalSequence): array
@@ -166,14 +188,13 @@ class ExportStatementPeriodJob implements ShouldQueue
$globalSequence++;
$runningBalance += (float) $item->amount_lcy;
$transactionDate = $this->formatTransactionDate($item);
$actualDate = $this->formatActualDate($item);
$processedData[] = [
'account_number' => $this->account_number,
'period' => $this->period,
'sequence_no' => $globalSequence,
'transaction_date' => $transactionDate,
'transaction_date' => $item->booking_date,
'reference_number' => $item->trans_reference,
'transaction_amount' => $item->amount_lcy,
'transaction_type' => $item->amount_lcy < 0 ? 'D' : 'C',

View File

@@ -11,41 +11,83 @@
<h3 class="card-title">Request Print Stetement</h3>
</div>
<div class="card-body">
<form action="{{ isset($statement) ? route('statements.update', $statement->id) : route('statements.store') }}" method="POST">
<form
action="{{ isset($statement) ? route('statements.update', $statement->id) : route('statements.store') }}"
method="POST">
@csrf
@if(isset($statement))
@if (isset($statement))
@method('PUT')
@endif
<div class="grid grid-cols-1 gap-5">
<div class="form-group">
<label class="form-label required" for="branch_code">Branch</label>
<select class="select tomselect @error('branch_code') is-invalid @enderror" id="branch_code" name="branch_code" required>
<select class="select tomselect @error('branch_code') is-invalid @enderror" id="branch_code"
name="branch_code" required>
<option value="">Select Branch</option>
@foreach($branches as $branch)
<option value="{{ $branch->code }}" {{ (old('branch_code', $statement->branch_code ?? '') == $branch->code) ? 'selected' : '' }}>
@foreach ($branches as $branch)
<option value="{{ $branch->code }}"
{{ old('branch_code', $statement->branch_code ?? '') == $branch->code ? 'selected' : '' }}>
{{ $branch->name }}
</option>
@endforeach
</select>
@error('branch_code')
<div class="invalid-feedback">{{ $message }}</div>
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label class="form-label required" for="stmt_sent_type">Statement Type</label>
<select class="select tomselect @error('stmt_sent_type') is-invalid @enderror"
id="stmt_sent_type" name="stmt_sent_type[]" multiple required>
<option value="ALL"
{{ in_array('ALL', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
ALL
</option>
<option value="BY.EMAIL"
{{ in_array('BY.EMAIL', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
BY EMAIL
</option>
<option value="BY.MAIL.TO.DOM.ADDR"
{{ in_array('BY.MAIL.TO.DOM.ADDR', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
BY MAIL TO DOM ADDR
</option>
<option value="BY.MAIL.TO.KTP.ADDR"
{{ in_array('BY.MAIL.TO.KTP.ADDR', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
BY MAIL TO KTP ADDR
</option>
<option value="NO.PRINT"
{{ in_array('NO.PRINT', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
NO PRINT
</option>
<option value="PRINT"
{{ in_array('PRINT', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
PRINT
</option>
</select>
@error('stmt_sent_type')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label class="form-label required" for="account_number">Account Number</label>
<input type="text" class="input form-control @error('account_number') is-invalid @enderror" id="account_number" name="account_number" value="{{ old('account_number', $statement->account_number ?? '') }}" required>
<input type="text" class="input form-control @error('account_number') is-invalid @enderror"
id="account_number" name="account_number"
value="{{ old('account_number', $statement->account_number ?? '') }}" required>
@error('account_number')
<div class="invalid-feedback">{{ $message }}</div>
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label class="form-label" for="email">Email</label>
<input type="email" class="input form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email', $statement->email ?? '') }}" placeholder="Optional email for send statement">
<input type="email" class="input form-control @error('email') is-invalid @enderror"
id="email" name="email" value="{{ old('email', $statement->email ?? '') }}"
placeholder="Optional email for send statement">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
@@ -53,24 +95,21 @@
<label class="form-label required" for="start_date">Start Date</label>
<input class="input @error('period_from') border-danger bg-danger-light @enderror"
type="month"
name="period_from"
value="{{ $statement->period_from ?? old('period_from') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
type="month" name="period_from"
value="{{ $statement->period_from ?? old('period_from') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
@error('period_from')
<em class="alert text-danger text-sm">{{ $message }}</em>
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
<div class="form-group">
<label class="form-label required" for="end_date">End Date</label>
<input class="input @error('period_to') border-danger bg-danger-light @enderror"
type="month"
name="period_to"
value="{{ $statement->period_to ?? old('period_to') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
<input class="input @error('period_to') border-danger bg-danger-light @enderror" type="month"
name="period_to" value="{{ $statement->period_to ?? old('period_to') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
@error('period_to')
<em class="alert text-danger text-sm">{{ $message }}</em>
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
</div>
@@ -85,8 +124,9 @@
</div>
</div>
<div class="col-span-6">
<div class="card card-grid min-w-full" data-datatable="false" data-datatable-page-size="10" data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}">
<div class="card-header py-5 flex-wrap">
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}">
<div class="flex-wrap py-5 card-header">
<h3 class="card-title">
Daftar Statement Request
</h3>
@@ -96,59 +136,67 @@
<input placeholder="Search Statement" id="search" type="text" value="">
</label>
</div>
</div>
</div>
<div class="card-body">
<div class="scrollable-x-auto">
<table class="table table-auto table-border align-middle text-gray-700 font-medium text-sm" data-datatable-table="true">
<table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
data-datatable-table="true">
<thead>
<tr>
<th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox"/>
</th>
<th class="min-w-[100px]" data-datatable-column="id">
<span class="sort"> <span class="sort-label"> ID </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="branch_name">
<span class="sort"> <span class="sort-label"> Branch </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="account_number">
<span class="sort"> <span class="sort-label"> Account Number </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="period">
<span class="sort"> <span class="sort-label"> Period </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="authorization_status">
<span class="sort"> <span class="sort-label"> Status </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="is_available">
<span class="sort"> <span class="sort-label"> Available </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="remarks">
<span class="sort"> <span class="sort-label"> Notes </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[180px]" data-datatable-column="created_at">
<span class="sort"> <span class="sort-label"> Created At </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
</tr>
<tr>
<th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
</th>
<th class="min-w-[100px]" data-datatable-column="id">
<span class="sort"> <span class="sort-label"> ID </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="branch_name">
<span class="sort"> <span class="sort-label"> Branch </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="account_number">
<span class="sort"> <span class="sort-label"> Account Number </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="period">
<span class="sort"> <span class="sort-label"> Period </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="authorization_status">
<span class="sort"> <span class="sort-label"> Status </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="is_available">
<span class="sort"> <span class="sort-label"> Available </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="is_generated">
<span class="sort"> <span class="sort-label"> Generated </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="remarks">
<span class="sort"> <span class="sort-label"> Notes </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[180px]" data-datatable-column="created_at">
<span class="sort"> <span class="sort-label"> Created At </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
</tr>
</thead>
</table>
</div>
<div class="card-footer justify-center md:justify-between flex-col md:flex-row gap-3 text-gray-600 text-2sm font-medium">
<div class="flex items-center gap-2">
<div
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
<div class="flex gap-2 items-center">
Show
<select class="select select-sm w-16" data-datatable-size="true" name="perpage"> </select> per page
<select class="w-16 select select-sm" data-datatable-size="true" name="perpage"> </select>
per page
</div>
<div class="flex items-center gap-4">
<div class="flex gap-4 items-center">
<span data-datatable-info="true"> </span>
<div class="pagination" data-datatable-pagination="true">
</div>
@@ -175,7 +223,7 @@
if (result.isConfirmed) {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
});
@@ -262,12 +310,22 @@
is_available: {
title: 'Available',
render: (item, data) => {
let statusClass = data.is_available ? 'badge badge-light-success' : 'badge badge-light-danger';
let statusClass = data.is_available ? 'badge badge-light-success' :
'badge badge-light-danger';
let statusText = data.is_available ? 'Yes' : 'No';
return `<span class="${statusClass}">${statusText}</span>`;
},
},
remarks : {
is_generated: {
title: 'Generated',
render: (item, data) => {
let statusClass = data.is_generated ? 'badge badge-light-success' :
'badge badge-light-danger';
let statusText = data.is_generated ? 'Yes' : 'No';
return `<span class="${statusClass}">${statusText}</span>`;
},
},
remarks: {
title: 'Notes',
},
created_at: {
@@ -315,7 +373,7 @@
let dataTable = new KTDataTable(element, dataTableOptions);
// Custom search functionality
searchInput.addEventListener('input', function () {
searchInput.addEventListener('input', function() {
const searchValue = this.value.trim();
dataTable.search(searchValue, true);
});