Compare commits

..

12 Commits

Author SHA1 Message Date
Daeng Deni Mardaeni
4616137e0c feat(webstatement): tambahkan fitur konfirmasi email dan optimasi proses download statement
- **Penambahan Fitur Konfirmasi Email:**
  - Menambahkan event listener untuk form submit:
    - Menampilkan SweetAlert jika field email telah diisi.
    - Mengonfirmasi pengiriman statement ke alamat email yang diisi pengguna.
    - Submit form hanya setelah user mengonfirmasi.

- **Optimalisasi Proses Download Statement:**
  - Menangani logic download statement dalam rentang periode (period range):
    - Mencatat log keberadaan file untuk setiap periode.
    - Membuat file ZIP yang berisi semua file statement yang tersedia dalam rentang tersebut.
    - Mengelola file sementara untuk proses kompresi dengan pembersihan otomatis.
    - Menambahkan log error dan warning untuk file yang hilang dalam rentang periode.
    - Mendukung mekanisme download file tunggal untuk periode tertentu.
  - Menyesuaikan log dengan detail proses, seperti:
    - Informasi periode yang tersedia dan tidak.
    - Notifikasi penyelesaian atau kegagalan proses download ZIP.
  - Menambahkan logging trace pada exception untuk debugging lebih rinci.

- **Perubahan Validasi Logic:**
  - Validasi baru pada `PrintStatementRequest`:
    - Menentukan `is_period_range` hanya jika `period_to` berbeda dengan `period_from`.

- **Perbaikan dan Penyesuaian Pengiriman Email:**
  - Menambahkan pengecekan field email sebelum menjalankan fungsi kirim email di `PrintStatementController`.
  - Mengintegrasikan fungsi `sendEmail` jika terdapat email pada statement.

- **Penambahan Dokumentasi Kode:**
  - Menambahkan komentar inline di beberapa bagian:
    - Logika konfirmasi email.
    - Proses pembuatan ZIP dan penanganan download.
  - Menjelaskan tiap langkah operasional untuk mempermudah pemahaman dan debugging.

Perubahan ini mengintegrasikan fitur konfirmasi email yang lebih interaktif, meningkatkan proses download statement berjenjang, serta memperbaiki validasi dan logging pada tiap langkah proses.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-23 10:24:23 +07:00
Daeng Deni Mardaeni
19c962307e feat(webstatement): tambahkan fitur multi-branch dan perbaikan validasi form pada halaman statements
- **Penambahan Fitur Multi-Branch:**
  - Tambahkan dropdown pilihan cabang (branch) saat fitur multi-branch diaktifkan.
  - Secara otomatis mengisi informasi branch jika hanya tersedia satu branch yang terkait dengan user.

- **Perbaikan Validasi Form:**
  - Memastikan field `account_number` dan `branch_id` memiliki validasi yang lebih ketat.
  - Tambahkan validasi untuk `period_from` agar hanya menerima data periode yang tersedia (`is_available`).

- **Perubahan Tampilan:**
  - Menyesuaikan desain form:
    - Tambahkan kondisi dynamic display pada field branch berdasarkan status multi-branch.
    - Reformat struktur HTML untuk meningkatkan keterbacaan dengan indentasi lebih konsisten.
  - Perbaikan tampilan elemen tabel pada daftar request statement:
    - Mengoptimalkan style menggunakan properti CSS baru pada grid dan typography.

- **Optimasi Query dan Akses Data:**
  - Tambahkan filter berdasarkan `branch_code` agar data hanya terlihat untuk cabang yang relevan dengan user.
  - Optimalkan pengambilan data branch dengan hanya memuat cabang yang aktif.

- **Peningkatan Logging:**
  - Tambahkan log pada pengolahan query untuk mendeteksi masalah akses branch saat user tidak memiliki akses multi-branch.

- **Refaktor Backend:**
  - Tambahkan variable `multiBranch` pada controller untuk mengatur logika UI secara dinamis.
  - Refaktor pencarian branch di server-side untuk mengantisipasi session `MULTI_BRANCH`.

Perubahan ini mendukung fleksibilitas akses cabang untuk user dengan mode multi-branch serta meningkatkan validasi dan pengalaman UI form.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-22 20:57:24 +07:00
Daeng Deni Mardaeni
a79b1bd99e feat(webstatement): perbarui penamaan file PDF dan tambahkan log debug pada PrintStatementController
- **Perubahan Penamaan File PDF:**
  - Mengubah format nama file dari `{account_number}.pdf` menjadi `{account_number}_{period_from}.pdf`.
  - Penyesuaian pada semua lokasi logika penentuan path file di SFTP:
    - Path file period single.
    - Path file pada mode period range.
    - Path file saat kompresi ke dalam ZIP.

- **Penambahan Logging untuk Debugging:**
  - Menambahkan **Log::info** untuk mencatat informasi terkait path file, termasuk:
    - Path relatif file berdasarkan periode dan kode cabang.
    - Root path konfigurasi SFTP.
    - Path final lengkap pada SFTP.

- **Penyesuaian Logika Path:**
  - Memastikan format nama file konsisten di semua fungsi handling periode tunggal dan periode range.
  - Menambahkan logging sebelum proses pengecekan eksistensi file pada SFTP.

- **Peningkatan Monitoring:**
  - Memastikan struktur file dan path dapat dipantau dengan logging untuk mendukung debugging lebih baik.
  - Memberikan konteks tambahan pada setiap log yang relevan untuk memudahkan tracking.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-22 16:50:37 +07:00
daengdeni
fd5b8e1dad feat(webstatement): tambahkan filter data berdasarkan peran pengguna
### Perubahan Utama
- Menambahkan filter data pada `PrintStatementLog` untuk pengguna non-administrator.
- Membatasi query hanya untuk data yang sesuai dengan `user_id` pengguna yang sedang login jika bukan administrator.

### Detail Perubahan
1. **Update Logika Query**:
   - Menambahkan kondisi pengecekan untuk peran pengguna menggunakan `auth()->user()->hasRole('administrator')`.
   - Jika pengguna bukan administrator, query akan otomatis difilter berdasarkan `user_id` dari pengguna yang sedang login dengan fungsi `Auth::id()`.

2. **Peningkatan Keamanan Data**:
   - Membatasi akses data supaya hanya pengguna yang berhak dapat melihat data milik mereka.
   - Memastikan administrator tetap memiliki akses penuh ke semua data tanpa pembatasan.
2025-06-20 14:33:56 +07:00
daengdeni
8fb16028d9 feat(webstatement): tambah validasi cabang rekening dan update logika penyimpanan statement
### Perubahan Utama
- Tambah validasi untuk memverifikasi bahwa nomor rekening sesuai dengan cabang pengguna.
- Cegah transaksi untuk rekening yang terdaftar di cabang khusus (`ID0019999`).
- Perbaikan sistem untuk menangani kasus rekening yang tidak ditemukan di database.

### Detail Perubahan
1. **Validasi Cabang Rekening**:
   - Tambah pengecekan untuk memastikan rekening yang dimasukkan adalah milik cabang pengguna (non-multi-branch).
   - Blokir transaksi jika rekening terdaftar pada cabang khusus (`ID0019999`) dengan menampilkan pesan error yang relevan.
   - Tambahkan pesan error jika nomor rekening tidak ditemukan dalam sistem.

2. **Update Logika Penyimpanan**:
   - Tambahkan validasi untuk mengisi kolom `branch_code` secara otomatis berdasarkan informasi rekening terkait.
   - Otomatis atur nilai awal `authorization_status` menjadi `approved`.

3. **Penghapusan Atribut Tidak Digunakan**:
   - Hapus form `branch_code` dari view terkait (`index.blade.php`) karena sekarang diisi secara otomatis berdasarkan data rekening.

4. **Perbaikan View dan Logika Terkait Status Otorisasi**:
   - Hapus logic dan elemen UI terkait `authorization_status` di halaman statement (`index.blade.php` dan `show.blade.php`).
   - Simplifikasi tampilan untuk hanya menampilkan informasi yang tersedia dan relevan.

5. **Optimasi Query Data Cabang**:
   - Update query untuk memfilter cabang berdasarkan kondisi `customer_company` dan mengecualikan kode cabang khusus.

6. **Penyesuaian Struktur Request**:
   - Hapus validasi terkait `branch_code` di `PrintStatementRequest` karena tidak lagi relevan.

7. **Log Aktivitas dan Kesalahan**:
   - Tambahkan log untuk mencatat aktivitas seperti validasi rekening dan penyimpanan batch data.
   - Penanganan lebih baik untuk logging jika terjadi error saat validasi nomor rekening atau penyimpanan statement.

### Manfaat Perubahan
- Meningkatkan akurasi data cabang dan validasi rekening sebelum penyimpanan.
- Menyederhanakan antarmuka pengguna dengan menghapus field input redundant.
- Memastikan proses menjadi lebih transparan dengan penanganan error yang lebih baik.

Langkah ini diterapkan untuk meningkatkan keamanan dan keandalan sistem dalam memverifikasi dan memproses pemintaan statement.
2025-06-20 13:59:58 +07:00
Daeng Deni Mardaeni
6035c61cc4 feat(webstatement): tingkatkan validasi dan logging pada ProcessStmtEntryDataJob
- **Validasi Data:**
  - Menambahkan validasi untuk memastikan bahwa setiap `entryData` adalah array dan memiliki properti `stmt_entry_id`.
  - Log peringatan ditambahkan untuk mendeteksi struktur data yang tidak valid.

- **Perbaikan Logging:**
  - Logging ditingkatkan untuk mencatat data invalid yang ditemukan selama proses.
  - Menambahkan log peringatan dengan struktur data detail saat validasi gagal.

