feat(webstatement): optimalkan validasi, logging, dan UI pada request statement

- **Peningkatan Validasi:**
  - Menambahkan validasi kompleks pada field `account_number` untuk memastikan input wajib jika `stmt_sent_type` tidak diisi.
  - Validasi baru untuk `stmt_sent_type` mendukung array nilai dengan parameter `in` yang diperbolehkan.
  - Menambahkan pengecekan duplikasi pada `PrintStatementRequest` dengan filter tambahan `user_id` untuk scope yang lebih jelas.

- **Peningkatan Logging:**
  - Mengganti penggunaan `\Log` dengan `Log` untuk konsistensi namespace.
  - Menambahkan logging user pada query statement log di controller.
  - Logging lebih terperinci pada proses ekspor dan error handling.

- **Perubahan UI pada Form `statements/index`:**
  - Menambahkan highlight warna merah pada field input yang memiliki error validasi (`border-danger`, `bg-danger-light`).
  - Memperbaiki tampilan dropdown untuk `branch_id` dan `stmt_sent_type`, termasuk pesan error yang lebih spesifik.
  - Menghapus validasi wajib pada field `stmt_sent_type` dan menambah fleksibilitas form pengisian.

- **Optimalisasi Query Backend:**
  - Menambah filter `whereNotNull('user_id')` pada query `PrintStatementLog` untuk meminimalisir data invalid.

- **Updated Blade Template:**
  - Tombol dan validasi form lebih ramah pengguna dengan feedback langsung.
  - Menambahkan badge status styling untuk kolom status otorisasi di datatable.
  - Dinamika field seperti dropdown bebas error dalam kondisi tertentu.

