Compare commits
14 Commits
2dd8024586
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5752427297 | ||
|
|
eb89916b1c | ||
|
|
80c866f646 | ||
|
|
e5c33bf631 | ||
|
|
f37707b2f6 | ||
|
|
ad9780ccd6 | ||
|
|
bcc6d814e9 | ||
|
|
5de1c19d09 | ||
|
|
3c01c1728c | ||
|
|
3beaf78872 | ||
|
|
23a0679f74 | ||
|
|
1564ce2efa | ||
|
|
e6c46701ce | ||
|
|
35bb173056 |
535
app/Console/GenerateClosingBalanceReportBulkCommand.php
Normal file
535
app/Console/GenerateClosingBalanceReportBulkCommand.php
Normal file
@@ -0,0 +1,535 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Usermanagement\Models\User;
|
||||
use Modules\Webstatement\Jobs\GenerateClosingBalanceReportJob;
|
||||
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* Console command untuk generate laporan closing balance untuk banyak rekening sekaligus
|
||||
* Command ini dapat dijalankan secara manual atau dijadwalkan
|
||||
* Mendukung periode range dan daftar rekening custom
|
||||
*/
|
||||
class GenerateClosingBalanceReportBulkCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:generate-closing-balance-bulk
|
||||
{start_date : Tanggal mulai periode format YYYYMMDD, contoh: 20250512}
|
||||
{end_date : Tanggal akhir periode format YYYYMMDD, contoh: 20250712}
|
||||
{--accounts= : Daftar rekening dipisahkan koma (opsional, jika tidak ada akan gunakan default list)}
|
||||
{--client= : Filter berdasarkan client tertentu (opsional)}
|
||||
{--user_id=1 : ID user yang menjalankan command (default: 1)}
|
||||
{--dry-run : Tampilkan daftar rekening yang akan diproses tanpa menjalankan job}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate Closing Balance report untuk banyak rekening sekaligus dengan periode range';
|
||||
|
||||
/**
|
||||
* Daftar rekening default yang akan diproses
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $defaultAccounts = [
|
||||
'IDR1723200010001',
|
||||
'IDR1728100010001',
|
||||
'IDR1728200010001',
|
||||
'IDR1733100010001',
|
||||
'IDR1728300010001',
|
||||
'IDR1733100030001',
|
||||
'IDR1723300010001',
|
||||
'IDR1733100020001',
|
||||
'IDR1733100040001',
|
||||
'IDR1733200010001',
|
||||
'IDR1733200020001',
|
||||
'IDR1733500010001',
|
||||
'IDR1733600010001',
|
||||
'IDR1733300010001',
|
||||
'IDR1733400010001',
|
||||
'IDR1354100010001',
|
||||
'IDR1354300010001',
|
||||
'IDR1354400010001',
|
||||
'IDR1728500010001',
|
||||
'IDR1728600010001',
|
||||
'IDR1720500010001',
|
||||
'1078333878',
|
||||
'1081647484',
|
||||
'1085552121',
|
||||
'1085677889',
|
||||
'1086677889',
|
||||
'IDR1744200010001',
|
||||
'IDR1744300010001',
|
||||
'IDR1744100010001',
|
||||
'IDR1744400010001',
|
||||
'IDR1364100010001',
|
||||
'IDR1723100010001',
|
||||
'IDR1354200010001'
|
||||
];
|
||||
|
||||
private $qrisAccount = [
|
||||
'IDR1354500010001',
|
||||
'IDR1354500020001',
|
||||
'IDR1354500030001',
|
||||
'IDR1354500040001',
|
||||
'IDR1354500050001',
|
||||
'IDR1354500060001',
|
||||
'IDR1354500070001',
|
||||
'IDR1354500080001',
|
||||
'IDR1354500090001',
|
||||
'IDR1354500100001',
|
||||
];
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* Menjalankan proses generate laporan closing balance untuk banyak rekening dengan periode range
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting Bulk Closing Balance report generation with date range...');
|
||||
|
||||
// Get parameters
|
||||
$startDate = $this->argument('start_date');
|
||||
$endDate = $this->argument('end_date');
|
||||
$accountsOption = $this->option('accounts');
|
||||
$clientFilter = $this->option('client');
|
||||
$userId = $this->option('user_id');
|
||||
$isDryRun = $this->option('dry-run');
|
||||
|
||||
// Validate parameters
|
||||
if (!$this->validateParameters($startDate, $endDate, $userId)) {
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get account list
|
||||
$accountList = $this->getAccountList($accountsOption, $clientFilter);
|
||||
|
||||
if (empty($accountList)) {
|
||||
$this->warn('No accounts found for processing.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// Generate date range
|
||||
$dateRange = $this->generateDateRange($startDate, $endDate);
|
||||
|
||||
// Show summary
|
||||
$this->showSummary($accountList, $dateRange, $isDryRun);
|
||||
|
||||
if ($isDryRun) {
|
||||
$this->info('Dry run completed. No jobs were dispatched.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// Confirm execution
|
||||
if (!$this->confirm('Do you want to proceed with generating reports for all accounts and periods?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// Process accounts for all dates in range
|
||||
$results = $this->processAccountsWithDateRange($accountList, $dateRange, $userId);
|
||||
|
||||
// Show results
|
||||
$this->showResults($results);
|
||||
|
||||
return Command::SUCCESS;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error in bulk closing balance report generation: ' . $e->getMessage());
|
||||
|
||||
Log::error('Console command: Error in bulk closing balance report generation', [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'client_filter' => $clientFilter,
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate command parameters
|
||||
* Validasi parameter command termasuk validasi range tanggal
|
||||
*
|
||||
* @param string $startDate
|
||||
* @param string $endDate
|
||||
* @param int $userId
|
||||
* @return bool
|
||||
*/
|
||||
private function validateParameters(string $startDate, string $endDate, int $userId): bool
|
||||
{
|
||||
// Validate date format (YYYYMMDD)
|
||||
if (!preg_match('/^\\d{8}$/', $startDate)) {
|
||||
$this->error('Invalid start_date format. Use YYYYMMDD format (example: 20250512)');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preg_match('/^\\d{8}$/', $endDate)) {
|
||||
$this->error('Invalid end_date format. Use YYYYMMDD format (example: 20250712)');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate start date
|
||||
$startYear = substr($startDate, 0, 4);
|
||||
$startMonth = substr($startDate, 4, 2);
|
||||
$startDay = substr($startDate, 6, 2);
|
||||
|
||||
if (!checkdate($startMonth, $startDay, $startYear)) {
|
||||
$this->error('Invalid start_date.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate end date
|
||||
$endYear = substr($endDate, 0, 4);
|
||||
$endMonth = substr($endDate, 4, 2);
|
||||
$endDay = substr($endDate, 6, 2);
|
||||
|
||||
if (!checkdate($endMonth, $endDay, $endYear)) {
|
||||
$this->error('Invalid end_date.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate date range
|
||||
$startCarbon = Carbon::createFromFormat('Ymd', $startDate);
|
||||
$endCarbon = Carbon::createFromFormat('Ymd', $endDate);
|
||||
|
||||
if ($startCarbon->gt($endCarbon)) {
|
||||
$this->error('Start date cannot be greater than end date.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate range not too long (max 3 months)
|
||||
if ($startCarbon->diffInDays($endCarbon) > 90) {
|
||||
$this->error('Date range cannot exceed 90 days.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate user exists
|
||||
$user = User::find($userId);
|
||||
if (!$user) {
|
||||
$this->error("User with ID {$userId} not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate date range array from start to end date
|
||||
* Menghasilkan array tanggal dari start sampai end date
|
||||
*
|
||||
* @param string $startDate
|
||||
* @param string $endDate
|
||||
* @return array
|
||||
*/
|
||||
private function generateDateRange(string $startDate, string $endDate): array
|
||||
{
|
||||
$dates = [];
|
||||
$current = Carbon::createFromFormat('Ymd', $startDate);
|
||||
$end = Carbon::createFromFormat('Ymd', $endDate);
|
||||
|
||||
while ($current->lte($end)) {
|
||||
$dates[] = $current->format('Ymd');
|
||||
$current->addDay();
|
||||
}
|
||||
|
||||
Log::info('Generated date range for bulk processing', [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'total_dates' => count($dates)
|
||||
]);
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account list based on options
|
||||
* Mengambil daftar rekening berdasarkan parameter atau menggunakan default
|
||||
*
|
||||
* @param string|null $accountsOption
|
||||
* @param string|null $clientFilter
|
||||
* @return array
|
||||
*/
|
||||
private function getAccountList(?string $accountsOption, ?string $clientFilter): array
|
||||
{
|
||||
// Jika ada parameter accounts, gunakan itu
|
||||
if ($accountsOption) {
|
||||
$accounts = array_map('trim', explode(',', $accountsOption));
|
||||
$accounts = array_filter($accounts); // Remove empty values
|
||||
|
||||
Log::info('Using custom account list from parameter', [
|
||||
'total_accounts' => count($accounts),
|
||||
'accounts' => $accounts
|
||||
]);
|
||||
|
||||
return ['CUSTOM' => $accounts];
|
||||
}
|
||||
|
||||
// Jika tidak ada parameter accounts, gunakan default list
|
||||
$accountList = ['DEFAULT' => $this->defaultAccounts, 'QRIS' => $this->qrisAccount];
|
||||
|
||||
// Filter by client jika ada (untuk backward compatibility)
|
||||
if ($clientFilter) {
|
||||
// Untuk saat ini, client filter tidak digunakan karena kita pakai list baru
|
||||
// Tapi tetap log untuk tracking
|
||||
Log::info('Client filter specified but using default account list', [
|
||||
'client_filter' => $clientFilter
|
||||
]);
|
||||
}
|
||||
|
||||
Log::info('Using default account list', [
|
||||
'total_accounts' => count($this->defaultAccounts)
|
||||
]);
|
||||
|
||||
return $accountList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show summary of accounts and dates to be processed
|
||||
* Menampilkan ringkasan rekening dan tanggal yang akan diproses
|
||||
*
|
||||
* @param array $accountList
|
||||
* @param array $dateRange
|
||||
* @param bool $isDryRun
|
||||
*/
|
||||
private function showSummary(array $accountList, array $dateRange, bool $isDryRun): void
|
||||
{
|
||||
$this->info('\n=== SUMMARY ===');
|
||||
$this->info("Date Range: {$dateRange[0]} to {$dateRange[count($dateRange)-1]} ({" . count($dateRange) . "} days)");
|
||||
$this->info("Mode: " . ($isDryRun ? 'DRY RUN' : 'LIVE'));
|
||||
$this->info('');
|
||||
|
||||
$totalAccounts = 0;
|
||||
foreach ($accountList as $groupName => $accounts) {
|
||||
$accountCount = count($accounts);
|
||||
$totalAccounts += $accountCount;
|
||||
$this->info("Group: {$groupName} ({$accountCount} accounts)");
|
||||
|
||||
// Show first 10 accounts, then summarize if more
|
||||
$displayAccounts = array_slice($accounts, 0, 10);
|
||||
foreach ($displayAccounts as $account) {
|
||||
$this->line(" - {$account}");
|
||||
}
|
||||
|
||||
if (count($accounts) > 10) {
|
||||
$remaining = count($accounts) - 10;
|
||||
$this->line(" ... and {$remaining} more accounts");
|
||||
}
|
||||
}
|
||||
|
||||
$totalJobs = $totalAccounts * count($dateRange);
|
||||
$this->info("\nTotal accounts: {$totalAccounts}");
|
||||
$this->info("Total dates: " . count($dateRange));
|
||||
$this->info("Total jobs to be created: {$totalJobs}");
|
||||
$this->info('===============\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all accounts for all dates in range
|
||||
* Memproses semua rekening untuk semua tanggal dalam range
|
||||
*
|
||||
* @param array $accountList
|
||||
* @param array $dateRange
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
private function processAccountsWithDateRange(array $accountList, array $dateRange, int $userId): array
|
||||
{
|
||||
$results = [
|
||||
'success' => [],
|
||||
'failed' => [],
|
||||
'total' => 0
|
||||
];
|
||||
|
||||
$totalJobs = $this->getTotalAccountCount($accountList) * count($dateRange);
|
||||
|
||||
$this->info('Starting report generation for date range...');
|
||||
$progressBar = $this->output->createProgressBar($totalJobs);
|
||||
$progressBar->start();
|
||||
|
||||
foreach ($dateRange as $period) {
|
||||
foreach ($accountList as $groupName => $accounts) {
|
||||
foreach ($accounts as $accountNumber) {
|
||||
$results['total']++;
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Create report log entry
|
||||
$reportLog = $this->createReportLog($accountNumber, $period, $userId, $groupName);
|
||||
|
||||
if (!$reportLog) {
|
||||
throw new Exception('Failed to create report log entry');
|
||||
}
|
||||
|
||||
// Dispatch the job
|
||||
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id, $groupName);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$results['success'][] = [
|
||||
'group' => $groupName,
|
||||
'account' => $accountNumber,
|
||||
'period' => $period,
|
||||
'report_log_id' => $reportLog->id
|
||||
];
|
||||
|
||||
Log::info('Bulk command: Report job dispatched successfully', [
|
||||
'group' => $groupName,
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'report_log_id' => $reportLog->id,
|
||||
'user_id' => $userId
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
$results['failed'][] = [
|
||||
'group' => $groupName,
|
||||
'account' => $accountNumber,
|
||||
'period' => $period,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
|
||||
Log::error('Bulk command: Error processing account', [
|
||||
'group' => $groupName,
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->info('\n');
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create report log entry
|
||||
* Membuat entry log laporan
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $period
|
||||
* @param int $userId
|
||||
* @param string $groupName
|
||||
* @return ClosingBalanceReportLog|null
|
||||
*/
|
||||
private function createReportLog(string $accountNumber, string $period, int $userId, string $groupName): ?ClosingBalanceReportLog
|
||||
{
|
||||
try {
|
||||
// Convert period string to Carbon date
|
||||
$reportDate = Carbon::createFromFormat('Ymd', $period);
|
||||
|
||||
$reportLog = ClosingBalanceReportLog::create([
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'report_date' => $reportDate,
|
||||
'status' => 'pending',
|
||||
'user_id' => $userId,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'ip_address' => request()->ip() ?? '127.0.0.1',
|
||||
'user_agent' => 'Console Command - Bulk Range',
|
||||
'remarks' => "Bulk generation for group: {$groupName}, period: {$period}",
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
return $reportLog;
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Bulk command: Error creating report log', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'user_id' => $userId,
|
||||
'group_name' => $groupName,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total account count
|
||||
* Menghitung total jumlah rekening
|
||||
*
|
||||
* @param array $accountList
|
||||
* @return int
|
||||
*/
|
||||
private function getTotalAccountCount(array $accountList): int
|
||||
{
|
||||
$total = 0;
|
||||
foreach ($accountList as $accounts) {
|
||||
$total += count($accounts);
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show processing results
|
||||
* Menampilkan hasil pemrosesan
|
||||
*
|
||||
* @param array $results
|
||||
*/
|
||||
private function showResults(array $results): void
|
||||
{
|
||||
$this->info('\n=== RESULTS ===');
|
||||
$this->info("Total processed: {$results['total']}");
|
||||
$this->info("Successful: " . count($results['success']));
|
||||
$this->info("Failed: " . count($results['failed']));
|
||||
|
||||
if (!empty($results['failed'])) {
|
||||
$this->error('\nFailed jobs:');
|
||||
foreach (array_slice($results['failed'], 0, 10) as $failed) {
|
||||
$this->error(" - {$failed['group']}: {$failed['account']} ({$failed['period']}) - {$failed['error']}");
|
||||
}
|
||||
|
||||
if (count($results['failed']) > 10) {
|
||||
$remaining = count($results['failed']) - 10;
|
||||
$this->error(" ... and {$remaining} more failed jobs");
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($results['success'])) {
|
||||
$this->info('\nSample successful jobs:');
|
||||
foreach (array_slice($results['success'], 0, 5) as $success) {
|
||||
$this->info(" - {$success['group']}: {$success['account']} ({$success['period']}) - Log ID: {$success['report_log_id']}");
|
||||
}
|
||||
|
||||
if (count($results['success']) > 5) {
|
||||
$remaining = count($results['success']) - 5;
|
||||
$this->info(" ... and {$remaining} more successful jobs");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('\nCheck the closing_balance_report_logs table for progress.');
|
||||
$this->info('===============\n');
|
||||
}
|
||||
}
|
||||
211
app/Console/GenerateClosingBalanceReportCommand.php
Normal file
211
app/Console/GenerateClosingBalanceReportCommand.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Usermanagement\Models\User;
|
||||
use Modules\Webstatement\Jobs\GenerateClosingBalanceReportJob;
|
||||
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* Console command untuk generate laporan closing balance
|
||||
* Command ini dapat dijalankan secara manual atau dijadwalkan
|
||||
*/
|
||||
class GenerateClosingBalanceReportCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:generate-closing-balance-report
|
||||
{account_number : Nomor rekening untuk generate laporan}
|
||||
{period : Period laporan format YYYYMMDD, contoh: 20250515}
|
||||
{--user_id=1 : ID user yang menjalankan command (default: 1)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate Closing Balance report untuk nomor rekening dan periode tertentu';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* Menjalankan proses generate laporan closing balance
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting Closing Balance report generation...');
|
||||
|
||||
// Get parameters
|
||||
$accountNumber = $this->argument('account_number');
|
||||
$period = $this->argument('period');
|
||||
$userId = $this->option('user_id');
|
||||
|
||||
// Validate parameters
|
||||
if (!$this->validateParameters($accountNumber, $period, $userId)) {
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Log start of process
|
||||
Log::info('Console command: Starting closing balance report generation', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'user_id' => $userId,
|
||||
'command' => 'webstatement:generate-closing-balance-report'
|
||||
]);
|
||||
|
||||
// Create report log entry
|
||||
$reportLog = $this->createReportLog($accountNumber, $period, $userId);
|
||||
|
||||
if (!$reportLog) {
|
||||
$this->error('Failed to create report log entry');
|
||||
DB::rollback();
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Dispatch the job
|
||||
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$this->info("Closing Balance report generation job queued successfully!");
|
||||
$this->info("Account Number: {$accountNumber}");
|
||||
$this->info("Period: {$period}");
|
||||
$this->info("Report Log ID: {$reportLog->id}");
|
||||
$this->info('The report will be generated in the background.');
|
||||
$this->info('Check the closing_balance_report_logs table for progress.');
|
||||
|
||||
// Log successful dispatch
|
||||
Log::info('Console command: Closing balance report job dispatched successfully', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'report_log_id' => $reportLog->id,
|
||||
'user_id' => $userId
|
||||
]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
$this->error('Error queuing Closing Balance report job: ' . $e->getMessage());
|
||||
|
||||
// Log error
|
||||
Log::error('Console command: Error generating closing balance report', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate command parameters
|
||||
* Validasi parameter command
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $period
|
||||
* @param int $userId
|
||||
* @return bool
|
||||
*/
|
||||
private function validateParameters(string $accountNumber, string $period, int $userId): bool
|
||||
{
|
||||
// Validate account number
|
||||
if (empty($accountNumber)) {
|
||||
$this->error('Account number parameter is required.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate period format (YYYYMMDD)
|
||||
if (!preg_match('/^\\d{8}$/', $period)) {
|
||||
$this->error('Invalid period format. Use YYYYMMDD format (example: 20250515)');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate date
|
||||
$year = substr($period, 0, 4);
|
||||
$month = substr($period, 4, 2);
|
||||
$day = substr($period, 6, 2);
|
||||
|
||||
if (!checkdate($month, $day, $year)) {
|
||||
$this->error('Invalid date in period parameter.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate user exists
|
||||
$user = User::find($userId);
|
||||
if (!$user) {
|
||||
$this->error("User with ID {$userId} not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create report log entry
|
||||
* Membuat entry log laporan
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $period
|
||||
* @param int $userId
|
||||
* @return ClosingBalanceReportLog|null
|
||||
*/
|
||||
private function createReportLog(string $accountNumber, string $period, int $userId): ?ClosingBalanceReportLog
|
||||
{
|
||||
try {
|
||||
// Convert period string to Carbon date
|
||||
$reportDate = Carbon::createFromFormat('Ymd', $period);
|
||||
|
||||
$reportLog = ClosingBalanceReportLog::create([
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'report_date' => $reportDate, // Required field yang sebelumnya missing
|
||||
'status' => 'pending',
|
||||
'user_id' => $userId,
|
||||
'created_by' => $userId, // Required field yang sebelumnya missing
|
||||
'updated_by' => $userId,
|
||||
'ip_address' => request()->ip() ?? '127.0.0.1', // Default untuk console
|
||||
'user_agent' => 'Console Command',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
Log::info('Console command: Report log created', [
|
||||
'report_log_id' => $reportLog->id,
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'report_date' => $reportDate->format('Y-m-d'),
|
||||
'user_id' => $userId
|
||||
]);
|
||||
|
||||
return $reportLog;
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Console command: Error creating report log', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessDailyMigration extends Command
|
||||
class ProcessDailyMigration extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:process-daily-migration
|
||||
{--process_parameter= : To process migration parameter true/false}
|
||||
{--period= : Period to process (default: -1 day, format: Ymd or relative date)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Process data migration for the specified period (default: previous day)';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:process-daily-migration
|
||||
{--process_parameter= : To process migration parameter true/false}';
|
||||
$processParameter = $this->option('process_parameter');
|
||||
$period = $this->option('period');
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Process data migration for the previous day\'s period';
|
||||
// Log start of process
|
||||
Log::info('Starting daily data migration process', [
|
||||
'process_parameter' => $processParameter ?? 'false',
|
||||
'period' => $period ?? '-1 day'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$processParameter = $this->option('process_parameter');
|
||||
$this->info('Starting daily data migration process...');
|
||||
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
|
||||
$this->info('Period: ' . ($period ?? '-1 day (default)'));
|
||||
|
||||
$this->info('Starting daily data migration process...');
|
||||
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
|
||||
try {
|
||||
$controller = app(MigrasiController::class);
|
||||
$response = $controller->index($processParameter, $period);
|
||||
|
||||
try {
|
||||
$controller = app(MigrasiController::class);
|
||||
$response = $controller->index($processParameter);
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$message = $responseData['message'] ?? 'Process completed';
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->info($responseData['message'] ?? 'Process completed');
|
||||
$this->info($message);
|
||||
Log::info('Daily migration process completed successfully', ['message' => $message]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error processing daily migration: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$errorMessage = 'Error processing daily migration: ' . $e->getMessage();
|
||||
$this->error($errorMessage);
|
||||
Log::error($errorMessage, ['exception' => $e->getTraceAsString()]);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
571
app/Http/Controllers/LaporanClosingBalanceController.php
Normal file
571
app/Http/Controllers/LaporanClosingBalanceController.php
Normal file
@@ -0,0 +1,571 @@
|
||||
<?php
|
||||
|
||||
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\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Webstatement\Jobs\GenerateClosingBalanceReportJob;
|
||||
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
||||
|
||||
/**
|
||||
* Controller untuk mengelola laporan closing balance
|
||||
* Menggunakan job processing untuk menangani laporan dengan banyak transaksi
|
||||
*/
|
||||
class LaporanClosingBalanceController extends Controller
|
||||
{
|
||||
/**
|
||||
* Menampilkan halaman utama laporan closing balance
|
||||
* dengan form untuk membuat permintaan laporan
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
Log::info('Mengakses halaman laporan closing balance');
|
||||
return view('webstatement::laporan-closing-balance.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Membuat permintaan laporan closing balance baru
|
||||
* Menggunakan job untuk memproses laporan secara asynchronous
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
Log::info('Membuat permintaan laporan closing balance', [
|
||||
'user_id' => Auth::id(),
|
||||
'request_data' => $request->all()
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$validated = $request->validate([
|
||||
'account_number' => ['required', 'string', 'max:50'],
|
||||
'report_date' => ['required', 'date_format:Y-m-d'],
|
||||
]);
|
||||
|
||||
// Convert date to Ymd format for period
|
||||
$period = Carbon::createFromFormat('Y-m-d', $validated['report_date'])->format('Ymd');
|
||||
|
||||
// Add user tracking data
|
||||
$reportData = [
|
||||
'account_number' => $validated['account_number'],
|
||||
'period' => $period,
|
||||
'report_date' => $validated['report_date'],
|
||||
'user_id' => Auth::id(),
|
||||
'created_by' => Auth::id(),
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'status' => 'pending',
|
||||
];
|
||||
|
||||
// Create the report request log
|
||||
$reportRequest = ClosingBalanceReportLog::create($reportData);
|
||||
|
||||
// Dispatch the job to generate the report
|
||||
GenerateClosingBalanceReportJob::dispatch(
|
||||
$validated['account_number'],
|
||||
$period,
|
||||
$reportRequest->id
|
||||
);
|
||||
|
||||
$reportRequest->update([
|
||||
'status' => 'processing',
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info('Permintaan laporan closing balance berhasil dibuat', [
|
||||
'report_id' => $reportRequest->id,
|
||||
'account_number' => $validated['account_number'],
|
||||
'period' => $period
|
||||
]);
|
||||
|
||||
return redirect()->route('laporan-closing-balance.index')
|
||||
->with('success', 'Permintaan laporan closing balance berhasil dibuat dan sedang diproses.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error('Error saat membuat permintaan laporan closing balance', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('error', 'Terjadi kesalahan saat membuat permintaan laporan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan form untuk membuat permintaan laporan baru
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
Log::info('Menampilkan form pembuatan laporan closing balance');
|
||||
return view('webstatement::laporan-closing-balance.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan detail permintaan laporan
|
||||
*
|
||||
* @param ClosingBalanceReportLog $closingBalanceReport
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function show(ClosingBalanceReportLog $closingBalanceReport)
|
||||
{
|
||||
Log::info('Menampilkan detail laporan closing balance', [
|
||||
'report_id' => $closingBalanceReport->id
|
||||
]);
|
||||
|
||||
$closingBalanceReport->load(['user', 'creator', 'authorizer']);
|
||||
return view('webstatement::laporan-closing-balance.show', compact('closingBalanceReport'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize permintaan laporan
|
||||
*
|
||||
* @param Request $request
|
||||
* @param ClosingBalanceReportLog $closingBalanceReport
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function authorize(Request $request, ClosingBalanceReportLog $closingBalanceReport)
|
||||
{
|
||||
Log::info('Authorize laporan closing balance', [
|
||||
'report_id' => $closingBalanceReport->id,
|
||||
'user_id' => Auth::id()
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$request->validate([
|
||||
'authorization_status' => ['required', Rule::in(['approved', 'rejected'])],
|
||||
'remarks' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
// Update authorization status
|
||||
$closingBalanceReport->update([
|
||||
'authorization_status' => $request->authorization_status,
|
||||
'authorized_by' => Auth::id(),
|
||||
'authorized_at' => now(),
|
||||
'remarks' => $request->remarks,
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$statusText = $request->authorization_status === 'approved' ? 'disetujui' : 'ditolak';
|
||||
|
||||
Log::info('Laporan closing balance berhasil diauthorize', [
|
||||
'report_id' => $closingBalanceReport->id,
|
||||
'status' => $request->authorization_status
|
||||
]);
|
||||
|
||||
return redirect()->route('laporan-closing-balance.show', $closingBalanceReport->id)
|
||||
->with('success', "Permintaan laporan closing balance berhasil {$statusText}.");
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error('Error saat authorize laporan', [
|
||||
'report_id' => $closingBalanceReport->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Terjadi kesalahan saat authorize laporan.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menyediakan data untuk datatables
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function dataForDatatables(Request $request)
|
||||
{
|
||||
Log::info('Mengambil data untuk datatables laporan closing balance', [
|
||||
'filters' => $request->all()
|
||||
]);
|
||||
|
||||
try {
|
||||
// Retrieve data from the database
|
||||
$query = ClosingBalanceReportLog::query();
|
||||
|
||||
// Apply search filter if provided (handle JSON search parameters)
|
||||
if ($request->has('search') && !empty($request->get('search'))) {
|
||||
$search = $request->get('search');
|
||||
|
||||
// Check if search is JSON format
|
||||
if (is_string($search) && json_decode($search, true) !== null) {
|
||||
$searchParams = json_decode($search, true);
|
||||
|
||||
// Apply account number filter
|
||||
if (!empty($searchParams['account_number'])) {
|
||||
$query->where('account_number', 'LIKE', "%{$searchParams['account_number']}%");
|
||||
}
|
||||
|
||||
// Apply date range filter
|
||||
if (!empty($searchParams['start_date'])) {
|
||||
$startPeriod = Carbon::createFromFormat('Y-m-d', $searchParams['start_date'])->format('Ymd');
|
||||
$query->where('period', '>=', $startPeriod);
|
||||
}
|
||||
|
||||
if (!empty($searchParams['end_date'])) {
|
||||
$endPeriod = Carbon::createFromFormat('Y-m-d', $searchParams['end_date'])->format('Ymd');
|
||||
$query->where('period', '<=', $endPeriod);
|
||||
}
|
||||
} else {
|
||||
// Handle regular string search (fallback)
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('account_number', 'LIKE', "%$search%")
|
||||
->orWhere('period', 'LIKE', "%$search%")
|
||||
->orWhere('status', 'LIKE', "%$search%")
|
||||
->orWhere('authorization_status', 'LIKE', "%$search%");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply individual parameter filters (for backward compatibility)
|
||||
if ($request->has('account_number') && !empty($request->get('account_number'))) {
|
||||
$query->where('account_number', 'LIKE', "%{$request->get('account_number')}%");
|
||||
}
|
||||
|
||||
if ($request->has('start_date') && !empty($request->get('start_date'))) {
|
||||
$startPeriod = Carbon::createFromFormat('Y-m-d', $request->get('start_date'))->format('Ymd');
|
||||
$query->where('period', '>=', $startPeriod);
|
||||
}
|
||||
|
||||
if ($request->has('end_date') && !empty($request->get('end_date'))) {
|
||||
$endPeriod = Carbon::createFromFormat('Y-m-d', $request->get('end_date'))->format('Ymd');
|
||||
$query->where('period', '<=', $endPeriod);
|
||||
}
|
||||
|
||||
// Apply column filters if provided
|
||||
if ($request->has('filters') && !empty($request->get('filters'))) {
|
||||
$filters = json_decode($request->get('filters'), true);
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if (!empty($filter['value'])) {
|
||||
if ($filter['column'] === 'status') {
|
||||
$query->where('status', $filter['value']);
|
||||
} else if ($filter['column'] === 'authorization_status') {
|
||||
$query->where('authorization_status', $filter['value']);
|
||||
} else if ($filter['column'] === 'account_number') {
|
||||
$query->where('account_number', 'LIKE', "%{$filter['value']}%");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sorting if provided
|
||||
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
|
||||
$order = $request->get('sortOrder');
|
||||
$column = $request->get('sortField');
|
||||
|
||||
// Map frontend column names to database column names if needed
|
||||
$columnMap = [
|
||||
'account_number' => 'account_number',
|
||||
'period' => 'period',
|
||||
'status' => 'status',
|
||||
];
|
||||
|
||||
$dbColumn = $columnMap[$column] ?? $column;
|
||||
$query->orderBy($dbColumn, $order);
|
||||
} else {
|
||||
// Default sorting
|
||||
$query->latest('created_at');
|
||||
}
|
||||
|
||||
// Get the total count of records
|
||||
$totalRecords = $query->count();
|
||||
|
||||
// Apply pagination if provided
|
||||
if ($request->has('page') && $request->has('size')) {
|
||||
$page = $request->get('page');
|
||||
$size = $request->get('size');
|
||||
$offset = ($page - 1) * $size;
|
||||
|
||||
$query->skip($offset)->take($size);
|
||||
}
|
||||
|
||||
// Get the filtered count of records
|
||||
$filteredRecords = $query->count();
|
||||
|
||||
// Eager load relationships
|
||||
$query->with(['user', 'authorizer']);
|
||||
|
||||
// Get the data for the current page
|
||||
$data = $query->get()->map(function ($item) {
|
||||
$processingHours = $item->status === 'processing' ? $item->updated_at->diffInHours(now()) : 0;
|
||||
$isProcessingTimeout = $item->status === 'processing' && $processingHours >= 1;
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'account_number' => $item->account_number,
|
||||
'period' => $item->period,
|
||||
'report_date' => Carbon::createFromFormat('Ymd', $item->period)->format('Y-m-d'),
|
||||
'status' => $item->status,
|
||||
'status_display' => $item->status . ($isProcessingTimeout ? ' (Timeout)' : ''),
|
||||
'processing_hours' => $processingHours,
|
||||
'is_processing_timeout' => $isProcessingTimeout,
|
||||
'authorization_status' => $item->authorization_status,
|
||||
'is_downloaded' => $item->is_downloaded,
|
||||
'created_at' => $item->created_at->format('Y-m-d H:i:s'),
|
||||
'created_by' => $item->user->name ?? 'N/A',
|
||||
'authorized_by' => $item->authorizer ? $item->authorizer->name : null,
|
||||
'authorized_at' => $item->authorized_at ? $item->authorized_at->format('Y-m-d H:i:s') : null,
|
||||
'file_path' => $item->file_path,
|
||||
'record_count' => $item->record_count,
|
||||
'can_retry' => in_array($item->status, ['failed', 'pending']) || $isProcessingTimeout || ($item->status === 'completed' && !$item->file_path),
|
||||
];
|
||||
});
|
||||
|
||||
// Calculate the page count
|
||||
$pageCount = ceil($filteredRecords / ($request->get('size') ?: 1));
|
||||
$currentPage = $request->get('page') ?: 1;
|
||||
|
||||
Log::info('Data laporan closing balance berhasil diambil', [
|
||||
'total_records' => $totalRecords,
|
||||
'filtered_records' => $filteredRecords
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'draw' => $request->get('draw'),
|
||||
'recordsTotal' => $totalRecords,
|
||||
'recordsFiltered' => $filteredRecords,
|
||||
'pageCount' => $pageCount,
|
||||
'page' => $currentPage,
|
||||
'totalCount' => $totalRecords,
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error saat mengambil data datatables', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'error' => 'Terjadi kesalahan saat mengambil data laporan',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus permintaan laporan
|
||||
*
|
||||
* @param ClosingBalanceReportLog $closingBalanceReport
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function destroy(ClosingBalanceReportLog $closingBalanceReport)
|
||||
{
|
||||
Log::info('Menghapus laporan closing balance', [
|
||||
'report_id' => $closingBalanceReport->id
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Delete the file if exists
|
||||
if ($closingBalanceReport->file_path && Storage::exists($closingBalanceReport->file_path)) {
|
||||
Storage::delete($closingBalanceReport->file_path);
|
||||
}
|
||||
|
||||
// Delete the report request
|
||||
$closingBalanceReport->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info('Laporan closing balance berhasil dihapus', [
|
||||
'report_id' => $closingBalanceReport->id
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Laporan closing balance berhasil dihapus.',
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error('Error saat menghapus laporan', [
|
||||
'report_id' => $closingBalanceReport->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'error' => 'Terjadi kesalahan saat menghapus laporan',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry generating laporan closing balance
|
||||
*
|
||||
* @param ClosingBalanceReportLog $closingBalanceReport
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function retry(ClosingBalanceReportLog $closingBalanceReport)
|
||||
{
|
||||
Log::info('Retry laporan closing balance', [
|
||||
'report_id' => $closingBalanceReport->id
|
||||
]);
|
||||
|
||||
try {
|
||||
// Check if retry is allowed
|
||||
$allowedStatuses = ['failed', 'pending'];
|
||||
$isProcessingTooLong = $closingBalanceReport->status === 'processing' &&
|
||||
$closingBalanceReport->updated_at->diffInHours(now()) >= 1;
|
||||
|
||||
if (!in_array($closingBalanceReport->status, $allowedStatuses) && !$isProcessingTooLong) {
|
||||
return back()->with('error', 'Laporan hanya dapat diulang jika status failed, pending, atau processing lebih dari 1 jam.');
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// If it was processing for too long, mark it as failed first
|
||||
if ($isProcessingTooLong) {
|
||||
$closingBalanceReport->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => 'Processing timeout - melebihi batas waktu 1 jam',
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
}
|
||||
|
||||
// Reset the report status and clear previous data
|
||||
$closingBalanceReport->update([
|
||||
'status' => 'processing',
|
||||
'error_message' => null,
|
||||
'file_path' => null,
|
||||
'file_size' => null,
|
||||
'record_count' => null,
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
// Dispatch the job again
|
||||
GenerateClosingBalanceReportJob::dispatch(
|
||||
$closingBalanceReport->account_number,
|
||||
$closingBalanceReport->period,
|
||||
$closingBalanceReport->id
|
||||
);
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info('Laporan closing balance berhasil diulang', [
|
||||
'report_id' => $closingBalanceReport->id
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Job laporan closing balance berhasil diulang.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error('Error saat retry laporan', [
|
||||
'report_id' => $closingBalanceReport->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
$closingBalanceReport->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Gagal mengulang generate laporan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download laporan berdasarkan nomor rekening dan periode
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $period
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function download($accountNumber, $period)
|
||||
{
|
||||
Log::info('Download laporan closing balance', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'user_id' => Auth::id()
|
||||
]);
|
||||
|
||||
try {
|
||||
// Cari laporan berdasarkan account number dan period
|
||||
$closingBalanceReport = ClosingBalanceReportLog::where('account_number', $accountNumber)
|
||||
->where('period', $period)
|
||||
->where('status', 'completed')
|
||||
->whereNotNull('file_path')
|
||||
->first();
|
||||
|
||||
if (!$closingBalanceReport) {
|
||||
Log::warning('Laporan tidak ditemukan atau belum selesai', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period
|
||||
]);
|
||||
return back()->with('error', 'Laporan tidak ditemukan atau belum selesai diproses.');
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Update download status
|
||||
$closingBalanceReport->update([
|
||||
'is_downloaded' => true,
|
||||
'downloaded_at' => now(),
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Download the file
|
||||
$filePath = $closingBalanceReport->file_path;
|
||||
if (Storage::exists($filePath)) {
|
||||
$fileName = "closing_balance_report_{$accountNumber}_{$period}.csv";
|
||||
|
||||
Log::info('File laporan berhasil didownload', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'file_path' => $filePath
|
||||
]);
|
||||
|
||||
return Storage::download($filePath, $fileName);
|
||||
}
|
||||
|
||||
Log::error('File laporan tidak ditemukan di storage', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'file_path' => $filePath
|
||||
]);
|
||||
|
||||
return back()->with('error', 'File laporan tidak ditemukan.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error('Error saat download laporan', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Terjadi kesalahan saat mengunduh laporan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@
|
||||
ProcessTellerDataJob,
|
||||
ProcessTransactionDataJob,
|
||||
ProcessSectorDataJob,
|
||||
ProcessProvinceDataJob};
|
||||
ProcessProvinceDataJob,
|
||||
ProcessStmtEntryDetailDataJob};
|
||||
|
||||
class MigrasiController extends Controller
|
||||
{
|
||||
@@ -38,6 +39,7 @@
|
||||
'customer' => ProcessCustomerDataJob::class,
|
||||
'account' => ProcessAccountDataJob::class,
|
||||
'stmtEntry' => ProcessStmtEntryDataJob::class,
|
||||
'stmtEntryDetail' => ProcessStmtEntryDetailDataJob::class, // Tambahan baru
|
||||
'dataCapture' => ProcessDataCaptureDataJob::class,
|
||||
'fundsTransfer' => ProcessFundsTransferDataJob::class,
|
||||
'teller' => ProcessTellerDataJob::class,
|
||||
@@ -63,6 +65,7 @@
|
||||
'customer',
|
||||
'account',
|
||||
'stmtEntry',
|
||||
'stmtEntryDetail', // Tambahan baru
|
||||
'dataCapture',
|
||||
'fundsTransfer',
|
||||
'teller',
|
||||
@@ -98,30 +101,99 @@
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
public function index($processParameter = false)
|
||||
/**
|
||||
* Proses migrasi data dengan parameter dan periode yang dapat dikustomisasi
|
||||
*
|
||||
* @param bool|string $processParameter Flag untuk memproses parameter
|
||||
* @param string|null $period Periode yang akan diproses (default: -1 day)
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function index($processParameter = false, $period = null)
|
||||
{
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
try {
|
||||
Log::info('Starting migration process', [
|
||||
'process_parameter' => $processParameter,
|
||||
'period' => $period
|
||||
]);
|
||||
|
||||
if ($processParameter) {
|
||||
foreach (self::PARAMETER_PROCESSES as $process) {
|
||||
$this->processData($process, '_parameter');
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
|
||||
if ($processParameter) {
|
||||
Log::info('Processing parameter data');
|
||||
|
||||
foreach (self::PARAMETER_PROCESSES as $process) {
|
||||
$this->processData($process, '_parameter');
|
||||
}
|
||||
|
||||
Log::info('Parameter processes completed successfully');
|
||||
return response()->json(['message' => 'Parameter processes completed successfully']);
|
||||
}
|
||||
return response()->json(['message' => 'Parameter processes completed successfully']);
|
||||
}
|
||||
|
||||
$period = date('Ymd', strtotime('-1 day'));
|
||||
if (!$disk->exists($period)) {
|
||||
// Tentukan periode yang akan diproses
|
||||
$targetPeriod = $this->determinePeriod($period);
|
||||
|
||||
Log::info('Processing data for period', ['period' => $targetPeriod]);
|
||||
|
||||
if (!$disk->exists($targetPeriod)) {
|
||||
$errorMessage = "Period {$targetPeriod} folder not found in SFTP storage";
|
||||
Log::warning($errorMessage);
|
||||
|
||||
return response()->json([
|
||||
"message" => $errorMessage
|
||||
], 404);
|
||||
}
|
||||
|
||||
foreach (self::DATA_PROCESSES as $process) {
|
||||
$this->processData($process, $targetPeriod);
|
||||
}
|
||||
|
||||
$successMessage = "Data processing for period {$targetPeriod} has been queued successfully";
|
||||
Log::info($successMessage);
|
||||
|
||||
return response()->json([
|
||||
"message" => "Period {$period} folder not found in SFTP storage"
|
||||
], 404);
|
||||
'message' => $successMessage
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in migration index method: ' . $e->getMessage());
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan periode berdasarkan input atau gunakan default
|
||||
*
|
||||
* @param string|null $period Input periode
|
||||
* @return string Periode dalam format Ymd
|
||||
*/
|
||||
private function determinePeriod($period = null): string
|
||||
{
|
||||
if ($period === null) {
|
||||
// Default: -1 day
|
||||
$calculatedPeriod = date('Ymd', strtotime('-1 day'));
|
||||
Log::info('Using default period', ['period' => $calculatedPeriod]);
|
||||
return $calculatedPeriod;
|
||||
}
|
||||
|
||||
foreach (self::DATA_PROCESSES as $process) {
|
||||
$this->processData($process, $period);
|
||||
// Jika periode sudah dalam format Ymd (8 digit)
|
||||
if (preg_match('/^\d{8}$/', $period)) {
|
||||
Log::info('Using provided period in Ymd format', ['period' => $period]);
|
||||
return $period;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => "Data processing for period {$period} has been queued successfully"
|
||||
]);
|
||||
// Jika periode dalam format relative date (contoh: -2 days, -1 week, etc.)
|
||||
try {
|
||||
$calculatedPeriod = date('Ymd', strtotime($period));
|
||||
Log::info('Calculated period from relative date', [
|
||||
'input' => $period,
|
||||
'calculated' => $calculatedPeriod
|
||||
]);
|
||||
return $calculatedPeriod;
|
||||
} catch (Exception $e) {
|
||||
Log::warning('Invalid period format, using default', [
|
||||
'input' => $period,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return date('Ymd', strtotime('-1 day'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,12 @@
|
||||
],
|
||||
'SWADAYA_PANDU' => [
|
||||
'0081272689',
|
||||
],
|
||||
"AWAN_LINTANG_SOLUSI"=> [
|
||||
"1084269430"
|
||||
],
|
||||
"MONETA"=> [
|
||||
"1085667890"
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -81,10 +81,10 @@
|
||||
$existingDataCount = $this->getExistingProcessedCount($accountQuery);
|
||||
|
||||
// Hanya proses jika data belum lengkap diproses
|
||||
if ($existingDataCount !== $totalCount) {
|
||||
//if ($existingDataCount !== $totalCount) {
|
||||
$this->deleteExistingProcessedData($accountQuery);
|
||||
$this->processAndSaveStatementEntries($totalCount);
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
private function getTotalEntryCount(array $criteria)
|
||||
@@ -156,7 +156,7 @@
|
||||
'description' => $this->generateNarrative($item),
|
||||
'end_balance' => $runningBalance,
|
||||
'actual_date' => $actualDate,
|
||||
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
||||
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
@@ -357,8 +357,7 @@
|
||||
/**
|
||||
* Export processed data to CSV file
|
||||
*/
|
||||
private function exportToCsv()
|
||||
: void
|
||||
private function exportToCsv(): void
|
||||
{
|
||||
// Determine the base path based on client
|
||||
$basePath = !empty($this->client)
|
||||
@@ -367,36 +366,11 @@
|
||||
|
||||
$accountPath = "{$basePath}/{$this->account_number}";
|
||||
|
||||
// Create client directory if it doesn't exist
|
||||
if (!empty($this->client)) {
|
||||
// Di fungsi exportToCsv untuk basePath
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
if ($this->disk === 'local') {
|
||||
$fullPath = storage_path("app/{$basePath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown(dirname($fullPath), 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp(dirname($fullPath), 'www-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
// PERBAIKAN: Selalu pastikan direktori dibuat
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||
|
||||
|
||||
// Untuk accountPath
|
||||
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
if ($this->disk === 'local') {
|
||||
$fullPath = storage_path("app/{$accountPath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown(dirname($fullPath), 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp(dirname($fullPath), 'www-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filePath = "{$accountPath}/{$this->fileName}";
|
||||
|
||||
@@ -405,13 +379,38 @@
|
||||
Storage::disk($this->disk)->delete($filePath);
|
||||
}
|
||||
|
||||
$csvContent = "NO|TRANSACTION.DATE|REFERENCE.NUMBER|TRANSACTION.AMOUNT|TRANSACTION.TYPE|DESCRIPTION|END.BALANCE|ACTUAL.DATE|NO.RECEIPT\n";
|
||||
|
||||
// Ambil data yang sudah diproses dalam chunk untuk mengurangi penggunaan memori
|
||||
// Tambahkan di awal fungsi exportToCsv
|
||||
Log::info("Starting CSV export", [
|
||||
'disk' => $this->disk,
|
||||
'client' => $this->client,
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period,
|
||||
'base_path' => $basePath,
|
||||
'account_path' => $accountPath,
|
||||
'file_path' => $filePath
|
||||
]);
|
||||
|
||||
// Cek apakah disk storage berfungsi
|
||||
$testFile = 'test_' . time() . '.txt';
|
||||
Storage::disk($this->disk)->put($testFile, 'test content');
|
||||
if (Storage::disk($this->disk)->exists($testFile)) {
|
||||
Log::info("Storage disk is working");
|
||||
Storage::disk($this->disk)->delete($testFile);
|
||||
} else {
|
||||
Log::error("Storage disk is not working properly");
|
||||
}
|
||||
|
||||
// PERBAIKAN: Buat file header terlebih dahulu
|
||||
$csvContent = "NO|TRANSACTION.DATE|REFERENCE.NUMBER|TRANSACTION.AMOUNT|TRANSACTION.TYPE|DESCRIPTION|END.BALANCE|ACTUAL.DATE|NO.RECEIPT\n";
|
||||
Storage::disk($this->disk)->put($filePath, $csvContent);
|
||||
|
||||
// Ambil data yang sudah diproses dalam chunk
|
||||
ProcessedStatement::where('account_number', $this->account_number)
|
||||
->where('period', $this->period)
|
||||
->orderBy('sequence_no')
|
||||
->chunk($this->chunkSize, function ($statements) use (&$csvContent, $filePath) {
|
||||
->chunk($this->chunkSize, function ($statements) use ($filePath) {
|
||||
$csvContent = '';
|
||||
foreach ($statements as $statement) {
|
||||
$csvContent .= implode('|', [
|
||||
$statement->sequence_no,
|
||||
@@ -426,12 +425,31 @@
|
||||
]) . "\n";
|
||||
}
|
||||
|
||||
// Tulis ke file secara bertahap untuk mengurangi penggunaan memori
|
||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||
$csvContent = ''; // Reset content setelah ditulis
|
||||
// Append ke file
|
||||
if (!empty($csvContent)) {
|
||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||
}
|
||||
});
|
||||
|
||||
Log::info("Statement exported to {$this->disk} disk: {$filePath}");
|
||||
// PERBAIKAN: Verifikasi file benar-benar ada
|
||||
if (Storage::disk($this->disk)->exists($filePath)) {
|
||||
$fileSize = Storage::disk($this->disk)->size($filePath);
|
||||
Log::info("Statement exported successfully", [
|
||||
'disk' => $this->disk,
|
||||
'file_path' => $filePath,
|
||||
'file_size' => $fileSize,
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period
|
||||
]);
|
||||
} else {
|
||||
Log::error("File was not created despite successful processing", [
|
||||
'disk' => $this->disk,
|
||||
'file_path' => $filePath,
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period
|
||||
]);
|
||||
throw new \Exception("Failed to create CSV file: {$filePath}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -201,25 +201,27 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
$processedData = [];
|
||||
|
||||
foreach ($entries as $item) {
|
||||
$globalSequence++;
|
||||
$runningBalance += (float) $item->amount_lcy;
|
||||
$globalSequence++;
|
||||
$runningBalance += (float) $item->amount_lcy;
|
||||
|
||||
$actualDate = $this->formatActualDate($item);
|
||||
$transactionDate = $this->formatTransactionDate($item);
|
||||
$actualDate = $this->formatActualDate($item);
|
||||
|
||||
$processedData[] = [
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period,
|
||||
'sequence_no' => $globalSequence,
|
||||
'transaction_date' => $item->booking_date,
|
||||
'reference_number' => $item->trans_reference,
|
||||
'transaction_amount' => $item->amount_lcy,
|
||||
'transaction_type' => $item->amount_lcy < 0 ? 'D' : 'C',
|
||||
'description' => $this->generateNarrative($item),
|
||||
'end_balance' => $runningBalance,
|
||||
'actual_date' => $actualDate,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
$processedData[] = [
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period,
|
||||
'sequence_no' => $globalSequence,
|
||||
'transaction_date' => $item->booking_date,
|
||||
'reference_number' => $item->trans_reference,
|
||||
'transaction_amount' => $item->amount_lcy,
|
||||
'transaction_type' => $item->amount_lcy < 0 ? 'D' : 'C',
|
||||
'description' => $this->generateNarrative($item),
|
||||
'end_balance' => $runningBalance,
|
||||
'actual_date' => $actualDate,
|
||||
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
return $processedData;
|
||||
@@ -480,27 +482,10 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
// Buat direktori temp jika belum ada
|
||||
if (!is_dir(dirname($tempPath))) {
|
||||
mkdir(dirname($tempPath), 0777, true);
|
||||
// Tambahkan pengecekan function dan error handling
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown(dirname($tempPath), 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp(dirname($tempPath), 'www-data');
|
||||
}
|
||||
}
|
||||
|
||||
// Pastikan direktori storage ada
|
||||
Storage::makeDirectory($storagePath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
if ($this->disk === 'local') {
|
||||
$fullPath = storage_path("app/{$storagePath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
// Tambahkan pengecekan function dan error handling
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown($fullPath, 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp($fullPath, 'www-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$period = $this->period;
|
||||
|
||||
@@ -629,17 +614,7 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
|
||||
$storagePath = "statements/{$this->period}/{$account->branch_code}";
|
||||
Storage::disk($this->disk)->makeDirectory($storagePath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
if ($this->disk === 'local') {
|
||||
$fullPath = storage_path("app/{$storagePath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown(dirname($fullPath), 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp(dirname($fullPath), 'www-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filePath = "{$storagePath}/{$this->fileName}";
|
||||
|
||||
// Delete existing file if it exists
|
||||
@@ -669,7 +644,6 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
|
||||
// Write to file incrementally to reduce memory usage
|
||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||
$csvContent = ''; // Reset content after writing
|
||||
});
|
||||
|
||||
Log::info("Statement exported to {$this->disk} disk: {$filePath}");
|
||||
|
||||
@@ -191,11 +191,11 @@
|
||||
$subQuery->where('product_code', '!=', '6021')
|
||||
->orWhere(function($nestedQuery) {
|
||||
$nestedQuery->where('product_code', '6021')
|
||||
->where('ctdesc', '!=', 'gold');
|
||||
->where('ctdesc', '!=', 'GOLD');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
$cards = $query->get();
|
||||
|
||||
|
||||
606
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
606
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
@@ -0,0 +1,606 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Modules\Webstatement\Models\AccountBalance;
|
||||
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
||||
use Modules\Webstatement\Models\StmtEntry;
|
||||
use Modules\Webstatement\Models\StmtEntryDetail;
|
||||
use Modules\Webstatement\Models\TempFundsTransfer;
|
||||
use Modules\Webstatement\Models\DataCapture;
|
||||
|
||||
/**
|
||||
* Job untuk generate laporan closing balance
|
||||
* Mengambil data transaksi dan menghitung closing balance
|
||||
*/
|
||||
class GenerateClosingBalanceReportJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $accountNumber;
|
||||
protected $period;
|
||||
protected $reportLogId;
|
||||
protected $groupName;
|
||||
protected $chunkSize = 1000;
|
||||
protected $disk = 'local';
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $period
|
||||
* @param int $reportLogId
|
||||
*/
|
||||
public function __construct(string $accountNumber, string $period, int $reportLogId, string $groupName='DEFAULT')
|
||||
{
|
||||
$this->accountNumber = $accountNumber;
|
||||
$this->period = $period;
|
||||
$this->reportLogId = $reportLogId;
|
||||
$this->groupName = $groupName ?? 'DEFAULT';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
* Memproses data transaksi dan generate laporan closing balance
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$reportLog = ClosingBalanceReportLog::find($this->reportLogId);
|
||||
|
||||
if (!$reportLog) {
|
||||
Log::error('Closing balance report log not found', ['id' => $this->reportLogId]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Log::info('Starting closing balance report generation', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'report_log_id' => $this->reportLogId
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Update status to processing
|
||||
$reportLog->update([
|
||||
'status' => 'processing',
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
// Get opening balance
|
||||
$openingBalance = $this->getOpeningBalance();
|
||||
|
||||
// Generate report data
|
||||
$reportData = $this->generateReportData($openingBalance);
|
||||
|
||||
// Export to CSV
|
||||
$filePath = $this->exportToCsv($reportData);
|
||||
|
||||
// Update report log with success
|
||||
$reportLog->update([
|
||||
'status' => 'completed',
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk($this->disk)->size($filePath),
|
||||
'record_count' => count($reportData),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info('Closing balance report generation completed successfully', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'file_path' => $filePath,
|
||||
'record_count' => count($reportData)
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error('Error generating closing balance report', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
$reportLog->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get opening balance from account balance table
|
||||
* Mengambil saldo awal dari tabel account balance
|
||||
*/
|
||||
private function getOpeningBalance(): float
|
||||
{
|
||||
Log::info('Getting opening balance', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period
|
||||
]);
|
||||
|
||||
// Get previous period based on current period
|
||||
$previousPeriod = $this->period === '20250512'
|
||||
? Carbon::createFromFormat('Ymd', $this->period)->subDays(2)->format('Ymd')
|
||||
: Carbon::createFromFormat('Ymd', $this->period)->subDay()->format('Ymd');
|
||||
|
||||
$accountBalance = AccountBalance::where('account_number', $this->accountNumber)
|
||||
->where('period', $previousPeriod)
|
||||
->first();
|
||||
|
||||
if (!$accountBalance) {
|
||||
Log::warning('Account balance not found, using 0 as opening balance', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period
|
||||
]);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$openingBalance = (float) $accountBalance->actual_balance;
|
||||
|
||||
Log::info('Opening balance retrieved', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'opening_balance' => $openingBalance
|
||||
]);
|
||||
|
||||
return $openingBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build transaction query using pure Eloquent relationships
|
||||
* Membangun query transaksi menggunakan relasi Eloquent murni
|
||||
*/
|
||||
private function buildTransactionQuery()
|
||||
{
|
||||
Log::info('Building transaction query using pure Eloquent relationships', [
|
||||
'group_name' => $this->groupName,
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period
|
||||
]);
|
||||
|
||||
// Tentukan model berdasarkan group name
|
||||
$modelClass = $this->getModelByGroup();
|
||||
|
||||
// Build query menggunakan pure Eloquent dengan eager loading
|
||||
$query = $modelClass::with([
|
||||
'ft' => function($query) {
|
||||
$query->select([
|
||||
'_id',
|
||||
'ref_no',
|
||||
'debit_acct_no',
|
||||
'debit_value_date',
|
||||
'credit_acct_no',
|
||||
'bif_rcv_acct',
|
||||
'bif_rcv_name',
|
||||
'credit_value_date',
|
||||
'at_unique_id',
|
||||
'bif_ref_no',
|
||||
'atm_order_id',
|
||||
'recipt_no',
|
||||
'api_iss_acct',
|
||||
'api_benff_acct',
|
||||
'authoriser',
|
||||
'remarks',
|
||||
'payment_details',
|
||||
'merchant_id',
|
||||
'term_id',
|
||||
'date_time'
|
||||
]);
|
||||
},
|
||||
'dc' => function($query) {
|
||||
$query->select([
|
||||
'id',
|
||||
'date_time'
|
||||
]);
|
||||
}
|
||||
])
|
||||
->select([
|
||||
'id',
|
||||
'trans_reference',
|
||||
'booking_date',
|
||||
'amount_lcy',
|
||||
'date_time'
|
||||
])
|
||||
->where('account_number', $this->accountNumber)
|
||||
->where('booking_date', $this->period)
|
||||
->orderBy('booking_date')
|
||||
->orderBy('date_time');
|
||||
|
||||
Log::info('Transaction query built successfully using pure Eloquent', [
|
||||
'model_class' => $modelClass,
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period
|
||||
]);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model class based on group name
|
||||
* Mendapatkan class model berdasarkan group name
|
||||
*/
|
||||
private function getModelByGroup()
|
||||
{
|
||||
Log::info('Determining model by group', [
|
||||
'group_name' => $this->groupName
|
||||
]);
|
||||
|
||||
$model = $this->groupName === 'QRIS' ? StmtEntryDetail::class : StmtEntry::class;
|
||||
|
||||
Log::info('Model determined', [
|
||||
'group_name' => $this->groupName,
|
||||
'model_class' => $model
|
||||
]);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process transaction data from ORM result
|
||||
* Memproses data transaksi dari hasil ORM
|
||||
*/
|
||||
private function processTransactionData($transaction): array
|
||||
{
|
||||
Log::info('Processing transaction data', [
|
||||
'trans_reference' => $transaction->trans_reference,
|
||||
'has_ft_relation' => !is_null($transaction->ft),
|
||||
'has_dc_relation' => !is_null($transaction->dc)
|
||||
]);
|
||||
|
||||
// Hitung debit dan credit amount
|
||||
$debitAmount = $transaction->amount_lcy < 0 ? abs($transaction->amount_lcy) : null;
|
||||
$creditAmount = $transaction->amount_lcy > 0 ? $transaction->amount_lcy : null;
|
||||
|
||||
// Ambil date_time dari prioritas: ft -> dc -> stmt
|
||||
$dateTime = $transaction->ft?->date_time ??
|
||||
$transaction->dc?->date_time ??
|
||||
$transaction->date_time;
|
||||
|
||||
$processedData = [
|
||||
'trans_reference' => $transaction->trans_reference,
|
||||
'booking_date' => $transaction->booking_date,
|
||||
'amount_lcy' => $transaction->amount_lcy,
|
||||
'debit_amount' => $debitAmount,
|
||||
'credit_amount' => $creditAmount,
|
||||
'date_time' => $dateTime,
|
||||
// Data dari TempFundsTransfer melalui relasi
|
||||
'debit_acct_no' => $transaction->ft?->debit_acct_no,
|
||||
'debit_value_date' => $transaction->ft?->debit_value_date,
|
||||
'credit_acct_no' => $transaction->ft?->credit_acct_no,
|
||||
'bif_rcv_acct' => $transaction->ft?->bif_rcv_acct,
|
||||
'bif_rcv_name' => $transaction->ft?->bif_rcv_name,
|
||||
'credit_value_date' => $transaction->ft?->credit_value_date,
|
||||
'at_unique_id' => $transaction->ft?->at_unique_id,
|
||||
'bif_ref_no' => $transaction->ft?->bif_ref_no,
|
||||
'atm_order_id' => $transaction->ft?->atm_order_id,
|
||||
'recipt_no' => $transaction->ft?->recipt_no,
|
||||
'api_iss_acct' => $transaction->ft?->api_iss_acct,
|
||||
'api_benff_acct' => $transaction->ft?->api_benff_acct,
|
||||
'authoriser' => $transaction->ft?->authoriser,
|
||||
'remarks' => $transaction->ft?->remarks,
|
||||
'payment_details' => $transaction->ft?->payment_details,
|
||||
'ref_no' => $transaction->ft?->ref_no,
|
||||
'merchant_id' => $transaction->ft?->merchant_id,
|
||||
'term_id' => $transaction->ft?->term_id,
|
||||
];
|
||||
|
||||
Log::info('Transaction data processed successfully', [
|
||||
'trans_reference' => $transaction->trans_reference,
|
||||
'final_date_time' => $dateTime,
|
||||
'debit_amount' => $debitAmount,
|
||||
'credit_amount' => $creditAmount
|
||||
]);
|
||||
|
||||
return $processedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated generateReportData method using pure ORM
|
||||
* Method generateReportData yang diperbarui menggunakan ORM murni
|
||||
*/
|
||||
private function generateReportData(): array
|
||||
{
|
||||
Log::info('Starting report data generation using pure ORM', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'group_name' => $this->groupName,
|
||||
'chunk_size' => $this->chunkSize
|
||||
]);
|
||||
|
||||
$reportData = [];
|
||||
$runningBalance = $this->getOpeningBalance();
|
||||
$sequenceNo = 1;
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Build query menggunakan pure ORM
|
||||
$query = $this->buildTransactionQuery();
|
||||
|
||||
// Process data dalam chunks untuk efisiensi memory
|
||||
$query->chunk($this->chunkSize, function ($transactions) use (&$reportData, &$runningBalance, &$sequenceNo) {
|
||||
Log::info('Processing transaction chunk', [
|
||||
'chunk_size' => $transactions->count(),
|
||||
'current_sequence' => $sequenceNo,
|
||||
'current_balance' => $runningBalance
|
||||
]);
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
// Process transaction data
|
||||
$processedData = $this->processTransactionData($transaction);
|
||||
|
||||
// Update running balance
|
||||
$amount = (float) $transaction->amount_lcy;
|
||||
$runningBalance += $amount;
|
||||
|
||||
// Format transaction date
|
||||
$transactionDate = $this->formatDateTime($processedData['date_time']);
|
||||
|
||||
// Build report data row
|
||||
$reportData[] = $this->buildReportDataRow(
|
||||
(object) $processedData,
|
||||
$sequenceNo,
|
||||
$transactionDate,
|
||||
$runningBalance
|
||||
);
|
||||
|
||||
$sequenceNo++;
|
||||
}
|
||||
|
||||
Log::info('Chunk processed successfully', [
|
||||
'processed_count' => $transactions->count(),
|
||||
'total_records_so_far' => count($reportData),
|
||||
'current_balance' => $runningBalance
|
||||
]);
|
||||
});
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info('Report data generation completed using pure ORM', [
|
||||
'total_records' => count($reportData),
|
||||
'final_balance' => $runningBalance,
|
||||
'final_sequence' => $sequenceNo - 1
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error('Error generating report data using pure ORM', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $reportData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table name based on group name
|
||||
* Mendapatkan nama tabel berdasarkan group name
|
||||
*/
|
||||
private function getTableNameByGroup(): string
|
||||
{
|
||||
return $this->groupName === 'QRIS' ? 'stmt_entry' : 'stmt_entry_details';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get select fields for the query
|
||||
* Mendapatkan field select untuk query
|
||||
*/
|
||||
private function getSelectFields(): array
|
||||
{
|
||||
return [
|
||||
's.trans_reference',
|
||||
's.booking_date',
|
||||
's.amount_lcy',
|
||||
'ft.debit_acct_no',
|
||||
'ft.debit_value_date',
|
||||
DB::raw('CASE WHEN s.amount_lcy::numeric < 0 THEN s.amount_lcy::numeric ELSE NULL END AS debit_amount'),
|
||||
'ft.credit_acct_no',
|
||||
'ft.bif_rcv_acct',
|
||||
'ft.bif_rcv_name',
|
||||
'ft.credit_value_date',
|
||||
DB::raw('CASE WHEN s.amount_lcy::numeric > 0 THEN s.amount_lcy::numeric ELSE NULL END AS credit_amount'),
|
||||
'ft.at_unique_id',
|
||||
'ft.bif_ref_no',
|
||||
'ft.atm_order_id',
|
||||
'ft.recipt_no',
|
||||
'ft.api_iss_acct',
|
||||
'ft.api_benff_acct',
|
||||
DB::raw('COALESCE(ft.date_time, dc.date_time, s.date_time) AS date_time'),
|
||||
'ft.authoriser',
|
||||
'ft.remarks',
|
||||
'ft.payment_details',
|
||||
'ft.ref_no',
|
||||
'ft.merchant_id',
|
||||
'ft.term_id'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build report data row from transaction
|
||||
* Membangun baris data laporan dari transaksi
|
||||
*/
|
||||
private function buildReportDataRow($transaction, int $sequenceNo, string $transactionDate, float $runningBalance): array
|
||||
{
|
||||
return [
|
||||
'sequence_no' => $sequenceNo,
|
||||
'trans_reference' => $transaction->trans_reference,
|
||||
'booking_date' => $transaction->booking_date,
|
||||
'transaction_date' => $transactionDate,
|
||||
'amount_lcy' => $transaction->amount_lcy,
|
||||
'debit_acct_no' => $transaction->debit_acct_no,
|
||||
'debit_value_date' => $transaction->debit_value_date,
|
||||
'debit_amount' => $transaction->debit_amount,
|
||||
'credit_acct_no' => $transaction->credit_acct_no,
|
||||
'bif_rcv_acct' => $transaction->bif_rcv_acct,
|
||||
'bif_rcv_name' => $transaction->bif_rcv_name,
|
||||
'credit_value_date' => $transaction->credit_value_date,
|
||||
'credit_amount' => $transaction->credit_amount,
|
||||
'at_unique_id' => $transaction->at_unique_id,
|
||||
'bif_ref_no' => $transaction->bif_ref_no,
|
||||
'atm_order_id' => $transaction->atm_order_id,
|
||||
'recipt_no' => $transaction->recipt_no,
|
||||
'api_iss_acct' => $transaction->api_iss_acct,
|
||||
'api_benff_acct' => $transaction->api_benff_acct,
|
||||
'authoriser' => $transaction->authoriser,
|
||||
'remarks' => $transaction->remarks,
|
||||
'payment_details' => $transaction->payment_details,
|
||||
'ref_no' => $transaction->ref_no,
|
||||
'merchant_id' => $transaction->merchant_id,
|
||||
'term_id' => $transaction->term_id,
|
||||
'closing_balance' => $runningBalance
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format datetime string
|
||||
* Memformat string datetime
|
||||
*/
|
||||
private function formatDateTime(?string $datetime): string
|
||||
{
|
||||
if (!$datetime) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::createFromFormat('ymdHi', $datetime)->format('d/m/Y H:i');
|
||||
} catch (Exception $e) {
|
||||
Log::warning('Error formatting datetime', [
|
||||
'datetime' => $datetime,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return $datetime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export report data to CSV file
|
||||
* Export data laporan ke file CSV
|
||||
*/
|
||||
private function exportToCsv(array $reportData): string
|
||||
{
|
||||
Log::info('Starting CSV export for closing balance report', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'record_count' => count($reportData)
|
||||
]);
|
||||
|
||||
// Create directory structure
|
||||
$basePath = "closing_balance_reports";
|
||||
$accountPath = "{$basePath}/{$this->accountNumber}";
|
||||
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||
|
||||
// Generate filename
|
||||
$fileName = "closing_balance_{$this->accountNumber}_{$this->period}.csv";
|
||||
$filePath = "{$accountPath}/{$fileName}";
|
||||
|
||||
// Delete existing file if exists
|
||||
if (Storage::disk($this->disk)->exists($filePath)) {
|
||||
Storage::disk($this->disk)->delete($filePath);
|
||||
}
|
||||
|
||||
// Create CSV header
|
||||
$csvHeader = [
|
||||
'NO',
|
||||
'TRANS_REFERENCE',
|
||||
'BOOKING_DATE',
|
||||
'TRANSACTION_DATE',
|
||||
'AMOUNT_LCY',
|
||||
'DEBIT_ACCT_NO',
|
||||
'DEBIT_VALUE_DATE',
|
||||
'DEBIT_AMOUNT',
|
||||
'CREDIT_ACCT_NO',
|
||||
'BIF_RCV_ACCT',
|
||||
'BIF_RCV_NAME',
|
||||
'CREDIT_VALUE_DATE',
|
||||
'CREDIT_AMOUNT',
|
||||
'AT_UNIQUE_ID',
|
||||
'BIF_REF_NO',
|
||||
'ATM_ORDER_ID',
|
||||
'RECIPT_NO',
|
||||
'API_ISS_ACCT',
|
||||
'API_BENFF_ACCT',
|
||||
'AUTHORISER',
|
||||
'REMARKS',
|
||||
'PAYMENT_DETAILS',
|
||||
'REF_NO',
|
||||
'MERCHANT_ID',
|
||||
'TERM_ID',
|
||||
'CLOSING_BALANCE'
|
||||
];
|
||||
|
||||
$csvContent = implode('|', $csvHeader) . "\n";
|
||||
|
||||
// Add data rows
|
||||
foreach ($reportData as $row) {
|
||||
$csvRow = [
|
||||
$row['sequence_no'],
|
||||
$row['trans_reference'] ?? '',
|
||||
$row['booking_date'] ?? '',
|
||||
$row['transaction_date'] ?? '',
|
||||
$row['amount_lcy'] ?? '',
|
||||
$row['debit_acct_no'] ?? '',
|
||||
$row['debit_value_date'] ?? '',
|
||||
$row['debit_amount'] ?? '',
|
||||
$row['credit_acct_no'] ?? '',
|
||||
$row['bif_rcv_acct'] ?? '',
|
||||
$row['bif_rcv_name'] ?? '',
|
||||
$row['credit_value_date'] ?? '',
|
||||
$row['credit_amount'] ?? '',
|
||||
$row['at_unique_id'] ?? '',
|
||||
$row['bif_ref_no'] ?? '',
|
||||
$row['atm_order_id'] ?? '',
|
||||
$row['recipt_no'] ?? '',
|
||||
$row['api_iss_acct'] ?? '',
|
||||
$row['api_benff_acct'] ?? '',
|
||||
$row['authoriser'] ?? '',
|
||||
$row['remarks'] ?? '',
|
||||
$row['payment_details'] ?? '',
|
||||
$row['ref_no'] ?? '',
|
||||
$row['merchant_id'] ?? '',
|
||||
$row['term_id'] ?? '',
|
||||
$row['closing_balance'] ?? ''
|
||||
];
|
||||
|
||||
$csvContent .= implode('|', $csvRow) . "\n";
|
||||
}
|
||||
|
||||
// Save file
|
||||
Storage::disk($this->disk)->put($filePath, $csvContent);
|
||||
|
||||
// Verify file creation
|
||||
if (!Storage::disk($this->disk)->exists($filePath)) {
|
||||
throw new Exception("Failed to create CSV file: {$filePath}");
|
||||
}
|
||||
|
||||
Log::info('CSV export completed successfully', [
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk($this->disk)->size($filePath)
|
||||
]);
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
}
|
||||
@@ -239,16 +239,6 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
|
||||
|
||||
// Ensure directory exists
|
||||
Storage::disk('local')->makeDirectory($storagePath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
$fullPath = storage_path("app/{$storagePath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
// Tambahkan pengecekan function dan error handling untuk chown
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown($fullPath, 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp($fullPath, 'www-data');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate PDF path
|
||||
$pdfPath = storage_path("app/{$fullStoragePath}");
|
||||
|
||||
@@ -144,6 +144,12 @@
|
||||
private function processRow(array $row, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
// Exclude the last field from CSV
|
||||
if (count($row) > 0) {
|
||||
array_pop($row);
|
||||
Log::info("Excluded last field from row $rowCount. New column count: " . count($row));
|
||||
}
|
||||
|
||||
$csvHeaders = array_keys(self::FIELD_MAP);
|
||||
|
||||
if (count($csvHeaders) !== count($row)) {
|
||||
|
||||
@@ -185,6 +185,12 @@
|
||||
private function processRow(array $row, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
// Exclude the last field from CSV
|
||||
if (count($row) > 0) {
|
||||
array_pop($row);
|
||||
Log::info("Excluded last field from row $rowCount. New column count: " . count($row));
|
||||
}
|
||||
|
||||
if (count(self::CSV_HEADERS) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count(self::CSV_HEADERS) . ", Got: " . count($row));
|
||||
|
||||
399
app/Jobs/ProcessStmtEntryDetailDataJob.php
Normal file
399
app/Jobs/ProcessStmtEntryDetailDataJob.php
Normal file
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\StmtEntryDetail;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ProcessStmtEntryDetailDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.STMT.ENTRY.DETAIL.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
private array $entryBatch = [];
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param string $period Periode data yang akan diproses
|
||||
*/
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->period = $period;
|
||||
Log::info('ProcessStmtEntryDetailDataJob initialized', ['period' => $period]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
Log::info('Memulai ProcessStmtEntryDetailDataJob', ['period' => $this->period]);
|
||||
|
||||
$this->initializeJob();
|
||||
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for statement entry detail data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
|
||||
Log::info('ProcessStmtEntryDetailDataJob selesai berhasil');
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessStmtEntryDetailDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inisialisasi job dengan pengaturan awal
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function initializeJob(): void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
$this->entryBatch = [];
|
||||
|
||||
Log::info('Job initialized', [
|
||||
'max_execution_time' => self::MAX_EXECUTION_TIME,
|
||||
'chunk_size' => self::CHUNK_SIZE
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses data untuk periode tertentu
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function processPeriod(): void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
Log::info('Memulai proses periode', ['file_path' => $filePath]);
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
|
||||
Log::info('Proses periode selesai', ['file_path' => $filePath]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validasi keberadaan file
|
||||
*
|
||||
* @param mixed $disk Storage disk instance
|
||||
* @param string $filePath Path file yang akan divalidasi
|
||||
* @return bool
|
||||
*/
|
||||
private function validateFile($disk, string $filePath): bool
|
||||
{
|
||||
Log::info("Processing statement entry detail file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::info("File validated successfully: $filePath");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buat file temporary untuk proses
|
||||
*
|
||||
* @param mixed $disk Storage disk instance
|
||||
* @param string $filePath Path file sumber
|
||||
* @param string $filename Nama file
|
||||
* @return string Path file temporary
|
||||
*/
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename): string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
Log::info('Temporary file created', ['temp_path' => $tempFilePath]);
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses file CSV
|
||||
*
|
||||
* @param string $tempFilePath Path file temporary
|
||||
* @param string $filePath Path file asli
|
||||
* @return void
|
||||
*/
|
||||
private function processFile(string $tempFilePath, string $filePath): void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new StmtEntryDetail())->getFillable();
|
||||
// Tambahkan field 'id' ke headers untuk menangani kolom tambahan di akhir CSV
|
||||
$expectedHeaders = array_merge($headers, ['id']);
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
Log::info('Memulai proses file', [
|
||||
'file_path' => $filePath,
|
||||
'headers_count' => count($headers),
|
||||
'expected_headers_count' => count($expectedHeaders)
|
||||
]);
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $expectedHeaders, $rowCount, $filePath);
|
||||
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->entryBatch) >= self::CHUNK_SIZE) {
|
||||
$this->saveBatch();
|
||||
$chunkCount++;
|
||||
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining records
|
||||
if (!empty($this->entryBatch)) {
|
||||
$this->saveBatch();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses setiap baris data dengan penanganan field id tambahan
|
||||
*
|
||||
* @param array $row Data baris
|
||||
* @param array $expectedHeaders Header kolom yang diharapkan (termasuk id)
|
||||
* @param int $rowCount Nomor baris
|
||||
* @param string $filePath Path file
|
||||
* @return void
|
||||
*/
|
||||
private function processRow(array $row, array $expectedHeaders, int $rowCount, string $filePath): void
|
||||
{
|
||||
// Validasi jumlah kolom - sekarang menggunakan expectedHeaders yang sudah include field 'id'
|
||||
if (count($expectedHeaders) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($expectedHeaders) . ", Got: " . count($row));
|
||||
$this->errorCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Kombinasikan data dengan headers
|
||||
$data = array_combine($expectedHeaders, $row);
|
||||
|
||||
// Log untuk debugging struktur data
|
||||
Log::debug('Processing row data', [
|
||||
'row_count' => $rowCount,
|
||||
'stmt_entry_id' => $data['stmt_entry_id'] ?? 'not_set',
|
||||
'id' => $data['id'] ?? 'not_set'
|
||||
]);
|
||||
|
||||
// Logika untuk menggunakan field 'id' sebagai fallback jika stmt_entry_id kosong
|
||||
$this->handleStmtEntryIdFallback($data);
|
||||
|
||||
// Hapus field 'id' dari data sebelum disimpan karena tidak ada di fillable model
|
||||
unset($data['id']);
|
||||
|
||||
$this->cleanTransReference($data);
|
||||
$this->addToBatch($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menangani logika fallback untuk stmt_entry_id menggunakan field id
|
||||
*
|
||||
* @param array $data Data yang akan diproses
|
||||
* @return void
|
||||
*/
|
||||
private function handleStmtEntryIdFallback(array &$data): void
|
||||
{
|
||||
// Jika stmt_entry_id kosong atau null, gunakan value dari field 'id'
|
||||
if (empty($data['stmt_entry_id']) || $data['stmt_entry_id'] === '' || $data['stmt_entry_id'] === null) {
|
||||
if (isset($data['id']) && !empty($data['id'])) {
|
||||
$data['stmt_entry_id'] = $data['id'];
|
||||
|
||||
Log::info('Using id as stmt_entry_id fallback', [
|
||||
'original_stmt_entry_id' => $data['stmt_entry_id'] ?? 'empty',
|
||||
'fallback_id' => $data['id']
|
||||
]);
|
||||
} else {
|
||||
Log::warning('Both stmt_entry_id and id are empty', [
|
||||
'stmt_entry_id' => $data['stmt_entry_id'] ?? 'not_set',
|
||||
'id' => $data['id'] ?? 'not_set'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tambahkan record ke batch untuk proses bulk insert
|
||||
*
|
||||
* @param array $data Data record
|
||||
* @param int $rowCount Nomor baris
|
||||
* @param string $filePath Path file
|
||||
* @return void
|
||||
*/
|
||||
private function addToBatch(array $data, int $rowCount, string $filePath): void
|
||||
{
|
||||
try {
|
||||
// Validasi bahwa stmt_entry_id tidak kosong dan bukan header
|
||||
if (isset($data['stmt_entry_id']) &&
|
||||
$data['stmt_entry_id'] !== 'stmt_entry_id' &&
|
||||
!empty($data['stmt_entry_id'])) {
|
||||
|
||||
// Add timestamp fields
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
// Add to entry batch
|
||||
$this->entryBatch[] = $data;
|
||||
$this->processedCount++;
|
||||
|
||||
Log::debug('Record added to batch', [
|
||||
'row' => $rowCount,
|
||||
'stmt_entry_id' => $data['stmt_entry_id']
|
||||
]);
|
||||
} else {
|
||||
Log::warning('Skipping row due to invalid stmt_entry_id', [
|
||||
'row' => $rowCount,
|
||||
'stmt_entry_id' => $data['stmt_entry_id'] ?? 'not_set'
|
||||
]);
|
||||
$this->errorCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Statement Entry Detail at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bersihkan trans_reference dari karakter tidak diinginkan
|
||||
*
|
||||
* @param array $data Data yang akan dibersihkan
|
||||
* @return void
|
||||
*/
|
||||
private function cleanTransReference(array &$data): void
|
||||
{
|
||||
if (isset($data['trans_reference'])) {
|
||||
// Clean trans_reference from \\BNK if present
|
||||
$data['trans_reference'] = preg_replace('/\\\\.*$/', '', $data['trans_reference']);
|
||||
Log::debug('Trans reference cleaned', ['original' => $data['trans_reference']]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan batch data ke database menggunakan updateOrCreate
|
||||
* untuk menghindari error unique constraint
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function saveBatch(): void
|
||||
{
|
||||
Log::info('Memulai proses saveBatch dengan updateOrCreate');
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
if (!empty($this->entryBatch)) {
|
||||
$totalProcessed = 0;
|
||||
|
||||
// Process each entry data directly
|
||||
foreach ($this->entryBatch as $entryData) {
|
||||
// Validasi bahwa entryData adalah array dan memiliki stmt_entry_id
|
||||
if (is_array($entryData) && isset($entryData['stmt_entry_id'])) {
|
||||
// Gunakan updateOrCreate untuk menghindari duplicate key error
|
||||
StmtEntryDetail::updateOrCreate(
|
||||
[
|
||||
'stmt_entry_id' => $entryData['stmt_entry_id']
|
||||
],
|
||||
$entryData
|
||||
);
|
||||
|
||||
$totalProcessed++;
|
||||
} else {
|
||||
Log::warning('Invalid entry data structure', ['data' => $entryData]);
|
||||
$this->errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info("Berhasil memproses {$totalProcessed} record dengan updateOrCreate");
|
||||
|
||||
// Reset entry batch after successful processing
|
||||
$this->entryBatch = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error("Error in saveBatch: " . $e->getMessage() . "\n" . $e->getTraceAsString());
|
||||
$this->errorCount += count($this->entryBatch);
|
||||
|
||||
// Reset batch even if there's an error to prevent reprocessing the same failed records
|
||||
$this->entryBatch = [];
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bersihkan file temporary
|
||||
*
|
||||
* @param string $tempFilePath Path file temporary
|
||||
* @return void
|
||||
*/
|
||||
private function cleanup(string $tempFilePath): void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
Log::info('Temporary file cleaned up', ['temp_path' => $tempFilePath]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log penyelesaian job
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function logJobCompletion(): void
|
||||
{
|
||||
Log::info("Statement Entry Detail data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
75
app/Models/ClosingBalanceReportLog.php
Normal file
75
app/Models/ClosingBalanceReportLog.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Usermanagement\Models\User;
|
||||
|
||||
/**
|
||||
* Model untuk menyimpan log permintaan laporan closing balance
|
||||
* Menyimpan informasi status, file path, dan tracking user
|
||||
*/
|
||||
class ClosingBalanceReportLog extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'account_number',
|
||||
'period',
|
||||
'report_date',
|
||||
'status',
|
||||
'authorization_status',
|
||||
'file_path',
|
||||
'file_size',
|
||||
'record_count',
|
||||
'error_message',
|
||||
'is_downloaded',
|
||||
'downloaded_at',
|
||||
'user_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'authorized_by',
|
||||
'authorized_at',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'remarks',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*/
|
||||
protected $casts = [
|
||||
'report_date' => 'date',
|
||||
'downloaded_at' => 'datetime',
|
||||
'authorized_at' => 'datetime',
|
||||
'is_downloaded' => 'boolean',
|
||||
'file_size' => 'integer',
|
||||
'record_count' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the user who created this report request.
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who created this report request.
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who authorized this report request.
|
||||
*/
|
||||
public function authorizer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'authorized_by');
|
||||
}
|
||||
}
|
||||
113
app/Models/StmtEntryDetail.php
Normal file
113
app/Models/StmtEntryDetail.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class StmtEntryDetail extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'stmt_entry_detail';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'stmt_entry_id',
|
||||
'account_number',
|
||||
'company_code',
|
||||
'amount_lcy',
|
||||
'transaction_code',
|
||||
'narrative',
|
||||
'product_category',
|
||||
'value_date',
|
||||
'amount_fcy',
|
||||
'exchange_rate',
|
||||
'trans_reference',
|
||||
'booking_date',
|
||||
'stmt_no',
|
||||
'date_time',
|
||||
'currency',
|
||||
'crf_type',
|
||||
'consol_key',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relasi ke model Account
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo(Account::class, 'account_number', 'account_number');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relasi ke model TempFundsTransfer
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function ft()
|
||||
{
|
||||
return $this->belongsTo(TempFundsTransfer::class, 'trans_reference', 'ref_no');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relasi ke model TempTransaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function transaction()
|
||||
{
|
||||
return $this->belongsTo(TempTransaction::class, 'transaction_code', 'transaction_code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relasi ke model Teller
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function tt()
|
||||
{
|
||||
return $this->belongsTo(Teller::class, 'trans_reference', 'id_teller');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relasi ke model DataCapture
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function dc()
|
||||
{
|
||||
return $this->belongsTo(DataCapture::class, 'trans_reference', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relasi ke model TempArrangement
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function aa()
|
||||
{
|
||||
return $this->belongsTo(TempArrangement::class, 'trans_reference', 'arrangement_id');
|
||||
}
|
||||
}
|
||||
@@ -6,20 +6,24 @@ use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Nwidart\Modules\Traits\PathNamespace;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Modules\Webstatement\Console\UnlockPdf;
|
||||
use Modules\Webstatement\Console\CombinePdf;
|
||||
use Modules\Webstatement\Console\ConvertHtmlToPdf;
|
||||
use Modules\Webstatement\Console\ExportDailyStatements;
|
||||
use Modules\Webstatement\Console\ProcessDailyMigration;
|
||||
use Modules\Webstatement\Console\ExportPeriodStatements;
|
||||
use Modules\Webstatement\Console\UpdateAllAtmCardsCommand;
|
||||
use Modules\Webstatement\Console\CheckEmailProgressCommand;
|
||||
use Modules\Webstatement\Console\AutoSendStatementEmailCommand;
|
||||
use Modules\Webstatement\Console\GenerateBiayakartuCommand;
|
||||
use Modules\Webstatement\Console\SendStatementEmailCommand;
|
||||
use Modules\Webstatement\Console\{
|
||||
UnlockPdf,
|
||||
CombinePdf,
|
||||
ConvertHtmlToPdf,
|
||||
ExportDailyStatements,
|
||||
ProcessDailyMigration,
|
||||
ExportPeriodStatements,
|
||||
UpdateAllAtmCardsCommand,
|
||||
CheckEmailProgressCommand,
|
||||
GenerateBiayakartuCommand,
|
||||
SendStatementEmailCommand,
|
||||
GenerateAtmTransactionReport,
|
||||
GenerateBiayaKartuCsvCommand,
|
||||
AutoSendStatementEmailCommand,
|
||||
GenerateClosingBalanceReportCommand,
|
||||
GenerateClosingBalanceReportBulkCommand,
|
||||
};
|
||||
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
|
||||
use Modules\Webstatement\Console\GenerateAtmTransactionReport;
|
||||
use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand;
|
||||
|
||||
class WebstatementServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -74,7 +78,9 @@ class WebstatementServiceProvider extends ServiceProvider
|
||||
SendStatementEmailCommand::class,
|
||||
CheckEmailProgressCommand::class,
|
||||
UpdateAllAtmCardsCommand::class,
|
||||
AutoSendStatementEmailCommand::class
|
||||
AutoSendStatementEmailCommand::class,
|
||||
GenerateClosingBalanceReportCommand::class,
|
||||
GenerateClosingBalanceReportBulkCommand::class,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('closing_balance_report_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('account_number', 50);
|
||||
$table->string('period', 8); // Format: YYYYMMDD
|
||||
$table->date('report_date');
|
||||
$table->enum('status', ['pending', 'processing', 'completed', 'failed'])->default('pending');
|
||||
$table->enum('authorization_status', ['pending', 'approved', 'rejected'])->nullable();
|
||||
$table->string('file_path')->nullable();
|
||||
$table->bigInteger('file_size')->nullable();
|
||||
$table->integer('record_count')->nullable();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->boolean('is_downloaded')->default(false);
|
||||
$table->timestamp('downloaded_at')->nullable();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->unsignedBigInteger('created_by');
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('authorized_by')->nullable();
|
||||
$table->timestamp('authorized_at')->nullable();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->text('remarks')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index(['account_number', 'period']);
|
||||
$table->index('status');
|
||||
$table->index('authorization_status');
|
||||
$table->index('created_at');
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('closing_balance_report_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateStmtEntryDetailTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('stmt_entry_detail', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('stmt_entry_id')->nullable();
|
||||
$table->string('account_number')->nullable();
|
||||
$table->string('company_code')->nullable();
|
||||
$table->string('amount_lcy')->nullable();
|
||||
$table->string('transaction_code')->nullable();
|
||||
$table->string('narrative')->nullable();
|
||||
$table->string('product_category')->nullable();
|
||||
$table->string('value_date')->nullable();
|
||||
$table->string('amount_fcy')->nullable();
|
||||
$table->string('exchange_rate')->nullable();
|
||||
$table->string('trans_reference')->nullable();
|
||||
$table->string('booking_date')->nullable();
|
||||
$table->string('stmt_no')->nullable();
|
||||
$table->string('date_time')->nullable();
|
||||
$table->string('currency')->nullable();
|
||||
$table->string('crf_type')->nullable();
|
||||
$table->string('consol_key')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Index untuk performa query
|
||||
$table->index('stmt_entry_id');
|
||||
$table->index('account_number');
|
||||
$table->index('trans_reference');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('stmt_entry_detail');
|
||||
}
|
||||
}
|
||||
10
module.json
10
module.json
@@ -77,6 +77,16 @@
|
||||
"roles": [
|
||||
"administrator"
|
||||
]
|
||||
},{
|
||||
"title": "Laporan Closing Balance",
|
||||
"path": "laporan-closing-balance",
|
||||
"icon": "ki-filled ki-printer text-lg text-primary",
|
||||
"classes": "",
|
||||
"attributes": [],
|
||||
"permission": "",
|
||||
"roles": [
|
||||
"administrator"
|
||||
]
|
||||
}
|
||||
],
|
||||
"master": [
|
||||
|
||||
303
resources/views/laporan-closing-balance/index.blade.php
Normal file
303
resources/views/laporan-closing-balance/index.blade.php
Normal file
@@ -0,0 +1,303 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@section('breadcrumbs')
|
||||
{{ Breadcrumbs::render('laporan-closing-balance.index') }}
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
|
||||
data-datatable-state-save="false" id="laporan-closing-balance-table"
|
||||
data-api-url="{{ route('laporan-closing-balance.datatables') }}">
|
||||
<div class="flex-wrap py-5 card-header">
|
||||
<h3 class="card-title">
|
||||
Laporan Closing Balance
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2 lg:gap-5">
|
||||
<!-- Filter Form -->
|
||||
<div class="flex flex-wrap gap-2.5 items-end">
|
||||
<!-- Nomor Rekening Filter -->
|
||||
<div class="flex flex-col">
|
||||
<input type="text" id="account-number-filter" class="input w-[200px]"
|
||||
placeholder="Masukkan nomor rekening">
|
||||
</div>
|
||||
|
||||
<!-- Tanggal Mulai Filter -->
|
||||
<div class="flex flex-col">
|
||||
<input type="date" id="start-date-filter" class="input w-[150px]">
|
||||
</div>
|
||||
|
||||
<!-- Tanggal Akhir Filter -->
|
||||
<div class="flex flex-col">
|
||||
<input type="date" id="end-date-filter" class="input w-[150px]">
|
||||
</div>
|
||||
|
||||
<!-- Tombol Filter -->
|
||||
<button type="button" id="apply-filter" class="btn btn-primary">
|
||||
<i class="ki-filled ki-magnifier"></i>
|
||||
Filter
|
||||
</button>
|
||||
|
||||
<!-- Tombol Reset -->
|
||||
<button type="button" id="reset-filter" class="btn btn-light">
|
||||
<i class="ki-filled ki-arrows-circle"></i>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="scrollable-x-auto">
|
||||
<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-[200px]" data-datatable-column="account_number">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Nomor Rekening</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="period">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Periode</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="updated_at">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Tanggal Update</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[100px] text-center" data-datatable-column="actions">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<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="w-16 select select-sm" data-datatable-size="true" name="perpage"></select> per page
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<span data-datatable-info="true"></span>
|
||||
<div class="pagination" data-datatable-pagination="true">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Fungsi untuk memformat angka menjadi format mata uang
|
||||
* @param {number} amount - Jumlah yang akan diformat
|
||||
* @returns {string} - String yang sudah diformat
|
||||
*/
|
||||
function formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 2
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk memformat periode dari format YYYYMMDD ke format yang lebih readable
|
||||
* @param {string} period - Periode dalam format YYYYMMDD
|
||||
* @returns {string} - Periode yang sudah diformat
|
||||
*/
|
||||
function formatPeriod(period) {
|
||||
if (!period || period.length !== 8) return period;
|
||||
|
||||
const year = period.substring(0, 4);
|
||||
const month = period.substring(4, 6);
|
||||
const day = period.substring(6, 8);
|
||||
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk memformat tanggal
|
||||
* @param {string} dateString - String tanggal
|
||||
* @returns {string} - Tanggal yang sudah diformat
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
const element = document.querySelector('#laporan-closing-balance-table');
|
||||
const accountNumberFilter = document.getElementById('account-number-filter');
|
||||
const startDateFilter = document.getElementById('start-date-filter');
|
||||
const endDateFilter = document.getElementById('end-date-filter');
|
||||
const applyFilterBtn = document.getElementById('apply-filter');
|
||||
const resetFilterBtn = document.getElementById('reset-filter');
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
|
||||
const apiUrl = element.getAttribute('data-api-url');
|
||||
|
||||
// Set default date range (last 30 days) SEBELUM inisialisasi DataTable
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
|
||||
|
||||
endDateFilter.value = today.toISOString().split('T')[0];
|
||||
startDateFilter.value = thirtyDaysAgo.toISOString().split('T')[0];
|
||||
|
||||
// Prepare initial filters
|
||||
let initialFilters = {
|
||||
start_date: startDateFilter.value,
|
||||
end_date: endDateFilter.value
|
||||
};
|
||||
|
||||
// Konfigurasi DataTable dengan filter awal
|
||||
const dataTableOptions = {
|
||||
apiEndpoint: apiUrl,
|
||||
pageSize: 10,
|
||||
searchParams: initialFilters, // Set filter awal di sini
|
||||
columns: {
|
||||
select: {
|
||||
render: (item, data, context) => {
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.className = 'checkbox checkbox-sm';
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = data.id ? data.id.toString() : '';
|
||||
checkbox.setAttribute('data-datatable-row-check', 'true');
|
||||
return checkbox.outerHTML.trim();
|
||||
},
|
||||
},
|
||||
account_number: {
|
||||
title: 'Nomor Rekening',
|
||||
render: (item, data) => {
|
||||
return `<span class="font-medium">${data.account_number || '-'}</span>`;
|
||||
}
|
||||
},
|
||||
period: {
|
||||
title: 'Periode',
|
||||
render: (item, data) => {
|
||||
return `<span class="text-gray-700">${formatPeriod(data.period)}</span>`;
|
||||
}
|
||||
},
|
||||
updated_at: {
|
||||
title: 'Tanggal Update',
|
||||
render: (item, data) => {
|
||||
return `<span class="text-sm text-gray-600">${formatDate(data.created_at)}</span>`;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
title: 'Action',
|
||||
render: (item, data) => {
|
||||
const downloadUrl =
|
||||
`{{ route('laporan-closing-balance.download', ['accountNumber' => '__ACCOUNT__', 'period' => '__PERIOD__']) }}`
|
||||
.replace('__ACCOUNT__', data.account_number)
|
||||
.replace('__PERIOD__', data.period);
|
||||
|
||||
return `<div class="flex flex-nowrap justify-center">
|
||||
<a class="btn btn-sm btn-icon btn-clear btn-success"
|
||||
href="${downloadUrl}"
|
||||
title="Download Laporan"
|
||||
download>
|
||||
<i class="ki-outline ki-file-down"></i>
|
||||
</a>
|
||||
</div>`;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Inisialisasi DataTable dengan filter awal sudah terset
|
||||
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||
|
||||
// Update export URL dengan filter awal
|
||||
updateExportUrl(initialFilters);
|
||||
|
||||
/**
|
||||
* Fungsi untuk menerapkan filter
|
||||
*/
|
||||
function applyFilters() {
|
||||
let filters = {};
|
||||
|
||||
if (accountNumberFilter.value.trim()) {
|
||||
filters.account_number = accountNumberFilter.value.trim();
|
||||
}
|
||||
|
||||
if (startDateFilter.value) {
|
||||
filters.start_date = startDateFilter.value;
|
||||
}
|
||||
|
||||
if (endDateFilter.value) {
|
||||
filters.end_date = endDateFilter.value;
|
||||
}
|
||||
|
||||
console.log('Applying filters:', filters);
|
||||
dataTable.search(filters);
|
||||
|
||||
// Update export URL dengan filter
|
||||
updateExportUrl(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk mereset filter
|
||||
*/
|
||||
function resetFilters() {
|
||||
accountNumberFilter.value = '';
|
||||
startDateFilter.value = '';
|
||||
endDateFilter.value = '';
|
||||
|
||||
dataTable.search({});
|
||||
updateExportUrl({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk update URL export dengan parameter filter
|
||||
*/
|
||||
function updateExportUrl(filters) {
|
||||
const baseUrl = '{{ route('laporan-closing-balance.export') }}';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Object.keys(filters).forEach(key => {
|
||||
if (filters[key]) {
|
||||
params.append(key, filters[key]);
|
||||
}
|
||||
});
|
||||
|
||||
const newUrl = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
||||
exportBtn.href = newUrl;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
applyFilterBtn.addEventListener('click', applyFilters);
|
||||
resetFilterBtn.addEventListener('click', resetFilters);
|
||||
|
||||
// Auto apply filter saat enter di input
|
||||
[accountNumberFilter, startDateFilter, endDateFilter].forEach(input => {
|
||||
input.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
applyFilters();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// HAPUS bagian ini yang menyebabkan double API call:
|
||||
// setTimeout(() => {
|
||||
// applyFilters();
|
||||
// }, 100);
|
||||
</script>
|
||||
@endpush
|
||||
291
resources/views/laporan-closing-balance/show.blade.php
Normal file
291
resources/views/laporan-closing-balance/show.blade.php
Normal file
@@ -0,0 +1,291 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@section('breadcrumbs')
|
||||
{{ Breadcrumbs::render('laporan-closing-balance.show', $closingBalance) }}
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<!-- Header Card -->
|
||||
<div class="card mb-5">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
Detail Laporan Closing Balance
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('laporan-closing-balance.index') }}" class="btn btn-light">
|
||||
<i class="ki-filled ki-left"></i>
|
||||
Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Information Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
Informasi Rekening
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Informasi Dasar -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Nomor Rekening</label>
|
||||
<div class="text-lg font-semibold text-gray-900">
|
||||
{{ $closingBalance->account_number }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Periode</label>
|
||||
<div class="text-lg font-semibold text-gray-900">
|
||||
@php
|
||||
$period = $closingBalance->period;
|
||||
if (strlen($period) === 8) {
|
||||
$formatted = substr($period, 6, 2) . '/' . substr($period, 4, 2) . '/' . substr($period, 0, 4);
|
||||
echo $formatted;
|
||||
} else {
|
||||
echo $period;
|
||||
}
|
||||
@endphp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Tanggal Update Terakhir</label>
|
||||
<div class="text-lg font-semibold text-gray-900">
|
||||
{{ $closingBalance->updated_at ? $closingBalance->updated_at->format('d/m/Y H:i:s') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informasi Saldo -->
|
||||
<div class="space-y-4">
|
||||
<div class="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<label class="text-sm font-medium text-blue-700 mb-2 block">Saldo Cleared</label>
|
||||
<div class="text-2xl font-bold text-blue-800">
|
||||
@php
|
||||
$clearedBalance = $closingBalance->cleared_balance ?? 0;
|
||||
echo 'Rp ' . number_format($clearedBalance, 2, ',', '.');
|
||||
@endphp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-50 p-4 rounded-lg border border-green-200">
|
||||
<label class="text-sm font-medium text-green-700 mb-2 block">Saldo Aktual</label>
|
||||
<div class="text-2xl font-bold text-green-800">
|
||||
@php
|
||||
$actualBalance = $closingBalance->actual_balance ?? 0;
|
||||
echo 'Rp ' . number_format($actualBalance, 2, ',', '.');
|
||||
@endphp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200">
|
||||
<label class="text-sm font-medium text-gray-700 mb-2 block">Selisih Saldo</label>
|
||||
<div class="text-2xl font-bold {{ ($actualBalance - $clearedBalance) >= 0 ? 'text-green-800' : 'text-red-800' }}">
|
||||
@php
|
||||
$difference = $actualBalance - $clearedBalance;
|
||||
$sign = $difference >= 0 ? '+' : '';
|
||||
echo $sign . 'Rp ' . number_format($difference, 2, ',', '.');
|
||||
@endphp
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Information Card -->
|
||||
@if($closingBalance->created_at || $closingBalance->updated_at)
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
Informasi Sistem
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
@if($closingBalance->created_at)
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Tanggal Dibuat</label>
|
||||
<div class="text-base text-gray-900">
|
||||
{{ $closingBalance->created_at->format('d/m/Y H:i:s') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $closingBalance->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($closingBalance->updated_at)
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Tanggal Diperbarui</label>
|
||||
<div class="text-base text-gray-900">
|
||||
{{ $closingBalance->updated_at->format('d/m/Y H:i:s') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $closingBalance->updated_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="card mt-5">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap gap-3 justify-center lg:justify-start">
|
||||
<a href="{{ route('laporan-closing-balance.index') }}" class="btn btn-light">
|
||||
<i class="ki-filled ki-left"></i>
|
||||
Kembali ke Daftar
|
||||
</a>
|
||||
|
||||
<a href="{{ route('laporan-closing-balance.export', ['account_number' => $closingBalance->account_number, 'start_date' => $closingBalance->period, 'end_date' => $closingBalance->period]) }}"
|
||||
class="btn btn-primary">
|
||||
<i class="ki-filled ki-file-down"></i>
|
||||
Export Data Ini
|
||||
</a>
|
||||
|
||||
<button type="button" class="btn btn-info" onclick="window.print()">
|
||||
<i class="ki-filled ki-printer"></i>
|
||||
Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.btn, .card-header .flex, nav, .breadcrumb {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Fungsi untuk memformat angka menjadi format mata uang Indonesia
|
||||
* @param {number} amount - Jumlah yang akan diformat
|
||||
* @returns {string} - String yang sudah diformat
|
||||
*/
|
||||
function formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 2
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk copy nomor rekening ke clipboard
|
||||
*/
|
||||
function copyAccountNumber() {
|
||||
const accountNumber = '{{ $closingBalance->account_number }}';
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(accountNumber).then(function() {
|
||||
// Show success message
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: 'Nomor rekening berhasil disalin ke clipboard',
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
alert('Nomor rekening berhasil disalin ke clipboard');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.error('Error copying to clipboard: ', err);
|
||||
fallbackCopyTextToClipboard(accountNumber);
|
||||
});
|
||||
} else {
|
||||
fallbackCopyTextToClipboard(accountNumber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback function untuk copy text jika clipboard API tidak tersedia
|
||||
*/
|
||||
function fallbackCopyTextToClipboard(text) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: 'Nomor rekening berhasil disalin ke clipboard',
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
alert('Nomor rekening berhasil disalin ke clipboard');
|
||||
}
|
||||
} else {
|
||||
console.error('Fallback: Copying text command was unsuccessful');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Add click event to account number for easy copying
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const accountNumberElements = document.querySelectorAll('.account-number-clickable');
|
||||
accountNumberElements.forEach(element => {
|
||||
element.style.cursor = 'pointer';
|
||||
element.title = 'Klik untuk menyalin nomor rekening';
|
||||
element.addEventListener('click', copyAccountNumber);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -130,6 +130,11 @@
|
||||
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 {
|
||||
@@ -278,7 +283,7 @@
|
||||
$totalDebit = 0;
|
||||
$totalKredit = 0;
|
||||
$line = 1;
|
||||
$linePerPage = 26;
|
||||
$linePerPage = 23;
|
||||
@endphp
|
||||
@php
|
||||
// Hitung tanggal periode berdasarkan $period
|
||||
|
||||
@@ -125,3 +125,18 @@
|
||||
$trail->parent('home');
|
||||
$trail->push('Statement Email Logs', route('email-statement-logs.index'));
|
||||
});
|
||||
|
||||
// Home > Laporan Closing Balance
|
||||
Breadcrumbs::for('laporan-closing-balance.index', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('home');
|
||||
$trail->push('Laporan Closing Balance', route('laporan-closing-balance.index'));
|
||||
});
|
||||
|
||||
// Home > Laporan Closing Balance > Detail
|
||||
Breadcrumbs::for('laporan-closing-balance.show', function (BreadcrumbTrail $trail, $closingBalance) {
|
||||
$trail->parent('laporan-closing-balance.index');
|
||||
$trail->push('Detail - ' . $closingBalance->account_number, route('laporan-closing-balance.show', [
|
||||
'accountNumber' => $closingBalance->account_number,
|
||||
'period' => $closingBalance->period
|
||||
]));
|
||||
});
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Webstatement\Http\Controllers\PeriodeStatementController;
|
||||
use Modules\Webstatement\Http\Controllers\PrintStatementController;
|
||||
use Modules\Webstatement\Http\Controllers\SyncLogsController;
|
||||
use Modules\Webstatement\Http\Controllers\JenisKartuController;
|
||||
use Modules\Webstatement\Http\Controllers\KartuAtmController;
|
||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||
use Modules\Webstatement\Http\Controllers\CustomerController;
|
||||
use Modules\Webstatement\Http\Controllers\EmailBlastController;
|
||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
||||
use Modules\Webstatement\Http\Controllers\DebugStatementController;
|
||||
use Modules\Webstatement\Http\Controllers\EmailStatementLogController;
|
||||
use Modules\Webstatement\Http\Controllers\AtmTransactionReportController;
|
||||
use Modules\Webstatement\Http\Controllers\{
|
||||
PeriodeStatementController,
|
||||
PrintStatementController,
|
||||
SyncLogsController,
|
||||
JenisKartuController,
|
||||
KartuAtmController,
|
||||
CustomerController,
|
||||
EmailBlastController,
|
||||
WebstatementController,
|
||||
DebugStatementController,
|
||||
EmailStatementLogController,
|
||||
AtmTransactionReportController,
|
||||
LaporanClosingBalanceController
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -91,7 +93,7 @@ Route::middleware(['auth'])->group(function () {
|
||||
});
|
||||
|
||||
Route::resource('statements', PrintStatementController::class);
|
||||
|
||||
|
||||
|
||||
// ATM Transaction Report Routes
|
||||
Route::group(['prefix' => 'atm-reports', 'as' => 'atm-reports.', 'middleware' => ['auth']], function () {
|
||||
@@ -110,6 +112,15 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::post('/{id}/resend-email', [EmailStatementLogController::class, 'resendEmail'])->name('resend-email');
|
||||
});
|
||||
Route::resource('email-statement-logs', EmailStatementLogController::class)->only(['index', 'show']);
|
||||
|
||||
// Laporan Closing Balance Routes
|
||||
Route::group(['prefix' => 'laporan-closing-balance', 'as' => 'laporan-closing-balance.', 'middleware' => ['auth']], function () {
|
||||
Route::get('/datatables', [LaporanClosingBalanceController::class, 'dataForDatatables'])->name('datatables');
|
||||
Route::get('/export', [LaporanClosingBalanceController::class, 'export'])->name('export');
|
||||
Route::get('/{accountNumber}/{period}/download', [LaporanClosingBalanceController::class, 'download'])->name('download');
|
||||
Route::get('/{accountNumber}/{period}', [LaporanClosingBalanceController::class, 'show'])->name('show');
|
||||
});
|
||||
Route::resource('laporan-closing-balance', LaporanClosingBalanceController::class)->only(['index']);
|
||||
});
|
||||
|
||||
Route::get('/stmt-export-csv', [WebstatementController::class, 'index'])->name('webstatement.index');
|
||||
|
||||
Reference in New Issue
Block a user