- **Penghapusan Nested Loop:**
  - Memperbaiki logika iterasi dengan menghapus nested loop dan langsung memproses tiap elemen `entryBatch`.

- **Penghitungan Kesalahan:**
  - Menambahkan penghitungan `errorCount` untuk melacak jumlah data yang mengalami validasi gagal.

Perubahan ini meningkatkan keandalan proses dengan validasi tambahan, mencegah error akibat struktur data tidak valid, serta memberikan informasi log yang lebih rinci.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-17 09:54:26 +07:00
Daeng Deni Mardaeni
2c8f49af20 feat(webstatement): optimalkan saveBatch pada ProcessStmtEntryDataJob
- **Perubahan Mekanisme Simpan Data:**
  - Mengganti pendekatan `delete` dan `insert` dengan `updateOrCreate` untuk mencegah duplicate key error.
  - Menambahkan transaksi database (`DB::beginTransaction()` dan `DB::commit()`) untuk memastikan konsistensi data.
  - Menambahkan logging pada awal dan akhir proses untuk memantau jumlah record yang berhasil diproses.

- **Penanganan Error:**
  - Menambahkan rollback transaksi (`DB::rollback()`) pada exception untuk menghindari data korup.
  - Logging eror ditingkatkan dengan menampilkan pesan dan trace exception secara rinci.

- **Optimasi Loop:**
  - Refinement looping pada `entryBatch` dengan menerapkan chunking untuk efisiensi memori.
  - Proses setiap record menggunakan `updateOrCreate` guna mengurangi overhead penghapusan data secara manual.

- **Peningkatan Logging:**
  - Menambahkan informasi log yang mencakup:
    - Proses awal dan akhir dari `saveBatch`.
    - Jumlah record yang diproses secara sukses.
    - Error yang terjadi selama proses berlangsung.

- **Dokumentasi dan Komentar:**
  - Menambahkan penjelasan detil pada method `saveBatch` untuk memperjelas logika baru.
  - Penyempurnaan komentar agar mencerminkan proses terkini dengan jelas.

Perubahan ini meningkatkan efisiensi dan keandalan proses penyimpanan data batch dengan mengurangi risiko konflik pada database serta memastikan rollback pada situasi error.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-17 09:45:41 +07:00
Daeng Deni Mardaeni
4bfd937490 feat(webstatement): tambahkan pengelolaan kartu ATM dengan fitur batch processing dan CSV tunggal
- **Penambahan Fitur**:
  - Menambahkan metode baru `generateSingleAtmCardCsv` untuk membuat file CSV tunggal tanpa pemisahan cabang:
    - Mencakup seluruh data kartu ATM yang memenuhi syarat.
    - File diunggah ke SFTP tanpa direktori spesifik cabang.
  - Implementasi command `UpdateAllAtmCardsCommand` untuk batch update:
    - Dukungan konfigurasi parameter seperti batch size, ID log sinkronisasi, queue, filter, dan dry-run.

- **Optimasi Logging**:
  - Logging rinci ditambahkan pada semua proses, termasuk:
    - Generasi CSV tunggal.
    - Proses upload CSV ke SFTP.
    - Pembaruan atau pembuatan `KartuSyncLog` dalam batch processing.
    - Progress dan status tiap batch.
    - Error handling dengan detail informasi pada setiap exception.

- **Perbaikan dan Penyesuaian Job**:
  - Penambahan `UpdateAllAtmCardsBatchJob` yang mengatur proses batch update:
    - Mendukung operasi batch dengan pengaturan ukuran dan parameter filtering kartu.
    - Pencatatan log progres secara dinamis dengan kalkulasi batch dan persentase.
    - Menyusun delay antar job untuk performa yang lebih baik.
  - Menyertakan validasi untuk sinkronisasi dan pembaruan data kartu ATM.

- **Refaktor Provider**:
  - Pendaftaran command baru:
    - `UpdateAllAtmCardsCommand` untuk batch update seluruh kartu ATM.
    - Command disertakan dalam provider `WebstatementServiceProvider`.

- **Error Handling**:
  - Peningkatan mekanisme rollback pada database saat error.
  - Menambahkan notifikasi log `failure` apabila job gagal dijalankan.

- **Dokumentasi dan Komentar**:
  - Menambahkan komentar mendetail pada setiap fungsi baru untuk penjelasan lebih baik.
  - Mendokumentasikan seluruh proses dan perubahan pada job serta command baru terkait kartu ATM.

  Perubahan ini meningkatkan efisiensi pengelolaan data kartu ATM, termasuk generasi CSV, proses batch, dan pengunggahan data ke SFTP.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-16 22:51:26 +07:00
Daeng Deni Mardaeni
7b32cb8d39 feat(webstatement): tambah filter product_code dan branch pada GenerateBiayaKartuCsvJob
- Menambahkan filter baru:
  - Memastikan `product_code` tidak termasuk dalam daftar `6002`, `6004`, `6042`, dan `6031`.
  - Menyaring data dengan kondisi branch tidak sama dengan `ID0019999`.

- Optimasi query:
  - Filter tambahan bertujuan untuk mempersempit data hasil pengambilan sehingga lebih relevan dan efisien dalam pembuatan file CSV.

- Peningkatan validasi:
  - Memastikan data yang diekspor sesuai dengan ketentuan baru guna meningkatkan akurasi laporan biaya kartu.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-13 15:11:25 +07:00
Daeng Deni Mardaeni
4b889da5a5 feat(webstatement): tambahkan pengelolaan product_code pada ATM Card
- **Penambahan Field Baru:**
  - Menambahkan field baru `product_code` pada tabel `atmcards` melalui migrasi database.
  - Field bersifat nullable dan memiliki komentar deskriptif untuk dokumentasi skema database.

- **Refaktor Logika pada UpdateAtmCardBranchCurrencyJob:**
  - Menambahkan assignment data `product_code` untuk update kartu ATM berdasarkan informasi account.
  - Mengoptimalkan proses query dengan memperbaiki penggunaan namespace model `Account`.

- **Peningkatan Model Atmcard:**
  - Menambahkan relasi baru `biaya` untuk mendapatkan informasi terkait jenis kartu (`JenisKartu`).
  - Menambah **scope** baru:
    - `active` untuk memfilter kartu ATM yang aktif.
    - `byProductCode` untuk memfilter berdasarkan kode produk (`product_code`).
  - Memperkenalkan accessor dan mutator untuk memastikan format `product_code` konsisten (uppercase, trimmed).
  - Menambahkan logging pada setiap akses relasi atau perubahan terkait field `product_code`.

- **Penyesuaian Logging:**
  - Memperbanyak log untuk monitoring aktivitas, termasuk:
    - Akses dan perubahan data `product_code`.
    - Scope query pada model `Atmcard`.

- **Migrasi Database:**
  - Menambahkan proses safe migration dengan transaksi pada operasi `up` dan `down`.
  - Mencatat log saat migrasi berhasil atau rollback diperlukan jika terjadi kesalahan.

- **Optimisasi dan Perbaikan Format:**
  - Mengorganisasi ulang import pada file `UpdateAtmCardBranchCurrencyJob` sesuai standar PSR-12.
  - Membenahi key output response dari `openCategory` menjadi `acctType` untuk dukungan data baru `product_code`.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-13 15:06:37 +07:00
Daeng Deni Mardaeni
dbdeceb4c0 feat(webstatement): optimalkan pengambilan informasi account untuk UpdateAtmCardBranchCurrencyJob
- **Penambahan Logika Pengambilan Data:**
  - Menambahkan proses pengambilan data account dari model `Account` sebelum memanggil API Fiorano.
  - Melakukan pencarian data berdasarkan nomor rekening (`account_number`) melalui query pada model.
  - Jika data ditemukan, mengembalikan informasi account berupa response format yang menyerupai hasil dari API.

- **Optimisasi Response:**
  - Menyusun data response lengkap dari model `Account`, seperti kode cabang (`branch_code`), mata uang (`currency`), kategori pembukaan (`open_category`), dan properti lain yang relevan.
  - Field response menyertakan nilai default atau diisi dengan data lain yang ada dalam model.

- **Fallback API Fiorano:**
  - Jika data dari database tidak ditemukan, tetap menggunakan mekanisme existing untuk melakukan request ke API Fiorano.
  - Tidak ada perubahan lain pada struktur permintaan atau penanganan response Fiorano.

- **Komentar dan Dokumentasi:**
  - Memperbarui komentar pada fungsi `getAccountInfo` untuk mencerminkan logika terbaru.
  - Menjelaskan fallback ke API jika data model tidak tersedia melalui komentar inline agar lebih mudah dipahami.

- **Peningkatan Efisiensi:**
  - Mengurangi frekuensi panggilan API Fiorano dengan memanfaatkan data lokal terlebih dahulu, sehingga mempercepat proses eksekusi job.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-13 14:33:47 +07:00
Daeng Deni Mardaeni
f7a92a5336 refactor(webstatement): sesuaikan format list pada template email statement
- **Perubahan Format List:**
  - Mengganti properti `class="dashed-list"` dengan `style="list-style-type: none;"` untuk meningkatkan estetika dan konsistensi tampilan.
  - Menambahkan simbol `-` pada setiap item list untuk memperjelas poin dalam deskripsi password.

- **Peningkatan Konsistensi:**
  - Perbaikan dilakukan pada bagian deskripsi password dalam bahasa Indonesia dan Inggris untuk menjaga keseragaman format.
  - Memastikan semua elemen list menggunakan format yang seragam.

- **Penyesuaian Detail:**
  - Menonjolkan elemen list dengan properti HTML agar lebih mudah dipahami oleh penerima email.
  - Perbaikan minor pada struktur dan whitespace untuk meningkatkan keterbacaan kode.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-12 12:15:56 +07:00
14 changed files with 1256 additions and 219 deletions

View File

