Compare commits
11 Commits
3720a24690
...
9199a4d748
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9199a4d748 | ||
|
|
f3c649572b | ||
|
|
55313fb0b0 | ||
|
|
8a7d4f351c | ||
|
|
f800c97a40 | ||
|
|
8fa4b2ea9e | ||
|
|
1f4d37370e | ||
|
|
49f90eef43 | ||
|
|
6eef6e89bf | ||
|
|
8d4accffaf | ||
|
|
903cbd1725 |
54
app/Console/CheckEmailProgressCommand.php
Normal file
54
app/Console/CheckEmailProgressCommand.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Console;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Modules\Webstatement\Models\PrintStatementLog;
|
||||||
|
|
||||||
|
class CheckEmailProgressCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'webstatement:check-progress {log-id : ID log untuk dicek progressnya}';
|
||||||
|
protected $description = 'Cek progress pengiriman email statement';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$logId = $this->argument('log-id');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$log = PrintStatementLog::findOrFail($logId);
|
||||||
|
|
||||||
|
$this->info("📊 Progress Pengiriman Email Statement");
|
||||||
|
$this->line("Log ID: {$log->id}");
|
||||||
|
$this->line("Batch ID: {$log->batch_id}");
|
||||||
|
$this->line("Request Type: {$log->request_type}");
|
||||||
|
$this->line("Status: {$log->status}");
|
||||||
|
|
||||||
|
if ($log->total_accounts) {
|
||||||
|
$this->line("Total Accounts: {$log->total_accounts}");
|
||||||
|
$this->line("Processed: {$log->processed_accounts}");
|
||||||
|
$this->line("Success: {$log->success_count}");
|
||||||
|
$this->line("Failed: {$log->failed_count}");
|
||||||
|
$this->line("Progress: {$log->getProgressPercentage()}%");
|
||||||
|
$this->line("Success Rate: {$log->getSuccessRate()}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($log->started_at) {
|
||||||
|
$this->line("Started: {$log->started_at}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($log->completed_at) {
|
||||||
|
$this->line("Completed: {$log->completed_at}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($log->error_message) {
|
||||||
|
$this->error("Error: {$log->error_message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("Log dengan ID {$logId} tidak ditemukan.");
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Console/GenerateAtmTransactionReport.php
Normal file
59
app/Console/GenerateAtmTransactionReport.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Console;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Modules\Webstatement\Jobs\GenerateAtmTransactionReportJob;
|
||||||
|
|
||||||
|
class GenerateAtmTransactionReport extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'webstatement:generate-atm-report {--period= : Period to generate report format Ymd, contoh: 20250512}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Generate ATM Transaction report for specified period';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Starting ATM Transaction report generation...');
|
||||||
|
$period = $this->option('period');
|
||||||
|
|
||||||
|
if (!$period) {
|
||||||
|
$this->error('Period parameter is required. Format: Ymd (example: 20250512)');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate period format
|
||||||
|
if (!preg_match('/^\d{8}$/', $period)) {
|
||||||
|
$this->error('Invalid period format. Use Ymd format (example: 20250512)');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Dispatch the job
|
||||||
|
GenerateAtmTransactionReportJob::dispatch($period);
|
||||||
|
|
||||||
|
$this->info("ATM Transaction report generation job queued for period: {$period}");
|
||||||
|
$this->info('The report will be generated in the background.');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error('Error queuing ATM Transaction report job: ' . $e->getMessage());
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
248
app/Console/SendStatementEmailCommand.php
Normal file
248
app/Console/SendStatementEmailCommand.php
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Console;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Modules\Webstatement\Jobs\SendStatementEmailJob;
|
||||||
|
use Modules\Webstatement\Models\Account;
|
||||||
|
use Modules\Webstatement\Models\PrintStatementLog;
|
||||||
|
use Modules\Basicdata\Models\Branch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command untuk mengirim email statement PDF ke nasabah
|
||||||
|
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
|
||||||
|
*/
|
||||||
|
class SendStatementEmailCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'webstatement:send-email
|
||||||
|
{period : Format periode YYYYMM (contoh: 202401)}
|
||||||
|
{--type=single : Tipe pengiriman: single, branch, all}
|
||||||
|
{--account= : Nomor rekening (untuk type=single)}
|
||||||
|
{--branch= : Kode cabang (untuk type=branch)}
|
||||||
|
{--batch-id= : ID batch untuk tracking (opsional)}
|
||||||
|
{--queue=emails : Nama queue untuk job (default: emails)}
|
||||||
|
{--delay=0 : Delay dalam menit sebelum job dijalankan}';
|
||||||
|
|
||||||
|
protected $description = 'Mengirim email statement PDF ke nasabah (per rekening, per cabang, atau seluruh cabang)';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('🚀 Memulai proses pengiriman email statement...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$period = $this->argument('period');
|
||||||
|
$type = $this->option('type');
|
||||||
|
$accountNumber = $this->option('account');
|
||||||
|
$branchCode = $this->option('branch');
|
||||||
|
$batchId = $this->option('batch-id');
|
||||||
|
$queueName = $this->option('queue');
|
||||||
|
$delay = (int) $this->option('delay');
|
||||||
|
|
||||||
|
// Validasi parameter
|
||||||
|
if (!$this->validateParameters($period, $type, $accountNumber, $branchCode)) {
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tentukan request type dan target value
|
||||||
|
[$requestType, $targetValue] = $this->determineRequestTypeAndTarget($type, $accountNumber, $branchCode);
|
||||||
|
|
||||||
|
// Buat log entry
|
||||||
|
$log = $this->createLogEntry($period, $requestType, $targetValue, $batchId);
|
||||||
|
|
||||||
|
// Dispatch job
|
||||||
|
$job = SendStatementEmailJob::dispatch($period, $requestType, $targetValue, $batchId, $log->id)
|
||||||
|
->onQueue($queueName);
|
||||||
|
|
||||||
|
if ($delay > 0) {
|
||||||
|
$job->delay(now()->addMinutes($delay));
|
||||||
|
$this->info("⏰ Job dijadwalkan untuk dijalankan dalam {$delay} menit");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->displayJobInfo($period, $requestType, $targetValue, $queueName, $log);
|
||||||
|
$this->info('✅ Job pengiriman email statement berhasil didispatch!');
|
||||||
|
$this->info('📊 Gunakan command berikut untuk monitoring:');
|
||||||
|
$this->line(" php artisan queue:work {$queueName}");
|
||||||
|
$this->line(' php artisan webstatement:check-progress ' . $log->id);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error('❌ Error saat mendispatch job: ' . $e->getMessage());
|
||||||
|
Log::error('SendStatementEmailCommand failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateParameters($period, $type, $accountNumber, $branchCode)
|
||||||
|
{
|
||||||
|
// Validasi format periode
|
||||||
|
if (!preg_match('/^\d{6}$/', $period)) {
|
||||||
|
$this->error('❌ Format periode tidak valid. Gunakan format YYYYMM (contoh: 202401)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi type
|
||||||
|
if (!in_array($type, ['single', 'branch', 'all'])) {
|
||||||
|
$this->error('❌ Type tidak valid. Gunakan: single, branch, atau all');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi parameter berdasarkan type
|
||||||
|
switch ($type) {
|
||||||
|
case 'single':
|
||||||
|
if (!$accountNumber) {
|
||||||
|
$this->error('❌ Parameter --account diperlukan untuk type=single');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = Account::with('customer')
|
||||||
|
->where('account_number', $accountNumber)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$account) {
|
||||||
|
$this->error("❌ Account {$accountNumber} tidak ditemukan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasEmail = !empty($account->stmt_email) ||
|
||||||
|
($account->customer && !empty($account->customer->email));
|
||||||
|
|
||||||
|
if (!$hasEmail) {
|
||||||
|
$this->error("❌ Account {$accountNumber} tidak memiliki email");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("✅ Account {$accountNumber} ditemukan dengan email");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'branch':
|
||||||
|
if (!$branchCode) {
|
||||||
|
$this->error('❌ Parameter --branch diperlukan untuk type=branch');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = Branch::where('code', $branchCode)->first();
|
||||||
|
if (!$branch) {
|
||||||
|
$this->error("❌ Branch {$branchCode} tidak ditemukan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$accountCount = Account::with('customer')
|
||||||
|
->where('branch_code', $branchCode)
|
||||||
|
->where('stmt_sent_type', 'BY.EMAIL')
|
||||||
|
->get()
|
||||||
|
->filter(function ($account) {
|
||||||
|
return !empty($account->stmt_email) ||
|
||||||
|
($account->customer && !empty($account->customer->email));
|
||||||
|
})
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($accountCount === 0) {
|
||||||
|
$this->error("❌ Tidak ada account dengan email di branch {$branchCode}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("✅ Ditemukan {$accountCount} account dengan email di branch {$branch->name}");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'all':
|
||||||
|
$accountCount = Account::with('customer')
|
||||||
|
->where('stmt_sent_type', 'BY.EMAIL')
|
||||||
|
->get()
|
||||||
|
->filter(function ($account) {
|
||||||
|
return !empty($account->stmt_email) ||
|
||||||
|
($account->customer && !empty($account->customer->email));
|
||||||
|
})
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($accountCount === 0) {
|
||||||
|
$this->error('❌ Tidak ada account dengan email ditemukan');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("✅ Ditemukan {$accountCount} account dengan email di seluruh cabang");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function determineRequestTypeAndTarget($type, $accountNumber, $branchCode)
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case 'single':
|
||||||
|
return ['single_account', $accountNumber];
|
||||||
|
case 'branch':
|
||||||
|
return ['branch', $branchCode];
|
||||||
|
case 'all':
|
||||||
|
return ['all_branches', null];
|
||||||
|
default:
|
||||||
|
throw new \InvalidArgumentException("Invalid type: {$type}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createLogEntry($period, $requestType, $targetValue, $batchId)
|
||||||
|
{
|
||||||
|
$logData = [
|
||||||
|
'user_id' => null, // Command line execution
|
||||||
|
'period_from' => $period,
|
||||||
|
'period_to' => $period,
|
||||||
|
'is_period_range' => false,
|
||||||
|
'request_type' => $requestType,
|
||||||
|
'batch_id' => $batchId ?? uniqid('cmd_'),
|
||||||
|
'status' => 'pending',
|
||||||
|
'authorization_status' => 'approved', // Auto-approved untuk command line
|
||||||
|
'created_by' => null,
|
||||||
|
'ip_address' => '127.0.0.1',
|
||||||
|
'user_agent' => 'Command Line'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set branch_code dan account_number berdasarkan request type
|
||||||
|
switch ($requestType) {
|
||||||
|
case 'single_account':
|
||||||
|
$account = Account::where('account_number', $targetValue)->first();
|
||||||
|
$logData['branch_code'] = $account->branch_code;
|
||||||
|
$logData['account_number'] = $targetValue;
|
||||||
|
break;
|
||||||
|
case 'branch':
|
||||||
|
$logData['branch_code'] = $targetValue;
|
||||||
|
$logData['account_number'] = null;
|
||||||
|
break;
|
||||||
|
case 'all_branches':
|
||||||
|
$logData['branch_code'] = 'ALL';
|
||||||
|
$logData['account_number'] = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PrintStatementLog::create($logData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function displayJobInfo($period, $requestType, $targetValue, $queueName, $log)
|
||||||
|
{
|
||||||
|
$this->info('📋 Detail Job:');
|
||||||
|
$this->line(" Log ID: {$log->id}");
|
||||||
|
$this->line(" Periode: {$period}");
|
||||||
|
$this->line(" Request Type: {$requestType}");
|
||||||
|
|
||||||
|
switch ($requestType) {
|
||||||
|
case 'single_account':
|
||||||
|
$this->line(" Account: {$targetValue}");
|
||||||
|
break;
|
||||||
|
case 'branch':
|
||||||
|
$branch = Branch::where('code', $targetValue)->first();
|
||||||
|
$this->line(" Branch: {$targetValue} ({$branch->name})");
|
||||||
|
break;
|
||||||
|
case 'all_branches':
|
||||||
|
$this->line(" Target: Seluruh cabang");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line(" Batch ID: {$log->batch_id}");
|
||||||
|
$this->line(" Queue: {$queueName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
366
app/Http/Controllers/AtmTransactionReportController.php
Normal file
366
app/Http/Controllers/AtmTransactionReportController.php
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
<?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\Storage;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Log;
|
||||||
|
use Modules\Webstatement\Jobs\GenerateAtmTransactionReportJob;
|
||||||
|
use Modules\Webstatement\Models\AtmTransactionReportLog;
|
||||||
|
|
||||||
|
class AtmTransactionReportController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the ATM transaction reports.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
return view('webstatement::atm-reports.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created ATM transaction report request.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'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 = [
|
||||||
|
'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 = AtmTransactionReportLog::create($reportData);
|
||||||
|
|
||||||
|
// Dispatch the job to generate the report
|
||||||
|
try {
|
||||||
|
GenerateAtmTransactionReportJob::dispatch($period, $reportRequest->id);
|
||||||
|
|
||||||
|
$reportRequest->update([
|
||||||
|
'status' => 'processing',
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$reportRequest->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'error_message' => $e->getMessage(),
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('atm-reports.index')
|
||||||
|
->with('success', 'ATM Transaction report request has been created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new report request.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('webstatement::atm-reports.create', compact('branches'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified report request.
|
||||||
|
*/
|
||||||
|
public function show(AtmTransactionReportLog $atmReport)
|
||||||
|
{
|
||||||
|
$atmReport->load(['user', 'creator', 'authorizer']);
|
||||||
|
return view('webstatement::atm-reports.show', compact('atmReport'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the report if available.
|
||||||
|
*/
|
||||||
|
public function download(AtmTransactionReportLog $atmReport)
|
||||||
|
{
|
||||||
|
// Check if report is available
|
||||||
|
if ($atmReport->status !== 'completed' || !$atmReport->file_path) {
|
||||||
|
return back()->with('error', 'Report is not available for download.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update download status
|
||||||
|
$atmReport->update([
|
||||||
|
'is_downloaded' => true,
|
||||||
|
'downloaded_at' => now(),
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Download the file
|
||||||
|
$filePath = $atmReport->file_path;
|
||||||
|
if (Storage::exists($filePath)) {
|
||||||
|
$fileName = "atm_transaction_report_{$atmReport->period}.csv";
|
||||||
|
return Storage::download($filePath, $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('error', 'Report file not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorize a report request.
|
||||||
|
*/
|
||||||
|
public function authorize(Request $request, AtmTransactionReportLog $atmReport)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'authorization_status' => ['required', Rule::in(['approved', 'rejected'])],
|
||||||
|
'remarks' => ['nullable', 'string', 'max:255'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update authorization status
|
||||||
|
$atmReport->update([
|
||||||
|
'authorization_status' => $request->authorization_status,
|
||||||
|
'authorized_by' => Auth::id(),
|
||||||
|
'authorized_at' => now(),
|
||||||
|
'remarks' => $request->remarks,
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$statusText = $request->authorization_status === 'approved' ? 'approved' : 'rejected';
|
||||||
|
|
||||||
|
return redirect()->route('atm-reports.show', $atmReport->id)
|
||||||
|
->with('success', "ATM Transaction report request has been {$statusText} successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide data for datatables.
|
||||||
|
*/
|
||||||
|
public function dataForDatatables(Request $request)
|
||||||
|
{
|
||||||
|
// Retrieve data from the database
|
||||||
|
$query = AtmTransactionReportLog::query();
|
||||||
|
|
||||||
|
// Apply search filter if provided
|
||||||
|
if ($request->has('search') && !empty($request->get('search'))) {
|
||||||
|
$search = $request->get('search');
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('period', 'LIKE', "%$search%")
|
||||||
|
->orWhere('status', 'LIKE', "%$search%")
|
||||||
|
->orWhere('authorization_status', 'LIKE', "%$search%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = [
|
||||||
|
'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 (remove branch since it's not used anymore)
|
||||||
|
$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,
|
||||||
|
'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' => dateFormat($item->created_at, 1, 1),
|
||||||
|
'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,
|
||||||
|
'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;
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'draw' => $request->get('draw'),
|
||||||
|
'recordsTotal' => $totalRecords,
|
||||||
|
'recordsFiltered' => $filteredRecords,
|
||||||
|
'pageCount' => $pageCount,
|
||||||
|
'page' => $currentPage,
|
||||||
|
'totalCount' => $totalRecords,
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a report request.
|
||||||
|
*/
|
||||||
|
public function destroy(AtmTransactionReportLog $atmReport)
|
||||||
|
{
|
||||||
|
// Delete the file if exists
|
||||||
|
if ($atmReport->file_path && Storage::exists($atmReport->file_path)) {
|
||||||
|
Storage::delete($atmReport->file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the report request
|
||||||
|
$atmReport->delete();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'ATM Transaction report deleted successfully.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send report to email
|
||||||
|
*/
|
||||||
|
public function sendEmail($id)
|
||||||
|
{
|
||||||
|
$atmReport = AtmTransactionReportLog::findOrFail($id);
|
||||||
|
|
||||||
|
// Check if report has email
|
||||||
|
if (empty($atmReport->email)) {
|
||||||
|
return redirect()->back()->with('error', 'No email address provided for this report.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if report is available
|
||||||
|
if ($atmReport->status !== 'completed' || !$atmReport->file_path) {
|
||||||
|
return redirect()->back()->with('error', 'Report is not available for sending.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send email with report attachment
|
||||||
|
// Implementation depends on your email system
|
||||||
|
// Mail::to($atmReport->email)->send(new AtmTransactionReportEmail($atmReport));
|
||||||
|
|
||||||
|
$atmReport->update([
|
||||||
|
'email_sent' => true,
|
||||||
|
'email_sent_at' => now(),
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'ATM Transaction report sent to email successfully.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Failed to send ATM Transaction report email: ' . $e->getMessage());
|
||||||
|
return redirect()->back()->with('error', 'Failed to send email: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry generating the ATM transaction report
|
||||||
|
*/
|
||||||
|
public function retry(AtmTransactionReportLog $atmReport)
|
||||||
|
{
|
||||||
|
// Check if retry is allowed (failed, pending, or processing for more than 1 hour)
|
||||||
|
$allowedStatuses = ['failed', 'pending'];
|
||||||
|
$isProcessingTooLong = $atmReport->status === 'processing' &&
|
||||||
|
$atmReport->updated_at->diffInHours(now()) >= 1;
|
||||||
|
|
||||||
|
if (!in_array($atmReport->status, $allowedStatuses) && !$isProcessingTooLong) {
|
||||||
|
return back()->with('error', 'Report can only be retried if status is failed, pending, or processing for more than 1 hour.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If it was processing for too long, mark it as failed first
|
||||||
|
if ($isProcessingTooLong) {
|
||||||
|
$atmReport->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'error_message' => 'Processing timeout - exceeded 1 hour limit',
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the report status and clear previous data
|
||||||
|
$atmReport->update([
|
||||||
|
'status' => 'processing',
|
||||||
|
'error_message' => null,
|
||||||
|
'file_path' => null,
|
||||||
|
'file_size' => null,
|
||||||
|
'record_count' => null,
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Dispatch the job again
|
||||||
|
GenerateAtmTransactionReportJob::dispatch($atmReport->period, $atmReport->id);
|
||||||
|
|
||||||
|
return back()->with('success', 'ATM Transaction report job has been retried successfully.');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$atmReport->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'error_message' => $e->getMessage(),
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('error', 'Failed to retry report generation: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if report can be retried
|
||||||
|
*/
|
||||||
|
public function canRetry(AtmTransactionReportLog $atmReport)
|
||||||
|
{
|
||||||
|
$allowedStatuses = ['failed', 'pending'];
|
||||||
|
$isProcessingTooLong = $atmReport->status === 'processing' &&
|
||||||
|
$atmReport->updated_at->diffInHours(now()) >= 1;
|
||||||
|
|
||||||
|
return in_array($atmReport->status, $allowedStatuses) ||
|
||||||
|
$isProcessingTooLong ||
|
||||||
|
($atmReport->status === 'completed' && !$atmReport->file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
297
app/Http/Controllers/EmailStatementLogController.php
Normal file
297
app/Http/Controllers/EmailStatementLogController.php
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Modules\Webstatement\Models\PrintStatementLog;
|
||||||
|
use Modules\Basicdata\Models\Branch;
|
||||||
|
use Modules\Webstatement\Jobs\SendStatementEmailJob;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller untuk mengelola log pengiriman statement email
|
||||||
|
* Mendukung log untuk pengiriman per rekening, per cabang, atau seluruh cabang
|
||||||
|
*/
|
||||||
|
class EmailStatementLogController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
Log::info('Accessing email statement log index page', [
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'ip_address' => $request->ip()
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$branches = Branch::orderBy('name')->get();
|
||||||
|
return view('webstatement::email-statement-logs.index', compact('branches'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to load email statement log index page', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
return back()->with('error', 'Gagal memuat halaman log pengiriman email statement.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataForDatatables(Request $request)
|
||||||
|
{
|
||||||
|
Log::info('Fetching email statement log data for datatables', [
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'filters' => $request->only(['branch_code', 'account_number', 'period_from', 'period_to', 'request_type', 'status'])
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$query = PrintStatementLog::query()
|
||||||
|
->with(['user', 'branch'])
|
||||||
|
->select([
|
||||||
|
'id',
|
||||||
|
'user_id',
|
||||||
|
'branch_code',
|
||||||
|
'account_number',
|
||||||
|
'request_type',
|
||||||
|
'batch_id',
|
||||||
|
'total_accounts',
|
||||||
|
'processed_accounts',
|
||||||
|
'success_count',
|
||||||
|
'failed_count',
|
||||||
|
'status',
|
||||||
|
'period_from',
|
||||||
|
'period_to',
|
||||||
|
'email',
|
||||||
|
'email_sent_at',
|
||||||
|
'is_available',
|
||||||
|
'authorization_status',
|
||||||
|
'started_at',
|
||||||
|
'completed_at',
|
||||||
|
'created_at',
|
||||||
|
'updated_at'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Filter berdasarkan branch
|
||||||
|
if ($request->filled('branch_code')) {
|
||||||
|
$query->where('branch_code', $request->branch_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter berdasarkan account number (hanya untuk single account)
|
||||||
|
if ($request->filled('account_number')) {
|
||||||
|
$query->where('account_number', 'like', '%' . $request->account_number . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter berdasarkan request type
|
||||||
|
if ($request->filled('request_type')) {
|
||||||
|
$query->where('request_type', $request->request_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter berdasarkan status
|
||||||
|
if ($request->filled('status')) {
|
||||||
|
$query->where('status', $request->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter berdasarkan periode
|
||||||
|
if ($request->filled('period_from')) {
|
||||||
|
$query->where('period_from', '>=', $request->period_from);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('period_to')) {
|
||||||
|
$query->where('period_to', '<=', $request->period_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter berdasarkan tanggal
|
||||||
|
if ($request->filled('date_from')) {
|
||||||
|
$query->whereDate('created_at', '>=', $request->date_from);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('date_to')) {
|
||||||
|
$query->whereDate('created_at', '<=', $request->date_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->orderBy('created_at', 'desc');
|
||||||
|
|
||||||
|
$totalRecords = $query->count();
|
||||||
|
|
||||||
|
if ($request->filled('start')) {
|
||||||
|
$query->skip($request->start);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('length') && $request->length != -1) {
|
||||||
|
$query->take($request->length);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $query->get();
|
||||||
|
|
||||||
|
$data = $logs->map(function ($log) {
|
||||||
|
return [
|
||||||
|
'id' => $log->id,
|
||||||
|
'request_type' => $this->formatRequestType($log->request_type),
|
||||||
|
'branch_code' => $log->branch_code,
|
||||||
|
'branch_name' => $log->branch->name ?? 'N/A',
|
||||||
|
'account_number' => $log->account_number ?? '-',
|
||||||
|
'period_display' => $log->period_display,
|
||||||
|
'batch_id' => $log->batch_id,
|
||||||
|
'total_accounts' => $log->total_accounts ?? 1,
|
||||||
|
'processed_accounts' => $log->processed_accounts ?? 0,
|
||||||
|
'success_count' => $log->success_count ?? 0,
|
||||||
|
'failed_count' => $log->failed_count ?? 0,
|
||||||
|
'progress_percentage' => $log->getProgressPercentage(),
|
||||||
|
'success_rate' => $log->getSuccessRate(),
|
||||||
|
'status' => $this->formatStatus($log->status),
|
||||||
|
'email' => $log->email,
|
||||||
|
'email_status' => $log->email_sent_at ? 'Terkirim' : 'Pending',
|
||||||
|
'email_sent_at' => $log->email_sent_at ?? '-',
|
||||||
|
'authorization_status' => ucfirst($log->authorization_status),
|
||||||
|
'user_name' => $log->user->name ?? 'System',
|
||||||
|
'started_at' => $log->started_at ? $log->started_at->format('d/m/Y H:i:s') : '-',
|
||||||
|
'completed_at' => $log->completed_at ? $log->completed_at->format('d/m/Y H:i:s') : '-',
|
||||||
|
'created_at' => $log->created_at->format('d/m/Y H:i:s'),
|
||||||
|
'actions' => $this->generateActionButtons($log)
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'draw' => intval($request->draw),
|
||||||
|
'recordsTotal' => $totalRecords,
|
||||||
|
'recordsFiltered' => $totalRecords,
|
||||||
|
'data' => $data
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Failed to fetch email statement log data', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'draw' => intval($request->draw),
|
||||||
|
'recordsTotal' => 0,
|
||||||
|
'recordsFiltered' => 0,
|
||||||
|
'data' => [],
|
||||||
|
'error' => 'Gagal memuat data log pengiriman email statement.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$log = PrintStatementLog::with(['user', 'branch'])->findOrFail($id);
|
||||||
|
return view('webstatement::email-statement-logs.show', compact('log'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to load email statement log detail', [
|
||||||
|
'log_id' => $id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
return back()->with('error', 'Log pengiriman email statement tidak ditemukan.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengirim ulang email statement untuk batch atau single account
|
||||||
|
*/
|
||||||
|
public function resendEmail(Request $request, $id)
|
||||||
|
{
|
||||||
|
Log::info('Attempting to resend statement email', [
|
||||||
|
'log_id' => $id,
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$log = PrintStatementLog::findOrFail($id);
|
||||||
|
|
||||||
|
// Buat batch ID baru untuk resend
|
||||||
|
$newBatchId = 'resend_' . time() . '_' . $log->id;
|
||||||
|
|
||||||
|
// Dispatch job dengan parameter yang sama
|
||||||
|
SendStatementEmailJob::dispatch(
|
||||||
|
$log->period_from,
|
||||||
|
$log->request_type,
|
||||||
|
$log->request_type === 'single_account' ? $log->account_number :
|
||||||
|
($log->request_type === 'branch' ? $log->branch_code : null),
|
||||||
|
$newBatchId,
|
||||||
|
$log->id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset status untuk tracking ulang
|
||||||
|
$log->update([
|
||||||
|
'status' => 'pending',
|
||||||
|
'batch_id' => $newBatchId,
|
||||||
|
'processed_accounts' => 0,
|
||||||
|
'success_count' => 0,
|
||||||
|
'failed_count' => 0,
|
||||||
|
'started_at' => null,
|
||||||
|
'completed_at' => null,
|
||||||
|
'error_message' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
Log::info('Statement email resend job dispatched successfully', [
|
||||||
|
'log_id' => $id,
|
||||||
|
'new_batch_id' => $newBatchId,
|
||||||
|
'request_type' => $log->request_type
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Email statement berhasil dijadwalkan untuk dikirim ulang.');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Failed to resend statement email', [
|
||||||
|
'log_id' => $id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
return back()->with('error', 'Gagal mengirim ulang email statement.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatRequestType($requestType)
|
||||||
|
{
|
||||||
|
$types = [
|
||||||
|
'single_account' => 'Single Account',
|
||||||
|
'branch' => 'Per Cabang',
|
||||||
|
'all_branches' => 'Seluruh Cabang'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $types[$requestType] ?? $requestType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatStatus($status)
|
||||||
|
{
|
||||||
|
$statuses = [
|
||||||
|
'pending' => '<span class="badge badge-warning">Pending</span>',
|
||||||
|
'processing' => '<span class="badge badge-info">Processing</span>',
|
||||||
|
'completed' => '<span class="badge badge-success">Completed</span>',
|
||||||
|
'failed' => '<span class="badge badge-danger">Failed</span>'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $statuses[$status] ?? $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateActionButtons(PrintStatementLog $log)
|
||||||
|
{
|
||||||
|
$buttons = [];
|
||||||
|
|
||||||
|
// Tombol view detail
|
||||||
|
$buttons[] = '<a href="' . route('email-statement-logs.show', $log->id) . '" class="btn btn-sm btn-icon btn-clear btn-light" title="Lihat Detail">' .
|
||||||
|
'<i class="text-base text-gray-500 ki-filled ki-eye"></i>' .
|
||||||
|
'</a>';
|
||||||
|
|
||||||
|
// Tombol resend email
|
||||||
|
if (in_array($log->status, ['completed', 'failed']) && $log->authorization_status === 'approved') {
|
||||||
|
$buttons[] = '<button onclick="resendEmail(' . $log->id . ')" class="btn btn-sm btn-icon btn-clear btn-light" title="Kirim Ulang Email">' .
|
||||||
|
'<i class="text-base ki-filled ki-message-text-2 text-primary"></i>' .
|
||||||
|
'</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' ', $buttons);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,10 @@
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Log;
|
|
||||||
use Modules\Basicdata\Models\Branch;
|
use Modules\Basicdata\Models\Branch;
|
||||||
use Modules\Webstatement\Http\Requests\PrintStatementRequest;
|
use Modules\Webstatement\Http\Requests\PrintStatementRequest;
|
||||||
use Modules\Webstatement\Mail\StatementEmail;
|
use Modules\Webstatement\Mail\StatementEmail;
|
||||||
@@ -30,25 +31,57 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a newly created statement request.
|
* Store a newly created statement request.
|
||||||
|
* Menangani pembuatan request statement baru dengan logging dan transaksi database
|
||||||
*/
|
*/
|
||||||
public function store(PrintStatementRequest $request)
|
public function store(PrintStatementRequest $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validated();
|
DB::beginTransaction();
|
||||||
|
|
||||||
// Add user tracking data
|
try {
|
||||||
$validated['user_id'] = Auth::id();
|
$validated = $request->validated();
|
||||||
$validated['created_by'] = Auth::id();
|
|
||||||
$validated['ip_address'] = $request->ip();
|
|
||||||
$validated['user_agent'] = $request->userAgent();
|
|
||||||
|
|
||||||
// Create the statement log
|
// Add user tracking data dan field baru untuk single account request
|
||||||
$statement = PrintStatementLog::create($validated);
|
$validated['user_id'] = Auth::id();
|
||||||
|
$validated['created_by'] = Auth::id();
|
||||||
|
$validated['ip_address'] = $request->ip();
|
||||||
|
$validated['user_agent'] = $request->userAgent();
|
||||||
|
$validated['request_type'] = 'single_account'; // Default untuk request manual
|
||||||
|
$validated['status'] = 'pending'; // Status awal
|
||||||
|
$validated['total_accounts'] = 1; // Untuk single account
|
||||||
|
$validated['processed_accounts'] = 0;
|
||||||
|
$validated['success_count'] = 0;
|
||||||
|
$validated['failed_count'] = 0;
|
||||||
|
|
||||||
// Process statement availability check (this would be implemented based on your system)
|
// Create the statement log
|
||||||
$this->checkStatementAvailability($statement);
|
$statement = PrintStatementLog::create($validated);
|
||||||
|
|
||||||
return redirect()->route('statements.index')
|
// Log aktivitas
|
||||||
|
Log::info('Statement request created', [
|
||||||
|
'statement_id' => $statement->id,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'account_number' => $statement->account_number,
|
||||||
|
'request_type' => $statement->request_type
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Process statement availability check
|
||||||
|
$this->checkStatementAvailability($statement);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()->route('statements.index')
|
||||||
->with('success', 'Statement request has been created successfully.');
|
->with('success', 'Statement request has been created successfully.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
Log::error('Failed to create statement request', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'user_id' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Failed to create statement request: ' . $e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,68 +95,106 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the statement is available in the system.
|
* Check if the statement is available in the system.
|
||||||
* This is a placeholder method - implement according to your system.
|
* Memperbarui status availability dengan logging
|
||||||
*/
|
*/
|
||||||
protected function checkStatementAvailability(PrintStatementLog $statement)
|
protected function checkStatementAvailability(PrintStatementLog $statement)
|
||||||
{
|
{
|
||||||
// This would be implemented based on your system's logic
|
DB::beginTransaction();
|
||||||
// For example, checking an API or database for statement availability
|
|
||||||
$disk = Storage::disk('sftpStatement');
|
|
||||||
|
|
||||||
//format folder /periode/Print/branch_code/account_number.pdf
|
try {
|
||||||
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
$disk = Storage::disk('sftpStatement');
|
||||||
|
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||||
|
|
||||||
// Check if the statement exists in the storage
|
if ($statement->is_period_range && $statement->period_to) {
|
||||||
if ($statement->is_period_range && $statement->period_to) {
|
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
|
||||||
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
|
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
|
||||||
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
|
|
||||||
|
|
||||||
// Loop through each month in the range
|
$missingPeriods = [];
|
||||||
$missingPeriods = [];
|
$availablePeriods = [];
|
||||||
$availablePeriods = [];
|
|
||||||
|
|
||||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||||
$periodFormatted = $period->format('Ym');
|
$periodFormatted = $period->format('Ym');
|
||||||
$periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
$periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||||
|
|
||||||
if ($disk->exists($periodPath)) {
|
if ($disk->exists($periodPath)) {
|
||||||
$availablePeriods[] = $periodFormatted;
|
$availablePeriods[] = $periodFormatted;
|
||||||
} else {
|
} else {
|
||||||
$missingPeriods[] = $periodFormatted;
|
$missingPeriods[] = $periodFormatted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if (count($missingPeriods) > 0) {
|
||||||
|
$notes = "Missing periods: " . implode(', ', $missingPeriods);
|
||||||
|
$statement->update([
|
||||||
|
'is_available' => false,
|
||||||
|
'remarks' => $notes,
|
||||||
|
'updated_by' => Auth::id(),
|
||||||
|
'status' => 'failed'
|
||||||
|
]);
|
||||||
|
|
||||||
// If any period is missing, the statement is not available
|
Log::warning('Statement not available - missing periods', [
|
||||||
if (count($missingPeriods) > 0) {
|
'statement_id' => $statement->id,
|
||||||
$notes = "Missing periods: " . implode(', ', $missingPeriods);
|
'missing_periods' => $missingPeriods
|
||||||
$statement->update([
|
]);
|
||||||
'is_available' => false,
|
} else {
|
||||||
'remarks' => $notes,
|
$statement->update([
|
||||||
'updated_by' => Auth::id()
|
'is_available' => true,
|
||||||
]);
|
'updated_by' => Auth::id(),
|
||||||
return;
|
'status' => 'completed',
|
||||||
} else {
|
'processed_accounts' => 1,
|
||||||
// All periods are available
|
'success_count' => 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('Statement available - all periods found', [
|
||||||
|
'statement_id' => $statement->id,
|
||||||
|
'available_periods' => $availablePeriods
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else if ($disk->exists($filePath)) {
|
||||||
$statement->update([
|
$statement->update([
|
||||||
'is_available' => true,
|
'is_available' => true,
|
||||||
'updated_by' => Auth::id()
|
'updated_by' => Auth::id(),
|
||||||
|
'status' => 'completed',
|
||||||
|
'processed_accounts' => 1,
|
||||||
|
'success_count' => 1
|
||||||
]);
|
]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if ($disk->exists($filePath)) {
|
|
||||||
$statement->update([
|
|
||||||
'is_available' => true,
|
|
||||||
'updated_by' => Auth::id()
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$statement->update([
|
Log::info('Statement available', [
|
||||||
'is_available' => false,
|
'statement_id' => $statement->id,
|
||||||
'updated_by' => Auth::id()
|
'file_path' => $filePath
|
||||||
]);
|
]);
|
||||||
return;
|
} else {
|
||||||
|
$statement->update([
|
||||||
|
'is_available' => false,
|
||||||
|
'updated_by' => Auth::id(),
|
||||||
|
'status' => 'failed',
|
||||||
|
'processed_accounts' => 1,
|
||||||
|
'failed_count' => 1,
|
||||||
|
'error_message' => 'Statement file not found'
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::warning('Statement not available', [
|
||||||
|
'statement_id' => $statement->id,
|
||||||
|
'file_path' => $filePath
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
Log::error('Error checking statement availability', [
|
||||||
|
'statement_id' => $statement->id,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$statement->update([
|
||||||
|
'is_available' => false,
|
||||||
|
'status' => 'failed',
|
||||||
|
'error_message' => $e->getMessage(),
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,94 +208,51 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the statement if available and authorized.
|
* Download the statement if available and authorized.
|
||||||
|
* Memperbarui status download dengan logging dan transaksi
|
||||||
*/
|
*/
|
||||||
public function download(PrintStatementLog $statement)
|
public function download(PrintStatementLog $statement)
|
||||||
{
|
{
|
||||||
// Check if statement is available and authorized
|
|
||||||
if (!$statement->is_available) {
|
if (!$statement->is_available) {
|
||||||
return back()->with('error', 'Statement is not available for download.');
|
return back()->with('error', 'Statement is not available for download.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* if ($statement->authorization_status !== 'approved') {
|
DB::beginTransaction();
|
||||||
return back()->with('error', 'Statement download requires authorization.');
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Update download status
|
try {
|
||||||
$statement->update([
|
// Update download status
|
||||||
'is_downloaded' => true,
|
$statement->update([
|
||||||
'downloaded_at' => now(),
|
'is_downloaded' => true,
|
||||||
'updated_by' => Auth::id()
|
'downloaded_at' => now(),
|
||||||
]);
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
// Generate or fetch the statement file (implementation depends on your system)
|
Log::info('Statement downloaded', [
|
||||||
$disk = Storage::disk('sftpStatement');
|
'statement_id' => $statement->id,
|
||||||
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
'user_id' => Auth::id(),
|
||||||
|
'account_number' => $statement->account_number
|
||||||
|
]);
|
||||||
|
|
||||||
if ($statement->is_period_range && $statement->period_to) {
|
DB::commit();
|
||||||
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
|
|
||||||
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
|
|
||||||
|
|
||||||
// Loop through each month in the range
|
// Generate or fetch the statement file
|
||||||
$missingPeriods = [];
|
$disk = Storage::disk('sftpStatement');
|
||||||
$availablePeriods = [];
|
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||||
|
|
||||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
if ($statement->is_period_range && $statement->period_to) {
|
||||||
$periodFormatted = $period->format('Ym');
|
// Handle period range download (existing logic)
|
||||||
$periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
// ... existing zip creation logic ...
|
||||||
|
} else if ($disk->exists($filePath)) {
|
||||||
if ($disk->exists($periodPath)) {
|
return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf");
|
||||||
$availablePeriods[] = $periodFormatted;
|
|
||||||
} else {
|
|
||||||
$missingPeriods[] = $periodFormatted;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
// If any period is missing, the statement is not available
|
Log::error('Failed to download statement', [
|
||||||
if (count($availablePeriods) > 0) {
|
'statement_id' => $statement->id,
|
||||||
// Create a temporary zip file
|
'error' => $e->getMessage()
|
||||||
$zipFileName = "{$statement->account_number}_{$statement->period_from}_to_{$statement->period_to}.zip";
|
]);
|
||||||
$zipFilePath = storage_path("app/temp/{$zipFileName}");
|
|
||||||
|
|
||||||
// Ensure the temp directory exists
|
return back()->with('error', 'Failed to download statement: ' . $e->getMessage());
|
||||||
if (!file_exists(storage_path('app/temp'))) {
|
|
||||||
mkdir(storage_path('app/temp'), 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new zip archive
|
|
||||||
$zip = new ZipArchive();
|
|
||||||
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
|
|
||||||
// Add each available statement to the zip
|
|
||||||
foreach ($availablePeriods as $period) {
|
|
||||||
$filePath = "{$period}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
|
||||||
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
|
||||||
|
|
||||||
// Download the file from SFTP to local storage temporarily
|
|
||||||
file_put_contents($localFilePath, $disk->get($filePath));
|
|
||||||
|
|
||||||
// Add the file to the zip
|
|
||||||
$zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
$zip->close();
|
|
||||||
|
|
||||||
// Clean up temporary files
|
|
||||||
foreach ($availablePeriods as $period) {
|
|
||||||
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
|
||||||
if (file_exists($localFilePath)) {
|
|
||||||
unlink($localFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the zip file for download
|
|
||||||
return response()->download($zipFilePath, $zipFileName)->deleteFileAfterSend(true);
|
|
||||||
} else {
|
|
||||||
return back()->with('error', 'Failed to create zip archive.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return back()->with('error', 'No statements available for download.');
|
|
||||||
}
|
|
||||||
} else if ($disk->exists($filePath)) {
|
|
||||||
return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +303,9 @@
|
|||||||
->orWhere('branch_code', 'LIKE', "%$search%")
|
->orWhere('branch_code', 'LIKE', "%$search%")
|
||||||
->orWhere('period_from', 'LIKE', "%$search%")
|
->orWhere('period_from', 'LIKE', "%$search%")
|
||||||
->orWhere('period_to', 'LIKE', "%$search%")
|
->orWhere('period_to', 'LIKE', "%$search%")
|
||||||
->orWhere('authorization_status', 'LIKE', "%$search%");
|
->orWhere('authorization_status', 'LIKE', "%$search%")
|
||||||
|
->orWhere('request_type', 'LIKE', "%$search%")
|
||||||
|
->orWhere('status', 'LIKE', "%$search%");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,6 +319,10 @@
|
|||||||
$query->where('branch_code', $filter['value']);
|
$query->where('branch_code', $filter['value']);
|
||||||
} else if ($filter['column'] === 'authorization_status') {
|
} else if ($filter['column'] === 'authorization_status') {
|
||||||
$query->where('authorization_status', $filter['value']);
|
$query->where('authorization_status', $filter['value']);
|
||||||
|
} else if ($filter['column'] === 'request_type') {
|
||||||
|
$query->where('request_type', $filter['value']);
|
||||||
|
} else if ($filter['column'] === 'status') {
|
||||||
|
$query->where('status', $filter['value']);
|
||||||
} else if ($filter['column'] === 'is_downloaded') {
|
} else if ($filter['column'] === 'is_downloaded') {
|
||||||
$query->where('is_downloaded', filter_var($filter['value'], FILTER_VALIDATE_BOOLEAN));
|
$query->where('is_downloaded', filter_var($filter['value'], FILTER_VALIDATE_BOOLEAN));
|
||||||
}
|
}
|
||||||
@@ -306,9 +340,10 @@
|
|||||||
'branch' => 'branch_code',
|
'branch' => 'branch_code',
|
||||||
'account' => 'account_number',
|
'account' => 'account_number',
|
||||||
'period' => 'period_from',
|
'period' => 'period_from',
|
||||||
'status' => 'authorization_status',
|
'auth_status' => 'authorization_status',
|
||||||
|
'request_type' => 'request_type',
|
||||||
|
'status' => 'status',
|
||||||
'remarks' => 'remarks',
|
'remarks' => 'remarks',
|
||||||
// Add more mappings as needed
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$dbColumn = $columnMap[$column] ?? $column;
|
$dbColumn = $columnMap[$column] ?? $column;
|
||||||
@@ -504,6 +539,14 @@
|
|||||||
'updated_by' => Auth::id()
|
'updated_by' => Auth::id()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Log::info('Statement email sent successfully', [
|
||||||
|
'statement_id' => $statement->id,
|
||||||
|
'email' => $statement->email,
|
||||||
|
'user_id' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Statement has been sent to ' . $statement->email);
|
return redirect()->back()->with('success', 'Statement has been sent to ' . $statement->email);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// Log the error
|
// Log the error
|
||||||
|
|||||||
@@ -1,125 +1,125 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Http\Requests;
|
namespace Modules\Webstatement\Http\Requests;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Modules\Webstatement\Models\PrintStatementLog as Statement;
|
use Modules\Webstatement\Models\PrintStatementLog as Statement;
|
||||||
|
|
||||||
class PrintStatementRequest extends FormRequest
|
class PrintStatementRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
/**
|
return true;
|
||||||
* Determine if the user is authorized to make this request.
|
}
|
||||||
*/
|
|
||||||
public function authorize()
|
|
||||||
: bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
*
|
*/
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
public function rules(): array
|
||||||
*/
|
{
|
||||||
public function rules()
|
$rules = [
|
||||||
: array
|
'branch_code' => ['required', 'string', 'exists:branches,code'],
|
||||||
{
|
'account_number' => ['required', 'string'],
|
||||||
$rules = [
|
'is_period_range' => ['sometimes', 'boolean'],
|
||||||
'branch_code' => ['required', 'string', 'exists:branches,code'],
|
'email' => ['nullable', 'email'],
|
||||||
'account_number' => ['required', 'string'],
|
'email_sent_at' => ['nullable', 'timestamp'],
|
||||||
'is_period_range' => ['sometimes', 'boolean'],
|
'request_type' => ['sometimes', 'string', 'in:single_account,branch,all_branches'],
|
||||||
'email' => ['nullable', 'email'],
|
'batch_id' => ['nullable', 'string'],
|
||||||
'email_sent_at' => ['nullable', 'timestamp'],
|
'period_from' => [
|
||||||
'period_from' => [
|
'required',
|
||||||
'required',
|
'string',
|
||||||
'string',
|
'regex:/^\d{6}$/', // YYYYMM format
|
||||||
'regex:/^\d{6}$/', // YYYYMM format
|
// Prevent duplicate requests with same account number and period
|
||||||
// Prevent duplicate requests with same account number and period
|
function ($attribute, $value, $fail) {
|
||||||
function ($attribute, $value, $fail) {
|
$query = Statement::where('account_number', $this->input('account_number'))
|
||||||
$query = Statement::where('account_number', $this->input('account_number'))
|
->where('authorization_status', '!=', 'rejected')
|
||||||
->where('authorization_status', '!=', 'rejected')
|
->where('period_from', $value);
|
||||||
->where('period_from', $value);
|
|
||||||
|
|
||||||
// If this is an update request, exclude the current record
|
// If this is an update request, exclude the current record
|
||||||
if ($this->route('statement')) {
|
if ($this->route('statement')) {
|
||||||
$query->where('id', '!=', $this->route('statement'));
|
$query->where('id', '!=', $this->route('statement'));
|
||||||
}
|
|
||||||
|
|
||||||
// If period_to is provided, check for overlapping periods
|
|
||||||
if ($this->input('period_to')) {
|
|
||||||
$query->where(function ($q) use ($value) {
|
|
||||||
$q->where('period_from', '<=', $this->input('period_to'))
|
|
||||||
->where('period_to', '>=', $value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($query->exists()) {
|
|
||||||
$fail('A statement request with this account number and period already exists.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
// If it's a period range, require period_to
|
// If period_to is provided, check for overlapping periods
|
||||||
if ($this->input('period_to')) {
|
if ($this->input('period_to')) {
|
||||||
$rules['period_to'] = [
|
$query->where(function ($q) use ($value) {
|
||||||
'required',
|
$q->where('period_from', '<=', $this->input('period_to'))
|
||||||
'string',
|
->where('period_to', '>=', $value);
|
||||||
'regex:/^\d{6}$/', // YYYYMM format
|
});
|
||||||
'gte:period_from' // period_to must be greater than or equal to period_from
|
}
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rules;
|
if ($query->exists()) {
|
||||||
}
|
$fail('A statement request with this account number and period already exists.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
// If it's a period range, require period_to
|
||||||
* Get custom messages for validator errors.
|
if ($this->input('period_to')) {
|
||||||
*
|
$rules['period_to'] = [
|
||||||
* @return array
|
'required',
|
||||||
*/
|
'string',
|
||||||
public function messages()
|
'regex:/^\d{6}$/', // YYYYMM format
|
||||||
: array
|
'gte:period_from' // period_to must be greater than or equal to period_from
|
||||||
{
|
|
||||||
return [
|
|
||||||
'branch_code.required' => 'Branch code is required',
|
|
||||||
'branch_code.exists' => 'Selected branch does not exist',
|
|
||||||
'account_number.required' => 'Account number is required',
|
|
||||||
'period_from.required' => 'Period is required',
|
|
||||||
'period_from.regex' => 'Period must be in YYYYMM format',
|
|
||||||
'period_to.required' => 'End period is required for period range',
|
|
||||||
'period_to.regex' => 'End period must be in YYYYMM format',
|
|
||||||
'period_to.gte' => 'End period must be after or equal to start period',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return $rules;
|
||||||
* Prepare the data for validation.
|
}
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function prepareForValidation()
|
|
||||||
: void
|
|
||||||
{
|
|
||||||
if ($this->has('period_from')) {
|
|
||||||
//conver to YYYYMM format
|
|
||||||
$this->merge([
|
|
||||||
'period_from' => substr($this->period_from, 0, 4) . substr($this->period_from, 5, 2),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->has('period_to')) {
|
/**
|
||||||
//conver to YYYYMM format
|
* Get custom messages for validator errors.
|
||||||
$this->merge([
|
*/
|
||||||
'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2),
|
public function messages(): array
|
||||||
]);
|
{
|
||||||
}
|
return [
|
||||||
|
'branch_code.required' => 'Branch code is required',
|
||||||
|
'branch_code.exists' => 'Selected branch does not exist',
|
||||||
|
'account_number.required' => 'Account number is required',
|
||||||
|
'period_from.required' => 'Period is required',
|
||||||
|
'period_from.regex' => 'Period must be in YYYYMM format',
|
||||||
|
'period_to.required' => 'End period is required for period range',
|
||||||
|
'period_to.regex' => 'End period must be in YYYYMM format',
|
||||||
|
'period_to.gte' => 'End period must be after or equal to start period',
|
||||||
|
'request_type.in' => 'Request type must be single_account, branch, or all_branches',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Convert is_period_range to boolean if it exists
|
/**
|
||||||
if ($this->has('period_to')) {
|
* Prepare the data for validation.
|
||||||
$this->merge([
|
*/
|
||||||
'is_period_range' => true,
|
protected function prepareForValidation(): void
|
||||||
]);
|
{
|
||||||
}
|
if ($this->has('period_from')) {
|
||||||
|
// Convert to YYYYMM format
|
||||||
|
$this->merge([
|
||||||
|
'period_from' => substr($this->period_from, 0, 4) . substr($this->period_from, 5, 2),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->has('period_to')) {
|
||||||
|
// Convert to YYYYMM format
|
||||||
|
$this->merge([
|
||||||
|
'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert is_period_range to boolean if it exists
|
||||||
|
if ($this->has('period_to')) {
|
||||||
|
$this->merge([
|
||||||
|
'is_period_range' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default request_type if not provided
|
||||||
|
if (!$this->has('request_type')) {
|
||||||
|
$this->merge([
|
||||||
|
'request_type' => 'single_account',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
156
app/Jobs/GenerateAtmTransactionReportJob.php
Normal file
156
app/Jobs/GenerateAtmTransactionReportJob.php
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<?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\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Modules\Webstatement\Models\AtmTransaction;
|
||||||
|
use Modules\Webstatement\Models\AtmTransactionReportLog;
|
||||||
|
|
||||||
|
class GenerateAtmTransactionReportJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
private string $period;
|
||||||
|
private const CHUNK_SIZE = 1000;
|
||||||
|
private const CSV_DELIMITER = ',';
|
||||||
|
private ?int $reportLogId;
|
||||||
|
|
||||||
|
public function __construct(string $period, ?int $reportLogId = null)
|
||||||
|
{
|
||||||
|
$this->period = $period;
|
||||||
|
$this->reportLogId = $reportLogId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$reportLog = null;
|
||||||
|
if ($this->reportLogId) {
|
||||||
|
$reportLog = AtmTransactionReportLog::find($this->reportLogId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log::info("Starting ATM Transaction report generation for period: {$this->period} and LogId: {$this->reportLogId}");
|
||||||
|
|
||||||
|
$filename = "atm_transaction_report_{$this->period}.csv";
|
||||||
|
$filePath = "reports/atm_transactions/{$filename}";
|
||||||
|
|
||||||
|
// Create directory if not exists
|
||||||
|
Storage::makeDirectory('reports/atm_transactions');
|
||||||
|
|
||||||
|
// Initialize CSV file with headers
|
||||||
|
$headers = [
|
||||||
|
'reff_no',
|
||||||
|
'pan',
|
||||||
|
'atm_id_terminal_id',
|
||||||
|
'amount',
|
||||||
|
'channel',
|
||||||
|
'account_no',
|
||||||
|
'internal_account',
|
||||||
|
'transaction_type',
|
||||||
|
'trans_ref',
|
||||||
|
'posting_date',
|
||||||
|
'stan',
|
||||||
|
'trans_status'
|
||||||
|
];
|
||||||
|
|
||||||
|
$csvContent = implode(self::CSV_DELIMITER, $headers) . "\n";
|
||||||
|
Storage::put($filePath, $csvContent);
|
||||||
|
|
||||||
|
$totalRecords = 0;
|
||||||
|
|
||||||
|
// Process data in chunks
|
||||||
|
AtmTransaction::select(
|
||||||
|
'retrieval_ref_no as reff_no',
|
||||||
|
'pan_number as pan',
|
||||||
|
'card_acc_id as atm_id_terminal_id',
|
||||||
|
'txn_amount as amount',
|
||||||
|
'merchant_id as channel',
|
||||||
|
'debit_acct_no as account_no',
|
||||||
|
'credit_acct_no as internal_account',
|
||||||
|
'txn_type as transaction_type',
|
||||||
|
'trans_ref',
|
||||||
|
'booking_date as posting_date',
|
||||||
|
'stan_no as stan',
|
||||||
|
'trans_status'
|
||||||
|
)
|
||||||
|
->where('booking_date', $this->period)
|
||||||
|
->chunk(self::CHUNK_SIZE, function ($transactions) use ($filePath, &$totalRecords) {
|
||||||
|
$csvRows = [];
|
||||||
|
|
||||||
|
foreach ($transactions as $transaction) {
|
||||||
|
$csvRows[] = implode(self::CSV_DELIMITER, [
|
||||||
|
$this->escapeCsvValue($transaction->reff_no),
|
||||||
|
$this->escapeCsvValue($transaction->pan),
|
||||||
|
$this->escapeCsvValue($transaction->atm_id_terminal_id),
|
||||||
|
$this->escapeCsvValue($transaction->amount),
|
||||||
|
$this->escapeCsvValue($transaction->channel),
|
||||||
|
$this->escapeCsvValue($transaction->account_no),
|
||||||
|
$this->escapeCsvValue($transaction->internal_account),
|
||||||
|
$this->escapeCsvValue($transaction->transaction_type),
|
||||||
|
$this->escapeCsvValue($transaction->trans_ref),
|
||||||
|
$this->escapeCsvValue($transaction->posting_date),
|
||||||
|
$this->escapeCsvValue($transaction->stan),
|
||||||
|
$this->escapeCsvValue($transaction->trans_status)
|
||||||
|
]);
|
||||||
|
$totalRecords++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($csvRows)) {
|
||||||
|
Storage::append($filePath, implode("\n", $csvRows));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update report log if exists
|
||||||
|
if ($reportLog) {
|
||||||
|
$reportLog->update([
|
||||||
|
'status' => 'completed',
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'file_size' => Storage::size($filePath),
|
||||||
|
'record_count' => $totalRecords,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("ATM Transaction report generated successfully. File: {$filePath}, Total records: {$totalRecords}");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($reportLog) {
|
||||||
|
$reportLog->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'error_message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error("Error generating ATM Transaction report for period {$this->period}: " . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape CSV values to handle commas and quotes
|
||||||
|
*/
|
||||||
|
private function escapeCsvValue($value): string
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = (string) $value;
|
||||||
|
|
||||||
|
// If value contains comma, quote, or newline, wrap in quotes and escape internal quotes
|
||||||
|
if (strpos($value, self::CSV_DELIMITER) !== false ||
|
||||||
|
strpos($value, '"') !== false ||
|
||||||
|
strpos($value, "\n") !== false) {
|
||||||
|
$value = '"' . str_replace('"', '""', $value) . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
418
app/Jobs/SendStatementEmailJob.php
Normal file
418
app/Jobs/SendStatementEmailJob.php
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
<?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\Mail;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Modules\Webstatement\Models\Account;
|
||||||
|
use Modules\Webstatement\Models\PrintStatementLog;
|
||||||
|
use Modules\Webstatement\Mail\StatementEmail;
|
||||||
|
use Modules\Basicdata\Models\Branch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job untuk mengirim email PDF statement ke nasabah
|
||||||
|
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
|
||||||
|
*/
|
||||||
|
class SendStatementEmailJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $period;
|
||||||
|
protected $requestType;
|
||||||
|
protected $targetValue; // account_number, branch_code, atau null untuk all
|
||||||
|
protected $batchId;
|
||||||
|
protected $logId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membuat instance job baru
|
||||||
|
*
|
||||||
|
* @param string $period Format: YYYYMM
|
||||||
|
* @param string $requestType 'single_account', 'branch', 'all_branches'
|
||||||
|
* @param string|null $targetValue account_number untuk single, branch_code untuk branch, null untuk all
|
||||||
|
* @param string|null $batchId ID batch untuk tracking
|
||||||
|
* @param int|null $logId ID log untuk update progress
|
||||||
|
*/
|
||||||
|
public function __construct($period, $requestType = 'single_account', $targetValue = null, $batchId = null, $logId = null)
|
||||||
|
{
|
||||||
|
$this->period = $period;
|
||||||
|
$this->requestType = $requestType;
|
||||||
|
$this->targetValue = $targetValue;
|
||||||
|
$this->batchId = $batchId ?? uniqid('batch_');
|
||||||
|
$this->logId = $logId;
|
||||||
|
|
||||||
|
Log::info('SendStatementEmailJob created', [
|
||||||
|
'period' => $this->period,
|
||||||
|
'request_type' => $this->requestType,
|
||||||
|
'target_value' => $this->targetValue,
|
||||||
|
'batch_id' => $this->batchId,
|
||||||
|
'log_id' => $this->logId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menjalankan job pengiriman email statement
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
Log::info('Starting SendStatementEmailJob execution', [
|
||||||
|
'batch_id' => $this->batchId,
|
||||||
|
'period' => $this->period,
|
||||||
|
'request_type' => $this->requestType,
|
||||||
|
'target_value' => $this->targetValue
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update log status menjadi processing
|
||||||
|
$this->updateLogStatus('processing', ['started_at' => now()]);
|
||||||
|
|
||||||
|
// Ambil accounts berdasarkan request type
|
||||||
|
$accounts = $this->getAccountsByRequestType();
|
||||||
|
|
||||||
|
if ($accounts->isEmpty()) {
|
||||||
|
Log::warning('No accounts with email found', [
|
||||||
|
'period' => $this->period,
|
||||||
|
'request_type' => $this->requestType,
|
||||||
|
'target_value' => $this->targetValue,
|
||||||
|
'batch_id' => $this->batchId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->updateLogStatus('completed', [
|
||||||
|
'completed_at' => now(),
|
||||||
|
'total_accounts' => 0,
|
||||||
|
'processed_accounts' => 0,
|
||||||
|
'success_count' => 0,
|
||||||
|
'failed_count' => 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total accounts
|
||||||
|
$this->updateLogStatus('processing', [
|
||||||
|
'total_accounts' => $accounts->count(),
|
||||||
|
'target_accounts' => $accounts->pluck('account_number')->toArray()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$successCount = 0;
|
||||||
|
$failedCount = 0;
|
||||||
|
$processedCount = 0;
|
||||||
|
|
||||||
|
foreach ($accounts as $account) {
|
||||||
|
try {
|
||||||
|
$this->sendStatementEmail($account);
|
||||||
|
$successCount++;
|
||||||
|
|
||||||
|
Log::info('Statement email sent successfully', [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'branch_code' => $account->branch_code,
|
||||||
|
'email' => $this->getEmailForAccount($account),
|
||||||
|
'batch_id' => $this->batchId
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$failedCount++;
|
||||||
|
|
||||||
|
Log::error('Failed to send statement email', [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'branch_code' => $account->branch_code,
|
||||||
|
'email' => $this->getEmailForAccount($account),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'batch_id' => $this->batchId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$processedCount++;
|
||||||
|
|
||||||
|
// Update progress setiap 10 account atau di akhir
|
||||||
|
if ($processedCount % 10 === 0 || $processedCount === $accounts->count()) {
|
||||||
|
$this->updateLogStatus('processing', [
|
||||||
|
'processed_accounts' => $processedCount,
|
||||||
|
'success_count' => $successCount,
|
||||||
|
'failed_count' => $failedCount
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status final
|
||||||
|
$finalStatus = $failedCount === 0 ? 'completed' : ($successCount === 0 ? 'failed' : 'completed');
|
||||||
|
$this->updateLogStatus($finalStatus, [
|
||||||
|
'completed_at' => now(),
|
||||||
|
'processed_accounts' => $processedCount,
|
||||||
|
'success_count' => $successCount,
|
||||||
|
'failed_count' => $failedCount
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
Log::info('SendStatementEmailJob completed', [
|
||||||
|
'batch_id' => $this->batchId,
|
||||||
|
'total_accounts' => $accounts->count(),
|
||||||
|
'success_count' => $successCount,
|
||||||
|
'failed_count' => $failedCount,
|
||||||
|
'final_status' => $finalStatus
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
$this->updateLogStatus('failed', [
|
||||||
|
'completed_at' => now(),
|
||||||
|
'error_message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::error('SendStatementEmailJob failed', [
|
||||||
|
'batch_id' => $this->batchId,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengambil accounts berdasarkan request type
|
||||||
|
*/
|
||||||
|
private function getAccountsByRequestType()
|
||||||
|
{
|
||||||
|
Log::info('Fetching accounts by request type', [
|
||||||
|
'period' => $this->period,
|
||||||
|
'request_type' => $this->requestType,
|
||||||
|
'target_value' => $this->targetValue
|
||||||
|
]);
|
||||||
|
|
||||||
|
$query = Account::with('customer')
|
||||||
|
->where('stmt_sent_type', 'BY.EMAIL');
|
||||||
|
|
||||||
|
switch ($this->requestType) {
|
||||||
|
case 'single_account':
|
||||||
|
if ($this->targetValue) {
|
||||||
|
$query->where('account_number', $this->targetValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'branch':
|
||||||
|
if ($this->targetValue) {
|
||||||
|
$query->where('branch_code', $this->targetValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'all_branches':
|
||||||
|
// Tidak ada filter tambahan, ambil semua
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new \InvalidArgumentException("Invalid request type: {$this->requestType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$accounts = $query->get();
|
||||||
|
|
||||||
|
// Filter accounts yang memiliki email
|
||||||
|
$accountsWithEmail = $accounts->filter(function ($account) {
|
||||||
|
return !empty($account->stmt_email) ||
|
||||||
|
($account->customer && !empty($account->customer->email));
|
||||||
|
});
|
||||||
|
|
||||||
|
Log::info('Accounts with email retrieved', [
|
||||||
|
'total_accounts' => $accounts->count(),
|
||||||
|
'accounts_with_email' => $accountsWithEmail->count(),
|
||||||
|
'request_type' => $this->requestType,
|
||||||
|
'batch_id' => $this->batchId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $accountsWithEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update status log
|
||||||
|
*/
|
||||||
|
private function updateLogStatus($status, $additionalData = [])
|
||||||
|
{
|
||||||
|
if (!$this->logId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$updateData = array_merge(['status' => $status], $additionalData);
|
||||||
|
PrintStatementLog::where('id', $this->logId)->update($updateData);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to update log status', [
|
||||||
|
'log_id' => $this->logId,
|
||||||
|
'status' => $status,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mendapatkan email untuk pengiriman statement
|
||||||
|
*
|
||||||
|
* @param Account $account
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
private function getEmailForAccount(Account $account)
|
||||||
|
{
|
||||||
|
// Prioritas pertama: stmt_email dari account
|
||||||
|
if (!empty($account->stmt_email)) {
|
||||||
|
Log::info('Using stmt_email from account', [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'email' => $account->stmt_email,
|
||||||
|
'batch_id' => $this->batchId
|
||||||
|
]);
|
||||||
|
return $account->stmt_email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritas kedua: email dari customer
|
||||||
|
if ($account->customer && !empty($account->customer->email)) {
|
||||||
|
Log::info('Using email from customer', [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'customer_code' => $account->customer_code,
|
||||||
|
'email' => $account->customer->email,
|
||||||
|
'batch_id' => $this->batchId
|
||||||
|
]);
|
||||||
|
return $account->customer->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('No email found for account', [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'customer_code' => $account->customer_code,
|
||||||
|
'batch_id' => $this->batchId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengirim email statement untuk account tertentu
|
||||||
|
*
|
||||||
|
* @param Account $account
|
||||||
|
* @return void
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function sendStatementEmail(Account $account)
|
||||||
|
{
|
||||||
|
// Dapatkan email untuk pengiriman
|
||||||
|
$emailAddress = $this->getEmailForAccount($account);
|
||||||
|
|
||||||
|
if (!$emailAddress) {
|
||||||
|
throw new \Exception("No email address found for account {$account->account_number}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah file PDF ada
|
||||||
|
$pdfPath = $this->getPdfPath($account->account_number, $account->branch_code);
|
||||||
|
|
||||||
|
if (!Storage::exists($pdfPath)) {
|
||||||
|
throw new \Exception("PDF file not found: {$pdfPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat atau update log statement
|
||||||
|
$statementLog = $this->createOrUpdateStatementLog($account);
|
||||||
|
|
||||||
|
// Dapatkan path absolut file
|
||||||
|
$absolutePdfPath = Storage::path($pdfPath);
|
||||||
|
|
||||||
|
// Kirim email
|
||||||
|
// Add delay between email sends to prevent rate limiting
|
||||||
|
sleep(1); // 2 second delay
|
||||||
|
Mail::to($emailAddress)->send(
|
||||||
|
new StatementEmail($statementLog, $absolutePdfPath, false)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update status log dengan email yang digunakan
|
||||||
|
$statementLog->update([
|
||||||
|
'email_sent_at' => now(),
|
||||||
|
'email_status' => 'sent',
|
||||||
|
'email_address' => $emailAddress // Simpan email yang digunakan untuk tracking
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('Email sent for account', [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'branch_code' => $account->branch_code,
|
||||||
|
'email' => $emailAddress,
|
||||||
|
'email_source' => !empty($account->stmt_email) ? 'account.stmt_email' : 'customer.email',
|
||||||
|
'pdf_path' => $pdfPath,
|
||||||
|
'batch_id' => $this->batchId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mendapatkan path file PDF statement
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @param string $branchCode
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getPdfPath($accountNumber, $branchCode)
|
||||||
|
{
|
||||||
|
return "combine/{$this->period}/{$branchCode}/{$accountNumber}_{$this->period}.pdf";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membuat atau update log statement
|
||||||
|
*
|
||||||
|
* @param Account $account
|
||||||
|
* @return PrintStatementLog
|
||||||
|
*/
|
||||||
|
private function createOrUpdateStatementLog(Account $account)
|
||||||
|
{
|
||||||
|
$emailAddress = $this->getEmailForAccount($account);
|
||||||
|
|
||||||
|
$logData = [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'customer_code' => $account->customer_code,
|
||||||
|
'branch_code' => $account->branch_code,
|
||||||
|
'period' => $this->period,
|
||||||
|
'print_date' => now(),
|
||||||
|
'batch_id' => $this->batchId,
|
||||||
|
'email_address' => $emailAddress,
|
||||||
|
'email_source' => !empty($account->stmt_email) ? 'account' : 'customer'
|
||||||
|
];
|
||||||
|
|
||||||
|
$statementLog = PrintStatementLog::updateOrCreate(
|
||||||
|
[
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'period_from' => $this->period,
|
||||||
|
'period_to' => $this->period
|
||||||
|
],
|
||||||
|
$logData
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::info('Statement log created/updated', [
|
||||||
|
'log_id' => $statementLog->id,
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'email_address' => $emailAddress,
|
||||||
|
'batch_id' => $this->batchId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $statementLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle job failure
|
||||||
|
*/
|
||||||
|
public function failed(\Throwable $exception)
|
||||||
|
{
|
||||||
|
$this->updateLogStatus('failed', [
|
||||||
|
'completed_at' => now(),
|
||||||
|
'error_message' => $exception->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::error('SendStatementEmailJob failed permanently', [
|
||||||
|
'batch_id' => $this->batchId,
|
||||||
|
'period' => $this->period,
|
||||||
|
'request_type' => $this->requestType,
|
||||||
|
'target_value' => $this->targetValue,
|
||||||
|
'error' => $exception->getMessage(),
|
||||||
|
'trace' => $exception->getTraceAsString()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +1,79 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Mail;
|
namespace Modules\Webstatement\Mail;
|
||||||
|
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Mail\Mailable;
|
use Illuminate\Mail\Mailable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Modules\Webstatement\Models\PrintStatementLog;
|
use Modules\Webstatement\Models\Account;
|
||||||
|
use Modules\Webstatement\Models\PrintStatementLog;
|
||||||
|
|
||||||
class StatementEmail extends Mailable
|
class StatementEmail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $statement;
|
||||||
|
protected $filePath;
|
||||||
|
protected $isZip;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
* Membuat instance email baru untuk pengiriman statement
|
||||||
|
*
|
||||||
|
* @param PrintStatementLog $statement
|
||||||
|
* @param string $filePath
|
||||||
|
* @param bool $isZip
|
||||||
|
*/
|
||||||
|
public function __construct(PrintStatementLog $statement, $filePath, $isZip = false)
|
||||||
{
|
{
|
||||||
use Queueable, SerializesModels;
|
$this->statement = $statement;
|
||||||
|
$this->filePath = $filePath;
|
||||||
protected $statement;
|
$this->isZip = $isZip;
|
||||||
protected $filePath;
|
|
||||||
protected $isZip;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new message instance.
|
|
||||||
*
|
|
||||||
* @param PrintStatementLog $statement
|
|
||||||
* @param string $filePath
|
|
||||||
* @param bool $isZip
|
|
||||||
*/
|
|
||||||
public function __construct(PrintStatementLog $statement, $filePath, $isZip = false)
|
|
||||||
{
|
|
||||||
$this->statement = $statement;
|
|
||||||
$this->filePath = $filePath;
|
|
||||||
$this->isZip = $isZip;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the message.
|
|
||||||
*
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function build()
|
|
||||||
{
|
|
||||||
$subject = 'Your Account Statement';
|
|
||||||
|
|
||||||
if ($this->statement->is_period_range) {
|
|
||||||
$subject .= " - {$this->statement->period_from} to {$this->statement->period_to}";
|
|
||||||
} else {
|
|
||||||
$subject .= " - {$this->statement->period_from}";
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = $this->subject($subject)
|
|
||||||
->view('webstatement::statements.email')
|
|
||||||
->with([
|
|
||||||
'statement' => $this->statement,
|
|
||||||
'accountNumber' => $this->statement->account_number,
|
|
||||||
'periodFrom' => $this->statement->period_from,
|
|
||||||
'periodTo' => $this->statement->period_to,
|
|
||||||
'isRange' => $this->statement->is_period_range,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($this->isZip) {
|
|
||||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip";
|
|
||||||
$email->attach($this->filePath, [
|
|
||||||
'as' => $fileName,
|
|
||||||
'mime' => 'application/zip',
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf";
|
|
||||||
$email->attach($this->filePath, [
|
|
||||||
'as' => $fileName,
|
|
||||||
'mime' => 'application/pdf',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $email;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the message.
|
||||||
|
* Membangun struktur email dengan attachment statement
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function build()
|
||||||
|
{
|
||||||
|
$subject = 'Statement Rekening Bank Artha Graha Internasional';
|
||||||
|
|
||||||
|
if ($this->statement->is_period_range) {
|
||||||
|
$subject .= " - {$this->statement->period_from} to {$this->statement->period_to}";
|
||||||
|
} else {
|
||||||
|
$subject .= " - " . \Carbon\Carbon::createFromFormat('Ym', $this->statement->period_from)->locale('id')->isoFormat('MMMM Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $this->subject($subject)
|
||||||
|
->view('webstatement::statements.email')
|
||||||
|
->with([
|
||||||
|
'statement' => $this->statement,
|
||||||
|
'accountNumber' => $this->statement->account_number,
|
||||||
|
'periodFrom' => $this->statement->period_from,
|
||||||
|
'periodTo' => $this->statement->period_to,
|
||||||
|
'isRange' => $this->statement->is_period_range,
|
||||||
|
'requestType' => $this->statement->request_type,
|
||||||
|
'batchId' => $this->statement->batch_id,
|
||||||
|
'accounts' => Account::where('account_number', $this->statement->account_number)->first()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->isZip) {
|
||||||
|
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip";
|
||||||
|
$email->attach($this->filePath, [
|
||||||
|
'as' => $fileName,
|
||||||
|
'mime' => 'application/zip',
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf";
|
||||||
|
$email->attach($this->filePath, [
|
||||||
|
'as' => $fileName,
|
||||||
|
'mime' => 'application/pdf',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
69
app/Models/AtmTransactionReportLog.php
Normal file
69
app/Models/AtmTransactionReportLog.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Modules\Usermanagement\Models\User;
|
||||||
|
|
||||||
|
class AtmTransactionReportLog extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,186 +1,288 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Models;
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Modules\Basicdata\Models\Branch;
|
use Modules\Basicdata\Models\Branch;
|
||||||
use Modules\Usermanagement\Models\User;
|
use Modules\Usermanagement\Models\User;
|
||||||
|
|
||||||
class PrintStatementLog extends Model
|
class PrintStatementLog extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'branch_code',
|
||||||
|
'account_number',
|
||||||
|
'request_type',
|
||||||
|
'batch_id',
|
||||||
|
'target_accounts',
|
||||||
|
'total_accounts',
|
||||||
|
'processed_accounts',
|
||||||
|
'success_count',
|
||||||
|
'failed_count',
|
||||||
|
'status',
|
||||||
|
'started_at',
|
||||||
|
'completed_at',
|
||||||
|
'error_message',
|
||||||
|
'period_from',
|
||||||
|
'period_to',
|
||||||
|
'is_period_range',
|
||||||
|
'is_available',
|
||||||
|
'is_downloaded',
|
||||||
|
'ip_address',
|
||||||
|
'user_agent',
|
||||||
|
'downloaded_at',
|
||||||
|
'authorization_status',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
'deleted_by',
|
||||||
|
'authorized_by',
|
||||||
|
'authorized_at',
|
||||||
|
'remarks',
|
||||||
|
'email',
|
||||||
|
'email_sent_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_period_range' => 'boolean',
|
||||||
|
'is_available' => 'boolean',
|
||||||
|
'is_downloaded' => 'boolean',
|
||||||
|
'downloaded_at' => 'datetime',
|
||||||
|
'authorized_at' => 'datetime',
|
||||||
|
'started_at' => 'datetime',
|
||||||
|
'completed_at' => 'datetime',
|
||||||
|
'target_accounts' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the formatted period display
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPeriodDisplayAttribute()
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
if ($this->is_period_range) {
|
||||||
|
return $this->formatPeriod($this->period_from) . ' - ' . $this->formatPeriod($this->period_to);
|
||||||
protected $fillable = [
|
|
||||||
'user_id',
|
|
||||||
'branch_code',
|
|
||||||
'account_number',
|
|
||||||
'period_from',
|
|
||||||
'period_to',
|
|
||||||
'is_period_range',
|
|
||||||
'is_available',
|
|
||||||
'is_downloaded',
|
|
||||||
'ip_address',
|
|
||||||
'user_agent',
|
|
||||||
'downloaded_at',
|
|
||||||
'authorization_status',
|
|
||||||
'created_by',
|
|
||||||
'updated_by',
|
|
||||||
'deleted_by',
|
|
||||||
'authorized_by',
|
|
||||||
'authorized_at',
|
|
||||||
'remarks',
|
|
||||||
'email',
|
|
||||||
'email_sent_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'is_period_range' => 'boolean',
|
|
||||||
'is_available' => 'boolean',
|
|
||||||
'is_downloaded' => 'boolean',
|
|
||||||
'downloaded_at' => 'datetime',
|
|
||||||
'authorized_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the formatted period display
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getPeriodDisplayAttribute()
|
|
||||||
{
|
|
||||||
if ($this->is_period_range) {
|
|
||||||
return $this->formatPeriod($this->period_from) . ' - ' . $this->formatPeriod($this->period_to);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->formatPeriod($this->period_from);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return $this->formatPeriod($this->period_from);
|
||||||
* Format period from YYYYMM to Month Year
|
|
||||||
*
|
|
||||||
* @param string $period
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function formatPeriod($period)
|
|
||||||
{
|
|
||||||
if (strlen($period) !== 6) {
|
|
||||||
return $period;
|
|
||||||
}
|
|
||||||
|
|
||||||
$year = substr($period, 0, 4);
|
|
||||||
$month = substr($period, 4, 2);
|
|
||||||
|
|
||||||
return date('F Y', mktime(0, 0, 0, (int) $month, 1, (int) $year));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user who requested the statement
|
|
||||||
*/
|
|
||||||
public function user()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'user_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user who created the record
|
|
||||||
*/
|
|
||||||
public function creator()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'created_by');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user who updated the record
|
|
||||||
*/
|
|
||||||
public function updater()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'updated_by');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user who authorized the record
|
|
||||||
*/
|
|
||||||
public function authorizer()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'authorized_by');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope a query to only include pending authorization records
|
|
||||||
*/
|
|
||||||
public function scopePending($query)
|
|
||||||
{
|
|
||||||
return $query->where('authorization_status', 'pending');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope a query to only include approved records
|
|
||||||
*/
|
|
||||||
public function scopeApproved($query)
|
|
||||||
{
|
|
||||||
return $query->where('authorization_status', 'approved');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope a query to only include rejected records
|
|
||||||
*/
|
|
||||||
public function scopeRejected($query)
|
|
||||||
{
|
|
||||||
return $query->where('authorization_status', 'rejected');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope a query to only include downloaded records
|
|
||||||
*/
|
|
||||||
public function scopeDownloaded($query)
|
|
||||||
{
|
|
||||||
return $query->where('is_downloaded', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope a query to only include available records
|
|
||||||
*/
|
|
||||||
public function scopeAvailable($query)
|
|
||||||
{
|
|
||||||
return $query->where('is_available', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the statement is for a single period
|
|
||||||
*/
|
|
||||||
public function isSinglePeriod()
|
|
||||||
{
|
|
||||||
return !$this->is_period_range;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the statement is authorized
|
|
||||||
*/
|
|
||||||
public function isAuthorized()
|
|
||||||
{
|
|
||||||
return $this->authorization_status === 'approved';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the statement is rejected
|
|
||||||
*/
|
|
||||||
public function isRejected()
|
|
||||||
{
|
|
||||||
return $this->authorization_status === 'rejected';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the statement is pending authorization
|
|
||||||
*/
|
|
||||||
public function isPending()
|
|
||||||
{
|
|
||||||
return $this->authorization_status === 'pending';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function branch(){
|
|
||||||
return $this->belongsTo(Branch::class, 'branch_code','code');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format period from YYYYMM to Month Year
|
||||||
|
*
|
||||||
|
* @param string $period
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function formatPeriod($period)
|
||||||
|
{
|
||||||
|
if (strlen($period) !== 6) {
|
||||||
|
return $period;
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = substr($period, 0, 4);
|
||||||
|
$month = substr($period, 4, 2);
|
||||||
|
|
||||||
|
return date('F Y', mktime(0, 0, 0, (int) $month, 1, (int) $year));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user who requested the statement
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user who created the record
|
||||||
|
*/
|
||||||
|
public function creator()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user who updated the record
|
||||||
|
*/
|
||||||
|
public function updater()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'updated_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user who authorized the record
|
||||||
|
*/
|
||||||
|
public function authorizer()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'authorized_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to only include pending authorization records
|
||||||
|
*/
|
||||||
|
public function scopePending($query)
|
||||||
|
{
|
||||||
|
return $query->where('authorization_status', 'pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to only include approved records
|
||||||
|
*/
|
||||||
|
public function scopeApproved($query)
|
||||||
|
{
|
||||||
|
return $query->where('authorization_status', 'approved');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to only include rejected records
|
||||||
|
*/
|
||||||
|
public function scopeRejected($query)
|
||||||
|
{
|
||||||
|
return $query->where('authorization_status', 'rejected');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to only include downloaded records
|
||||||
|
*/
|
||||||
|
public function scopeDownloaded($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_downloaded', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to only include available records
|
||||||
|
*/
|
||||||
|
public function scopeAvailable($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_available', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the statement is for a single period
|
||||||
|
*/
|
||||||
|
public function isSinglePeriod()
|
||||||
|
{
|
||||||
|
return !$this->is_period_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the statement is authorized
|
||||||
|
*/
|
||||||
|
public function isAuthorized()
|
||||||
|
{
|
||||||
|
return $this->authorization_status === 'approved';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the statement is rejected
|
||||||
|
*/
|
||||||
|
public function isRejected()
|
||||||
|
{
|
||||||
|
return $this->authorization_status === 'rejected';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the statement is pending authorization
|
||||||
|
*/
|
||||||
|
public function isPending()
|
||||||
|
{
|
||||||
|
return $this->authorization_status === 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function branch(){
|
||||||
|
return $this->belongsTo(Branch::class, 'branch_code','code');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a single account request
|
||||||
|
*/
|
||||||
|
public function isSingleAccountRequest()
|
||||||
|
{
|
||||||
|
return $this->request_type === 'single_account';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a branch request
|
||||||
|
*/
|
||||||
|
public function isBranchRequest()
|
||||||
|
{
|
||||||
|
return $this->request_type === 'branch';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is an all branches request
|
||||||
|
*/
|
||||||
|
public function isAllBranchesRequest()
|
||||||
|
{
|
||||||
|
return $this->request_type === 'all_branches';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if processing is completed
|
||||||
|
*/
|
||||||
|
public function isCompleted()
|
||||||
|
{
|
||||||
|
return $this->status === 'completed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if processing is in progress
|
||||||
|
*/
|
||||||
|
public function isProcessing()
|
||||||
|
{
|
||||||
|
return $this->status === 'processing';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if processing failed
|
||||||
|
*/
|
||||||
|
public function isFailed()
|
||||||
|
{
|
||||||
|
return $this->status === 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get progress percentage
|
||||||
|
*/
|
||||||
|
public function getProgressPercentage()
|
||||||
|
{
|
||||||
|
if (!$this->total_accounts || $this->total_accounts == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round(($this->processed_accounts / $this->total_accounts) * 100, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get success rate percentage
|
||||||
|
*/
|
||||||
|
public function getSuccessRate()
|
||||||
|
{
|
||||||
|
if (!$this->processed_accounts || $this->processed_accounts == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round(($this->success_count / $this->processed_accounts) * 100, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for batch requests
|
||||||
|
*/
|
||||||
|
public function scopeBatch($query)
|
||||||
|
{
|
||||||
|
return $query->whereIn('request_type', ['branch', 'all_branches']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for single account requests
|
||||||
|
*/
|
||||||
|
public function scopeSingleAccount($query)
|
||||||
|
{
|
||||||
|
return $query->where('request_type', 'single_account');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,15 +6,18 @@ 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\CheckEmailProgressCommand;
|
||||||
|
use Modules\Webstatement\Console\UnlockPdf;
|
||||||
use Modules\Webstatement\Console\CombinePdf;
|
use Modules\Webstatement\Console\CombinePdf;
|
||||||
use Modules\Webstatement\Console\ConvertHtmlToPdf;
|
use Modules\Webstatement\Console\ConvertHtmlToPdf;
|
||||||
use Modules\Webstatement\Console\ExportDailyStatements;
|
use Modules\Webstatement\Console\ExportDailyStatements;
|
||||||
use Modules\Webstatement\Console\ExportPeriodStatements;
|
|
||||||
use Modules\Webstatement\Console\ProcessDailyMigration;
|
use Modules\Webstatement\Console\ProcessDailyMigration;
|
||||||
|
use Modules\Webstatement\Console\ExportPeriodStatements;
|
||||||
use Modules\Webstatement\Console\GenerateBiayakartuCommand;
|
use Modules\Webstatement\Console\GenerateBiayakartuCommand;
|
||||||
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
|
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
|
||||||
|
use Modules\Webstatement\Console\GenerateAtmTransactionReport;
|
||||||
use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand;
|
use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand;
|
||||||
use Modules\Webstatement\Console\UnlockPdf;
|
use Modules\Webstatement\Console\SendStatementEmailCommand;
|
||||||
|
|
||||||
class WebstatementServiceProvider extends ServiceProvider
|
class WebstatementServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -65,6 +68,9 @@ class WebstatementServiceProvider extends ServiceProvider
|
|||||||
ConvertHtmlToPdf::class,
|
ConvertHtmlToPdf::class,
|
||||||
UnlockPdf::class,
|
UnlockPdf::class,
|
||||||
ExportPeriodStatements::class,
|
ExportPeriodStatements::class,
|
||||||
|
GenerateAtmTransactionReport::class,
|
||||||
|
SendStatementEmailCommand::class,
|
||||||
|
CheckEmailProgressCommand::class
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?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('atm_transaction_report_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('period', 8); // Format: Ymd (20250512)
|
||||||
|
$table->date('report_date');
|
||||||
|
$table->enum('status', ['pending', 'processing', 'completed', 'failed'])->default('pending');
|
||||||
|
$table->enum('authorization_status', ['pending', 'approved', 'rejected'])->default('pending');
|
||||||
|
$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->timestamps();
|
||||||
|
|
||||||
|
$table->index(['period']);
|
||||||
|
$table->index(['status']);
|
||||||
|
$table->index(['authorization_status']);
|
||||||
|
$table->index(['created_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('atm_transaction_report_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?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::table('print_statement_logs', function (Blueprint $table) {
|
||||||
|
// Field untuk mendukung pengiriman dinamis
|
||||||
|
$table->enum('request_type', ['single_account', 'branch', 'all_branches'])
|
||||||
|
->default('single_account')
|
||||||
|
->after('account_number')
|
||||||
|
->comment('Type of statement request');
|
||||||
|
|
||||||
|
$table->string('batch_id')->nullable()
|
||||||
|
->after('request_type')
|
||||||
|
->comment('Batch ID for bulk operations');
|
||||||
|
|
||||||
|
$table->json('target_accounts')->nullable()
|
||||||
|
->after('batch_id')
|
||||||
|
->comment('JSON array of target account numbers for batch processing');
|
||||||
|
|
||||||
|
$table->integer('total_accounts')->nullable()
|
||||||
|
->after('target_accounts')
|
||||||
|
->comment('Total number of accounts in batch');
|
||||||
|
|
||||||
|
$table->integer('processed_accounts')->default(0)
|
||||||
|
->after('total_accounts')
|
||||||
|
->comment('Number of accounts processed');
|
||||||
|
|
||||||
|
$table->integer('success_count')->default(0)
|
||||||
|
->after('processed_accounts')
|
||||||
|
->comment('Number of successful email sends');
|
||||||
|
|
||||||
|
$table->integer('failed_count')->default(0)
|
||||||
|
->after('success_count')
|
||||||
|
->comment('Number of failed email sends');
|
||||||
|
|
||||||
|
$table->enum('status', ['pending', 'processing', 'completed', 'failed'])
|
||||||
|
->default('pending')
|
||||||
|
->after('failed_count')
|
||||||
|
->comment('Overall status of the request');
|
||||||
|
|
||||||
|
$table->timestamp('started_at')->nullable()
|
||||||
|
->after('status')
|
||||||
|
->comment('When processing started');
|
||||||
|
|
||||||
|
$table->timestamp('completed_at')->nullable()
|
||||||
|
->after('started_at')
|
||||||
|
->comment('When processing completed');
|
||||||
|
|
||||||
|
$table->text('error_message')->nullable()
|
||||||
|
->after('completed_at')
|
||||||
|
->comment('Error message if processing failed');
|
||||||
|
|
||||||
|
// Ubah account_number menjadi nullable untuk request batch
|
||||||
|
$table->string('account_number')->nullable()->change();
|
||||||
|
|
||||||
|
// Index untuk performa
|
||||||
|
$table->index(['request_type', 'status']);
|
||||||
|
$table->index(['batch_id']);
|
||||||
|
$table->index(['branch_code', 'request_type']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'request_type',
|
||||||
|
'batch_id',
|
||||||
|
'target_accounts',
|
||||||
|
'total_accounts',
|
||||||
|
'processed_accounts',
|
||||||
|
'success_count',
|
||||||
|
'failed_count',
|
||||||
|
'status',
|
||||||
|
'started_at',
|
||||||
|
'completed_at',
|
||||||
|
'error_message'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table->string('account_number')->nullable(false)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
24
module.json
24
module.json
@@ -63,6 +63,19 @@
|
|||||||
"roles": []
|
"roles": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"laporan": [
|
||||||
|
{
|
||||||
|
"title": "Laporan Transaksi ATM",
|
||||||
|
"path": "atm-reports",
|
||||||
|
"icon": "ki-filled ki-printer text-lg text-primary",
|
||||||
|
"classes": "",
|
||||||
|
"attributes": [],
|
||||||
|
"permission": "",
|
||||||
|
"roles": [
|
||||||
|
"administrator"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"master": [
|
"master": [
|
||||||
{
|
{
|
||||||
"title": "Basic Data",
|
"title": "Basic Data",
|
||||||
@@ -121,6 +134,17 @@
|
|||||||
"roles": [
|
"roles": [
|
||||||
"administrator"
|
"administrator"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Log Email Statement",
|
||||||
|
"path": "email-statement-logs",
|
||||||
|
"icon": "ki-filled ki-message-text-2 text-lg text-primary",
|
||||||
|
"classes": "",
|
||||||
|
"attributes": [],
|
||||||
|
"permission": "",
|
||||||
|
"roles": [
|
||||||
|
"administrator"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
322
resources/views/atm-reports/index.blade.php
Normal file
322
resources/views/atm-reports/index.blade.php
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
@extends('layouts.main')
|
||||||
|
|
||||||
|
@section('title', 'ATM Transaction Reports')
|
||||||
|
|
||||||
|
@section('breadcrumbs')
|
||||||
|
{{ Breadcrumbs::render(request()->route()->getName()) }}
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="grid grid-cols-8 gap-5">
|
||||||
|
<div class="col-span-2 card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Request ATM Transaction Report</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('atm-reports.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-5">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label required" for="report_date">Report Date</label>
|
||||||
|
<input type="date" class="input form-control @error('report_date') is-invalid @enderror"
|
||||||
|
id="report_date" name="report_date" value="{{ old('report_date') }}" required>
|
||||||
|
@error('report_date')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="w-full btn btn-primary">
|
||||||
|
<i class="ki-filled ki-plus"></i>
|
||||||
|
Generate Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6">
|
||||||
|
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
|
||||||
|
data-datatable-state-save="false" id="atm-reports-table"
|
||||||
|
data-api-url="{{ route('atm-reports.datatables') }}">
|
||||||
|
<div class="flex-wrap py-5 card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
ATM Transaction Reports
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 lg:gap-5">
|
||||||
|
<div class="flex">
|
||||||
|
<label class="input input-sm"> <i class="ki-filled ki-magnifier"> </i>
|
||||||
|
<input placeholder="Search Reports" id="search" type="text" value="">
|
||||||
|
</label>
|
||||||
|
</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-[100px]" data-datatable-column="id">
|
||||||
|
<span class="sort"> <span class="sort-label"> ID </span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[150px]" data-datatable-column="period">
|
||||||
|
<span class="sort"> <span class="sort-label"> Period </span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[150px]" data-datatable-column="report_date">
|
||||||
|
<span class="sort"> <span class="sort-label"> Report Date </span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[150px]" data-datatable-column="status">
|
||||||
|
<span class="sort"> <span class="sort-label"> Status </span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[150px]" data-datatable-column="authorization_status">
|
||||||
|
<span class="sort"> <span class="sort-label"> Authorization </span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[180px]" data-datatable-column="created_at">
|
||||||
|
<span class="sort"> <span class="sort-label"> Created At </span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="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>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script type="text/javascript">
|
||||||
|
function deleteData(data) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
text: "You won't be able to revert this!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#3085d6',
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
confirmButtonText: 'Yes, delete it!'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
$.ajaxSetup({
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$.ajax(`atm-reports/${data}`, {
|
||||||
|
type: 'DELETE'
|
||||||
|
}).then((response) => {
|
||||||
|
swal.fire('Deleted!', 'ATM Transaction report has been deleted.', 'success').then(
|
||||||
|
() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
Swal.fire('Error!', 'An error occurred while deleting the record.', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the missing retryReport function
|
||||||
|
function retryReport(id) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
text: 'This will reset the current job and start a new one.',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#3085d6',
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
confirmButtonText: 'Yes, retry it!'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
$.ajaxSetup({
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$.ajax(`atm-reports/${id}/retry`, {
|
||||||
|
type: 'POST'
|
||||||
|
}).then((response) => {
|
||||||
|
Swal.fire('Success!', 'Report retry initiated successfully.', 'success').then(
|
||||||
|
() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
Swal.fire('Error!', 'Failed to retry report: ' + (error.responseJSON?.message ||
|
||||||
|
'Unknown error'), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
const element = document.querySelector('#atm-reports-table');
|
||||||
|
const searchInput = document.getElementById('search');
|
||||||
|
|
||||||
|
const apiUrl = element.getAttribute('data-api-url');
|
||||||
|
const dataTableOptions = {
|
||||||
|
apiEndpoint: apiUrl,
|
||||||
|
pageSize: 10,
|
||||||
|
columns: {
|
||||||
|
select: {
|
||||||
|
render: (item, data, context) => {
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.className = 'checkbox checkbox-sm';
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.value = data.id.toString();
|
||||||
|
checkbox.setAttribute('data-datatable-row-check', 'true');
|
||||||
|
return checkbox.outerHTML.trim();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
title: 'ID',
|
||||||
|
},
|
||||||
|
period: {
|
||||||
|
title: 'Period',
|
||||||
|
render: (item, data) => {
|
||||||
|
return data.period || '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
report_date: {
|
||||||
|
title: 'Report Date',
|
||||||
|
render: (item, data) => {
|
||||||
|
return data.report_date || '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
title: 'Status',
|
||||||
|
render: (item, data) => {
|
||||||
|
let statusClass = 'badge badge-light-primary';
|
||||||
|
let statusText = data.status;
|
||||||
|
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
statusClass = 'badge badge-light-success';
|
||||||
|
} else if (data.status === 'failed') {
|
||||||
|
statusClass = 'badge badge-light-danger';
|
||||||
|
} else if (data.status === 'processing') {
|
||||||
|
if (data.is_processing_timeout) {
|
||||||
|
statusClass = 'badge badge-light-danger';
|
||||||
|
statusText += ` (${data.processing_hours}h)`;
|
||||||
|
} else {
|
||||||
|
statusClass = 'badge badge-light-warning';
|
||||||
|
}
|
||||||
|
} else if (data.status === 'pending') {
|
||||||
|
statusClass = 'badge badge-light-info';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<span class="${statusClass}">${statusText}</span>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorization_status: {
|
||||||
|
title: 'Authorization',
|
||||||
|
render: (item, data) => {
|
||||||
|
let statusClass = 'badge badge-light-primary';
|
||||||
|
|
||||||
|
if (data.authorization_status === 'approved') {
|
||||||
|
statusClass = 'badge badge-light-success';
|
||||||
|
} else if (data.authorization_status === 'rejected') {
|
||||||
|
statusClass = 'badge badge-light-danger';
|
||||||
|
} else if (data.authorization_status === 'pending') {
|
||||||
|
statusClass = 'badge badge-light-warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<span class="${statusClass}">${data.authorization_status || 'pending'}</span>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
title: 'Created At',
|
||||||
|
render: (item, data) => {
|
||||||
|
return data.created_at ?? '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
render: (item, data) => {
|
||||||
|
let buttons = `<div class="flex flex-nowrap justify-center">
|
||||||
|
<a class="btn btn-sm btn-icon btn-clear btn-info" href="atm-reports/${data.id}">
|
||||||
|
<i class="ki-outline ki-eye"></i>
|
||||||
|
</a>`;
|
||||||
|
|
||||||
|
// Show download button if report is completed
|
||||||
|
if (data.status === 'completed' && data.file_path) {
|
||||||
|
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-success" href="atm-reports/${data.id}/download">
|
||||||
|
<i class="ki-outline ki-cloud-download"></i>
|
||||||
|
</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry button
|
||||||
|
if (data.can_retry) {
|
||||||
|
let retryClass = 'btn-warning';
|
||||||
|
if (data.is_processing_timeout) {
|
||||||
|
retryClass = 'btn-danger';
|
||||||
|
}
|
||||||
|
buttons += `<button class="btn btn-sm btn-icon btn-clear ${retryClass}" onclick="retryReport(${data.id})">
|
||||||
|
<i class="ki-outline ki-arrows-circle"></i>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show delete button if status is pending or failed
|
||||||
|
if (data.status === 'pending' || data.status === 'failed') {
|
||||||
|
buttons += `<a onclick="deleteData(${data.id})" class="delete btn btn-sm btn-icon btn-clear btn-danger">
|
||||||
|
<i class="ki-outline ki-trash"></i>
|
||||||
|
</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons += `</div>`;
|
||||||
|
return buttons;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||||
|
// Custom search functionality
|
||||||
|
searchInput.addEventListener('input', function() {
|
||||||
|
const searchValue = this.value.trim();
|
||||||
|
dataTable.search(searchValue, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle the "select all" checkbox
|
||||||
|
const selectAllCheckbox = document.querySelector('input[data-datatable-check="true"]');
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.addEventListener('change', function() {
|
||||||
|
const isChecked = this.checked;
|
||||||
|
const rowCheckboxes = document.querySelectorAll('input[data-datatable-row-check="true"]');
|
||||||
|
|
||||||
|
rowCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = isChecked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
291
resources/views/atm-reports/show.blade.php
Normal file
291
resources/views/atm-reports/show.blade.php
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
@extends('layouts.main')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">ATM Transaction Report Details</h3>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<a href="{{ route('atm-reports.index') }}" class="btn btn-sm btn-info me-2">
|
||||||
|
<i class="ki-duotone ki-arrow-left fs-2"></i>Back to List
|
||||||
|
</a>
|
||||||
|
|
||||||
|
@if ($atmReport->status === 'completed' && $atmReport->file_path)
|
||||||
|
<a href="{{ route('atm-reports.download', $atmReport->id) }}" class="btn btn-sm btn-primary">
|
||||||
|
<i class="ki-duotone ki-document fs-2"></i>Download Report
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@php
|
||||||
|
$canRetry = in_array($atmReport->status, ['failed', 'pending']) ||
|
||||||
|
($atmReport->status === 'processing' && $atmReport->updated_at->diffInHours(now()) >= 1) ||
|
||||||
|
($atmReport->status === 'completed' && !$atmReport->file_path);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if ($canRetry)
|
||||||
|
<form action="{{ route('atm-reports.retry', $atmReport->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to retry generating this report?')">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="btn btn-sm btn-warning me-2">
|
||||||
|
<i class="ki-duotone ki-arrows-circle fs-2"></i>
|
||||||
|
@if($atmReport->status === 'processing' && $atmReport->updated_at->diffInHours(now()) >= 1)
|
||||||
|
Retry (Timeout)
|
||||||
|
@else
|
||||||
|
Retry Job
|
||||||
|
@endif
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="alert alert-success">
|
||||||
|
{{ session('success') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (session('error'))
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{ session('error') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-5 g-5">
|
||||||
|
<!-- Left Column - Report Information -->
|
||||||
|
<div class="shadow-sm card card-flush h-xl-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>Report Information</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="py-5 card-body">
|
||||||
|
<div class="gap-5 d-flex flex-column">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Period</div>
|
||||||
|
<div class="fw-bold fs-5">
|
||||||
|
@php
|
||||||
|
// Convert format YYYYMMDD to readable date
|
||||||
|
$date = \Carbon\Carbon::createFromFormat('Ymd', $atmReport->period);
|
||||||
|
$periodText = $date->format('d F Y');
|
||||||
|
@endphp
|
||||||
|
{{ $periodText }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Report Date</div>
|
||||||
|
<div class="fw-bold fs-5">{{ $atmReport->report_date }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Status</div>
|
||||||
|
<div>
|
||||||
|
@if ($atmReport->status === 'pending')
|
||||||
|
<span class="badge badge-info">Pending</span>
|
||||||
|
@elseif($atmReport->status === 'processing')
|
||||||
|
@php
|
||||||
|
$processingHours = $atmReport->updated_at->diffInHours(now());
|
||||||
|
@endphp
|
||||||
|
<span class="badge {{ $processingHours >= 1 ? 'badge-danger' : 'badge-warning' }}">
|
||||||
|
Processing
|
||||||
|
@if($processingHours >= 1)
|
||||||
|
({{ $processingHours }}h - Timeout)
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@if($processingHours >= 1)
|
||||||
|
<div class="mt-1 text-danger small">
|
||||||
|
Processing for more than 1 hour. You can retry this job.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@elseif($atmReport->status === 'completed')
|
||||||
|
<span class="badge badge-success">Completed</span>
|
||||||
|
@elseif($atmReport->status === 'failed')
|
||||||
|
<span class="badge badge-danger">Failed</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Authorization Status</div>
|
||||||
|
<div>
|
||||||
|
@if ($atmReport->authorization_status === 'pending')
|
||||||
|
<span class="badge badge-warning">Pending Authorization</span>
|
||||||
|
@elseif($atmReport->authorization_status === 'approved')
|
||||||
|
<span class="badge badge-success">Approved</span>
|
||||||
|
@elseif($atmReport->authorization_status === 'rejected')
|
||||||
|
<span class="badge badge-danger">Rejected</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($atmReport->status === 'completed')
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">File Information</div>
|
||||||
|
<div class="fw-bold fs-6">
|
||||||
|
@if ($atmReport->file_path)
|
||||||
|
<div>Path: {{ $atmReport->file_path }}</div>
|
||||||
|
@else
|
||||||
|
<div class="text-warning">File not available -
|
||||||
|
<form action="{{ route('atm-reports.retry', $atmReport->id) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="p-0 btn btn-link text-warning" onclick="return confirm('File is missing. Retry generating the report?')">
|
||||||
|
Click here to retry
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if ($atmReport->file_size)
|
||||||
|
<div>Size: {{ number_format($atmReport->file_size / 1024, 2) }} KB</div>
|
||||||
|
@endif
|
||||||
|
@if ($atmReport->record_count)
|
||||||
|
<div>Records: {{ number_format($atmReport->record_count) }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($atmReport->status === 'failed' && $atmReport->error_message)
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Error Message</div>
|
||||||
|
<div class="text-danger fw-bold fs-6">{{ $atmReport->error_message }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Downloaded</div>
|
||||||
|
<div>
|
||||||
|
@if ($atmReport->is_downloaded)
|
||||||
|
<span class="badge badge-success">Yes</span>
|
||||||
|
<div class="mt-1 text-muted">
|
||||||
|
Downloaded at:
|
||||||
|
{{ $atmReport->downloaded_at ? $atmReport->downloaded_at->format('d M Y H:i:s') : 'N/A' }}
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-light-primary">No</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column - Request Information -->
|
||||||
|
<div class="shadow-sm card card-flush h-xl-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>Request Information</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="py-5 card-body">
|
||||||
|
<div class="gap-5 d-flex flex-column">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Requested By</div>
|
||||||
|
<div class="fw-bold fs-5">{{ $atmReport->user->name ?? 'N/A' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Requested At</div>
|
||||||
|
<div class="fw-bold fs-5">{{ dateFormat($atmReport->created_at, 1, 1) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">IP Address</div>
|
||||||
|
<div class="fw-bold fs-5">{{ $atmReport->ip_address }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">User Agent</div>
|
||||||
|
<div class="text-muted small">{{ $atmReport->user_agent }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($atmReport->authorization_status !== 'pending')
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Authorized By</div>
|
||||||
|
<div class="fw-bold fs-5">{{ $atmReport->authorizer->name ?? 'N/A' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Authorized At</div>
|
||||||
|
<div class="fw-bold fs-5">
|
||||||
|
{{ $atmReport->authorized_at ? $atmReport->authorized_at->format('d M Y H:i:s') : 'N/A' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($atmReport->created_by && $atmReport->created_by !== $atmReport->user_id)
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Created By</div>
|
||||||
|
<div class="fw-bold fs-5">{{ $atmReport->creator->name ?? 'N/A' }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($atmReport->updated_by)
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Last Updated By</div>
|
||||||
|
<div class="fw-bold fs-5">{{ $atmReport->updater->name ?? 'N/A' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-gray-500 fw-semibold">Last Updated At</div>
|
||||||
|
<div class="fw-bold fs-5">{{ dateFormat($atmReport->updated_at, 1, 1) }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($atmReport->authorization_status === 'pending' && auth()->user()->can('authorize_atm_reports'))
|
||||||
|
<div class="mt-7 shadow-sm card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Authorization</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('atm-reports.authorize', $atmReport->id) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="form-label required">Authorization Decision</label>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="form-check form-check-custom form-check-solid me-5">
|
||||||
|
<input class="form-check-input" type="radio" name="authorization_status"
|
||||||
|
value="approved" id="status_approved" required />
|
||||||
|
<label class="form-check-label" for="status_approved">
|
||||||
|
Approve
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-custom form-check-solid">
|
||||||
|
<input class="form-check-input" type="radio" name="authorization_status"
|
||||||
|
value="rejected" id="status_rejected" required />
|
||||||
|
<label class="form-check-label" for="status_rejected">
|
||||||
|
Reject
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="form-label">Remarks</label>
|
||||||
|
<textarea class="form-control" name="remarks" rows="3"
|
||||||
|
placeholder="Enter any remarks or reasons for your decision"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-end">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit Authorization</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
// Any additional JavaScript for this page
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize any components if needed
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
393
resources/views/email-statement-logs/index.blade.php
Normal file
393
resources/views/email-statement-logs/index.blade.php
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
@extends('layouts.main')
|
||||||
|
|
||||||
|
@section('breadcrumbs')
|
||||||
|
{{ Breadcrumbs::render(request()->route()->getName()) }}
|
||||||
|
@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="email-statement-logs-table"
|
||||||
|
data-api-url="{{ route('email-statement-logs.datatables') }}">
|
||||||
|
<div class="flex-wrap py-5 card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
Log Pengiriman Email Statement
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 lg:gap-5">
|
||||||
|
<div class="flex">
|
||||||
|
<label class="input input-sm"> <i class="ki-filled ki-magnifier"> </i>
|
||||||
|
<input placeholder="Cari Log Email Statement" id="search" type="text" value="">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<select class="select select-sm" id="filter-branch">
|
||||||
|
<option value="">Cabang (Semua)</option>
|
||||||
|
@foreach ($branches as $branch)
|
||||||
|
<option value="{{ $branch }}">{{ $branch }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<select class="select select-sm" id="filter-email-status">
|
||||||
|
<option value="">Status Email (Semua)</option>
|
||||||
|
<option value="sent">Terkirim</option>
|
||||||
|
<option value="failed">Gagal</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<select class="select select-sm" id="filter-email-source">
|
||||||
|
<option value="">Sumber Email (Semua)</option>
|
||||||
|
<option value="account">Email Akun</option>
|
||||||
|
<option value="customer">Email Nasabah</option>
|
||||||
|
</select>
|
||||||
|
</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="min-w-[120px]" data-datatable-column="branch_code">
|
||||||
|
<span class="sort"> <span class="sort-label">Cabang</span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[150px]" data-datatable-column="account_number">
|
||||||
|
<span class="sort"> <span class="sort-label">No. Rekening</span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[120px]" data-datatable-column="period_from">
|
||||||
|
<span class="sort"> <span class="sort-label">Periode Dari</span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[120px]" data-datatable-column="period_to">
|
||||||
|
<span class="sort"> <span class="sort-label">Periode Sampai</span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[200px]" data-datatable-column="email_address">
|
||||||
|
<span class="sort"> <span class="sort-label">Email</span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[120px]" data-datatable-column="email_source">
|
||||||
|
<span class="sort"> <span class="sort-label">Sumber Email</span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[120px]" data-datatable-column="email_status">
|
||||||
|
<span class="sort"> <span class="sort-label">Status Email</span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[150px]" data-datatable-column="email_sent_at">
|
||||||
|
<span class="sort"> <span class="sort-label">Waktu Kirim</span>
|
||||||
|
<span class="sort-icon"> </span> </span>
|
||||||
|
</th>
|
||||||
|
<th class="min-w-[100px]" data-datatable-column="actions">
|
||||||
|
<span>Aksi</span>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Modal for detail view -->
|
||||||
|
<div class="modal modal-open:!flex" data-modal="true" id="detail-modal">
|
||||||
|
<div class="overflow-hidden px-5 w-full modal-content pt-7.5 container-fixed">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title">Detail Log Email Statement</h2>
|
||||||
|
<button class="btn btn-sm btn-icon btn-active-color-danger" data-modal-dismiss="true">
|
||||||
|
<i class="ki-outline ki-cross fs-1"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-5">
|
||||||
|
<h3 class="mb-2 font-bold">Informasi Umum</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div>Cabang:</div>
|
||||||
|
<div id="detail-branch-code"></div>
|
||||||
|
<div>No. Rekening:</div>
|
||||||
|
<div id="detail-account-number"></div>
|
||||||
|
<div>Periode:</div>
|
||||||
|
<div id="detail-period"></div>
|
||||||
|
<div>Email:</div>
|
||||||
|
<div id="detail-email-address"></div>
|
||||||
|
<div>Sumber Email:</div>
|
||||||
|
<div id="detail-email-source"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<h3 class="mb-2 font-bold">Status Pengiriman Email</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div>Status:</div>
|
||||||
|
<div id="detail-email-status"></div>
|
||||||
|
<div>Waktu Kirim:</div>
|
||||||
|
<div id="detail-email-sent-at"></div>
|
||||||
|
<div>Error Message:</div>
|
||||||
|
<div id="detail-error-message"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<h3 class="mb-2 font-bold">Informasi Tambahan</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div>Dibuat Oleh:</div>
|
||||||
|
<div id="detail-user-id"></div>
|
||||||
|
<div>Waktu Dibuat:</div>
|
||||||
|
<div id="detail-created-at"></div>
|
||||||
|
<div>Waktu Update:</div>
|
||||||
|
<div id="detail-updated-at"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" id="btn-resend-email" class="btn btn-primary btn-sm me-3">
|
||||||
|
<i class="ki-outline ki-send me-1"></i> Kirim Ulang Email
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-light" data-modal-dismiss="true">Tutup</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script type="module">
|
||||||
|
const element = document.querySelector('#email-statement-logs-table');
|
||||||
|
const searchInput = document.getElementById('search');
|
||||||
|
const filterBranch = document.getElementById('filter-branch');
|
||||||
|
const filterEmailStatus = document.getElementById('filter-email-status');
|
||||||
|
const filterEmailSource = document.getElementById('filter-email-source');
|
||||||
|
const detailModal = document.getElementById('detail-modal');
|
||||||
|
const btnResendEmail = document.getElementById('btn-resend-email');
|
||||||
|
|
||||||
|
const apiUrl = element.getAttribute('data-api-url');
|
||||||
|
const dataTableOptions = {
|
||||||
|
apiEndpoint: apiUrl,
|
||||||
|
pageSize: 10,
|
||||||
|
columns: {
|
||||||
|
branch_code: {
|
||||||
|
title: 'Cabang',
|
||||||
|
},
|
||||||
|
account_number: {
|
||||||
|
title: 'No. Rekening',
|
||||||
|
},
|
||||||
|
period_from: {
|
||||||
|
title: 'Periode Dari',
|
||||||
|
render: (item, data) => {
|
||||||
|
return data.period_from ? new Date(data.period_from).toLocaleDateString('id-ID') : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
period_to: {
|
||||||
|
title: 'Periode Sampai',
|
||||||
|
render: (item, data) => {
|
||||||
|
return data.period_to ? new Date(data.period_to).toLocaleDateString('id-ID') : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email_address: {
|
||||||
|
title: 'Email',
|
||||||
|
},
|
||||||
|
email_source: {
|
||||||
|
title: 'Sumber Email',
|
||||||
|
render: (item, data) => {
|
||||||
|
if (data.email_source === 'account') {
|
||||||
|
return '<span class="badge badge-info">Email Akun</span>';
|
||||||
|
} else if (data.email_source === 'customer') {
|
||||||
|
return '<span class="badge badge-primary">Email Nasabah</span>';
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email_status: {
|
||||||
|
title: 'Status Email',
|
||||||
|
render: (item, data) => {
|
||||||
|
if (data.email_status === 'sent') {
|
||||||
|
return '<span class="badge badge-success">Terkirim</span>';
|
||||||
|
} else if (data.email_status === 'failed') {
|
||||||
|
return '<span class="badge badge-danger">Gagal</span>';
|
||||||
|
} else if (data.email_status === 'pending') {
|
||||||
|
return '<span class="badge badge-warning">Pending</span>';
|
||||||
|
}
|
||||||
|
return '<span class="badge badge-secondary">Unknown</span>';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email_sent_at: {
|
||||||
|
title: 'Waktu Kirim',
|
||||||
|
render: (item, data) => {
|
||||||
|
return data.email_sent_at ? new Date(data.email_sent_at).toLocaleString('id-ID') : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
title: 'Aksi',
|
||||||
|
render: (item, data) => {
|
||||||
|
let actions = `<div class="flex gap-2">`;
|
||||||
|
|
||||||
|
// Detail button
|
||||||
|
actions += `<button class="btn btn-sm btn-icon btn-light btn-detail" data-id="${data.id}">
|
||||||
|
<i class="ki-outline ki-eye fs-3"></i>
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
// Resend button for failed emails
|
||||||
|
if (data.email_status === 'failed') {
|
||||||
|
actions += `<button class="btn btn-sm btn-icon btn-warning btn-resend" data-id="${data.id}" title="Kirim Ulang Email">
|
||||||
|
<i class="ki-outline ki-send fs-3"></i>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
actions += `</div>`;
|
||||||
|
return actions;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||||
|
|
||||||
|
// Custom search functionality
|
||||||
|
searchInput.addEventListener('change', function() {
|
||||||
|
const searchValue = this.value.trim();
|
||||||
|
dataTable.goPage(1);
|
||||||
|
dataTable.search(searchValue, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter functionality
|
||||||
|
const applyFilters = () => {
|
||||||
|
const branchValue = filterBranch.value;
|
||||||
|
const emailStatusValue = filterEmailStatus.value;
|
||||||
|
const emailSourceValue = filterEmailSource.value;
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
if (searchInput.value) {
|
||||||
|
params.search = searchInput.value;
|
||||||
|
}
|
||||||
|
if (branchValue !== '') params.branch_code = branchValue;
|
||||||
|
if (emailStatusValue !== '') params.email_status = emailStatusValue;
|
||||||
|
if (emailSourceValue !== '') params.email_source = emailSourceValue;
|
||||||
|
|
||||||
|
dataTable.goPage(1);
|
||||||
|
dataTable.search(params);
|
||||||
|
dataTable.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
filterBranch.addEventListener('change', applyFilters);
|
||||||
|
filterEmailStatus.addEventListener('change', applyFilters);
|
||||||
|
filterEmailSource.addEventListener('change', applyFilters);
|
||||||
|
|
||||||
|
// Detail modal functionality
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.closest('.btn-detail')) {
|
||||||
|
const id = e.target.closest('.btn-detail').getAttribute('data-id');
|
||||||
|
|
||||||
|
// Fetch log details by ID
|
||||||
|
fetch(`{{ url('email-statement-logs') }}/${id}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Store current log ID for resend functionality
|
||||||
|
btnResendEmail.setAttribute('data-id', data.id);
|
||||||
|
|
||||||
|
// Show/hide resend button based on email status
|
||||||
|
if (data.email_status === 'failed') {
|
||||||
|
btnResendEmail.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
btnResendEmail.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate modal with data
|
||||||
|
document.getElementById('detail-branch-code').textContent = data.branch_code;
|
||||||
|
document.getElementById('detail-account-number').textContent = data.account_number;
|
||||||
|
document.getElementById('detail-period').textContent =
|
||||||
|
`${new Date(data.period_from).toLocaleDateString('id-ID')} - ${new Date(data.period_to).toLocaleDateString('id-ID')}`;
|
||||||
|
document.getElementById('detail-email-address').textContent = data.email_address;
|
||||||
|
|
||||||
|
const emailSourceText = data.email_source === 'account' ? 'Email Akun' : (data
|
||||||
|
.email_source === 'customer' ? 'Email Nasabah' : '-');
|
||||||
|
document.getElementById('detail-email-source').textContent = emailSourceText;
|
||||||
|
|
||||||
|
let emailStatusBadge = '';
|
||||||
|
if (data.email_status === 'sent') {
|
||||||
|
emailStatusBadge = '<span class="badge badge-success">Terkirim</span>';
|
||||||
|
} else if (data.email_status === 'failed') {
|
||||||
|
emailStatusBadge = '<span class="badge badge-danger">Gagal</span>';
|
||||||
|
} else if (data.email_status === 'pending') {
|
||||||
|
emailStatusBadge = '<span class="badge badge-warning">Pending</span>';
|
||||||
|
}
|
||||||
|
document.getElementById('detail-email-status').innerHTML = emailStatusBadge;
|
||||||
|
|
||||||
|
document.getElementById('detail-email-sent-at').textContent = data.email_sent_at ?
|
||||||
|
new Date(data.email_sent_at).toLocaleString('id-ID') :
|
||||||
|
'-';
|
||||||
|
document.getElementById('detail-error-message').textContent = data.error_message || '-';
|
||||||
|
document.getElementById('detail-user-id').textContent = data.user_id || '-';
|
||||||
|
document.getElementById('detail-created-at').textContent = data.created_at ?
|
||||||
|
new Date(data.created_at).toLocaleString('id-ID') :
|
||||||
|
'-';
|
||||||
|
document.getElementById('detail-updated-at').textContent = data.updated_at ?
|
||||||
|
new Date(data.updated_at).toLocaleString('id-ID') :
|
||||||
|
'-';
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
const modalEl = KTDom.getElement('#detail-modal');
|
||||||
|
const modal = KTModal.getInstance(modalEl);
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching log details:', error);
|
||||||
|
alert('Gagal mengambil detail log');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resend email functionality
|
||||||
|
if (e.target.closest('.btn-resend') || e.target.closest('#btn-resend-email')) {
|
||||||
|
const id = e.target.closest('.btn-resend, #btn-resend-email').getAttribute('data-id');
|
||||||
|
|
||||||
|
if (confirm('Apakah Anda yakin ingin mengirim ulang email ini?')) {
|
||||||
|
fetch(`{{ url('email-statement-logs') }}/${id}/resend`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||||
|
'content')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Email berhasil dikirim ulang');
|
||||||
|
dataTable.reload();
|
||||||
|
|
||||||
|
// Close modal if open
|
||||||
|
const modalEl = KTDom.getElement('#detail-modal');
|
||||||
|
const modal = KTModal.getInstance(modalEl);
|
||||||
|
if (modal) {
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('Gagal mengirim ulang email: ' + (data.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error resending email:', error);
|
||||||
|
alert('Gagal mengirim ulang email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.dataTable = dataTable;
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
176
resources/views/email-statement-logs/show.blade.php
Normal file
176
resources/views/email-statement-logs/show.blade.php
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
@extends('layouts.main')
|
||||||
|
|
||||||
|
@section('title', 'Detail Log Pengiriman Email Statement')
|
||||||
|
|
||||||
|
@section('breadcrumbs')
|
||||||
|
{{ Breadcrumbs::render(request()->route()->getName(), $log) }}
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Detail Log Pengiriman Email Statement</h3>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<a href="{{ route('email-statement-logs.index') }}" class="btn btn-sm btn-light">
|
||||||
|
<i class="text-base ki-filled ki-black-left"></i>
|
||||||
|
Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">ID Log:</td>
|
||||||
|
<td>{{ $log->id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Branch:</td>
|
||||||
|
<td>{{ $log->branch->name ?? 'N/A' }} ({{ $log->branch_code }})</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">No. Rekening:</td>
|
||||||
|
<td>{{ $log->account_number }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Periode:</td>
|
||||||
|
<td>{{ $log->period_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Email Tujuan:</td>
|
||||||
|
<td>{{ $log->email }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Status Email:</td>
|
||||||
|
<td>
|
||||||
|
@if ($log->email_sent_at)
|
||||||
|
<span class="badge badge-success">Terkirim</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-warning">Pending</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Tanggal Kirim:</td>
|
||||||
|
<td>{{ $log->email_sent_at ? $log->email_sent_at->format('d/m/Y H:i:s') : '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Status Otorisasi:</td>
|
||||||
|
<td>
|
||||||
|
@php
|
||||||
|
$badgeClass = 'badge-secondary';
|
||||||
|
if ($log->authorization_status === 'approved') {
|
||||||
|
$badgeClass = 'badge-success';
|
||||||
|
} elseif ($log->authorization_status === 'rejected') {
|
||||||
|
$badgeClass = 'badge-danger';
|
||||||
|
} elseif ($log->authorization_status === 'pending') {
|
||||||
|
$badgeClass = 'badge-warning';
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
<span
|
||||||
|
class="badge {{ $badgeClass }}">{{ ucfirst($log->authorization_status) }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Statement Tersedia:</td>
|
||||||
|
<td>
|
||||||
|
@if ($log->is_available)
|
||||||
|
<span class="badge badge-success">Ya</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-danger">Tidak</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">User Pembuat:</td>
|
||||||
|
<td>{{ $log->user->name ?? 'N/A' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Tanggal Dibuat:</td>
|
||||||
|
<td>{{ $log->created_at->format('d/m/Y H:i:s') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Terakhir Update:</td>
|
||||||
|
<td>{{ $log->updated_at->format('d/m/Y H:i:s') }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($log->remarks)
|
||||||
|
<div class="mt-4 row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5>Catatan:</h5>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{{ $log->remarks }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Aksi</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if ($log->is_available && $log->authorization_status === 'approved')
|
||||||
|
<button onclick="resendEmail({{ $log->id }})" class="mb-3 btn btn-primary w-100">
|
||||||
|
<i class="text-base ki-filled ki-message-text-2"></i>
|
||||||
|
Kirim Ulang Email
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($log->is_available)
|
||||||
|
<a href="{{ route('statements.download', $log->id) }}" class="mb-3 btn btn-success w-100">
|
||||||
|
<i class="text-base ki-filled ki-file-down"></i>
|
||||||
|
Download Statement
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<a href="{{ route('statements.show', $log->id) }}" class="btn btn-info w-100">
|
||||||
|
<i class="text-base ki-filled ki-eye"></i>
|
||||||
|
Lihat Statement Log
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
// Function untuk resend email
|
||||||
|
function resendEmail(logId) {
|
||||||
|
if (confirm('Apakah Anda yakin ingin mengirim ulang email statement ini?')) {
|
||||||
|
$.ajax({
|
||||||
|
url: '{{ route('email-statement-logs.resend-email', ':id') }}'.replace(':id', logId),
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
_token: '{{ csrf_token() }}'
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
alert('Email statement berhasil dijadwalkan untuk dikirim ulang.');
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
alert('Gagal mengirim ulang email statement.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -59,85 +60,96 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul.dashed-list {
|
ul.dashed-list {
|
||||||
list-style-type: none; /* Remove default bullet */
|
list-style-type: none;
|
||||||
padding-left: 1em; /* Add some left padding for spacing */
|
/* Remove default bullet */
|
||||||
|
padding-left: 1em;
|
||||||
|
/* Add some left padding for spacing */
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.dashed-list li::before {
|
ul.dashed-list li::before {
|
||||||
content: "– "; /* Use an en dash (U+2013) or a hyphen "-" */
|
content: "– ";
|
||||||
display: inline-block; /* Ensure proper spacing */
|
/* Use an en dash (U+2013) or a hyphen "-" */
|
||||||
width: 1em; /* Adjust width as needed */
|
display: inline-block;
|
||||||
margin-left: 0.5em; /* Align the dash properly */
|
/* Ensure proper spacing */
|
||||||
|
width: 1em;
|
||||||
|
/* Adjust width as needed */
|
||||||
|
margin-left: 0.5em;
|
||||||
|
/* Align the dash properly */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div>
|
<div>
|
||||||
Yang Terhormat <strong>Bapak/Ibu Daeng Deni Mardaeni</strong>,<br><br>
|
Yang Terhormat <strong>Bapak/Ibu {{ $accounts->customer->name }}</strong>,<br><br>
|
||||||
|
|
||||||
Terlampir adalah Electronic Statement Rekening Anda.<br>
|
Terlampir adalah Electronic Statement Rekening Anda.<br>
|
||||||
Silahkan gunakan password Electronic Statement Anda untuk membukanya.<br><br>
|
Silahkan gunakan password Electronic Statement Anda untuk membukanya.<br><br>
|
||||||
Password standar Elektronic Statement ini adalah <strong>ddMonyyyyxx</strong> (contoh: 01Aug1970xx) dimana :
|
Password standar Elektronic Statement ini adalah <strong>ddMonyyyyxx</strong> (contoh: 01Aug1970xx)
|
||||||
<ul class="dashed-list">
|
dimana :
|
||||||
<li>dd : <strong>2 digit</strong> tanggal lahir anda, contoh: 01</li>
|
<ul class="dashed-list">
|
||||||
<li>Mon :
|
<li>dd : <strong>2 digit</strong> tanggal lahir anda, contoh: 01</li>
|
||||||
<strong>3 huruf pertama</strong> bulan lahir anda dalam bahasa Ingris. Huruf pertama adalah huruf besar dan selanjutnya huruf kecil, contoh : Aug
|
<li>Mon :
|
||||||
</li>
|
<strong>3 huruf pertama</strong> bulan lahir anda dalam bahasa Ingris. Huruf pertama adalah
|
||||||
<li>yyyy : <strong>4 digit</strong> tahun kelahiran anda, contoh : 1970</li>
|
huruf besar dan selanjutnya huruf kecil, contoh : Aug
|
||||||
<li>xx : <strong>2 digit terakhir</strong> dari nomer rekening anda, contoh : 12</li>
|
</li>
|
||||||
</ul>
|
<li>yyyy : <strong>4 digit</strong> tahun kelahiran anda, contoh : 1970</li>
|
||||||
<br>
|
<li>xx : <strong>2 digit terakhir</strong> dari nomer rekening anda, contoh : 12</li>
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
|
|
||||||
Terima Kasih,<br><br>
|
Terima Kasih,<br><br>
|
||||||
|
|
||||||
<strong>Bank Artha Graha Internasional</strong><br>
|
<strong>Bank Artha Graha Internasional</strong><br>
|
||||||
------------------------------
|
------------------------------
|
||||||
<wbr>
|
<wbr>
|
||||||
------------------------------
|
------------------------------
|
||||||
<wbr>
|
<wbr>
|
||||||
--------<br>
|
--------<br>
|
||||||
Kami sangat menghargai masukan dan saran Anda untuk meningkatkan layanan dan produk kami.<br>
|
Kami sangat menghargai masukan dan saran Anda untuk meningkatkan layanan dan produk kami.<br>
|
||||||
Untuk memberikan masukan, silakan hubungi <strong>GrahaCall 24 Jam</strong> kami di
|
Untuk memberikan masukan, silakan hubungi <strong>GrahaCall 24 Jam</strong> kami di
|
||||||
<strong>0-800-191-8880</strong>.<br><br><br>
|
<strong>0-800-191-8880</strong>.<br><br><br>
|
||||||
|
|
||||||
Dear <strong>Mr/Mrs/Ms Daeng Deni Mardaeni</strong>,<br><br>
|
Dear <strong>Mr/Mrs/Ms {{ $accounts->customer->name }}</strong>,<br><br>
|
||||||
|
|
||||||
Attached is your Electronic Account Statement.<br>
|
Attached is your Electronic Account Statement.<br>
|
||||||
Please use your Electronic Statement password to open it.<br><br>
|
Please use your Electronic Statement password to open it.<br><br>
|
||||||
|
|
||||||
The Electronic Statement standard password is <strong>ddMonyyyyxx</strong> (example: 01Aug1970xx) where:
|
The Electronic Statement standard password is <strong>ddMonyyyyxx</strong> (example: 01Aug1970xx) where:
|
||||||
<ul class="dashed-list">
|
<ul class="dashed-list">
|
||||||
<li>dd : <strong>The first 2 digits</strong> of your birthdate, example: 01</li>
|
<li>dd : <strong>The first 2 digits</strong> of your birthdate, example: 01</li>
|
||||||
<li>Mon :
|
<li>Mon :
|
||||||
<strong>The first 3 letters</strong> of your birth month in English. The first letter is uppercase and the rest are lowercase, example: Aug
|
<strong>The first 3 letters</strong> of your birth month in English. The first letter is
|
||||||
</li>
|
uppercase and the rest are lowercase, example: Aug
|
||||||
<li>yyyy : <strong>4 digit</strong> of your birth year, example: 1970</li>
|
</li>
|
||||||
<li>xx : <strong>The last 2 digits</strong> of your account number, example: 12.</li>
|
<li>yyyy : <strong>4 digit</strong> of your birth year, example: 1970</li>
|
||||||
</ul>
|
<li>xx : <strong>The last 2 digits</strong> of your account number, example: 12.</li>
|
||||||
<br>
|
</ul>
|
||||||
|
<br>
|
||||||
|
|
||||||
Regards,<br><br>
|
Regards,<br><br>
|
||||||
|
|
||||||
<strong>Bank Artha Graha Internasional</strong><br>
|
<strong>Bank Artha Graha Internasional</strong><br>
|
||||||
------------------------------
|
------------------------------
|
||||||
<wbr>
|
<wbr>
|
||||||
------------------------------
|
------------------------------
|
||||||
<wbr>
|
<wbr>
|
||||||
--------<br>
|
--------<br>
|
||||||
We welcome any feedback or suggestions to improve our product and services.<br>
|
We welcome any feedback or suggestions to improve our product and services.<br>
|
||||||
If you have any feedback, please contact our <strong>GrahaCall 24 Hours</strong> at
|
If you have any feedback, please contact our <strong>GrahaCall 24 Hours</strong> at
|
||||||
<strong>0-800-191-8880</strong>.
|
<strong>0-800-191-8880</strong>.
|
||||||
<div class="yj6qo"></div>
|
<div class="yj6qo"></div>
|
||||||
<div class="adL"><br>
|
<div class="adL"><br>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© {{ date('Y') }} Bank Artha Graha Internasional. All rights reserved.</p>
|
||||||
|
<p>Jika Anda memiliki pertanyaan, silakan hubungi customer service kami.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
|
||||||
<p>© 2023 Bank Artha Graha Internasional. All rights reserved.</p>
|
|
||||||
<p>Jika Anda memiliki pertanyaan, silakan hubungi customer service kami.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -114,3 +114,14 @@
|
|||||||
$trail->parent('home');
|
$trail->parent('home');
|
||||||
$trail->push('Print Stetement', route('statements.index'));
|
$trail->push('Print Stetement', route('statements.index'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Breadcrumbs::for('atm-reports.index', function (BreadcrumbTrail $trail) {
|
||||||
|
$trail->parent('home');
|
||||||
|
$trail->push('Laporan Transaksi ATM', route('atm-reports.index'));
|
||||||
|
});
|
||||||
|
|
||||||
|
Breadcrumbs::for('email-statement-logs.index', function (BreadcrumbTrail $trail) {
|
||||||
|
$trail->parent('home');
|
||||||
|
$trail->push('Statement Email Logs', route('email-statement-logs.index'));
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ use Illuminate\Support\Facades\Route;
|
|||||||
use Modules\Webstatement\Http\Controllers\KartuAtmController;
|
use Modules\Webstatement\Http\Controllers\KartuAtmController;
|
||||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||||
use Modules\Webstatement\Http\Controllers\CustomerController;
|
use Modules\Webstatement\Http\Controllers\CustomerController;
|
||||||
use Modules\Webstatement\Http\Controllers\DebugStatementController;
|
|
||||||
use Modules\Webstatement\Http\Controllers\EmailBlastController;
|
use Modules\Webstatement\Http\Controllers\EmailBlastController;
|
||||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
||||||
|
use Modules\Webstatement\Http\Controllers\DebugStatementController;
|
||||||
|
use Modules\Webstatement\Http\Controllers\EmailStatementLogController;
|
||||||
|
use Modules\Webstatement\Http\Controllers\AtmTransactionReportController;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -89,6 +91,25 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Route::resource('statements', PrintStatementController::class);
|
Route::resource('statements', PrintStatementController::class);
|
||||||
|
|
||||||
|
|
||||||
|
// ATM Transaction Report Routes
|
||||||
|
Route::group(['prefix' => 'atm-reports', 'as' => 'atm-reports.', 'middleware' => ['auth']], function () {
|
||||||
|
Route::get('/datatables', [AtmTransactionReportController::class, 'dataForDatatables'])->name('datatables');
|
||||||
|
Route::get('/{atmReport}/download', [AtmTransactionReportController::class, 'download'])->name('download');
|
||||||
|
Route::post('/{atmReport}/authorize', [AtmTransactionReportController::class, 'authorize'])->name('authorize');
|
||||||
|
Route::get('/{atmReport}/send-email', [AtmTransactionReportController::class, 'sendEmail'])->name('send-email');
|
||||||
|
Route::post('/{atmReport}/retry', [AtmTransactionReportController::class, 'retry'])->name('retry');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::resource('atm-reports', AtmTransactionReportController::class);
|
||||||
|
|
||||||
|
// Email Statement Log Routes
|
||||||
|
Route::group(['prefix' => 'email-statement-logs', 'as' => 'email-statement-logs.', 'middleware' => ['auth']], function () {
|
||||||
|
Route::get('/datatables', [EmailStatementLogController::class, 'dataForDatatables'])->name('datatables');
|
||||||
|
Route::post('/{id}/resend-email', [EmailStatementLogController::class, 'resendEmail'])->name('resend-email');
|
||||||
|
});
|
||||||
|
Route::resource('email-statement-logs', EmailStatementLogController::class)->only(['index', 'show']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('migrasi', [MigrasiController::class, 'index'])->name('migrasi.index');
|
Route::get('migrasi', [MigrasiController::class, 'index'])->name('migrasi.index');
|
||||||
|
|||||||
Reference in New Issue
Block a user