Perubahan ini meningkatkan keakuratan validasi, logging proses lebih rinci untuk debugging, dan memberikan pengalaman pengguna yang lebih baik pada interface request statement.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
This commit is contained in:
Daeng Deni Mardaeni
2025-07-09 10:50:22 +07:00
parent 40f552cb66
commit e2c9f3480d
3 changed files with 80 additions and 35 deletions

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\{Auth, DB, Log, Mail, Storage}; use Illuminate\Support\Facades\{Auth, DB, Log, Mail, Storage};
use Modules\Basicdata\Models\Branch; use Modules\Basicdata\Models\Branch;
use Modules\Webstatement\{ use Modules\Webstatement\{
@@ -91,7 +92,7 @@ use ZipArchive;
$validated['processed_accounts'] = 0; $validated['processed_accounts'] = 0;
$validated['success_count'] = 0; $validated['success_count'] = 0;
$validated['failed_count'] = 0; $validated['failed_count'] = 0;
$validated['stmt_sent_type'] = implode(',', $request->input('stmt_sent_type')); $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'] = $branch_code; // Awal tidak tersedia
// Create the statement log // Create the statement log
@@ -478,6 +479,7 @@ use ZipArchive;
// Retrieve data from the database // Retrieve data from the database
$query = PrintStatementLog::query(); $query = PrintStatementLog::query();
$query->whereNotNull('user_id');
if (!auth()->user()->hasRole('administrator')) { if (!auth()->user()->hasRole('administrator')) {
$query->where(function($q) { $query->where(function($q) {
@@ -800,7 +802,7 @@ use ZipArchive;
$clientName = 'client1'; $clientName = 'client1';
try { try {
\Log::info("Starting statement export for account: {$accountNumber}, period: {$period}, client: {$clientName}"); Log::info("Starting statement export for account: {$accountNumber}, period: {$period}, client: {$clientName}");
// Validate inputs // Validate inputs
if (empty($accountNumber) || empty($period) || empty($clientName)) { if (empty($accountNumber) || empty($period) || empty($clientName)) {
@@ -810,7 +812,7 @@ use ZipArchive;
// Dispatch the job // Dispatch the job
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName); $job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName);
\Log::info("Statement export job dispatched successfully", [ Log::info("Statement export job dispatched successfully", [
'job_id' => $job->job_id ?? null, 'job_id' => $job->job_id ?? null,
'account' => $accountNumber, 'account' => $accountNumber,
'period' => $period, 'period' => $period,
@@ -829,7 +831,7 @@ use ZipArchive;
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error("Failed to export statement", [ Log::error("Failed to export statement", [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'account' => $accountNumber, 'account' => $accountNumber,
'period' => $period 'period' => $period

View File

@@ -21,7 +21,22 @@ class PrintStatementRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
$rules = [ $rules = [
'account_number' => ['required', 'string'], // account_number required jika stmt_sent_type tidak diisi atau kosong
'account_number' => [
function ($attribute, $value, $fail) {
$stmtSentType = $this->input('stmt_sent_type');
// Jika stmt_sent_type kosong atau tidak ada, maka account_number wajib diisi
if (empty($stmtSentType) || (is_array($stmtSentType) && count(array_filter($stmtSentType)) === 0)) {
if (empty($value)) {
$fail('Account number is required when statement type is not specified.');
}
}
},
'string'
],
'stmt_sent_type' => ['nullable', 'array'],
'stmt_sent_type.*' => ['string', 'in:ALL,BY.EMAIL,BY.MAIL.TO.DOM.ADDR,BY.MAIL.TO.KTP.ADDR,NO.PRINT,PRINT'],
'is_period_range' => ['sometimes', 'boolean'], 'is_period_range' => ['sometimes', 'boolean'],
'email' => ['nullable', 'email'], 'email' => ['nullable', 'email'],
'email_sent_at' => ['nullable', 'timestamp'], 'email_sent_at' => ['nullable', 'timestamp'],
@@ -33,26 +48,33 @@ class PrintStatementRequest extends FormRequest
'regex:/^\d{6}$/', // YYYYMM format 'regex:/^\d{6}$/', // YYYYMM format
// Prevent duplicate requests with same account number and period // Prevent duplicate requests with same account number and period
function ($attribute, $value, $fail) { function ($attribute, $value, $fail) {
$query = Statement::where('account_number', $this->input('account_number')) // Hanya cek duplikasi jika account_number ada
->where('authorization_status', '!=', 'rejected') if (!empty($this->input('account_number'))) {
->where('is_available', true) $query = Statement::where('account_number', $this->input('account_number'))
->where('period_from', $value); ->where('authorization_status', '!=', 'rejected')
->where(function($query) {
$query->where('is_available', true)
->orWhere('is_generated', true);
})
->where('user_id', $this->user()->id)
->where('period_from', $value);
// If this is an update request, exclude the current record // If this is an update request, exclude the current record
if ($this->route('statement')) { if ($this->route('statement')) {
$query->where('id', '!=', $this->route('statement')); $query->where('id', '!=', $this->route('statement'));
} }
// If period_to is provided, check for overlapping periods // If period_to is provided, check for overlapping periods
if ($this->input('period_to')) { if ($this->input('period_to')) {
$query->where(function ($q) use ($value) { $query->where(function ($q) use ($value) {
$q->where('period_from', '<=', $this->input('period_to')) $q->where('period_from', '<=', $this->input('period_to'))
->where('period_to', '>=', $value); ->where('period_to', '>=', $value);
}); });
} }
if ($query->exists()) { if ($query->exists()) {
$fail('A statement request with this account number and period already exists.'); $fail('A statement request with this account number and period already exists.');
}
} }
} }
], ],
@@ -77,7 +99,8 @@ class PrintStatementRequest extends FormRequest
public function messages(): array public function messages(): array
{ {
return [ return [
'account_number.required' => 'Account number is required', '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', 'period_from.required' => 'Period is required',
'period_from.regex' => 'Period must be in YYYYMM format', 'period_from.regex' => 'Period must be in YYYYMM format',
'period_to.required' => 'End period is required for period range', 'period_to.required' => 'End period is required for period range',

View File

@@ -23,7 +23,8 @@
@if ($multiBranch) @if ($multiBranch)
<div class="form-group"> <div class="form-group">
<label class="form-label required" for="branch_id">Branch/Cabang</label> <label class="form-label required" for="branch_id">Branch/Cabang</label>
<select class="input form-control tomselect @error('branch_id') is-invalid @enderror" <select
class="input form-control tomselect @error('branch_id') border-danger bg-danger-light @enderror"
id="branch_id" name="branch_id" required> id="branch_id" name="branch_id" required>
<option value="">Pilih Branch/Cabang</option> <option value="">Pilih Branch/Cabang</option>
@foreach ($branches as $branchOption) @foreach ($branches as $branchOption)
@@ -34,7 +35,7 @@
@endforeach @endforeach
</select> </select>
@error('branch_id') @error('branch_id')
<div class="invalid-feedback">{{ $message }}</div> <div class="text-sm alert text-danger">{{ $message }}</div>
@enderror @enderror
</div> </div>
@else @else
@@ -47,9 +48,10 @@
@endif @endif
<div class="form-group"> <div class="form-group">
<label class="form-label required" for="stmt_sent_type">Statement Type</label> <label class="form-label" for="stmt_sent_type">Statement Type</label>
<select class="select tomselect @error('stmt_sent_type') is-invalid @enderror" <select
id="stmt_sent_type" name="stmt_sent_type[]" multiple required> class="select tomselect @error('stmt_sent_type') border-danger bg-danger-light @enderror"
id="stmt_sent_type" name="stmt_sent_type[]" multiple>
<option value="ALL" <option value="ALL"
{{ in_array('ALL', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}> {{ in_array('ALL', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
ALL ALL
@@ -76,27 +78,29 @@
</option> </option>
</select> </select>
@error('stmt_sent_type') @error('stmt_sent_type')
<div class="invalid-feedback">{{ $message }}</div> <div class="text-sm alert text-danger">{{ $message }}</div>
@enderror @enderror
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label required" for="account_number">Account Number</label> <label class="form-label" for="account_number">Account Number</label>
<input type="text" class="input form-control @error('account_number') is-invalid @enderror" <input type="text"
class="input form-control @error('account_number') border-danger bg-danger-light @enderror"
id="account_number" name="account_number" id="account_number" name="account_number"
value="{{ old('account_number', $statement->account_number ?? '') }}" required> value="{{ old('account_number', $statement->account_number ?? '') }}">
@error('account_number') @error('account_number')
<div class="invalid-feedback">{{ $message }}</div> <div class="text-sm alert text-danger">{{ $message }}</div>
@enderror @enderror
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label" for="email">Email</label> <label class="form-label" for="email">Email</label>
<input type="email" class="input form-control @error('email') is-invalid @enderror" <input type="email"
class="input form-control @error('email') border-danger bg-danger-light @enderror"
id="email" name="email" value="{{ old('email', $statement->email ?? '') }}" id="email" name="email" value="{{ old('email', $statement->email ?? '') }}"
placeholder="Optional email for send statement"> placeholder="Optional email for send statement">
@error('email') @error('email')
<div class="invalid-feedback">{{ $message }}</div> <div class="text-sm alert text-danger">{{ $message }}</div>
@enderror @enderror
</div> </div>
@@ -369,6 +373,22 @@
return fromPeriod + toPeriod; return fromPeriod + toPeriod;
}, },
}, },
authorization_status: {
title: 'Status',
render: (item, data) => {
let statusClass = 'badge badge-light-primary';
if (data.authorization_status === 'approved') {
statusClass = 'badge badge-light-success';
} else if (data.authorization_status === 'rejected') {
statusClass = 'badge badge-light-danger';
} else if (data.authorization_status === 'pending') {
statusClass = 'badge badge-light-warning';
}
return `<span class="${statusClass}">${data.authorization_status}</span>`;
},
},
is_available: { is_available: {
title: 'Available', title: 'Available',
render: (item, data) => { render: (item, data) => {