@@ -0,0 +1,110 @@
<?php
namespace Modules\Webstatement\Console;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Jobs\UpdateAllAtmCardsBatchJob;
class UpdateAllAtmCardsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'atmcard:update-all
{--sync-log-id= : ID sync log yang akan digunakan}
{--batch-size=100 : Ukuran batch untuk processing}
{--queue=atmcard-update : Nama queue untuk job}
{--filters= : Filter JSON untuk kondisi kartu}
{--dry-run : Preview tanpa eksekusi aktual}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Jalankan job untuk update seluruh kartu ATM secara batch';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
Log::info('Memulai command update seluruh kartu ATM');
try {
$syncLogId = $this->option('sync-log-id');
$batchSize = (int) $this->option('batch-size');
$queueName = $this->option('queue');
$filtersJson = $this->option('filters');
$isDryRun = $this->option('dry-run');
// Parse filters jika ada
$filters = [];
if ($filtersJson) {
$filters = json_decode($filtersJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->error('Format JSON filters tidak valid');
return Command::FAILURE;
}
}
// Validasi input
if ($batchSize <= 0) {
$this->error('Batch size harus lebih besar dari 0');
return Command::FAILURE;
}
$this->info('Konfigurasi job:');
$this->info("- Sync Log ID: " . ($syncLogId ?: 'Akan dibuat baru'));
$this->info("- Batch Size: {$batchSize}");
$this->info("- Queue: {$queueName}");
$this->info("- Filters: " . ($filtersJson ?: 'Tidak ada'));
$this->info("- Dry Run: " . ($isDryRun ? 'Ya' : 'Tidak'));
if ($isDryRun) {
$this->warn('Mode DRY RUN - Job tidak akan dijalankan');
return Command::SUCCESS;
}
// Konfirmasi sebelum menjalankan
if (!$this->confirm('Apakah Anda yakin ingin menjalankan job update seluruh kartu ATM?')) {
$this->info('Operasi dibatalkan');
return Command::SUCCESS;
}
// Dispatch job
$job = new UpdateAllAtmCardsBatchJob($syncLogId, $batchSize, $filters);
$job->onQueue($queueName);
dispatch($job);
$this->info('Job berhasil dijadwalkan!');
$this->info("Queue: {$queueName}");
$this->info('Gunakan command berikut untuk memonitor:');
$this->info('php artisan queue:work --queue=' . $queueName);
Log::info('Command update seluruh kartu ATM selesai', [
'sync_log_id' => $syncLogId,
'batch_size' => $batchSize,
'queue' => $queueName,
'filters' => $filters
]);
return Command::SUCCESS;
} catch (Exception $e) {
$this->error('Terjadi error: ' . $e->getMessage());
Log::error('Error dalam command update seluruh kartu ATM: ' . $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return Command::FAILURE;
}
}
}

View File

