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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class ProcessDailyMigration extends Command
|
class ProcessDailyMigration extends Command
|
||||||
{
|
{
|
||||||
@@ -14,14 +15,15 @@
|
|||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $signature = 'webstatement:process-daily-migration
|
protected $signature = 'webstatement:process-daily-migration
|
||||||
{--process_parameter= : To process migration parameter true/false}';
|
{--process_parameter= : To process migration parameter true/false}
|
||||||
|
{--period= : Period to process (default: -1 day, format: Ymd or relative date)}';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The console command description.
|
* The console command description.
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $description = 'Process data migration for the previous day\'s period';
|
protected $description = 'Process data migration for the specified period (default: previous day)';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the console command.
|
* Execute the console command.
|
||||||
@@ -31,20 +33,34 @@
|
|||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$processParameter = $this->option('process_parameter');
|
$processParameter = $this->option('process_parameter');
|
||||||
|
$period = $this->option('period');
|
||||||
|
|
||||||
|
// Log start of process
|
||||||
|
Log::info('Starting daily data migration process', [
|
||||||
|
'process_parameter' => $processParameter ?? 'false',
|
||||||
|
'period' => $period ?? '-1 day'
|
||||||
|
]);
|
||||||
|
|
||||||
$this->info('Starting daily data migration process...');
|
$this->info('Starting daily data migration process...');
|
||||||
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
|
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
|
||||||
|
$this->info('Period: ' . ($period ?? '-1 day (default)'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$controller = app(MigrasiController::class);
|
$controller = app(MigrasiController::class);
|
||||||
$response = $controller->index($processParameter);
|
$response = $controller->index($processParameter, $period);
|
||||||
|
|
||||||
$responseData = json_decode($response->getContent(), true);
|
$responseData = json_decode($response->getContent(), true);
|
||||||
$this->info($responseData['message'] ?? 'Process completed');
|
$message = $responseData['message'] ?? 'Process completed';
|
||||||
|
|
||||||
|
$this->info($message);
|
||||||
|
Log::info('Daily migration process completed successfully', ['message' => $message]);
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->error('Error processing daily migration: ' . $e->getMessage());
|
$errorMessage = 'Error processing daily migration: ' . $e->getMessage();
|
||||||
|
$this->error($errorMessage);
|
||||||
|
Log::error($errorMessage, ['exception' => $e->getTraceAsString()]);
|
||||||
|
|
||||||
return Command::FAILURE;
|
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,
|
ProcessTellerDataJob,
|
||||||
ProcessTransactionDataJob,
|
ProcessTransactionDataJob,
|
||||||
ProcessSectorDataJob,
|
ProcessSectorDataJob,
|
||||||
ProcessProvinceDataJob};
|
ProcessProvinceDataJob,
|
||||||
|
ProcessStmtEntryDetailDataJob};
|
||||||
|
|
||||||
class MigrasiController extends Controller
|
class MigrasiController extends Controller
|
||||||
{
|
{
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
'customer' => ProcessCustomerDataJob::class,
|
'customer' => ProcessCustomerDataJob::class,
|
||||||
'account' => ProcessAccountDataJob::class,
|
'account' => ProcessAccountDataJob::class,
|
||||||
'stmtEntry' => ProcessStmtEntryDataJob::class,
|
'stmtEntry' => ProcessStmtEntryDataJob::class,
|
||||||
|
'stmtEntryDetail' => ProcessStmtEntryDetailDataJob::class, // Tambahan baru
|
||||||
'dataCapture' => ProcessDataCaptureDataJob::class,
|
'dataCapture' => ProcessDataCaptureDataJob::class,
|
||||||
'fundsTransfer' => ProcessFundsTransferDataJob::class,
|
'fundsTransfer' => ProcessFundsTransferDataJob::class,
|
||||||
'teller' => ProcessTellerDataJob::class,
|
'teller' => ProcessTellerDataJob::class,
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
'customer',
|
'customer',
|
||||||
'account',
|
'account',
|
||||||
'stmtEntry',
|
'stmtEntry',
|
||||||
|
'stmtEntryDetail', // Tambahan baru
|
||||||
'dataCapture',
|
'dataCapture',
|
||||||
'fundsTransfer',
|
'fundsTransfer',
|
||||||
'teller',
|
'teller',
|
||||||
@@ -98,30 +101,99 @@
|
|||||||
return response()->json(['error' => $e->getMessage()], 500);
|
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)
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
|
Log::info('Starting migration process', [
|
||||||
|
'process_parameter' => $processParameter,
|
||||||
|
'period' => $period
|
||||||
|
]);
|
||||||
|
|
||||||
$disk = Storage::disk('sftpStatement');
|
$disk = Storage::disk('sftpStatement');
|
||||||
|
|
||||||
if ($processParameter) {
|
if ($processParameter) {
|
||||||
|
Log::info('Processing parameter data');
|
||||||
|
|
||||||
foreach (self::PARAMETER_PROCESSES as $process) {
|
foreach (self::PARAMETER_PROCESSES as $process) {
|
||||||
$this->processData($process, '_parameter');
|
$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'));
|
// Tentukan periode yang akan diproses
|
||||||
if (!$disk->exists($period)) {
|
$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([
|
return response()->json([
|
||||||
"message" => "Period {$period} folder not found in SFTP storage"
|
"message" => $errorMessage
|
||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (self::DATA_PROCESSES as $process) {
|
foreach (self::DATA_PROCESSES as $process) {
|
||||||
$this->processData($process, $period);
|
$this->processData($process, $targetPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$successMessage = "Data processing for period {$targetPeriod} has been queued successfully";
|
||||||
|
Log::info($successMessage);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => "Data processing for period {$period} has been queued successfully"
|
'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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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' => [
|
'SWADAYA_PANDU' => [
|
||||||
'0081272689',
|
'0081272689',
|
||||||
|
],
|
||||||
|
"AWAN_LINTANG_SOLUSI"=> [
|
||||||
|
"1084269430"
|
||||||
|
],
|
||||||
|
"MONETA"=> [
|
||||||
|
"1085667890"
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,10 +81,10 @@
|
|||||||
$existingDataCount = $this->getExistingProcessedCount($accountQuery);
|
$existingDataCount = $this->getExistingProcessedCount($accountQuery);
|
||||||
|
|
||||||
// Hanya proses jika data belum lengkap diproses
|
// Hanya proses jika data belum lengkap diproses
|
||||||
if ($existingDataCount !== $totalCount) {
|
//if ($existingDataCount !== $totalCount) {
|
||||||
$this->deleteExistingProcessedData($accountQuery);
|
$this->deleteExistingProcessedData($accountQuery);
|
||||||
$this->processAndSaveStatementEntries($totalCount);
|
$this->processAndSaveStatementEntries($totalCount);
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTotalEntryCount(array $criteria)
|
private function getTotalEntryCount(array $criteria)
|
||||||
@@ -357,8 +357,7 @@
|
|||||||
/**
|
/**
|
||||||
* Export processed data to CSV file
|
* Export processed data to CSV file
|
||||||
*/
|
*/
|
||||||
private function exportToCsv()
|
private function exportToCsv(): void
|
||||||
: void
|
|
||||||
{
|
{
|
||||||
// Determine the base path based on client
|
// Determine the base path based on client
|
||||||
$basePath = !empty($this->client)
|
$basePath = !empty($this->client)
|
||||||
@@ -367,36 +366,11 @@
|
|||||||
|
|
||||||
$accountPath = "{$basePath}/{$this->account_number}";
|
$accountPath = "{$basePath}/{$this->account_number}";
|
||||||
|
|
||||||
// Create client directory if it doesn't exist
|
// PERBAIKAN: Selalu pastikan direktori dibuat
|
||||||
if (!empty($this->client)) {
|
|
||||||
// Di fungsi exportToCsv untuk basePath
|
|
||||||
Storage::disk($this->disk)->makeDirectory($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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Untuk accountPath
|
|
||||||
Storage::disk($this->disk)->makeDirectory($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}";
|
$filePath = "{$accountPath}/{$this->fileName}";
|
||||||
|
|
||||||
@@ -405,13 +379,38 @@
|
|||||||
Storage::disk($this->disk)->delete($filePath);
|
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)
|
ProcessedStatement::where('account_number', $this->account_number)
|
||||||
->where('period', $this->period)
|
->where('period', $this->period)
|
||||||
->orderBy('sequence_no')
|
->orderBy('sequence_no')
|
||||||
->chunk($this->chunkSize, function ($statements) use (&$csvContent, $filePath) {
|
->chunk($this->chunkSize, function ($statements) use ($filePath) {
|
||||||
|
$csvContent = '';
|
||||||
foreach ($statements as $statement) {
|
foreach ($statements as $statement) {
|
||||||
$csvContent .= implode('|', [
|
$csvContent .= implode('|', [
|
||||||
$statement->sequence_no,
|
$statement->sequence_no,
|
||||||
@@ -426,12 +425,31 @@
|
|||||||
]) . "\n";
|
]) . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tulis ke file secara bertahap untuk mengurangi penggunaan memori
|
// Append ke file
|
||||||
|
if (!empty($csvContent)) {
|
||||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||||
$csvContent = ''; // Reset content setelah ditulis
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ class ExportStatementPeriodJob implements ShouldQueue
|
|||||||
$globalSequence++;
|
$globalSequence++;
|
||||||
$runningBalance += (float) $item->amount_lcy;
|
$runningBalance += (float) $item->amount_lcy;
|
||||||
|
|
||||||
|
$transactionDate = $this->formatTransactionDate($item);
|
||||||
$actualDate = $this->formatActualDate($item);
|
$actualDate = $this->formatActualDate($item);
|
||||||
|
|
||||||
$processedData[] = [
|
$processedData[] = [
|
||||||
@@ -217,6 +218,7 @@ class ExportStatementPeriodJob implements ShouldQueue
|
|||||||
'description' => $this->generateNarrative($item),
|
'description' => $this->generateNarrative($item),
|
||||||
'end_balance' => $runningBalance,
|
'end_balance' => $runningBalance,
|
||||||
'actual_date' => $actualDate,
|
'actual_date' => $actualDate,
|
||||||
|
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
];
|
];
|
||||||
@@ -480,27 +482,10 @@ class ExportStatementPeriodJob implements ShouldQueue
|
|||||||
// Buat direktori temp jika belum ada
|
// Buat direktori temp jika belum ada
|
||||||
if (!is_dir(dirname($tempPath))) {
|
if (!is_dir(dirname($tempPath))) {
|
||||||
mkdir(dirname($tempPath), 0777, true);
|
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
|
// Pastikan direktori storage ada
|
||||||
Storage::makeDirectory($storagePath);
|
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;
|
$period = $this->period;
|
||||||
|
|
||||||
@@ -629,17 +614,7 @@ class ExportStatementPeriodJob implements ShouldQueue
|
|||||||
|
|
||||||
$storagePath = "statements/{$this->period}/{$account->branch_code}";
|
$storagePath = "statements/{$this->period}/{$account->branch_code}";
|
||||||
Storage::disk($this->disk)->makeDirectory($storagePath);
|
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}";
|
$filePath = "{$storagePath}/{$this->fileName}";
|
||||||
|
|
||||||
// Delete existing file if it exists
|
// Delete existing file if it exists
|
||||||
@@ -669,7 +644,6 @@ class ExportStatementPeriodJob implements ShouldQueue
|
|||||||
|
|
||||||
// Write to file incrementally to reduce memory usage
|
// Write to file incrementally to reduce memory usage
|
||||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||||
$csvContent = ''; // Reset content after writing
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Log::info("Statement exported to {$this->disk} disk: {$filePath}");
|
Log::info("Statement exported to {$this->disk} disk: {$filePath}");
|
||||||
|
|||||||
@@ -191,7 +191,7 @@
|
|||||||
$subQuery->where('product_code', '!=', '6021')
|
$subQuery->where('product_code', '!=', '6021')
|
||||||
->orWhere(function($nestedQuery) {
|
->orWhere(function($nestedQuery) {
|
||||||
$nestedQuery->where('product_code', '6021')
|
$nestedQuery->where('product_code', '6021')
|
||||||
->where('ctdesc', '!=', 'gold');
|
->where('ctdesc', '!=', 'GOLD');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
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
|
// Ensure directory exists
|
||||||
Storage::disk('local')->makeDirectory($storagePath);
|
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
|
// Generate PDF path
|
||||||
$pdfPath = storage_path("app/{$fullStoragePath}");
|
$pdfPath = storage_path("app/{$fullStoragePath}");
|
||||||
|
|||||||
@@ -144,6 +144,12 @@
|
|||||||
private function processRow(array $row, int $rowCount, string $filePath)
|
private function processRow(array $row, int $rowCount, string $filePath)
|
||||||
: void
|
: 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);
|
$csvHeaders = array_keys(self::FIELD_MAP);
|
||||||
|
|
||||||
if (count($csvHeaders) !== count($row)) {
|
if (count($csvHeaders) !== count($row)) {
|
||||||
|
|||||||
@@ -185,6 +185,12 @@
|
|||||||
private function processRow(array $row, int $rowCount, string $filePath)
|
private function processRow(array $row, int $rowCount, string $filePath)
|
||||||
: void
|
: 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)) {
|
if (count(self::CSV_HEADERS) !== count($row)) {
|
||||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||||
count(self::CSV_HEADERS) . ", Got: " . count($row));
|
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 Illuminate\Support\ServiceProvider;
|
||||||
use Nwidart\Modules\Traits\PathNamespace;
|
use Nwidart\Modules\Traits\PathNamespace;
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
use Modules\Webstatement\Console\UnlockPdf;
|
use Modules\Webstatement\Console\{
|
||||||
use Modules\Webstatement\Console\CombinePdf;
|
UnlockPdf,
|
||||||
use Modules\Webstatement\Console\ConvertHtmlToPdf;
|
CombinePdf,
|
||||||
use Modules\Webstatement\Console\ExportDailyStatements;
|
ConvertHtmlToPdf,
|
||||||
use Modules\Webstatement\Console\ProcessDailyMigration;
|
ExportDailyStatements,
|
||||||
use Modules\Webstatement\Console\ExportPeriodStatements;
|
ProcessDailyMigration,
|
||||||
use Modules\Webstatement\Console\UpdateAllAtmCardsCommand;
|
ExportPeriodStatements,
|
||||||
use Modules\Webstatement\Console\CheckEmailProgressCommand;
|
UpdateAllAtmCardsCommand,
|
||||||
use Modules\Webstatement\Console\AutoSendStatementEmailCommand;
|
CheckEmailProgressCommand,
|
||||||
use Modules\Webstatement\Console\GenerateBiayakartuCommand;
|
GenerateBiayakartuCommand,
|
||||||
use Modules\Webstatement\Console\SendStatementEmailCommand;
|
SendStatementEmailCommand,
|
||||||
|
GenerateAtmTransactionReport,
|
||||||
|
GenerateBiayaKartuCsvCommand,
|
||||||
|
AutoSendStatementEmailCommand,
|
||||||
|
GenerateClosingBalanceReportCommand,
|
||||||
|
GenerateClosingBalanceReportBulkCommand,
|
||||||
|
};
|
||||||
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
|
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
|
||||||
use Modules\Webstatement\Console\GenerateAtmTransactionReport;
|
|
||||||
use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand;
|
|
||||||
|
|
||||||
class WebstatementServiceProvider extends ServiceProvider
|
class WebstatementServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -74,7 +78,9 @@ class WebstatementServiceProvider extends ServiceProvider
|
|||||||
SendStatementEmailCommand::class,
|
SendStatementEmailCommand::class,
|
||||||
CheckEmailProgressCommand::class,
|
CheckEmailProgressCommand::class,
|
||||||
UpdateAllAtmCardsCommand::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": [
|
"roles": [
|
||||||
"administrator"
|
"administrator"
|
||||||
]
|
]
|
||||||
|
},{
|
||||||
|
"title": "Laporan Closing Balance",
|
||||||
|
"path": "laporan-closing-balance",
|
||||||
|
"icon": "ki-filled ki-printer text-lg text-primary",
|
||||||
|
"classes": "",
|
||||||
|
"attributes": [],
|
||||||
|
"permission": "",
|
||||||
|
"roles": [
|
||||||
|
"administrator"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"master": [
|
"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;
|
padding: 5px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
table th {
|
table th {
|
||||||
@@ -278,7 +283,7 @@
|
|||||||
$totalDebit = 0;
|
$totalDebit = 0;
|
||||||
$totalKredit = 0;
|
$totalKredit = 0;
|
||||||
$line = 1;
|
$line = 1;
|
||||||
$linePerPage = 26;
|
$linePerPage = 23;
|
||||||
@endphp
|
@endphp
|
||||||
@php
|
@php
|
||||||
// Hitung tanggal periode berdasarkan $period
|
// Hitung tanggal periode berdasarkan $period
|
||||||
|
|||||||
@@ -125,3 +125,18 @@
|
|||||||
$trail->parent('home');
|
$trail->parent('home');
|
||||||
$trail->push('Statement Email Logs', route('email-statement-logs.index'));
|
$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
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Modules\Webstatement\Http\Controllers\PeriodeStatementController;
|
use Modules\Webstatement\Http\Controllers\{
|
||||||
use Modules\Webstatement\Http\Controllers\PrintStatementController;
|
PeriodeStatementController,
|
||||||
use Modules\Webstatement\Http\Controllers\SyncLogsController;
|
PrintStatementController,
|
||||||
use Modules\Webstatement\Http\Controllers\JenisKartuController;
|
SyncLogsController,
|
||||||
use Modules\Webstatement\Http\Controllers\KartuAtmController;
|
JenisKartuController,
|
||||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
KartuAtmController,
|
||||||
use Modules\Webstatement\Http\Controllers\CustomerController;
|
CustomerController,
|
||||||
use Modules\Webstatement\Http\Controllers\EmailBlastController;
|
EmailBlastController,
|
||||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
WebstatementController,
|
||||||
use Modules\Webstatement\Http\Controllers\DebugStatementController;
|
DebugStatementController,
|
||||||
use Modules\Webstatement\Http\Controllers\EmailStatementLogController;
|
EmailStatementLogController,
|
||||||
use Modules\Webstatement\Http\Controllers\AtmTransactionReportController;
|
AtmTransactionReportController,
|
||||||
|
LaporanClosingBalanceController
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -110,6 +112,15 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
Route::post('/{id}/resend-email', [EmailStatementLogController::class, 'resendEmail'])->name('resend-email');
|
Route::post('/{id}/resend-email', [EmailStatementLogController::class, 'resendEmail'])->name('resend-email');
|
||||||
});
|
});
|
||||||
Route::resource('email-statement-logs', EmailStatementLogController::class)->only(['index', 'show']);
|
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');
|
Route::get('/stmt-export-csv', [WebstatementController::class, 'index'])->name('webstatement.index');
|
||||||
|
|||||||
Reference in New Issue
Block a user