@@ -14,6 +14,7 @@
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;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog; use Modules\Webstatement\Models\PrintStatementLog;
use ZipArchive; use ZipArchive;
@@ -24,9 +25,15 @@
*/ */
public function index(Request $request) public function index(Request $request)
{ {
$branches = Branch::orderBy('name')->get(); $branches = Branch::whereNotNull('customer_company')
->where('code', '!=', 'ID0019999')
->orderBy('name')
->get();
return view('webstatement::statements.index', compact('branches')); $branch = Branch::find(Auth::user()->branch_id);
$multiBranch = session('MULTI_BRANCH') ?? false;
return view('webstatement::statements.index', compact('branches', 'branch', 'multiBranch'));
} }
/** /**
@@ -35,52 +42,90 @@
*/ */
public function store(PrintStatementRequest $request) public function store(PrintStatementRequest $request)
{ {
// Add account verification before storing
$accountNumber = $request->input('account_number'); // Assuming this is the field name for account number
// First, check if the account exists and get branch information
$account = Account::where('account_number', $accountNumber)->first();
if ($account) {
$branch_code = $account->branch_code;
$userBranchId = session('branch_id'); // Assuming branch ID is stored in session
$multiBranch = session('MULTI_BRANCH');
if (!$multiBranch) {
// Check if account branch matches user's branch
if ($account->branch_id !== $userBranchId) {
return redirect()->route('statements.index')
->with('error', 'Nomor rekening tidak sesuai dengan cabang Anda. Transaksi tidak dapat dilanjutkan.');
}
}
// Check if account belongs to restricted branch ID0019999
if ($account->branch_id === 'ID0019999') {
return redirect()->route('statements.index')
->with('error', 'Nomor rekening terdaftar pada cabang khusus. Silakan hubungi bagian HC untuk informasi lebih lanjut.');
}
// If all checks pass, proceed with storing data
// Your existing store logic here
} else {
// Account not found
return redirect()->route('statements.index')
->with('error', 'Nomor rekening tidak ditemukan dalam sistem.');
}
DB::beginTransaction(); DB::beginTransaction();
try { try {
$validated = $request->validated(); $validated = $request->validated();
// Add user tracking data dan field baru untuk single account request // Add user tracking data dan field baru untuk single account request
$validated['user_id'] = Auth::id(); $validated['user_id'] = Auth::id();
$validated['created_by'] = Auth::id(); $validated['created_by'] = Auth::id();
$validated['ip_address'] = $request->ip(); $validated['ip_address'] = $request->ip();
$validated['user_agent'] = $request->userAgent(); $validated['user_agent'] = $request->userAgent();
$validated['request_type'] = 'single_account'; // Default untuk request manual $validated['request_type'] = 'single_account'; // Default untuk request manual
$validated['status'] = 'pending'; // Status awal $validated['status'] = 'pending'; // Status awal
$validated['total_accounts'] = 1; // Untuk single account $validated['authorization_status'] = 'approved'; // Status otorisasi awal
$validated['total_accounts'] = 1; // Untuk single account
$validated['processed_accounts'] = 0; $validated['processed_accounts'] = 0;
$validated['success_count'] = 0; $validated['success_count'] = 0;
$validated['failed_count'] = 0; $validated['failed_count'] = 0;
$validated['branch_code'] = $branch_code; // Awal tidak tersedia
// Create the statement log // Create the statement log
$statement = PrintStatementLog::create($validated); $statement = PrintStatementLog::create($validated);
// Log aktivitas // Log aktivitas
Log::info('Statement request created', [ Log::info('Statement request created', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'user_id' => Auth::id(), 'user_id' => Auth::id(),
'account_number' => $statement->account_number, 'account_number' => $statement->account_number,
'request_type' => $statement->request_type 'request_type' => $statement->request_type
]); ]);
// Process statement availability check // Process statement availability check
$this->checkStatementAvailability($statement); $this->checkStatementAvailability($statement);
$statement = PrintStatementLog::find($statement->id);
if($statement->email){
$this->sendEmail($statement->id);
}
DB::commit(); DB::commit();
return redirect()->route('statements.index') return redirect()->route('statements.index')
->with('success', 'Statement request has been created successfully.'); ->with('success', 'Statement request has been created successfully.');
} catch (Exception $e) { } catch (Exception $e) {
DB::rollBack(); DB::rollBack();
Log::error('Failed to create statement request', [ Log::error('Failed to create statement request', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'user_id' => Auth::id() 'user_id' => Auth::id()
]); ]);
return redirect()->back() return redirect()->back()
->withInput() ->withInput()
->with('error', 'Failed to create statement request: ' . $e->getMessage()); ->with('error', 'Failed to create statement request: ' . $e->getMessage());
} }
} }
@@ -102,19 +147,26 @@
DB::beginTransaction(); DB::beginTransaction();
try { try {
$disk = Storage::disk('sftpStatement'); $disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; $filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
// Log untuk debugging
Log::info('Checking SFTP file path', [
'file_path' => $filePath,
'sftp_root' => config('filesystems.disks.sftpStatement.root'),
'full_path' => config('filesystems.disks.sftpStatement.root') . '/' . $filePath
]);
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);
$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 . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
if ($disk->exists($periodPath)) { if ($disk->exists($periodPath)) {
$availablePeriods[] = $periodFormatted; $availablePeriods[] = $periodFormatted;
@@ -127,55 +179,55 @@
$notes = "Missing periods: " . implode(', ', $missingPeriods); $notes = "Missing periods: " . implode(', ', $missingPeriods);
$statement->update([ $statement->update([
'is_available' => false, 'is_available' => false,
'remarks' => $notes, 'remarks' => $notes,
'updated_by' => Auth::id(), 'updated_by' => Auth::id(),
'status' => 'failed' 'status' => 'failed'
]); ]);
Log::warning('Statement not available - missing periods', [ Log::warning('Statement not available - missing periods', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'missing_periods' => $missingPeriods 'missing_periods' => $missingPeriods
]); ]);
} else { } else {
$statement->update([ $statement->update([
'is_available' => true, 'is_available' => true,
'updated_by' => Auth::id(), 'updated_by' => Auth::id(),
'status' => 'completed', 'status' => 'completed',
'processed_accounts' => 1, 'processed_accounts' => 1,
'success_count' => 1 'success_count' => 1
]); ]);
Log::info('Statement available - all periods found', [ Log::info('Statement available - all periods found', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'available_periods' => $availablePeriods 'available_periods' => $availablePeriods
]); ]);
} }
} else if ($disk->exists($filePath)) { } 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', 'status' => 'completed',
'processed_accounts' => 1, 'processed_accounts' => 1,
'success_count' => 1 'success_count' => 1
]); ]);
Log::info('Statement available', [ Log::info('Statement available', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'file_path' => $filePath 'file_path' => $filePath
]); ]);
} else { } else {
$statement->update([ $statement->update([
'is_available' => false, 'is_available' => false,
'updated_by' => Auth::id(), 'updated_by' => Auth::id(),
'status' => 'failed', 'status' => 'failed',
'processed_accounts' => 1, 'processed_accounts' => 1,
'failed_count' => 1, 'failed_count' => 1,
'error_message' => 'Statement file not found' 'error_message' => 'Statement file not found'
]); ]);
Log::warning('Statement not available', [ Log::warning('Statement not available', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'file_path' => $filePath 'file_path' => $filePath
]); ]);
} }
@@ -185,14 +237,14 @@
Log::error('Error checking statement availability', [ Log::error('Error checking statement availability', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'error' => $e->getMessage() 'error' => $e->getMessage()
]); ]);
$statement->update([ $statement->update([
'is_available' => false, 'is_available' => false,
'status' => 'failed', 'status' => 'failed',
'error_message' => $e->getMessage(), 'error_message' => $e->getMessage(),
'updated_by' => Auth::id() 'updated_by' => Auth::id()
]); ]);
} }
} }
@@ -223,33 +275,161 @@
$statement->update([ $statement->update([
'is_downloaded' => true, 'is_downloaded' => true,
'downloaded_at' => now(), 'downloaded_at' => now(),
'updated_by' => Auth::id() 'updated_by' => Auth::id()
]); ]);
Log::info('Statement downloaded', [ Log::info('Statement downloaded', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'user_id' => Auth::id(), 'user_id' => Auth::id(),
'account_number' => $statement->account_number 'account_number' => $statement->account_number
]); ]);
DB::commit(); DB::commit();
// Generate or fetch the statement file // Generate or fetch the statement file
$disk = Storage::disk('sftpStatement'); $disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; $filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
if ($statement->is_period_range && $statement->period_to) { if ($statement->is_period_range && $statement->period_to) {
// Handle period range download (existing logic) // Log: Memulai proses download period range
// ... existing zip creation logic ... Log::info('Starting period range download', [
'statement_id' => $statement->id,
'period_from' => $statement->period_from,
'period_to' => $statement->period_to
]);
/**
* Handle period range download dengan membuat zip file
* yang berisi semua statement dalam rentang periode
*/
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
// Loop through each month in the range
$missingPeriods = [];
$availablePeriods = [];
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
$periodFormatted = $period->format('Ym');
$periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
if ($disk->exists($periodPath)) {
$availablePeriods[] = $periodFormatted;
Log::info('Period available for download', [
'period' => $periodFormatted,
'path' => $periodPath
]);
} else {
$missingPeriods[] = $periodFormatted;
Log::warning('Period not available for download', [
'period' => $periodFormatted,
'path' => $periodPath
]);
}
}
// If any period is available, create a zip and download it
if (count($availablePeriods) > 0) {
/**
* Membuat zip file temporary untuk download
* dengan semua statement yang tersedia dalam periode
*/
$zipFileName = "{$statement->account_number}_{$statement->period_from}_to_{$statement->period_to}.zip";
$zipFilePath = storage_path("app/temp/{$zipFileName}");
// Ensure the temp directory exists
if (!file_exists(storage_path('app/temp'))) {
mkdir(storage_path('app/temp'), 0755, true);
Log::info('Created temp directory for zip files');
}
// Create a new zip archive
$zip = new ZipArchive();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
Log::info('Zip archive created successfully', ['zip_path' => $zipFilePath]);
// Add each available statement to the zip
foreach ($availablePeriods as $period) {
$periodFilePath = "{$period}/{$statement->branch_code}/{$statement->account_number}_{$period}.pdf";
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
try {
// Download the file from SFTP to local storage temporarily
file_put_contents($localFilePath, $disk->get($periodFilePath));
// Add the file to the zip
$zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf");
Log::info('Added file to zip', [
'period' => $period,
'local_path' => $localFilePath
]);
} catch (Exception $e) {
Log::error('Failed to add file to zip', [
'period' => $period,
'error' => $e->getMessage()
]);
}
}
$zip->close();
Log::info('Zip archive closed successfully');
// Return the zip file for download
$response = response()->download($zipFilePath, $zipFileName)->deleteFileAfterSend(true);
// Clean up temporary PDF files
foreach ($availablePeriods as $period) {
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
if (file_exists($localFilePath)) {
unlink($localFilePath);
Log::info('Cleaned up temporary file', ['file' => $localFilePath]);
}
}
Log::info('Period range download completed successfully', [
'statement_id' => $statement->id,
'available_periods' => count($availablePeriods),
'missing_periods' => count($missingPeriods)
]);
return $response;
} else {
Log::error('Failed to create zip archive', ['zip_path' => $zipFilePath]);
return back()->with('error', 'Failed to create zip archive for download.');
}
} else {
Log::warning('No statements available for download in period range', [
'statement_id' => $statement->id,
'missing_periods' => $missingPeriods
]);
return back()->with('error', 'No statements available for download in the specified period range.');
}
} else if ($disk->exists($filePath)) { } else if ($disk->exists($filePath)) {
/**
* Handle single period download
* Download file PDF tunggal untuk periode tertentu
*/
Log::info('Single period download', [
'statement_id' => $statement->id,
'file_path' => $filePath
]);
return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf"); return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf");
} else {
Log::warning('Statement file not found', [
'statement_id' => $statement->id,
'file_path' => $filePath
]);
return back()->with('error', 'Statement file not found.');
} }
} catch (Exception $e) { } catch (Exception $e) {
DB::rollBack(); DB::rollBack();
Log::error('Failed to download statement', [ Log::error('Failed to download statement', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'error' => $e->getMessage() 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]); ]);
return back()->with('error', 'Failed to download statement: ' . $e->getMessage()); return back()->with('error', 'Failed to download statement: ' . $e->getMessage());
@@ -295,6 +475,13 @@
// Retrieve data from the database // Retrieve data from the database
$query = PrintStatementLog::query(); $query = PrintStatementLog::query();
if (!auth()->user()->hasRole('administrator')) {
$query->where(function($q) {
$q->where('user_id', Auth::id())
->orWhere('branch_code', Auth::user()->branch->code);
});
}
// Apply search filter if provided // Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) { if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search'); $search = $request->get('search');
@@ -337,13 +524,13 @@
// Map frontend column names to database column names if needed // Map frontend column names to database column names if needed
$columnMap = [ $columnMap = [
'branch' => 'branch_code', 'branch' => 'branch_code',
'account' => 'account_number', 'account' => 'account_number',
'period' => 'period_from', 'period' => 'period_from',
'auth_status' => 'authorization_status', 'auth_status' => 'authorization_status',
'request_type' => 'request_type', 'request_type' => 'request_type',
'status' => 'status', 'status' => 'status',
'remarks' => 'remarks', 'remarks' => 'remarks',
]; ];
$dbColumn = $columnMap[$column] ?? $column; $dbColumn = $columnMap[$column] ?? $column;
@@ -426,6 +613,7 @@
public function sendEmail($id) public function sendEmail($id)
{ {
$statement = PrintStatementLog::findOrFail($id); $statement = PrintStatementLog::findOrFail($id);
// Check if statement has email // Check if statement has email
if (empty($statement->email)) { if (empty($statement->email)) {
return redirect()->back()->with('error', 'No email address provided for this statement.'); return redirect()->back()->with('error', 'No email address provided for this statement.');
@@ -438,7 +626,7 @@
try { try {
$disk = Storage::disk('sftpStatement'); $disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; $filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
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);
@@ -450,7 +638,7 @@
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 . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
if ($disk->exists($periodPath)) { if ($disk->exists($periodPath)) {
$availablePeriods[] = $periodFormatted; $availablePeriods[] = $periodFormatted;
@@ -475,7 +663,7 @@
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) { if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
// Add each available statement to the zip // Add each available statement to the zip
foreach ($availablePeriods as $period) { foreach ($availablePeriods as $period) {
$filePath = "{$period}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; $filePath = "{$period}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf"); $localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
// Download the file from SFTP to local storage temporarily // Download the file from SFTP to local storage temporarily
@@ -541,8 +729,8 @@
Log::info('Statement email sent successfully', [ Log::info('Statement email sent successfully', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'email' => $statement->email, 'email' => $statement->email,
'user_id' => Auth::id() 'user_id' => Auth::id()
]); ]);
DB::commit(); DB::commit();

View File

@@ -21,7 +21,6 @@ class PrintStatementRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
$rules = [ $rules = [
'branch_code' => ['required', 'string', 'exists:branches,code'],
'account_number' => ['required', 'string'], 'account_number' => ['required', 'string'],
'is_period_range' => ['sometimes', 'boolean'], 'is_period_range' => ['sometimes', 'boolean'],
'email' => ['nullable', 'email'], 'email' => ['nullable', 'email'],
@@ -36,6 +35,7 @@ class PrintStatementRequest extends FormRequest
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('is_available', true)
->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
@@ -77,8 +77,6 @@ class PrintStatementRequest extends FormRequest
public function messages(): array public function messages(): array
{ {
return [ return [
'branch_code.required' => 'Branch code is required',
'branch_code.exists' => 'Selected branch does not exist',
'account_number.required' => 'Account number is required', 'account_number.required' => 'Account number is required',
'period_from.required' => 'Period is required', 'period_from.required' => 'Period is required',
'period_from.regex' => 'Period must be in YYYYMM format', 'period_from.regex' => 'Period must be in YYYYMM format',
@@ -106,13 +104,13 @@ class PrintStatementRequest extends FormRequest
$this->merge([ $this->merge([
'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2), 'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2),
]); ]);
}
// Convert is_period_range to boolean if it exists // Only set is_period_range to true if period_to is different from period_from
if ($this->has('period_to')) { if ($this->period_to !== $this->period_from) {
$this->merge([ $this->merge([
'is_period_range' => true, 'is_period_range' => true,
]); ]);
}
} }
// Set default request_type if not provided // Set default request_type if not provided

View File

@@ -63,7 +63,9 @@
$this->updateCsvLogStart(); $this->updateCsvLogStart();
// Generate CSV file // Generate CSV file
$result = $this->generateAtmCardCsv(); // $result = $this->generateAtmCardCsv();
$result = $this->generateSingleAtmCardCsv();
// Update status CSV generation berhasil // Update status CSV generation berhasil
$this->updateCsvLogSuccess($result); $this->updateCsvLogSuccess($result);
@@ -175,6 +177,8 @@
->whereNotNull('currency') ->whereNotNull('currency')
->where('currency', '!=', '') ->where('currency', '!=', '')
->whereIn('ctdesc', $cardTypes) ->whereIn('ctdesc', $cardTypes)
->whereNotIn('product_code',['6002','6004','6042','6031'])
->where('branch','!=','ID0019999')
->get(); ->get();
} }
@@ -413,4 +417,155 @@
Log::error('Pembuatan file CSV gagal: ' . $errorMessage); Log::error('Pembuatan file CSV gagal: ' . $errorMessage);
} }
/**
* Generate single CSV file with all ATM card data without branch separation
*
* @return array Information about the generated file and upload status
* @throws RuntimeException
*/
private function generateSingleAtmCardCsv(): array
{
Log::info('Memulai pembuatan file CSV tunggal untuk semua kartu ATM');
try {
// Ambil semua kartu yang memenuhi syarat
$cards = $this->getEligibleAtmCards();
if ($cards->isEmpty()) {
Log::warning('Tidak ada kartu ATM yang memenuhi syarat untuk periode ini');
throw new RuntimeException('Tidak ada kartu ATM yang memenuhi syarat untuk diproses');
}
// Buat nama file dengan timestamp
$dateTime = now()->format('Ymd_Hi');
$singleFilename = pathinfo($this->csvFilename, PATHINFO_FILENAME)
. '_ALL_BRANCHES_'
. $dateTime . '.'
. pathinfo($this->csvFilename, PATHINFO_EXTENSION);
$filename = storage_path('app/' . $singleFilename);
Log::info('Membuat file CSV: ' . $filename);
// Buka file untuk menulis
$handle = fopen($filename, 'w+');
if (!$handle) {
throw new RuntimeException("Tidak dapat membuat file CSV: $filename");
}
$recordCount = 0;
try {
// Tulis semua kartu ke dalam satu file
foreach ($cards as $card) {
$fee = $this->determineCardFee($card);
$csvRow = $this->createCsvRow($card, $fee);
if (fputcsv($handle, $csvRow, '|') === false) {
throw new RuntimeException("Gagal menulis data kartu ke file CSV: {$card->crdno}");
}
$recordCount++;
// Log progress setiap 1000 record
if ($recordCount % 1000 === 0) {
Log::info("Progress: {$recordCount} kartu telah diproses");
}
}
} finally {
fclose($handle);
}
Log::info("Selesai menulis {$recordCount} kartu ke file CSV");
// Bersihkan file CSV (hapus double quotes)
$this->cleanupCsvFile($filename);
Log::info('File CSV berhasil dibersihkan dari double quotes');
// Upload file ke SFTP (tanpa branch specific directory)
$uploadSuccess = true; // $this->uploadSingleFileToSftp($filename);
$result = [
'localFilePath' => $filename,
'recordCount' => $recordCount,
'uploadToSftp' => $uploadSuccess,
'timestamp' => now()->format('Y-m-d H:i:s'),
'fileName' => $singleFilename
];
Log::info('Pembuatan file CSV tunggal selesai', $result);
return $result;
} catch (Exception $e) {
Log::error('Error dalam generateSingleAtmCardCsv: ' . $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* Upload single CSV file to SFTP server without branch directory
*
* @param string $localFilePath Path to the local CSV file
* @return bool True if upload successful, false otherwise
*/
private function uploadSingleFileToSftp(string $localFilePath): bool
{
try {
Log::info('Memulai upload file tunggal ke SFTP: ' . $localFilePath);
// Update status SFTP upload dimulai
$this->updateSftpLogStart();
// Ambil nama file dari path
$filename = basename($localFilePath);
// Ambil konten file
$fileContent = file_get_contents($localFilePath);
if ($fileContent === false) {
Log::error("Tidak dapat membaca file untuk upload: {$localFilePath}");
return false;
}
// Dapatkan disk SFTP
$disk = Storage::disk('sftpKartu');
// Tentukan path tujuan di server SFTP (root directory)
$remotePath = env('BIAYA_KARTU_REMOTE_PATH', '/');
$remoteFilePath = rtrim($remotePath, '/') . '/' . $filename;
Log::info('Mengunggah ke path remote: ' . $remoteFilePath);
// Upload file ke server SFTP
$result = $disk->put($remoteFilePath, $fileContent);
if ($result) {
$this->updateSftpLogSuccess();
Log::info("File CSV tunggal berhasil diunggah ke SFTP: {$remoteFilePath}");
return true;
} else {
$errorMsg = "Gagal mengunggah file CSV tunggal ke SFTP: {$remoteFilePath}";
$this->updateSftpLogFailed($errorMsg);
Log::error($errorMsg);
return false;
}
} catch (Exception $e) {
$errorMsg = "Error saat mengunggah file tunggal ke SFTP: " . $e->getMessage();
$this->updateSftpLogFailed($errorMsg);
Log::error($errorMsg, [
'file' => $e->getFile(),
'line' => $e->getLine(),
'periode' => $this->periode
]);
return false;
}
}
} }

View File

@@ -1,5 +1,4 @@
<?php <?php
namespace Modules\Webstatement\Jobs; namespace Modules\Webstatement\Jobs;
use Exception; use Exception;
@@ -11,6 +10,7 @@
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Modules\Webstatement\Models\StmtEntry; use Modules\Webstatement\Models\StmtEntry;
use Illuminate\Support\Facades\DB;
class ProcessStmtEntryDataJob implements ShouldQueue class ProcessStmtEntryDataJob implements ShouldQueue
{ {
@@ -184,33 +184,57 @@
} }
/** /**
* Save batched records to the database * Simpan batch data ke database menggunakan updateOrCreate
* untuk menghindari error unique constraint
*
* @return void
*/ */
private function saveBatch() private function saveBatch(): void
: void
{ {
Log::info('Memulai proses saveBatch dengan updateOrCreate');
DB::beginTransaction();
try { try {
if (!empty($this->entryBatch)) { if (!empty($this->entryBatch)) {
// Process in smaller chunks for better memory management $totalProcessed = 0;
foreach ($this->entryBatch as $entry) {
// Extract all stmt_entry_ids from the current chunk
$entryIds = array_column($entry, 'stmt_entry_id');
// Delete existing records with these IDs to avoid conflicts // Process each entry data directly (tidak ada nested array)
StmtEntry::whereIn('stmt_entry_id', $entryIds)->delete(); foreach ($this->entryBatch as $entryData) {
// Validasi bahwa entryData adalah array dan memiliki stmt_entry_id
if (is_array($entryData) && isset($entryData['stmt_entry_id'])) {
// Gunakan updateOrCreate untuk menghindari duplicate key error
StmtEntry::updateOrCreate(
[
'stmt_entry_id' => $entryData['stmt_entry_id']
],
$entryData
);
// Insert all records in the chunk at once $totalProcessed++;
StmtEntry::insert($entry); } else {
Log::warning('Invalid entry data structure', ['data' => $entryData]);
$this->errorCount++;
}
} }
// Reset entry batch after processing DB::commit();
Log::info("Berhasil memproses {$totalProcessed} record dengan updateOrCreate");
// Reset entry batch after successful processing
$this->entryBatch = []; $this->entryBatch = [];
} }
} catch (Exception $e) { } catch (Exception $e) {
DB::rollback();
Log::error("Error in saveBatch: " . $e->getMessage() . "\n" . $e->getTraceAsString()); Log::error("Error in saveBatch: " . $e->getMessage() . "\n" . $e->getTraceAsString());
$this->errorCount += count($this->entryBatch); $this->errorCount += count($this->entryBatch);
// Reset batch even if there's an error to prevent reprocessing the same failed records // Reset batch even if there's an error to prevent reprocessing the same failed records
$this->entryBatch = []; $this->entryBatch = [];
throw $e;
} }
} }

View File

@@ -0,0 +1,379 @@
<?php
namespace Modules\Webstatement\Jobs;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Modules\Webstatement\Models\Atmcard;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Modules\Webstatement\Models\KartuSyncLog;
class UpdateAllAtmCardsBatchJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Konstanta untuk konfigurasi batch processing
*/
private const BATCH_SIZE = 100;
private const MAX_EXECUTION_TIME = 7200; // 2 jam dalam detik
private const DELAY_BETWEEN_JOBS = 2; // 2 detik delay antar job
private const MAX_DELAY_SPREAD = 300; // Spread maksimal 5 menit
/**
* ID log sinkronisasi
*
* @var int
*/
protected $syncLogId;
/**
* Model log sinkronisasi
*
* @var KartuSyncLog
*/
protected $syncLog;
/**
* Batch size untuk processing
*
* @var int
*/
protected $batchSize;
/**
* Filter kondisi kartu yang akan diupdate
*
* @var array
*/
protected $filters;
/**
* Create a new job instance.
*
* @param int|null $syncLogId ID log sinkronisasi
* @param int $batchSize Ukuran batch untuk processing
* @param array $filters Filter kondisi kartu
*/
public function __construct(?int $syncLogId = null, int $batchSize = self::BATCH_SIZE, array $filters = [])
{
$this->syncLogId = $syncLogId;
$this->batchSize = $batchSize > 0 ? $batchSize : self::BATCH_SIZE;
$this->filters = $filters;
}
/**
* Execute the job untuk update seluruh kartu ATM
*
* @return void
* @throws Exception
*/
public function handle(): void
{
set_time_limit(self::MAX_EXECUTION_TIME);
Log::info('Memulai job update seluruh kartu ATM', [
'sync_log_id' => $this->syncLogId,
'batch_size' => $this->batchSize,
'filters' => $this->filters
]);
try {
DB::beginTransaction();
// Load atau buat log sinkronisasi
$this->loadOrCreateSyncLog();
// Update status job dimulai
$this->updateJobStartStatus();
// Ambil total kartu yang akan diproses
$totalCards = $this->getTotalCardsCount();
if ($totalCards === 0) {
Log::info('Tidak ada kartu ATM yang perlu diupdate');
$this->updateJobCompletedStatus(0, 0);
DB::commit();
return;
}
Log::info("Ditemukan {$totalCards} kartu ATM yang akan diproses");
// Proses kartu dalam batch
$processedCount = $this->processCardsInBatches($totalCards);
// Update status job selesai
$this->updateJobCompletedStatus($totalCards, $processedCount);
Log::info('Job update seluruh kartu ATM selesai', [
'total_cards' => $totalCards,
'processed_count' => $processedCount,
'sync_log_id' => $this->syncLog->id
]);
DB::commit();
} catch (Exception $e) {
DB::rollBack();
$this->updateJobFailedStatus($e->getMessage());
Log::error('Gagal menjalankan job update seluruh kartu ATM: ' . $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'sync_log_id' => $this->syncLogId,
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* Load atau buat log sinkronisasi baru
*
* @return void
* @throws Exception
*/
private function loadOrCreateSyncLog(): void
{
Log::info('Loading atau membuat sync log', ['sync_log_id' => $this->syncLogId]);
if ($this->syncLogId) {
$this->syncLog = KartuSyncLog::find($this->syncLogId);
if (!$this->syncLog) {
throw new Exception("Sync log dengan ID {$this->syncLogId} tidak ditemukan");
}
} else {
// Buat log sinkronisasi baru
$this->syncLog = KartuSyncLog::create([
'periode' => now()->format('Y-m'),
'sync_notes' => 'Batch update seluruh kartu ATM dimulai',
'is_sync' => false,
'sync_at' => null,
'is_csv' => false,
'csv_at' => null,
'is_ftp' => false,
'ftp_at' => null
]);
}
Log::info('Sync log berhasil dimuat/dibuat', ['sync_log_id' => $this->syncLog->id]);
}
/**
* Update status saat job dimulai
*
* @return void
*/
private function updateJobStartStatus(): void
{
Log::info('Memperbarui status job dimulai');
$this->syncLog->update([
'sync_notes' => $this->syncLog->sync_notes . "\nBatch update seluruh kartu ATM dimulai pada " . now()->format('Y-m-d H:i:s'),
'is_sync' => false,
'sync_at' => null
]);
}
/**
* Ambil total jumlah kartu yang akan diproses
*
* @return int
*/
private function getTotalCardsCount(): int
{
Log::info('Menghitung total kartu yang akan diproses', ['filters' => $this->filters]);
$query = $this->buildCardQuery();
$count = $query->count();
Log::info("Total kartu ditemukan: {$count}");
return $count;
}
/**
* Build query untuk mengambil kartu berdasarkan filter
*
* @return \Illuminate\Database\Eloquent\Builder
*/
private function buildCardQuery()
{
$query = Atmcard::where('crsts', 1) // Kartu aktif
->whereNotNull('accflag')
->where('accflag', '!=', '');
// Terapkan filter default untuk kartu yang perlu update branch/currency
if (empty($this->filters) || !isset($this->filters['skip_branch_currency_filter'])) {
$query->where(function ($q) {
$q->whereNull('branch')
->orWhere('branch', '')
->orWhereNull('currency')
->orWhere('currency', '');
});
}
// Terapkan filter tambahan jika ada
if (!empty($this->filters)) {
foreach ($this->filters as $field => $value) {
if ($field === 'skip_branch_currency_filter') {
continue;
}
if (is_array($value)) {
$query->whereIn($field, $value);
} else {
$query->where($field, $value);
}
}
}
return $query;
}
/**
* Proses kartu dalam batch
*
* @param int $totalCards
* @return int Jumlah kartu yang berhasil diproses
*/
private function processCardsInBatches(int $totalCards): int
{
Log::info('Memulai pemrosesan kartu dalam batch', [
'total_cards' => $totalCards,
'batch_size' => $this->batchSize
]);
$processedCount = 0;
$batchNumber = 1;
$totalBatches = ceil($totalCards / $this->batchSize);
// Proses kartu dalam chunk/batch
$this->buildCardQuery()->chunk($this->batchSize, function ($cards) use (&$processedCount, &$batchNumber, $totalBatches, $totalCards) {
Log::info("Memproses batch {$batchNumber}/{$totalBatches}", [
'cards_in_batch' => $cards->count(),
'processed_so_far' => $processedCount
]);
try {
// Dispatch job untuk setiap kartu dalam batch dengan delay
foreach ($cards as $index => $card) {
// Hitung delay berdasarkan nomor batch dan index untuk menyebar eksekusi job
$delay = (($batchNumber - 1) * $this->batchSize + $index) % self::MAX_DELAY_SPREAD;
$delay += self::DELAY_BETWEEN_JOBS; // Tambah delay minimum
// Dispatch job UpdateAtmCardBranchCurrencyJob
UpdateAtmCardBranchCurrencyJob::dispatch($card, $this->syncLog->id)
->delay(now()->addSeconds($delay))
->onQueue('default');
$processedCount++;
}
// Update progress di log setiap 10 batch
if ($batchNumber % 10 === 0) {
$this->updateProgressStatus($processedCount, $totalCards, $batchNumber, $totalBatches);
}
Log::info("Batch {$batchNumber} berhasil dijadwalkan", [
'cards_scheduled' => $cards->count(),
'total_processed' => $processedCount
]);
} catch (Exception $e) {
Log::error("Error saat memproses batch {$batchNumber}: " . $e->getMessage(), [
'batch_number' => $batchNumber,
'cards_count' => $cards->count(),
'error' => $e->getMessage()
]);
throw $e;
}
$batchNumber++;
});
Log::info('Selesai memproses semua batch', [
'total_processed' => $processedCount,
'total_batches' => $batchNumber - 1
]);
return $processedCount;
}
/**
* Update status progress pemrosesan
*
* @param int $processedCount
* @param int $totalCards
* @param int $batchNumber
* @param int $totalBatches
* @return void
*/
private function updateProgressStatus(int $processedCount, int $totalCards, int $batchNumber, int $totalBatches): void
{
Log::info('Memperbarui status progress', [
'processed' => $processedCount,
'total' => $totalCards,
'batch' => $batchNumber,
'total_batches' => $totalBatches
]);
$percentage = round(($processedCount / $totalCards) * 100, 2);
$progressNote = "\nProgress: {$processedCount}/{$totalCards} kartu dijadwalkan ({$percentage}%) - Batch {$batchNumber}/{$totalBatches}";
$this->syncLog->update([
'sync_notes' => $this->syncLog->sync_notes . $progressNote
]);
}
/**
* Update status saat job selesai
*
* @param int $totalCards
* @param int $processedCount
* @return void
*/
private function updateJobCompletedStatus(int $totalCards, int $processedCount): void
{
Log::info('Memperbarui status job selesai', [
'total_cards' => $totalCards,
'processed_count' => $processedCount
]);
$completionNote = "\nBatch update selesai pada " . now()->format('Y-m-d H:i:s') .
" - Total {$processedCount} kartu dari {$totalCards} berhasil dijadwalkan untuk update";
$this->syncLog->update([
'is_sync' => true,
'sync_at' => now(),
'sync_notes' => $this->syncLog->sync_notes . $completionNote
]);
}
/**
* Update status saat job gagal
*
* @param string $errorMessage
* @return void
*/
private function updateJobFailedStatus(string $errorMessage): void
{
Log::error('Memperbarui status job gagal', ['error' => $errorMessage]);
if ($this->syncLog) {
$failureNote = "\nBatch update gagal pada " . now()->format('Y-m-d H:i:s') .
" - Error: {$errorMessage}";
$this->syncLog->update([
'is_sync' => false,
'sync_notes' => $this->syncLog->sync_notes . $failureNote
]);
}
}
}

View File

@@ -4,13 +4,14 @@ namespace Modules\Webstatement\Jobs;
use Exception; use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\Atmcard;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Models\Atmcard;
class UpdateAtmCardBranchCurrencyJob implements ShouldQueue class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
{ {
@@ -77,7 +78,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
} }
/** /**
* Get account information from the API * Get account information from Account model or API
* *
* @param string $accountNumber * @param string $accountNumber
* @return array|null * @return array|null
@@ -85,10 +86,26 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
private function getAccountInfo(string $accountNumber): ?array private function getAccountInfo(string $accountNumber): ?array
{ {
try { try {
// Coba dapatkan data dari model Account terlebih dahulu
$account = Account::where('account_number', $accountNumber)->first();
if ($account) {
// Jika account ditemukan, format data sesuai dengan format response dari API
return [
'responseCode' => '00',
'acctCompany' => $account->branch_code,
'acctCurrency' => $account->currency,
'acctType' => $account->open_category
// Tambahkan field lain yang mungkin diperlukan
];
}
// Jika tidak ditemukan di database, ambil dari Fiorano API
$url = env('FIORANO_URL') . self::API_BASE_PATH; $url = env('FIORANO_URL') . self::API_BASE_PATH;
$path = self::API_INQUIRY_PATH; $path = self::API_INQUIRY_PATH;
$data = [ $data = [
'accountNo' => $accountNumber 'accountNo' => $accountNumber,
]; ];
$response = Http::post($url . $path, $data); $response = Http::post($url . $path, $data);
@@ -110,6 +127,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
$cardData = [ $cardData = [
'branch' => !empty($accountInfo['acctCompany']) ? $accountInfo['acctCompany'] : null, 'branch' => !empty($accountInfo['acctCompany']) ? $accountInfo['acctCompany'] : null,
'currency' => !empty($accountInfo['acctCurrency']) ? $accountInfo['acctCurrency'] : null, 'currency' => !empty($accountInfo['acctCurrency']) ? $accountInfo['acctCurrency'] : null,
'product_code' => !empty($accountInfo['acctType']) ? $accountInfo['acctType'] : null,
]; ];
$this->card->update($cardData); $this->card->update($cardData);

View File

@@ -4,6 +4,7 @@ namespace Modules\Webstatement\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Log;
// use Modules\Webstatement\Database\Factories\AtmcardFactory; // use Modules\Webstatement\Database\Factories\AtmcardFactory;
class Atmcard extends Model class Atmcard extends Model
@@ -15,7 +16,64 @@ class Atmcard extends Model
*/ */
protected $guarded = ['id']; protected $guarded = ['id'];
/**
* Relasi ke tabel JenisKartu untuk mendapatkan informasi biaya kartu
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function biaya(){ public function biaya(){
Log::info('Mengakses relasi biaya untuk ATM card', ['card_id' => $this->id]);
return $this->belongsTo(JenisKartu::class,'ctdesc','code'); return $this->belongsTo(JenisKartu::class,'ctdesc','code');
} }
/**
* Scope untuk mendapatkan kartu ATM yang aktif
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
Log::info('Menggunakan scope active untuk filter kartu ATM aktif');
return $query->where('crsts', 1);
}
/**
* Scope untuk mendapatkan kartu berdasarkan product_code
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $productCode
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeByProductCode($query, $productCode)
{
Log::info('Menggunakan scope byProductCode', ['product_code' => $productCode]);
return $query->where('product_code', $productCode);
}
/**
* Accessor untuk mendapatkan product_code dengan format yang konsisten
*
* @param string $value
* @return string|null
*/
public function getProductCodeAttribute($value)
{
return $value ? strtoupper(trim($value)) : null;
}
/**
* Mutator untuk menyimpan product_code dengan format yang konsisten
*
* @param string $value
* @return void
*/
public function setProductCodeAttribute($value)
{
$this->attributes['product_code'] = $value ? strtoupper(trim($value)) : null;
Log::info('Product code diset untuk ATM card', [
'card_id' => $this->id ?? 'new',
'product_code' => $this->attributes['product_code']
]);
}
} }

View File

@@ -6,18 +6,19 @@ 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\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\ProcessDailyMigration; use Modules\Webstatement\Console\ProcessDailyMigration;
use Modules\Webstatement\Console\ExportPeriodStatements; use Modules\Webstatement\Console\ExportPeriodStatements;
use Modules\Webstatement\Console\UpdateAllAtmCardsCommand;
use Modules\Webstatement\Console\CheckEmailProgressCommand;
use Modules\Webstatement\Console\GenerateBiayakartuCommand; use Modules\Webstatement\Console\GenerateBiayakartuCommand;
use Modules\Webstatement\Console\SendStatementEmailCommand;
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob; use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
use Modules\Webstatement\Console\GenerateAtmTransactionReport; use Modules\Webstatement\Console\GenerateAtmTransactionReport;
use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand; use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand;
use Modules\Webstatement\Console\SendStatementEmailCommand;
class WebstatementServiceProvider extends ServiceProvider class WebstatementServiceProvider extends ServiceProvider
{ {
@@ -70,7 +71,8 @@ class WebstatementServiceProvider extends ServiceProvider
ExportPeriodStatements::class, ExportPeriodStatements::class,
GenerateAtmTransactionReport::class, GenerateAtmTransactionReport::class,
SendStatementEmailCommand::class, SendStatementEmailCommand::class,
CheckEmailProgressCommand::class CheckEmailProgressCommand::class,
UpdateAllAtmCardsCommand::class
]); ]);
} }

View File

@@ -0,0 +1,63 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Menjalankan migration untuk menambahkan field product_code pada tabel atmcards
*
* @return void
*/
public function up(): void
{
Log::info('Memulai migration: menambahkan field product_code ke tabel atmcards');
DB::beginTransaction();
try {
Schema::table('atmcards', function (Blueprint $table) {
// Menambahkan field product_code setelah field ctdesc
$table->string('product_code')->nullable()->after('ctdesc')->comment('Kode produk kartu ATM');
});
DB::commit();
Log::info('Migration berhasil: field product_code telah ditambahkan ke tabel atmcards');
} catch (Exception $e) {
DB::rollback();
Log::error('Migration gagal: ' . $e->getMessage());
throw $e;
}
}
/**
* Membalikkan migration dengan menghapus field product_code dari tabel atmcards
*
* @return void
*/
public function down(): void
{
Log::info('Memulai rollback migration: menghapus field product_code dari tabel atmcards');
DB::beginTransaction();
try {
Schema::table('atmcards', function (Blueprint $table) {
$table->dropColumn('product_code');
});
DB::commit();
Log::info('Rollback migration berhasil: field product_code telah dihapus dari tabel atmcards');
} catch (Exception $e) {
DB::rollback();
Log::error('Rollback migration gagal: ' . $e->getMessage());
throw $e;
}
}
};

View File

@@ -30,7 +30,8 @@
"attributes": [], "attributes": [],
"permission": "", "permission": "",
"roles": [ "roles": [
"administrator" "administrator",
"customer_service"
] ]
}, },
{ {

View File

@@ -89,14 +89,14 @@
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) Password standar Elektronic Statement ini adalah <strong>ddMonyyyyxx</strong> (contoh: 01Aug1970xx)
dimana : dimana :
<ul class="dashed-list"> <ul style="list-style-type: none;">
<li>dd : <strong>2 digit</strong> tanggal lahir anda, contoh: 01</li> <li>- dd : <strong>2 digit</strong> tanggal lahir anda, contoh: 01</li>
<li>Mon : <li>- Mon :
<strong>3 huruf pertama</strong> bulan lahir anda dalam bahasa Ingris. Huruf pertama adalah <strong>3 huruf pertama</strong> bulan lahir anda dalam bahasa Ingris. Huruf pertama adalah
huruf besar dan selanjutnya huruf kecil, contoh : Aug huruf besar dan selanjutnya huruf kecil, contoh : Aug
</li> </li>
<li>yyyy : <strong>4 digit</strong> tahun kelahiran anda, contoh : 1970</li> <li>- yyyy : <strong>4 digit</strong> tahun kelahiran anda, contoh : 1970</li>
<li>xx : <strong>2 digit terakhir</strong> dari nomer rekening anda, contoh : 12</li> <li>- xx : <strong>2 digit terakhir</strong> dari nomer rekening anda, contoh : 12</li>
</ul> </ul>
<br> <br>
@@ -114,14 +114,14 @@
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 style="list-style-type: none;">
<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 <strong>The first 3 letters</strong> of your birth month in English. The first letter is
uppercase and the rest are lowercase, example: Aug uppercase and the rest are lowercase, example: Aug
</li> </li>
<li>yyyy : <strong>4 digit</strong> of your birth year, example: 1970</li> <li>- yyyy : <strong>4 digit</strong> of your birth year, example: 1970</li>
<li>xx : <strong>The last 2 digits</strong> of your account number, example: 12.</li> <li>- xx : <strong>The last 2 digits</strong> of your account number, example: 12.</li>
</ul> </ul>
<br> <br>

View File

@@ -11,41 +11,59 @@
<h3 class="card-title">Request Print Stetement</h3> <h3 class="card-title">Request Print Stetement</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="{{ isset($statement) ? route('statements.update', $statement->id) : route('statements.store') }}" method="POST"> <form
action="{{ isset($statement) ? route('statements.update', $statement->id) : route('statements.store') }}"
method="POST">
@csrf @csrf
@if(isset($statement)) @if (isset($statement))
@method('PUT') @method('PUT')
@endif @endif
<div class="grid grid-cols-1 gap-5"> <div class="grid grid-cols-1 gap-5">
<div class="form-group">
<label class="form-label required" for="branch_code">Branch</label> @if ($multiBranch)
<select class="select tomselect @error('branch_code') is-invalid @enderror" id="branch_code" name="branch_code" required> <div class="form-group">
<option value="">Select Branch</option> <label class="form-label required" for="branch_id">Branch/Cabang</label>
@foreach($branches as $branch) <select class="input form-control tomselect @error('branch_id') is-invalid @enderror"
<option value="{{ $branch->code }}" {{ (old('branch_code', $statement->branch_code ?? '') == $branch->code) ? 'selected' : '' }}> id="branch_id" name="branch_id" required>
{{ $branch->name }} <option value="">Pilih Branch/Cabang</option>
</option> @foreach ($branches as $branchOption)
@endforeach <option value="{{ $branchOption->code }}"
</select> {{ old('branch_id', $statement->branch_id ?? ($branch->code ?? '')) == $branchOption->code ? 'selected' : '' }}>
@error('branch_code') {{ $branchOption->code }} - {{ $branchOption->name }}
<div class="invalid-feedback">{{ $message }}</div> </option>
@enderror @endforeach
</div> </select>
@error('branch_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
@else
<div class="form-group">
<label class="form-label" for="branch_display">Branch/Cabang</label>
<input type="text" class="input form-control" id="branch_display"
value="{{ $branch->code ?? '' }} - {{ $branch->name ?? '' }}" readonly>
<input type="hidden" name="branch_id" value="{{ $branch->code ?? '' }}">
</div>
@endif
<div class="form-group"> <div class="form-group">
<label class="form-label required" for="account_number">Account Number</label> <label class="form-label required" for="account_number">Account Number</label>
<input type="text" class="input form-control @error('account_number') is-invalid @enderror" id="account_number" name="account_number" value="{{ old('account_number', $statement->account_number ?? '') }}" required> <input type="text" class="input form-control @error('account_number') is-invalid @enderror"
id="account_number" name="account_number"
value="{{ old('account_number', $statement->account_number ?? '') }}" required>
@error('account_number') @error('account_number')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label" for="email">Email</label> <label class="form-label" for="email">Email</label>
<input type="email" class="input form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email', $statement->email ?? '') }}" placeholder="Optional email for send statement"> <input type="email" class="input form-control @error('email') is-invalid @enderror"
id="email" name="email" value="{{ old('email', $statement->email ?? '') }}"
placeholder="Optional email for send statement">
@error('email') @error('email')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
</div> </div>
@@ -53,24 +71,21 @@
<label class="form-label required" for="start_date">Start Date</label> <label class="form-label required" for="start_date">Start Date</label>
<input class="input @error('period_from') border-danger bg-danger-light @enderror" <input class="input @error('period_from') border-danger bg-danger-light @enderror"
type="month" type="month" name="period_from"
name="period_from" value="{{ $statement->period_from ?? old('period_from') }}"
value="{{ $statement->period_from ?? old('period_from') }}" max="{{ date('Y-m', strtotime('-1 month')) }}">
max="{{ date('Y-m', strtotime('-1 month')) }}">
@error('period_from') @error('period_from')
<em class="alert text-danger text-sm">{{ $message }}</em> <em class="text-sm alert text-danger">{{ $message }}</em>
@enderror @enderror
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label required" for="end_date">End Date</label> <label class="form-label required" for="end_date">End Date</label>
<input class="input @error('period_to') border-danger bg-danger-light @enderror" <input class="input @error('period_to') border-danger bg-danger-light @enderror" type="month"
type="month" name="period_to" value="{{ $statement->period_to ?? old('period_to') }}"
name="period_to" max="{{ date('Y-m', strtotime('-1 month')) }}">
value="{{ $statement->period_to ?? old('period_to') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
@error('period_to') @error('period_to')
<em class="alert text-danger text-sm">{{ $message }}</em> <em class="text-sm alert text-danger">{{ $message }}</em>
@enderror @enderror
</div> </div>
</div> </div>
@@ -85,8 +100,9 @@
</div> </div>
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<div class="card card-grid min-w-full" data-datatable="false" data-datatable-page-size="10" data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}"> <div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
<div class="card-header py-5 flex-wrap"> data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}">
<div class="flex-wrap py-5 card-header">
<h3 class="card-title"> <h3 class="card-title">
Daftar Statement Request Daftar Statement Request
</h3> </h3>
@@ -100,55 +116,54 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="scrollable-x-auto"> <div class="scrollable-x-auto">
<table class="table table-auto table-border align-middle text-gray-700 font-medium text-sm" data-datatable-table="true"> <table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
data-datatable-table="true">
<thead> <thead>
<tr> <tr>
<th class="w-14"> <th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox"/> <input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
</th> </th>
<th class="min-w-[100px]" data-datatable-column="id"> <th class="min-w-[100px]" data-datatable-column="id">
<span class="sort"> <span class="sort-label"> ID </span> <span class="sort"> <span class="sort-label"> ID </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[150px]" data-datatable-column="branch_name"> <th class="min-w-[150px]" data-datatable-column="branch_name">
<span class="sort"> <span class="sort-label"> Branch </span> <span class="sort"> <span class="sort-label"> Branch </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[150px]" data-datatable-column="account_number"> <th class="min-w-[150px]" data-datatable-column="account_number">
<span class="sort"> <span class="sort-label"> Account Number </span> <span class="sort"> <span class="sort-label"> Account Number </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[150px]" data-datatable-column="period"> <th class="min-w-[150px]" data-datatable-column="period">
<span class="sort"> <span class="sort-label"> Period </span> <span class="sort"> <span class="sort-label"> Period </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[150px]" data-datatable-column="authorization_status"> <th class="min-w-[150px]" data-datatable-column="is_available">
<span class="sort"> <span class="sort-label"> Status </span> <span class="sort"> <span class="sort-label"> Available </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[150px]" data-datatable-column="is_available"> <th class="min-w-[150px]" data-datatable-column="remarks">
<span class="sort"> <span class="sort-label"> Available </span> <span class="sort"> <span class="sort-label"> Notes </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[150px]" data-datatable-column="remarks"> <th class="min-w-[180px]" data-datatable-column="created_at">
<span class="sort"> <span class="sort-label"> Notes </span> <span class="sort"> <span class="sort-label"> Created At </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[180px]" data-datatable-column="created_at"> <th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
<span class="sort"> <span class="sort-label"> Created At </span> </tr>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
</tr>
</thead> </thead>
</table> </table>
</div> </div>
<div class="card-footer justify-center md:justify-between flex-col md:flex-row gap-3 text-gray-600 text-2sm font-medium"> <div
<div class="flex items-center gap-2"> 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 Show
<select class="select select-sm w-16" data-datatable-size="true" name="perpage"> </select> per page <select class="w-16 select select-sm" data-datatable-size="true" name="perpage"> </select>
per page
</div> </div>
<div class="flex items-center gap-4"> <div class="flex gap-4 items-center">
<span data-datatable-info="true"> </span> <span data-datatable-info="true"> </span>
<div class="pagination" data-datatable-pagination="true"> <div class="pagination" data-datatable-pagination="true">
</div> </div>
@@ -162,6 +177,10 @@
@push('scripts') @push('scripts')
<script type="text/javascript"> <script type="text/javascript">
/**
* Fungsi untuk menghapus data statement
* @param {number} data - ID statement yang akan dihapus
*/
function deleteData(data) { function deleteData(data) {
Swal.fire({ Swal.fire({
title: 'Are you sure?', title: 'Are you sure?',
@@ -175,7 +194,7 @@
if (result.isConfirmed) { if (result.isConfirmed) {
$.ajaxSetup({ $.ajaxSetup({
headers: { headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}' 'X-CSRF-TOKEN': '{{ csrf_token() }}'
} }
}); });
@@ -192,6 +211,56 @@
} }
}) })
} }
/**
* Konfirmasi email sebelum submit form
* Menampilkan SweetAlert jika email diisi untuk konfirmasi pengiriman
*/
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const emailInput = document.getElementById('email');
// Log: Inisialisasi event listener untuk konfirmasi email
console.log('Email confirmation listener initialized');
form.addEventListener('submit', function(e) {
const emailValue = emailInput.value.trim();
// Jika email diisi, tampilkan konfirmasi
if (emailValue) {
e.preventDefault(); // Hentikan submit form sementara
// Log: Email terdeteksi, menampilkan konfirmasi
console.log('Email detected:', emailValue);
Swal.fire({
title: 'Konfirmasi Pengiriman Email',
text: `Apakah Anda yakin ingin mengirimkan statement ke email: ${emailValue}?`,
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Ya, Kirim Email',
cancelButtonText: 'Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
// Log: User konfirmasi pengiriman email
console.log('User confirmed email sending');
// Submit form setelah konfirmasi
form.submit();
} else {
// Log: User membatalkan pengiriman email
console.log('User cancelled email sending');
}
});
} else {
// Log: Tidak ada email, submit form normal
console.log('No email provided, submitting form normally');
}
});
});
</script> </script>
<script type="module"> <script type="module">
@@ -243,31 +312,16 @@
return fromPeriod + toPeriod; return fromPeriod + toPeriod;
}, },
}, },
authorization_status: {
title: 'Status',
render: (item, data) => {
let statusClass = 'badge badge-light-primary';
if (data.authorization_status === 'approved') {
statusClass = 'badge badge-light-success';
} else if (data.authorization_status === 'rejected') {
statusClass = 'badge badge-light-danger';
} else if (data.authorization_status === 'pending') {
statusClass = 'badge badge-light-warning';
}
return `<span class="${statusClass}">${data.authorization_status}</span>`;
},
},
is_available: { is_available: {
title: 'Available', title: 'Available',
render: (item, data) => { render: (item, data) => {
let statusClass = data.is_available ? 'badge badge-light-success' : 'badge badge-light-danger'; let statusClass = data.is_available ? 'badge badge-light-success' :
'badge badge-light-danger';
let statusText = data.is_available ? 'Yes' : 'No'; let statusText = data.is_available ? 'Yes' : 'No';
return `<span class="${statusClass}">${statusText}</span>`; return `<span class="${statusClass}">${statusText}</span>`;
}, },
}, },
remarks : { remarks: {
title: 'Notes', title: 'Notes',
}, },
created_at: { created_at: {
@@ -315,7 +369,7 @@
let dataTable = new KTDataTable(element, dataTableOptions); let dataTable = new KTDataTable(element, dataTableOptions);
// Custom search functionality // Custom search functionality
searchInput.addEventListener('input', function () { searchInput.addEventListener('input', function() {
const searchValue = this.value.trim(); const searchValue = this.value.trim();
dataTable.search(searchValue, true); dataTable.search(searchValue, true);
}); });

View File

@@ -73,19 +73,6 @@
</div> </div>
</div> </div>
<div class="d-flex flex-column">
<div class="text-gray-500 fw-semibold">Status</div>
<div>
@if($statement->authorization_status === 'pending')
<span class="badge badge-warning">Pending Authorization</span>
@elseif($statement->authorization_status === 'approved')
<span class="badge badge-success">Approved</span>
@elseif($statement->authorization_status === 'rejected')
<span class="badge badge-danger">Rejected</span>
@endif
</div>
</div>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<div class="text-gray-500 fw-semibold">Availability</div> <div class="text-gray-500 fw-semibold">Availability</div>
<div> <div>