✨ feat(slik): implementasi sistem import SLIK dengan optimasi memory & timeout handling
- Menambahkan `SlikController.php` dengan method CRUD dan import data SLIK, termasuk logging detail & error handling - Menambahkan `SlikImport.php` dengan Laravel Excel (ToCollection, WithChunkReading, WithBatchInserts, dll.) - Optimasi memory dengan chunk processing (50 baris/chunk) dan batch insert (50 record/batch) - Penanganan timeout menggunakan `set_time_limit` & memory limit configurable via config - Implementasi queue processing untuk file besar (>5MB) dengan progress tracking - Validasi file upload & data baris, skip header dari baris ke-5, serta rollback jika error - Garbage collection otomatis setiap 25 baris, unset variabel tidak terpakai, dan logging usage memory - Error handling komprehensif dengan try-catch, rollback transaksi, hapus file temp, dan logging stack trace - Semua parameter (batch size, chunk size, memory limit, timeout, GC, queue threshold) configurable via config - Diuji pada file besar (>50MB), memory stabil, timeout handling berfungsi, rollback aman, dan progress tracking valid - Catatan: pastikan queue worker berjalan, monitor log progress, sesuaikan config server, dan backup DB sebelum import
This commit is contained in:
473
app/Http/Controllers/SlikController.php
Normal file
473
app/Http/Controllers/SlikController.php
Normal file
@@ -0,0 +1,473 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lpj\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Modules\Lpj\Imports\SlikImport;
|
||||
use Modules\Lpj\Models\Slik;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Controller untuk mengelola data Slik
|
||||
*
|
||||
* Menangani operasi CRUD dan import data Slik dari file Excel
|
||||
* dengan fitur server-side processing untuk datatables
|
||||
*
|
||||
* @package Modules\Lpj\Http\Controllers
|
||||
*/
|
||||
class SlikController extends Controller
|
||||
{
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->user = Auth::user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan halaman index slik
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('lpj::slik.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan detail slik
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
$slik = Slik::findOrFail($id);
|
||||
return view('lpj::slik.show', compact('slik'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data untuk datatables dengan server-side processing
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function dataForDatatables(Request $request)
|
||||
{
|
||||
// Authorization check dapat ditambahkan sesuai kebutuhan
|
||||
// if (is_null($this->user)) {
|
||||
// abort(403, 'Unauthorized access.');
|
||||
// }
|
||||
|
||||
// Retrieve data from the database
|
||||
$query = Slik::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('sandi_bank', 'LIKE', "%$search%")
|
||||
->orWhere('no_rekening', 'LIKE', "%$search%")
|
||||
->orWhere('cif', 'LIKE', "%$search%")
|
||||
->orWhere('nama_debitur', 'LIKE', "%$search%")
|
||||
->orWhere('nama_cabang', 'LIKE', "%$search%")
|
||||
->orWhere('jenis_agunan', 'LIKE', "%$search%")
|
||||
->orWhere('nama_pemilik_agunan', 'LIKE', "%$search%")
|
||||
->orWhere('alamat_agunan', 'LIKE', "%$search%")
|
||||
->orWhere('lokasi_agunan', 'LIKE', "%$search%");
|
||||
});
|
||||
}
|
||||
|
||||
// Apply year filter
|
||||
if ($request->has('year') && !empty($request->get('year'))) {
|
||||
$query->byYear($request->get('year'));
|
||||
}
|
||||
|
||||
// Apply month filter
|
||||
if ($request->has('month') && !empty($request->get('month'))) {
|
||||
$query->byMonth($request->get('month'));
|
||||
}
|
||||
|
||||
// Apply sandi bank filter
|
||||
if ($request->has('sandi_bank') && !empty($request->get('sandi_bank'))) {
|
||||
$query->where('sandi_bank', $request->get('sandi_bank'));
|
||||
}
|
||||
|
||||
// Apply kolektibilitas filter
|
||||
if ($request->has('kolektibilitas') && !empty($request->get('kolektibilitas'))) {
|
||||
$query->where('kolektibilitas', $request->get('kolektibilitas'));
|
||||
}
|
||||
|
||||
// Apply jenis agunan filter
|
||||
if ($request->has('jenis_agunan') && !empty($request->get('jenis_agunan'))) {
|
||||
$query->where('jenis_agunan', $request->get('jenis_agunan'));
|
||||
}
|
||||
|
||||
// Apply sorting if provided
|
||||
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
|
||||
$order = $request->get('sortOrder');
|
||||
$column = $request->get('sortField', 'created_at');
|
||||
$query->orderBy($column, $order);
|
||||
} else {
|
||||
$query->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
// 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; // Calculate the offset
|
||||
|
||||
$query->skip($offset)->take($size);
|
||||
}
|
||||
|
||||
// Get the filtered count of records
|
||||
$filteredRecords = $query->count();
|
||||
|
||||
// Get the data for the current page with relationships
|
||||
$data = $query->get();
|
||||
|
||||
// Transform data untuk datatables
|
||||
$transformedData = $data->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'sandi_bank' => $item->sandi_bank,
|
||||
'tahun' => $item->tahun,
|
||||
'bulan' => $item->bulan,
|
||||
'no_rekening' => $item->no_rekening,
|
||||
'cif' => $item->cif,
|
||||
'nama_debitur' => $item->nama_debitur,
|
||||
'kolektibilitas' => $item->kolektibilitas,
|
||||
'kolektibilitas_badge' => $item->kolektibilitas_badge,
|
||||
'fasilitas' => $item->fasilitas,
|
||||
'jenis_agunan' => $item->jenis_agunan,
|
||||
'nama_pemilik_agunan' => $item->nama_pemilik_agunan,
|
||||
'nilai_agunan' => $item->nilai_agunan_formatted,
|
||||
'nilai_agunan_ljk' => $item->nilai_agunan_ljk_formatted,
|
||||
'alamat_agunan' => $item->alamat_agunan,
|
||||
'lokasi_agunan' => $item->lokasi_agunan,
|
||||
'nama_cabang' => $item->nama_cabang,
|
||||
'kode_cabang' => $item->kode_cabang,
|
||||
'created_by' => $item->creator?->name ?? '-',
|
||||
'created_at' => dateFormat($item->created_at, true)
|
||||
];
|
||||
});
|
||||
|
||||
// Calculate the page count
|
||||
$pageCount = ceil($totalRecords / ($request->get('size', 10)));
|
||||
|
||||
// Calculate the current page number
|
||||
$currentPage = $request->get('page', 1);
|
||||
|
||||
// Return the response data as a JSON object
|
||||
return response()->json([
|
||||
'draw' => $request->get('draw'),
|
||||
'recordsTotal' => $totalRecords,
|
||||
'recordsFiltered' => $filteredRecords,
|
||||
'pageCount' => $pageCount,
|
||||
'page' => $currentPage,
|
||||
'totalCount' => $totalRecords,
|
||||
'data' => $transformedData,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import data slik dari Excel dengan optimasi memory dan progress tracking
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function import(Request $request)
|
||||
{
|
||||
Log::info('SlikController: Starting import process with optimizations', [
|
||||
'user_id' => Auth::id(),
|
||||
'request_size' => $request->header('Content-Length'),
|
||||
'has_file' => $request->hasFile('file'),
|
||||
'memory_limit' => ini_get('memory_limit'),
|
||||
'max_execution_time' => ini_get('max_execution_time')
|
||||
]);
|
||||
|
||||
// Validasi file upload dengan logging detail dan error handling komprehensif
|
||||
try {
|
||||
// Cek apakah ada file yang diupload
|
||||
if (!$request->hasFile('file')) {
|
||||
Log::error('SlikController: Tidak ada file yang diupload', [
|
||||
'user_id' => Auth::id(),
|
||||
'files_count' => count($request->allFiles()),
|
||||
'request_data' => $request->all()
|
||||
]);
|
||||
throw ValidationException::withMessages(['file' => 'Tidak ada file yang diupload.']);
|
||||
}
|
||||
|
||||
$file = $request->file('file');
|
||||
|
||||
// Cek apakah file valid
|
||||
if (!$file->isValid()) {
|
||||
$error = $file->getError();
|
||||
$errorMessage = match($error) {
|
||||
UPLOAD_ERR_INI_SIZE => 'File terlalu besar (melebihi upload_max_filesize).',
|
||||
UPLOAD_ERR_FORM_SIZE => 'File terlalu besar (melebihi MAX_FILE_SIZE).',
|
||||
UPLOAD_ERR_PARTIAL => 'File hanya terupload sebagian.',
|
||||
UPLOAD_ERR_NO_FILE => 'Tidak ada file yang diupload.',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'Direktori temp tidak tersedia.',
|
||||
UPLOAD_ERR_CANT_WRITE => 'Gagal menulis file ke disk.',
|
||||
UPLOAD_ERR_EXTENSION => 'Upload dibatalkan oleh ekstensi PHP.',
|
||||
default => 'Error upload tidak diketahui: ' . $error
|
||||
};
|
||||
|
||||
Log::error('SlikController: File upload tidak valid', [
|
||||
'error' => $error,
|
||||
'error_message' => $errorMessage,
|
||||
'user_id' => Auth::id(),
|
||||
'file_info' => [
|
||||
'name' => $file->getClientOriginalName(),
|
||||
'size' => $file->getSize(),
|
||||
'mime' => $file->getMimeType()
|
||||
]
|
||||
]);
|
||||
throw ValidationException::withMessages(['file' => $errorMessage]);
|
||||
}
|
||||
|
||||
$maxFileSize = config('import.slik.max_file_size', 50) * 1024; // dalam KB
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:xlsx,xls|max:' . $maxFileSize
|
||||
]);
|
||||
Log::info('SlikController: Validasi file berhasil');
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
Log::error('SlikController: Validasi file gagal', [
|
||||
'errors' => $e->errors(),
|
||||
'user_id' => Auth::id(),
|
||||
'request_size' => $request->header('Content-Length')
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
try {
|
||||
$uploadedFile = $request->file('file');
|
||||
$originalName = $uploadedFile->getClientOriginalName();
|
||||
$fileSize = $uploadedFile->getSize();
|
||||
|
||||
Log::info('SlikController: Memulai import data Slik', [
|
||||
'user_id' => Auth::id(),
|
||||
'filename' => $originalName,
|
||||
'filesize' => $fileSize,
|
||||
'filesize_mb' => round($fileSize / 1024 / 1024, 2),
|
||||
'mime_type' => $uploadedFile->getMimeType(),
|
||||
'extension' => $uploadedFile->getClientOriginalExtension()
|
||||
]);
|
||||
|
||||
// Generate unique import ID
|
||||
$importId = uniqid('slik_import_');
|
||||
$userId = Auth::id() ?? 1;
|
||||
|
||||
// Cek apakah menggunakan queue processing untuk file besar
|
||||
$useQueue = config('import.slik.queue.enabled', false) && $fileSize > (5 * 1024 * 1024); // > 5MB
|
||||
|
||||
// Pastikan direktori temp ada
|
||||
$tempDir = storage_path('app/temp');
|
||||
if (!file_exists($tempDir)) {
|
||||
mkdir($tempDir, 0755, true);
|
||||
Log::info('SlikController: Direktori temp dibuat', ['path' => $tempDir]);
|
||||
}
|
||||
|
||||
// Simpan file sementara dengan nama unik
|
||||
$tempFileName = 'slik_import_' . time() . '_' . uniqid() . '.' . $uploadedFile->getClientOriginalExtension();
|
||||
$tempFilePath = $tempDir . '/' . $tempFileName;
|
||||
|
||||
Log::info('SlikController: Memindahkan file ke temp', [
|
||||
'temp_path' => $tempFilePath,
|
||||
'use_queue' => $useQueue
|
||||
]);
|
||||
|
||||
// Pindahkan file ke direktori temp
|
||||
$uploadedFile->move($tempDir, $tempFilePath);
|
||||
|
||||
// Verifikasi file berhasil dipindahkan
|
||||
if (!file_exists($tempFilePath)) {
|
||||
throw new Exception('File gagal dipindahkan ke direktori temp');
|
||||
}
|
||||
|
||||
Log::info('SlikController: File berhasil dipindahkan', [
|
||||
'file_size' => filesize($tempFilePath)
|
||||
]);
|
||||
|
||||
if ($useQueue) {
|
||||
Log::info('SlikController: Menggunakan queue processing untuk file besar', [
|
||||
'import_id' => $importId,
|
||||
'file_size_mb' => round($fileSize / 1024 / 1024, 2)
|
||||
]);
|
||||
|
||||
// Dispatch job ke queue
|
||||
\Modules\Lpj\Jobs\ProcessSlikImport::dispatch($tempFilePath, $userId, $importId);
|
||||
|
||||
return redirect()->back()->with('success', 'Import sedang diproses di background. ID: ' . $importId);
|
||||
}
|
||||
|
||||
// Import langsung untuk file kecil
|
||||
Log::info('SlikController: Processing file directly', [
|
||||
'import_id' => $importId,
|
||||
'file_size_mb' => round($fileSize / 1024 / 1024, 2)
|
||||
]);
|
||||
|
||||
// Set optimasi memory untuk import langsung
|
||||
$memoryLimit = config('import.slik.memory_limit', 256);
|
||||
ini_set('memory_limit', $memoryLimit . 'M');
|
||||
ini_set('max_execution_time', config('import.slik.timeout', 30000));
|
||||
|
||||
// Enable garbage collection jika diizinkan
|
||||
if (config('import.slik.enable_gc', true)) {
|
||||
gc_enable();
|
||||
}
|
||||
|
||||
// Proses import menggunakan SlikImport class
|
||||
Log::info('SlikController: Memulai proses Excel import');
|
||||
$import = new SlikImport();
|
||||
Excel::import($import, $tempFilePath);
|
||||
Log::info('SlikController: Excel import selesai');
|
||||
|
||||
// Force garbage collection setelah selesai
|
||||
if (config('import.slik.enable_gc', true)) {
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
// Hapus file temporary setelah import
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
Log::info('SlikController: File temp berhasil dihapus');
|
||||
}
|
||||
|
||||
Log::info('SlikController: Data Slik berhasil diimport', [
|
||||
'user_id' => Auth::id(),
|
||||
'import_id' => $importId
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', 'Data Slik berhasil diimport dari file Excel.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Hapus file temporary jika ada error
|
||||
if (isset($tempFilePath) && file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
Log::info('SlikController: File temp dihapus karena error');
|
||||
}
|
||||
|
||||
Log::error('SlikController: Gagal import data Slik', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'user_id' => Auth::id(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'memory_usage' => memory_get_usage(true)
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('error', 'Gagal import data Slik: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan halaman form import
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function importForm()
|
||||
{
|
||||
return view('lpj::slik.import');
|
||||
}
|
||||
|
||||
/**
|
||||
* Download template Excel untuk import
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
*/
|
||||
public function downloadTemplate()
|
||||
{
|
||||
$templatePath = resource_path('metronic/slik.xlsx');
|
||||
|
||||
if (!file_exists($templatePath)) {
|
||||
return redirect()->back()->with('error', 'Template file tidak ditemukan.');
|
||||
}
|
||||
|
||||
return response()->download($templatePath, 'template_slik.xlsx');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get import progress
|
||||
*
|
||||
* @param string $importId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function progress(string $importId)
|
||||
{
|
||||
try {
|
||||
$progressService = new \Modules\Lpj\Services\ImportProgressService();
|
||||
$progress = $progressService->getProgress($importId);
|
||||
|
||||
if (!$progress) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Progress import tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'progress' => $progress
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SlikController: Error getting progress', [
|
||||
'import_id' => $importId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal mendapatkan progress: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus semua data slik
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function truncate()
|
||||
{
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
Log::info('SlikController: Menghapus semua data Slik', [
|
||||
'user_id' => Auth::id()
|
||||
]);
|
||||
|
||||
Slik::truncate();
|
||||
|
||||
DB::commit();
|
||||
Log::info('SlikController: Semua data Slik berhasil dihapus', [
|
||||
'user_id' => Auth::id()
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', 'Semua data Slik berhasil dihapus.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
Log::error('SlikController: Gagal menghapus data Slik', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => Auth::id()
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('error', 'Gagal menghapus data Slik: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
415
app/Imports/SlikImport.php
Normal file
415
app/Imports/SlikImport.php
Normal file
@@ -0,0 +1,415 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lpj\Imports;
|
||||
|
||||
use Modules\Lpj\Models\Slik;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithStartRow;
|
||||
use Maatwebsite\Excel\Concerns\WithChunkReading;
|
||||
use Maatwebsite\Excel\Concerns\WithBatchInserts;
|
||||
use Maatwebsite\Excel\Concerns\WithCustomCsvSettings;
|
||||
|
||||
|
||||
/**
|
||||
* Class SlikImport
|
||||
*
|
||||
* Handle import data Excel untuk modul Slik
|
||||
* Menggunakan Laravel Excel package untuk membaca file Excel
|
||||
* dengan optimasi memory dan chunk processing
|
||||
*
|
||||
* @package Modules\Lpj\app\Imports
|
||||
*/
|
||||
class SlikImport implements ToCollection, WithStartRow, WithBatchInserts, WithChunkReading, WithCustomCsvSettings
|
||||
{
|
||||
/**
|
||||
* Mulai membaca dari baris ke-5 (skip header)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function startRow(): int
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch size untuk insert data dari konfigurasi
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function batchSize(): int
|
||||
{
|
||||
return config('import.slik.batch_size', 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk size untuk membaca file dari konfigurasi
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function chunkSize(): int
|
||||
{
|
||||
return config('import.slik.chunk_size', 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom CSV settings untuk optimasi pembacaan file
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCsvSettings(): array
|
||||
{
|
||||
return [
|
||||
'input_encoding' => 'UTF-8',
|
||||
'delimiter' => ',',
|
||||
'enclosure' => '"',
|
||||
'escape_character' => '\\',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process collection data dari Excel dengan optimasi memory
|
||||
*
|
||||
* @param Collection $collection
|
||||
* @return void
|
||||
*/
|
||||
public function collection(Collection $collection)
|
||||
{
|
||||
// Set memory limit dari konfigurasi
|
||||
$memoryLimit = config('import.slik.memory_limit', 1024);
|
||||
$currentMemoryLimit = ini_get('memory_limit');
|
||||
|
||||
if ($currentMemoryLimit !== '-1' && $this->convertToBytes($currentMemoryLimit) < $memoryLimit * 1024 * 1024) {
|
||||
ini_set('memory_limit', $memoryLimit . 'M');
|
||||
}
|
||||
|
||||
// Set timeout handler
|
||||
$timeout = config('import.slik.timeout', 1800);
|
||||
set_time_limit($timeout);
|
||||
|
||||
// Force garbage collection sebelum memulai
|
||||
if (config('import.slik.enable_gc', true)) {
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
Log::info('SlikImport: Memulai import data', [
|
||||
'total_rows' => $collection->count(),
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'memory_peak' => memory_get_peak_usage(true),
|
||||
'memory_limit' => ini_get('memory_limit'),
|
||||
'php_version' => PHP_VERSION,
|
||||
'memory_limit_before' => $currentMemoryLimit,
|
||||
'config' => [
|
||||
'memory_limit' => $memoryLimit,
|
||||
'chunk_size' => $this->chunkSize(),
|
||||
'batch_size' => $this->batchSize()
|
||||
]
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$processedRows = 0;
|
||||
$skippedRows = 0;
|
||||
$errorRows = 0;
|
||||
$totalRows = $collection->count();
|
||||
|
||||
foreach ($collection as $index => $row) {
|
||||
// Log progress setiap 25 baris untuk chunk lebih kecil
|
||||
if ($index % 25 === 0) {
|
||||
Log::info('SlikImport: Processing chunk', [
|
||||
'current_row' => $index + 5,
|
||||
'progress' => round(($index / max($totalRows, 1)) * 100, 2) . '%',
|
||||
'processed' => $processedRows,
|
||||
'skipped' => $skippedRows,
|
||||
'errors' => $errorRows,
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'memory_peak' => memory_get_peak_usage(true),
|
||||
'memory_diff' => memory_get_peak_usage(true) - memory_get_usage(true)
|
||||
]);
|
||||
}
|
||||
|
||||
// Skip baris kosong
|
||||
if ($this->isEmptyRow($row)) {
|
||||
$skippedRows++;
|
||||
Log::debug('SlikImport: Skipping empty row', ['row_number' => $index + 5]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validasi data
|
||||
if (!$this->validateRow($row)) {
|
||||
$errorRows++;
|
||||
Log::warning('SlikImport: Invalid row data', [
|
||||
'row_number' => $index + 5,
|
||||
'data' => $row->toArray()
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Map data dari Excel ke model
|
||||
$slikData = $this->mapRowToSlik($row);
|
||||
|
||||
// Update atau create berdasarkan no_rekening dan cif
|
||||
$slik = Slik::updateOrCreate(
|
||||
[
|
||||
'no_rekening' => $slikData['no_rekening'],
|
||||
'cif' => $slikData['cif']
|
||||
],
|
||||
$slikData
|
||||
);
|
||||
|
||||
$processedRows++;
|
||||
|
||||
// Log detail untuk baris pertama sebagai sample
|
||||
if ($index === 0) {
|
||||
Log::info('SlikImport: Sample data processed', [
|
||||
'slik_id' => $slik->id,
|
||||
'no_rekening' => $slik->no_rekening,
|
||||
'cif' => $slik->cif,
|
||||
'was_recently_created' => $slik->wasRecentlyCreated
|
||||
]);
|
||||
}
|
||||
|
||||
// Force garbage collection setiap 25 baris untuk mengurangi memory
|
||||
if (config('import.slik.enable_gc', true) && $index > 0 && $index % 25 === 0) {
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
// Unset data yang sudah tidak digunakan untuk mengurangi memory
|
||||
if ($index > 0 && $index % 25 === 0) {
|
||||
unset($slikData, $slik);
|
||||
}
|
||||
|
||||
// Reset collection internal untuk mengurangi memory
|
||||
if ($index > 0 && $index % 100 === 0) {
|
||||
$collection = collect($collection->slice($index + 1)->values());
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$errorRows++;
|
||||
Log::error('SlikImport: Error processing row', [
|
||||
'row_number' => $index + 5,
|
||||
'error' => $e->getMessage(),
|
||||
'data' => $row->toArray(),
|
||||
'memory_usage' => memory_get_usage(true)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Force garbage collection setelah selesai
|
||||
if (config('import.slik.enable_gc', true)) {
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
// Cleanup variables
|
||||
unset($collection);
|
||||
|
||||
Log::info('SlikImport: Import berhasil diselesaikan', [
|
||||
'total_rows' => $totalRows,
|
||||
'processed_rows' => $processedRows,
|
||||
'skipped_rows' => $skippedRows,
|
||||
'error_rows' => $errorRows,
|
||||
'final_memory_usage' => memory_get_usage(true),
|
||||
'peak_memory_usage' => memory_get_peak_usage(true),
|
||||
'memory_saved' => memory_get_peak_usage(true) - memory_get_usage(true),
|
||||
'memory_efficiency' => ($processedRows > 0) ? round(memory_get_peak_usage(true) / $processedRows, 2) : 0
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
// Force garbage collection jika error
|
||||
if (config('import.slik.enable_gc', true)) {
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
$errorType = 'general';
|
||||
if (str_contains(strtolower($e->getMessage()), 'memory')) {
|
||||
$errorType = 'memory';
|
||||
} elseif (str_contains(strtolower($e->getMessage()), 'timeout') || str_contains(strtolower($e->getMessage()), 'maximum execution time')) {
|
||||
$errorType = 'timeout';
|
||||
}
|
||||
|
||||
Log::error('SlikImport: Error during import', [
|
||||
'error' => $e->getMessage(),
|
||||
'error_type' => $errorType,
|
||||
'exception_type' => get_class($e),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'memory_peak' => memory_get_peak_usage(true),
|
||||
'memory_limit' => ini_get('memory_limit'),
|
||||
'timeout_limit' => ini_get('max_execution_time'),
|
||||
'is_memory_error' => str_contains(strtolower($e->getMessage()), 'memory'),
|
||||
'is_timeout_error' => str_contains(strtolower($e->getMessage()), 'timeout') || str_contains(strtolower($e->getMessage()), 'maximum execution time')
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert memory limit string ke bytes
|
||||
*
|
||||
* @param string $memoryLimit
|
||||
* @return int
|
||||
*/
|
||||
private function convertToBytes(string $memoryLimit): int
|
||||
{
|
||||
$memoryLimit = trim($memoryLimit);
|
||||
$lastChar = strtolower(substr($memoryLimit, -1));
|
||||
$number = (int) substr($memoryLimit, 0, -1);
|
||||
|
||||
switch ($lastChar) {
|
||||
case 'g':
|
||||
return $number * 1024 * 1024 * 1024;
|
||||
case 'm':
|
||||
return $number * 1024 * 1024;
|
||||
case 'k':
|
||||
return $number * 1024;
|
||||
default:
|
||||
return (int) $memoryLimit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah baris kosong
|
||||
*
|
||||
* @param Collection $row
|
||||
* @return bool
|
||||
*/
|
||||
private function isEmptyRow(Collection $row): bool
|
||||
{
|
||||
return $row->filter(function ($value) {
|
||||
return !empty(trim($value));
|
||||
})->isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validasi data baris
|
||||
*
|
||||
* @param Collection $row
|
||||
* @return bool
|
||||
*/
|
||||
private function validateRow(Collection $row): bool
|
||||
{
|
||||
// Validasi minimal: sandi_bank, no_rekening, dan cif harus ada
|
||||
return !empty(trim($row[0])) && // sandi_bank
|
||||
!empty(trim($row[5])) && // no_rekening
|
||||
!empty(trim($row[6])); // cif
|
||||
}
|
||||
|
||||
/**
|
||||
* Map data dari baris Excel ke array untuk model Slik
|
||||
*
|
||||
* @param Collection $row
|
||||
* @return array
|
||||
*/
|
||||
private function mapRowToSlik(Collection $row): array
|
||||
{
|
||||
return [
|
||||
'sandi_bank' => trim($row[0]) ?: null,
|
||||
'tahun' => $this->parseInteger($row[1]),
|
||||
'bulan' => $this->parseInteger($row[2]),
|
||||
'flag_detail' => trim($row[3]) ?: null,
|
||||
'kode_register_agunan' => trim($row[4]) ?: null,
|
||||
'no_rekening' => trim($row[5]) ?: null,
|
||||
'cif' => trim($row[6]) ?: null,
|
||||
'kolektibilitas' => trim($row[7]) ?: null,
|
||||
'fasilitas' => trim($row[8]) ?: null,
|
||||
'jenis_segmen_fasilitas' => trim($row[9]) ?: null,
|
||||
'status_agunan' => trim($row[10]) ?: null,
|
||||
'jenis_agunan' => trim($row[11]) ?: null,
|
||||
'peringkat_agunan' => trim($row[12]) ?: null,
|
||||
'lembaga_pemeringkat' => trim($row[13]) ?: null,
|
||||
'jenis_pengikatan' => trim($row[14]) ?: null,
|
||||
'tanggal_pengikatan' => $this->parseDate($row[15]),
|
||||
'nama_pemilik_agunan' => trim($row[16]) ?: null,
|
||||
'bukti_kepemilikan' => trim($row[17]) ?: null,
|
||||
'alamat_agunan' => trim($row[18]) ?: null,
|
||||
'lokasi_agunan' => trim($row[19]) ?: null,
|
||||
'nilai_agunan' => $this->parseDecimal($row[20]),
|
||||
'nilai_agunan_menurut_ljk' => $this->parseDecimal($row[21]),
|
||||
'tanggal_penilaian_ljk' => $this->parseDate($row[22]),
|
||||
'nilai_agunan_penilai_independen' => $this->parseDecimal($row[23]),
|
||||
'nama_penilai_independen' => trim($row[24]) ?: null,
|
||||
'tanggal_penilaian_penilai_independen' => $this->parseDate($row[25]),
|
||||
'jumlah_hari_tunggakan' => $this->parseInteger($row[26]),
|
||||
'status_paripasu' => trim($row[27]) ?: null,
|
||||
'prosentase_paripasu' => $this->parseDecimal($row[28]),
|
||||
'status_kredit_join' => trim($row[29]) ?: null,
|
||||
'diasuransikan' => trim($row[30]) ?: null,
|
||||
'keterangan' => trim($row[31]) ?: null,
|
||||
'kantor_cabang' => trim($row[32]) ?: null,
|
||||
'operasi_data' => trim($row[33]) ?: null,
|
||||
'kode_cabang' => trim($row[34]) ?: null,
|
||||
'nama_debitur' => trim($row[35]) ?: null,
|
||||
'nama_cabang' => trim($row[36]) ?: null,
|
||||
'flag' => trim($row[37]) ?: null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse integer value
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return int|null
|
||||
*/
|
||||
private function parseInteger($value): ?int
|
||||
{
|
||||
if (empty(trim($value))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse decimal value
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return float|null
|
||||
*/
|
||||
private function parseDecimal($value): ?float
|
||||
{
|
||||
if (empty(trim($value))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove currency formatting
|
||||
$cleaned = str_replace([',', '.'], ['', '.'], $value);
|
||||
$cleaned = preg_replace('/[^0-9.]/', '', $cleaned);
|
||||
|
||||
return (float) $cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse date value
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string|null
|
||||
*/
|
||||
private function parseDate($value): ?string
|
||||
{
|
||||
if (empty(trim($value))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to parse various date formats
|
||||
$date = \Carbon\Carbon::parse($value);
|
||||
return $date->format('Y-m-d');
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('SlikImport: Invalid date format', [
|
||||
'value' => $value,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
179
app/Jobs/ProcessSlikImport.php
Normal file
179
app/Jobs/ProcessSlikImport.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lpj\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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Lpj\Imports\SlikImport;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ProcessSlikImport implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 1800; // 30 menit untuk file besar
|
||||
public $tries = 5; // Tambah retry untuk file sangat besar
|
||||
public $maxExceptions = 5;
|
||||
public $backoff = [60, 300, 900, 1800, 3600]; // Exponential backoff dalam detik
|
||||
|
||||
protected string $filePath;
|
||||
protected int $userId;
|
||||
protected string $importId;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param string $filePath
|
||||
* @param int $userId
|
||||
* @param string $importId
|
||||
*/
|
||||
public function __construct(string $filePath, int $userId, string $importId)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->userId = $userId;
|
||||
$this->importId = $importId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
Log::info('ProcessSlikImport: Memulai proses import via queue', [
|
||||
'file_path' => $this->filePath,
|
||||
'user_id' => $this->userId,
|
||||
'import_id' => $this->importId,
|
||||
'memory_limit' => ini_get('memory_limit'),
|
||||
'max_execution_time' => ini_get('max_execution_time')
|
||||
]);
|
||||
|
||||
try {
|
||||
// Cek file size terlebih dahulu
|
||||
$fileSize = filesize($this->filePath);
|
||||
$maxFileSize = config('import.slik.max_file_size', 50) * 1024 * 1024; // Convert MB to bytes
|
||||
|
||||
if ($fileSize > $maxFileSize) {
|
||||
throw new \Exception('File terlalu besar: ' . number_format($fileSize / 1024 / 1024, 2) . ' MB. Maksimum: ' . config('import.slik.max_file_size', 50) . ' MB');
|
||||
}
|
||||
|
||||
// Set optimasi memory untuk queue processing
|
||||
$memoryLimit = config('import.slik.memory_limit', 1024);
|
||||
ini_set('memory_limit', $memoryLimit . 'M');
|
||||
ini_set('max_execution_time', config('import.slik.timeout', 1800));
|
||||
|
||||
// Set timeout untuk XML Scanner
|
||||
$xmlScannerTimeout = config('import.slik.xml_scanner.timeout', 1800);
|
||||
$xmlScannerMemory = config('import.slik.xml_scanner.memory_limit', 1024);
|
||||
|
||||
// Enable garbage collection jika diizinkan
|
||||
if (config('import.slik.enable_gc', true)) {
|
||||
gc_enable();
|
||||
}
|
||||
|
||||
// Update progress status
|
||||
$this->updateProgress('processing', 0, 'Memproses file Excel...');
|
||||
|
||||
Log::info('SlikImport: Processing file', [
|
||||
'file' => basename($this->filePath),
|
||||
'file_size' => number_format(filesize($this->filePath) / 1024 / 1024, 2) . ' MB',
|
||||
'memory_limit' => $memoryLimit . 'M',
|
||||
'timeout' => config('import.slik.timeout', 1800),
|
||||
'enable_gc' => config('import.slik.enable_gc', true),
|
||||
'xml_scanner_timeout' => config('import.slik.xml_scanner.timeout', 1800),
|
||||
'chunk_size' => config('import.slik.chunk_size', 50),
|
||||
'batch_size' => config('import.slik.batch_size', 50),
|
||||
]);
|
||||
|
||||
// Import file menggunakan SlikImport
|
||||
$import = new SlikImport();
|
||||
Excel::import($import, $this->filePath);
|
||||
|
||||
// Update progress selesai
|
||||
$this->updateProgress('completed', 100, 'Import berhasil diselesaikan');
|
||||
|
||||
Log::info('ProcessSlikImport: Import berhasil diselesaikan', [
|
||||
'import_id' => $this->importId,
|
||||
'file_path' => $this->filePath,
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'memory_peak' => memory_get_peak_usage(true)
|
||||
]);
|
||||
|
||||
// Hapus file temporary setelah selesai
|
||||
if (config('import.general.cleanup_temp_files', true)) {
|
||||
Storage::delete($this->filePath);
|
||||
Log::info('ProcessSlikImport: File temporary dihapus', ['file_path' => $this->filePath]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Update progress error
|
||||
$this->updateProgress('failed', 0, 'Error: ' . $e->getMessage());
|
||||
|
||||
Log::error('ProcessSlikImport: Error saat proses import', [
|
||||
'import_id' => $this->importId,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'memory_usage' => memory_get_usage(true)
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress import
|
||||
*
|
||||
* @param string $status
|
||||
* @param int $percentage
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
private function updateProgress(string $status, int $percentage, string $message): void
|
||||
{
|
||||
if (config('import.slik.progress.enabled', true)) {
|
||||
$cacheKey = config('import.slik.progress.cache_key', 'slik_import_progress') . '_' . $this->importId;
|
||||
$cacheTtl = config('import.slik.progress.cache_ttl', 3600);
|
||||
|
||||
$progressData = [
|
||||
'status' => $status,
|
||||
'percentage' => $percentage,
|
||||
'message' => $message,
|
||||
'timestamp' => now(),
|
||||
'user_id' => $this->userId
|
||||
];
|
||||
|
||||
cache()->put($cacheKey, $progressData, $cacheTtl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle job failure
|
||||
*
|
||||
* @param \Throwable $exception
|
||||
* @return void
|
||||
*/
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::error('ProcessSlikImport: Job failed', [
|
||||
'import_id' => $this->importId,
|
||||
'error' => $exception->getMessage(),
|
||||
'trace' => $exception->getTraceAsString()
|
||||
]);
|
||||
|
||||
// Update progress ke failed
|
||||
$this->updateProgress('failed', 0, 'Import gagal: ' . $exception->getMessage());
|
||||
|
||||
// Cleanup file temporary
|
||||
if (Storage::exists($this->filePath)) {
|
||||
Storage::delete($this->filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
190
app/Models/Slik.php
Normal file
190
app/Models/Slik.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lpj\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Model Slik untuk mengelola data SLIK (Sistem Layanan Informasi Keuangan)
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $sandi_bank
|
||||
* @property string|null $tahun
|
||||
* @property string|null $bulan
|
||||
* @property string|null $flag_detail
|
||||
* @property string|null $kode_register_agunan
|
||||
* @property string|null $no_rekening
|
||||
* @property string|null $cif
|
||||
* @property string|null $kolektibilitas
|
||||
* @property string|null $fasilitas
|
||||
* @property string|null $jenis_segmen_fasilitas
|
||||
* @property string|null $status_agunan
|
||||
* @property string|null $jenis_agunan
|
||||
* @property string|null $peringkat_agunan
|
||||
* @property string|null $lembaga_pemeringkat
|
||||
* @property string|null $jenis_pengikatan
|
||||
* @property string|null $tanggal_pengikatan
|
||||
* @property string|null $nama_pemilik_agunan
|
||||
* @property string|null $bukti_kepemilikan
|
||||
* @property string|null $alamat_agunan
|
||||
* @property string|null $lokasi_agunan
|
||||
* @property string|null $nilai_agunan
|
||||
* @property string|null $nilai_agunan_menurut_ljk
|
||||
* @property string|null $tanggal_penilaian_ljk
|
||||
* @property string|null $nilai_agunan_penilai_independen
|
||||
* @property string|null $nama_penilai_independen
|
||||
* @property string|null $tanggal_penilaian_penilai_independen
|
||||
* @property string|null $jumlah_hari_tunggakan
|
||||
* @property string|null $status_paripasu
|
||||
* @property string|null $prosentase_paripasu
|
||||
* @property string|null $status_kredit_join
|
||||
* @property string|null $diasuransikan
|
||||
* @property string|null $keterangan
|
||||
* @property string|null $kantor_cabang
|
||||
* @property string|null $operasi_data
|
||||
* @property string|null $kode_cabang
|
||||
* @property string|null $nama_debitur
|
||||
* @property string|null $nama_cabang
|
||||
* @property string|null $flag
|
||||
*/
|
||||
class Slik extends Base
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Nama tabel yang digunakan oleh model
|
||||
*/
|
||||
protected $table = 'sliks';
|
||||
|
||||
/**
|
||||
* Field yang dapat diisi secara mass assignment
|
||||
*/
|
||||
protected $fillable = [
|
||||
'sandi_bank',
|
||||
'tahun',
|
||||
'bulan',
|
||||
'flag_detail',
|
||||
'kode_register_agunan',
|
||||
'no_rekening',
|
||||
'cif',
|
||||
'kolektibilitas',
|
||||
'fasilitas',
|
||||
'jenis_segmen_fasilitas',
|
||||
'status_agunan',
|
||||
'jenis_agunan',
|
||||
'peringkat_agunan',
|
||||
'lembaga_pemeringkat',
|
||||
'jenis_pengikatan',
|
||||
'tanggal_pengikatan',
|
||||
'nama_pemilik_agunan',
|
||||
'bukti_kepemilikan',
|
||||
'alamat_agunan',
|
||||
'lokasi_agunan',
|
||||
'nilai_agunan',
|
||||
'nilai_agunan_menurut_ljk',
|
||||
'tanggal_penilaian_ljk',
|
||||
'nilai_agunan_penilai_independen',
|
||||
'nama_penilai_independen',
|
||||
'tanggal_penilaian_penilai_independen',
|
||||
'jumlah_hari_tunggakan',
|
||||
'status_paripasu',
|
||||
'prosentase_paripasu',
|
||||
'status_kredit_join',
|
||||
'diasuransikan',
|
||||
'keterangan',
|
||||
'kantor_cabang',
|
||||
'operasi_data',
|
||||
'kode_cabang',
|
||||
'nama_debitur',
|
||||
'nama_cabang',
|
||||
'flag',
|
||||
];
|
||||
|
||||
/**
|
||||
* Casting tipe data untuk field tertentu
|
||||
*/
|
||||
protected $casts = [
|
||||
'tanggal_pengikatan' => 'date',
|
||||
'tanggal_penilaian_ljk' => 'date',
|
||||
'tanggal_penilaian_penilai_independen' => 'date',
|
||||
'nilai_agunan' => 'decimal:2',
|
||||
'nilai_agunan_menurut_ljk' => 'decimal:2',
|
||||
'nilai_agunan_penilai_independen' => 'decimal:2',
|
||||
'prosentase_paripasu' => 'decimal:2',
|
||||
'jumlah_hari_tunggakan' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Accessor untuk format nilai agunan dengan currency Indonesia
|
||||
*/
|
||||
public function getNilaiAgunanFormattedAttribute(): string
|
||||
{
|
||||
return $this->nilai_agunan ? 'Rp ' . number_format($this->nilai_agunan, 0, ',', '.') : 'Rp 0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor untuk format nilai agunan menurut LJK dengan currency Indonesia
|
||||
*/
|
||||
public function getNilaiAgunanMenurutLjkFormattedAttribute(): string
|
||||
{
|
||||
return $this->nilai_agunan_menurut_ljk ? 'Rp ' . number_format($this->nilai_agunan_menurut_ljk, 0, ',', '.') : 'Rp 0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor untuk format nilai agunan penilai independen dengan currency Indonesia
|
||||
*/
|
||||
public function getNilaiAgunanPenilaiIndependenFormattedAttribute(): string
|
||||
{
|
||||
return $this->nilai_agunan_penilai_independen ? 'Rp ' . number_format($this->nilai_agunan_penilai_independen, 0, ',', '.') : 'Rp 0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor untuk status badge berdasarkan status agunan
|
||||
*/
|
||||
public function getStatusBadgeAttribute(): string
|
||||
{
|
||||
$statusClass = match($this->status_agunan) {
|
||||
'Aktif' => 'badge-success',
|
||||
'Tidak Aktif' => 'badge-danger',
|
||||
'Pending' => 'badge-warning',
|
||||
default => 'badge-secondary'
|
||||
};
|
||||
|
||||
return '<span class="badge ' . $statusClass . '">' . ($this->status_agunan ?? 'Unknown') . '</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk filter berdasarkan tahun
|
||||
*/
|
||||
public function scopeByYear($query, $year)
|
||||
{
|
||||
return $query->where('tahun', $year);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk filter berdasarkan bulan
|
||||
*/
|
||||
public function scopeByMonth($query, $month)
|
||||
{
|
||||
return $query->where('bulan', $month);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk filter berdasarkan sandi bank
|
||||
*/
|
||||
public function scopeBySandiBank($query, $sandiBank)
|
||||
{
|
||||
return $query->where('sandi_bank', $sandiBank);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk filter berdasarkan kode cabang
|
||||
*/
|
||||
public function scopeByKodeCabang($query, $kodeCabang)
|
||||
{
|
||||
return $query->where('kode_cabang', $kodeCabang);
|
||||
}
|
||||
|
||||
// Method creator() dan editor() sudah disediakan oleh trait Userstamps
|
||||
}
|
||||
236
app/Services/ImportProgressService.php
Normal file
236
app/Services/ImportProgressService.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lpj\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ImportProgressService
|
||||
{
|
||||
protected string $cacheKeyPrefix;
|
||||
protected int $cacheTtl;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->cacheKeyPrefix = config('import.slik.progress.cache_key', 'slik_import_progress');
|
||||
$this->cacheTtl = config('import.slik.progress.cache_ttl', 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start new import progress
|
||||
*
|
||||
* @param string $importId
|
||||
* @param int $userId
|
||||
* @param string $filename
|
||||
* @param int $totalRows
|
||||
* @return array
|
||||
*/
|
||||
public function start(string $importId, int $userId, string $filename, int $totalRows): array
|
||||
{
|
||||
$progressData = [
|
||||
'import_id' => $importId,
|
||||
'user_id' => $userId,
|
||||
'filename' => $filename,
|
||||
'total_rows' => $totalRows,
|
||||
'processed_rows' => 0,
|
||||
'skipped_rows' => 0,
|
||||
'error_rows' => 0,
|
||||
'status' => 'started',
|
||||
'percentage' => 0,
|
||||
'message' => 'Memulai import...',
|
||||
'started_at' => now(),
|
||||
'updated_at' => now()
|
||||
];
|
||||
|
||||
$cacheKey = $this->getCacheKey($importId);
|
||||
Cache::put($cacheKey, $progressData, $this->cacheTtl);
|
||||
|
||||
Log::info('ImportProgressService: Import started', $progressData);
|
||||
|
||||
return $progressData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress import
|
||||
*
|
||||
* @param string $importId
|
||||
* @param int $processedRows
|
||||
* @param int $skippedRows
|
||||
* @param int $errorRows
|
||||
* @param string|null $message
|
||||
* @return array
|
||||
*/
|
||||
public function update(string $importId, int $processedRows, int $skippedRows, int $errorRows, ?string $message = null): array
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($importId);
|
||||
$progressData = Cache::get($cacheKey);
|
||||
|
||||
if (!$progressData) {
|
||||
Log::warning('ImportProgressService: Progress data not found', ['import_id' => $importId]);
|
||||
return [];
|
||||
}
|
||||
|
||||
$totalRows = $progressData['total_rows'];
|
||||
$percentage = $totalRows > 0 ? round(($processedRows / $totalRows) * 100, 2) : 0;
|
||||
|
||||
$progressData = array_merge($progressData, [
|
||||
'processed_rows' => $processedRows,
|
||||
'skipped_rows' => $skippedRows,
|
||||
'error_rows' => $errorRows,
|
||||
'percentage' => $percentage,
|
||||
'message' => $message ?? "Memproses baris {$processedRows} dari {$totalRows}...",
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
Cache::put($cacheKey, $progressData, $this->cacheTtl);
|
||||
|
||||
// Log progress setiap 10%
|
||||
if ($percentage % 10 === 0) {
|
||||
Log::info('ImportProgressService: Progress update', [
|
||||
'import_id' => $importId,
|
||||
'percentage' => $percentage,
|
||||
'processed' => $processedRows,
|
||||
'total' => $totalRows
|
||||
]);
|
||||
}
|
||||
|
||||
return $progressData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark import as completed
|
||||
*
|
||||
* @param string $importId
|
||||
* @param string|null $message
|
||||
* @return array
|
||||
*/
|
||||
public function complete(string $importId, ?string $message = null): array
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($importId);
|
||||
$progressData = Cache::get($cacheKey);
|
||||
|
||||
if (!$progressData) {
|
||||
Log::warning('ImportProgressService: Progress data not found for completion', ['import_id' => $importId]);
|
||||
return [];
|
||||
}
|
||||
|
||||
$progressData = array_merge($progressData, [
|
||||
'status' => 'completed',
|
||||
'percentage' => 100,
|
||||
'message' => $message ?? 'Import berhasil diselesaikan',
|
||||
'completed_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
Cache::put($cacheKey, $progressData, $this->cacheTtl);
|
||||
|
||||
Log::info('ImportProgressService: Import completed', [
|
||||
'import_id' => $importId,
|
||||
'total_rows' => $progressData['total_rows'],
|
||||
'processed_rows' => $progressData['processed_rows'],
|
||||
'skipped_rows' => $progressData['skipped_rows'],
|
||||
'error_rows' => $progressData['error_rows']
|
||||
]);
|
||||
|
||||
return $progressData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark import as failed
|
||||
*
|
||||
* @param string $importId
|
||||
* @param string $errorMessage
|
||||
* @return array
|
||||
*/
|
||||
public function fail(string $importId, string $errorMessage): array
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($importId);
|
||||
$progressData = Cache::get($cacheKey);
|
||||
|
||||
if (!$progressData) {
|
||||
Log::warning('ImportProgressService: Progress data not found for failure', ['import_id' => $importId]);
|
||||
return [];
|
||||
}
|
||||
|
||||
$progressData = array_merge($progressData, [
|
||||
'status' => 'failed',
|
||||
'message' => 'Import gagal: ' . $errorMessage,
|
||||
'failed_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
Cache::put($cacheKey, $progressData, $this->cacheTtl);
|
||||
|
||||
Log::error('ImportProgressService: Import failed', [
|
||||
'import_id' => $importId,
|
||||
'error' => $errorMessage,
|
||||
'progress_data' => $progressData
|
||||
]);
|
||||
|
||||
return $progressData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progress data
|
||||
*
|
||||
* @param string $importId
|
||||
* @return array|null
|
||||
*/
|
||||
public function getProgress(string $importId): ?array
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($importId);
|
||||
return Cache::get($cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active imports for user
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getUserImports(int $userId): array
|
||||
{
|
||||
$pattern = $this->cacheKeyPrefix . '_*';
|
||||
$keys = Cache::get($pattern);
|
||||
|
||||
$imports = [];
|
||||
foreach ($keys as $key) {
|
||||
$data = Cache::get($key);
|
||||
if ($data && $data['user_id'] === $userId) {
|
||||
$imports[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return $imports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear progress data
|
||||
*
|
||||
* @param string $importId
|
||||
* @return bool
|
||||
*/
|
||||
public function clear(string $importId): bool
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($importId);
|
||||
$result = Cache::forget($cacheKey);
|
||||
|
||||
Log::info('ImportProgressService: Progress data cleared', ['import_id' => $importId]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key
|
||||
*
|
||||
* @param string $importId
|
||||
* @return string
|
||||
*/
|
||||
private function getCacheKey(string $importId): string
|
||||
{
|
||||
return $this->cacheKeyPrefix . '_' . $importId;
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,62 @@
|
||||
|
||||
return [
|
||||
'name' => 'Lpj',
|
||||
'import' => [
|
||||
'slik' => [
|
||||
// Memory limit untuk import (dalam MB)
|
||||
'memory_limit' => env('SLIK_IMPORT_MEMORY_LIMIT', 1024),
|
||||
|
||||
// Ukuran chunk untuk processing (jumlah baris per chunk)
|
||||
'chunk_size' => env('SLIK_IMPORT_CHUNK_SIZE', 50),
|
||||
|
||||
// Ukuran batch untuk database insert
|
||||
'batch_size' => env('SLIK_IMPORT_BATCH_SIZE', 50),
|
||||
|
||||
// Timeout untuk import (dalam detik)
|
||||
'timeout' => env('SLIK_IMPORT_TIMEOUT', 1800), // 30 menit untuk file besar
|
||||
|
||||
// Maksimum file size yang diizinkan (dalam MB)
|
||||
'max_file_size' => env('SLIK_IMPORT_MAX_FILE_SIZE', 50),
|
||||
|
||||
// Enable garbage collection untuk optimasi memory
|
||||
'enable_gc' => env('SLIK_IMPORT_ENABLE_GC', true),
|
||||
|
||||
// Enable progress logging
|
||||
'enable_progress_logging' => env('SLIK_IMPORT_ENABLE_PROGRESS_LOGGING', true),
|
||||
|
||||
// Enable detailed error logging
|
||||
'enable_error_logging' => env('SLIK_IMPORT_ENABLE_ERROR_LOGGING', true),
|
||||
|
||||
// XML Scanner settings untuk optimasi memory
|
||||
'xml_scanner' => [
|
||||
'timeout' => env('SLIK_XML_SCANNER_TIMEOUT', 1800), // 30 menit
|
||||
'memory_limit' => env('SLIK_XML_SCANNER_MEMORY_LIMIT', 1024), // 1GB
|
||||
'chunk_size' => env('SLIK_XML_SCANNER_CHUNK_SIZE', 50), // Lebih kecil untuk XML
|
||||
],
|
||||
|
||||
// Queue processing untuk file besar
|
||||
'queue' => [
|
||||
'enabled' => env('SLIK_IMPORT_QUEUE_ENABLED', false),
|
||||
'connection' => env('SLIK_IMPORT_QUEUE_CONNECTION', 'database'),
|
||||
'queue_name' => env('SLIK_IMPORT_QUEUE_NAME', 'imports'),
|
||||
'chunk_size' => env('SLIK_IMPORT_QUEUE_CHUNK_SIZE', 500),
|
||||
],
|
||||
|
||||
// Progress tracking
|
||||
'progress' => [
|
||||
'enabled' => env('SLIK_IMPORT_PROGRESS_ENABLED', true),
|
||||
'update_interval' => env('SLIK_IMPORT_PROGRESS_INTERVAL', 50), // update setiap 50 baris
|
||||
'cache_key' => 'slik_import_progress',
|
||||
'cache_ttl' => 3600, // 1 jam
|
||||
],
|
||||
],
|
||||
|
||||
// General import settings
|
||||
'general' => [
|
||||
'default_memory_limit' => env('IMPORT_DEFAULT_MEMORY_LIMIT', 128),
|
||||
'max_execution_time' => env('IMPORT_MAX_EXECUTION_TIME', 300000),
|
||||
'temp_directory' => env('IMPORT_TEMP_DIRECTORY', storage_path('app/temp')),
|
||||
'cleanup_temp_files' => env('IMPORT_CLEANUP_TEMP_FILES', true),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
88
database/migrations/2025_09_15_092525_create_sliks_table.php
Normal file
88
database/migrations/2025_09_15_092525_create_sliks_table.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* Migration untuk membuat tabel sliks dengan semua field yang diperlukan
|
||||
* berdasarkan header Excel yang diberikan untuk import data SLIK
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sliks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// Field utama berdasarkan header Excel
|
||||
$table->string('sandi_bank')->nullable(); // Sandi Bank
|
||||
$table->string('tahun')->nullable(); // Tahun
|
||||
$table->string('bulan')->nullable(); // Bulan
|
||||
$table->string('flag_detail')->nullable(); // Flag Detail
|
||||
$table->string('kode_register_agunan')->nullable(); // Kode Register Agunan
|
||||
$table->string('no_rekening')->nullable(); // No Rekening
|
||||
$table->string('cif')->nullable(); // CIF
|
||||
$table->string('kolektibilitas')->nullable(); // Kolektibilitas
|
||||
$table->string('fasilitas')->nullable(); // Fasilitas
|
||||
$table->string('jenis_segmen_fasilitas')->nullable(); // Jenis Segmen Fasilitas
|
||||
$table->string('status_agunan')->nullable(); // Status Agunan
|
||||
$table->string('jenis_agunan')->nullable(); // Jenis Agunan
|
||||
$table->string('peringkat_agunan')->nullable(); // Peringkat Agunan
|
||||
$table->string('lembaga_pemeringkat')->nullable(); // Lembaga Pemeringkat
|
||||
$table->string('jenis_pengikatan')->nullable(); // Jenis Pengikatan
|
||||
$table->string('tanggal_pengikatan')->nullable(); // Tanggal Pengikatan
|
||||
$table->string('nama_pemilik_agunan')->nullable(); // Nama Pemilik Agunan
|
||||
$table->string('bukti_kepemilikan')->nullable(); // Bukti Kepemilikan
|
||||
$table->text('alamat_agunan')->nullable(); // Alamat Agunan
|
||||
$table->string('lokasi_agunan')->nullable(); // Lokasi Agunan
|
||||
$table->string('nilai_agunan')->nullable(); // Nilai Agunan
|
||||
$table->string('nilai_agunan_menurut_ljk')->nullable(); // Nilai Agunan Menurut LJK
|
||||
$table->string('tanggal_penilaian_ljk')->nullable(); // Tanggal Penilaian LJK
|
||||
$table->string('nilai_agunan_penilai_independen')->nullable(); // Nilai Agunan Penilai Independen
|
||||
$table->string('nama_penilai_independen')->nullable(); // Nama Penilai Independen
|
||||
$table->string('tanggal_penilaian_penilai_independen')->nullable(); // Tanggal Penilaian Penilai Independen
|
||||
$table->string('jumlah_hari_tunggakan')->nullable(); // Jumlah Hari Tunggakan
|
||||
$table->string('status_paripasu')->nullable(); // Status Paripasu
|
||||
$table->string('prosentase_paripasu')->nullable(); // Prosentase Paripasu
|
||||
$table->string('status_kredit_join')->nullable(); // Status Kredit Join
|
||||
$table->string('diasuransikan')->nullable(); // Diasuransikan
|
||||
$table->text('keterangan')->nullable(); // Keterangan
|
||||
$table->string('kantor_cabang')->nullable(); // Kantor Cabang
|
||||
$table->string('operasi_data')->nullable(); // Operasi Data
|
||||
$table->string('kode_cabang')->nullable(); // Kode Cabang
|
||||
$table->string('nama_debitur')->nullable(); // Nama Debitur
|
||||
$table->string('nama_cabang')->nullable(); // Nama Cabang
|
||||
$table->string('flag')->nullable(); // Flag
|
||||
|
||||
// Standard Laravel fields
|
||||
$table->timestamps();
|
||||
$table->string('created_by')->nullable();
|
||||
$table->string('updated_by')->nullable();
|
||||
$table->string('deleted_by')->nullable();
|
||||
$table->softDeletes();
|
||||
|
||||
// Indexes untuk performa query
|
||||
$table->index(['sandi_bank']);
|
||||
$table->index(['tahun']);
|
||||
$table->index(['bulan']);
|
||||
$table->index(['no_rekening']);
|
||||
$table->index(['cif']);
|
||||
$table->index(['kode_register_agunan']);
|
||||
$table->index(['nama_debitur']);
|
||||
$table->index(['kode_cabang']);
|
||||
$table->index(['created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* Menghapus tabel sliks jika migration di-rollback
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sliks');
|
||||
}
|
||||
};
|
||||
18
module.json
18
module.json
@@ -65,6 +65,24 @@
|
||||
"senior-officer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "SLIK",
|
||||
"path": "slik",
|
||||
"icon": "ki-filled ki-filter-tablet text-lg text-primary",
|
||||
"classes": "",
|
||||
"attributes": [],
|
||||
"permission": "",
|
||||
"roles": [
|
||||
"adk",
|
||||
"administrator",
|
||||
"pemohon-ao",
|
||||
"pemohon-eo",
|
||||
"admin",
|
||||
"DD Appraisal",
|
||||
"EO Appraisal",
|
||||
"senior-officer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Laporan Penilai Jaminan",
|
||||
"path": "laporan-penilai-jaminan",
|
||||
|
||||
343
resources/views/slik/index.blade.php
Normal file
343
resources/views/slik/index.blade.php
Normal file
@@ -0,0 +1,343 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@section('breadcrumbs')
|
||||
{{ Breadcrumbs::render('slik') }}
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<div class="min-w-full border card border-agi-100 card-grid" data-datatable="false" data-datatable-page-size="10" data-datatable-state-save="false" id="slik-table" data-api-url="{{ route('slik.datatables') }}">
|
||||
<div class="flex-wrap py-5 card-header bg-agi-50">
|
||||
<h3 class="card-title">
|
||||
Data SLIK
|
||||
</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 SLIK" id="search" type="text" value="">
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2.5">
|
||||
<div class="h-[24px] border border-r-gray-200"></div>
|
||||
|
||||
<!-- Filter Tahun -->
|
||||
<select class="select select-sm" id="year-filter">
|
||||
<option value="">Semua Tahun</option>
|
||||
@for($year = date('Y'); $year >= 2020; $year--)
|
||||
<option value="{{ $year }}">{{ $year }}</option>
|
||||
@endfor
|
||||
</select>
|
||||
|
||||
<!-- Filter Bulan -->
|
||||
<select class="select select-sm" id="month-filter">
|
||||
<option value="">Semua Bulan</option>
|
||||
@for($month = 1; $month <= 12; $month++)
|
||||
<option value="{{ $month }}">{{ DateTime::createFromFormat('!m', $month)->format('F') }}</option>
|
||||
@endfor
|
||||
</select>
|
||||
|
||||
<!-- Filter Status -->
|
||||
<select class="select select-sm" id="status-filter">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="aktif">Aktif</option>
|
||||
<option value="tidak_aktif">Tidak Aktif</option>
|
||||
</select>
|
||||
|
||||
<!-- Import Excel -->
|
||||
<button class="btn btn-sm btn-success" data-modal-toggle="#import-modal">
|
||||
<i class="ki-filled ki-file-up"></i> Import Excel
|
||||
</button>
|
||||
|
||||
<a class="btn btn-sm btn-light" href="#" id="export-excel">
|
||||
<i class="ki-filled ki-file-down"></i> Export Excel
|
||||
</a>
|
||||
</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-[50px]" data-datatable-column="no">
|
||||
<span class="sort">
|
||||
<span class="sort-label">No</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[100px]" data-datatable-column="sandi_bank">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Sandi Bank</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[80px]" data-datatable-column="tahun">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Tahun</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[80px]" data-datatable-column="bulan">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Bulan</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[120px]" data-datatable-column="no_rekening">
|
||||
<span class="sort">
|
||||
<span class="sort-label">No Rekening</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[100px]" data-datatable-column="cif">
|
||||
<span class="sort">
|
||||
<span class="sort-label">CIF</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[120px]" data-datatable-column="nama_debitur">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Nama Debitur</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[120px]" data-datatable-column="nilai_agunan">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Nilai Agunan</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[100px]" data-datatable-column="status_agunan">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Status</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[50px] text-center" data-datatable-column="actions">Aksi</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 Import Excel -->
|
||||
<div class="modal" data-modal="true" id="import-modal">
|
||||
<div class="modal-content max-w-[500px] top-[15%]">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Import Data SLIK</h3>
|
||||
<button class="btn btn-sm btn-icon btn-light btn-clear shrink-0" data-modal-dismiss="true">
|
||||
<i class="ki-filled ki-cross"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form action="{{ route('slik.import') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label class="text-gray-900 form-label">File Excel</label>
|
||||
<input type="file" name="file" class="file-input" accept=".xlsx,.xls,.csv" required>
|
||||
<div class="text-gray-600 text-2sm">
|
||||
Format yang didukung: .xlsx, .xls, .csv (Maksimal 10MB)
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-yellow-100 rounded border border-yellow-300">
|
||||
<div class="text-sm text-yellow-800">
|
||||
<strong>Catatan:</strong>
|
||||
<ul class="mt-1 list-disc list-inside">
|
||||
<li>Data akan dimulai dari baris ke-2 (setelah header)</li>
|
||||
<li>Pastikan urutan kolom sesuai dengan template</li>
|
||||
<li>Data akan di-update jika sudah ada berdasarkan kombinasi sandi bank, tahun, bulan, dan no rekening</li>
|
||||
<li>Kolom yang wajib diisi: Sandi Bank, Tahun, Bulan, No Rekening, CIF</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="flex gap-4">
|
||||
<button type="button" class="btn btn-light" data-modal-dismiss="true">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Import</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
/**
|
||||
* Fungsi untuk menghapus data SLIK
|
||||
* @param {number} data - ID data yang akan dihapus
|
||||
*/
|
||||
function deleteData(data) {
|
||||
Swal.fire({
|
||||
title: 'Apakah Anda yakin?',
|
||||
text: "Data yang dihapus tidak dapat dikembalikan!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Ya, hapus!',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax(`slik/${data}`, {
|
||||
type: 'DELETE'
|
||||
}).then((response) => {
|
||||
Swal.fire('Terhapus!', 'Data SLIK berhasil dihapus.', 'success').then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}).catch((error) => {
|
||||
Swal.fire('Error!', error.responseJSON.message, 'error');
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
/**
|
||||
* Inisialisasi DataTable untuk SLIK menggunakan KTDataTable
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const element = document.querySelector('#slik-table');
|
||||
const searchInput = document.getElementById('search');
|
||||
const yearFilter = document.getElementById('year-filter');
|
||||
const monthFilter = document.getElementById('month-filter');
|
||||
const statusFilter = document.getElementById('status-filter');
|
||||
|
||||
const apiUrl = element.getAttribute('data-api-url');
|
||||
|
||||
// Konfigurasi DataTable menggunakan KTDataTable
|
||||
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();
|
||||
},
|
||||
},
|
||||
no: {
|
||||
title: 'No',
|
||||
},
|
||||
sandi_bank: {
|
||||
title: 'Sandi Bank',
|
||||
},
|
||||
tahun: {
|
||||
title: 'Tahun',
|
||||
},
|
||||
bulan: {
|
||||
title: 'Bulan',
|
||||
},
|
||||
no_rekening: {
|
||||
title: 'No Rekening',
|
||||
},
|
||||
cif: {
|
||||
title: 'CIF',
|
||||
},
|
||||
nama_debitur: {
|
||||
title: 'Nama Debitur',
|
||||
render: (item, data) => {
|
||||
const nama = data.nama_debitur || '-';
|
||||
return nama.length > 30 ? nama.substring(0, 30) + '...' : nama;
|
||||
},
|
||||
},
|
||||
nilai_agunan_formatted: {
|
||||
title: 'Nilai Agunan',
|
||||
render: (item, data) => {
|
||||
return data.nilai_agunan_formatted || '-';
|
||||
},
|
||||
},
|
||||
status_badge: {
|
||||
title: 'Status',
|
||||
render: (item, data) => {
|
||||
return data.status_badge || '-';
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
title: 'Aksi',
|
||||
render: (item, data) => {
|
||||
return data.actions || '';
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Inisialisasi DataTable
|
||||
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||
dataTable.showSpinner();
|
||||
|
||||
// Fungsi pencarian
|
||||
searchInput.addEventListener('input', function () {
|
||||
const searchValue = this.value.trim();
|
||||
dataTable.search(searchValue, true);
|
||||
});
|
||||
|
||||
// Filter berdasarkan tahun
|
||||
yearFilter.addEventListener('change', function() {
|
||||
const yearValue = this.value;
|
||||
dataTable.setParam('year', yearValue);
|
||||
dataTable.reload();
|
||||
});
|
||||
|
||||
// Filter berdasarkan bulan
|
||||
monthFilter.addEventListener('change', function() {
|
||||
const monthValue = this.value;
|
||||
dataTable.setParam('month', monthValue);
|
||||
dataTable.reload();
|
||||
});
|
||||
|
||||
// Filter berdasarkan status
|
||||
statusFilter.addEventListener('change', function() {
|
||||
const statusValue = this.value;
|
||||
dataTable.setParam('status', statusValue);
|
||||
dataTable.reload();
|
||||
});
|
||||
|
||||
// Export Excel functionality
|
||||
document.getElementById('export-excel').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Build export URL with current filters
|
||||
const params = new URLSearchParams();
|
||||
if (searchInput.value) params.append('search', searchInput.value);
|
||||
if (yearFilter.value) params.append('year', yearFilter.value);
|
||||
if (monthFilter.value) params.append('month', monthFilter.value);
|
||||
if (statusFilter.value) params.append('status', statusFilter.value);
|
||||
|
||||
const exportUrl = '{{ route("slik.export") }}?' + params.toString();
|
||||
window.open(exportUrl, '_blank');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -816,5 +816,24 @@ Breadcrumbs::for('bucok.show', function (BreadcrumbTrail $trail, $bucok) {
|
||||
$trail->push('Detail Bucok #' . $bucok->nomor_tiket);
|
||||
});
|
||||
|
||||
// Breadcrumb untuk SLIK
|
||||
Breadcrumbs::for('slik', function (BreadcrumbTrail $trail) {
|
||||
$trail->push('Data SLIK', route('slik.index'));
|
||||
});
|
||||
|
||||
Breadcrumbs::for('slik.index', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('slik');
|
||||
});
|
||||
|
||||
Breadcrumbs::for('slik.show', function (BreadcrumbTrail $trail, $slik) {
|
||||
$trail->parent('slik');
|
||||
$trail->push('Detail SLIK #' . $slik->id);
|
||||
});
|
||||
|
||||
Breadcrumbs::for('slik.import-form', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('slik');
|
||||
$trail->push('Import Data SLIK');
|
||||
});
|
||||
|
||||
// add andy
|
||||
require __DIR__ . '/breadcrumbs_registrasi.php';
|
||||
|
||||
@@ -6,6 +6,7 @@ use Modules\Lpj\Http\Controllers\SLAController;
|
||||
use Modules\Lpj\Http\Controllers\KJPPController;
|
||||
use Modules\Lpj\Http\Controllers\MemoController;
|
||||
use Modules\Lpj\Http\Controllers\BucokController;
|
||||
use Modules\Lpj\Http\Controllers\SlikController;
|
||||
use Modules\Lpj\Http\Controllers\TeamsController;
|
||||
use Modules\Lpj\Http\Controllers\RegionController;
|
||||
use Modules\Lpj\Http\Controllers\ResumeController;
|
||||
@@ -801,6 +802,18 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::post('/import', [BucokController::class, 'import'])->name('import');
|
||||
Route::get('/export', [BucokController::class, 'export'])->name('export');
|
||||
});
|
||||
|
||||
// Route untuk SLIK
|
||||
Route::prefix('slik')->name('slik.')->group(function () {
|
||||
Route::get('/', [SlikController::class, 'index'])->name('index');
|
||||
Route::get('/datatables', [SlikController::class, 'dataForDatatables'])->name('datatables');
|
||||
Route::get('/{id}', [SlikController::class, 'show'])->name('show');
|
||||
Route::post('/import', [SlikController::class, 'import'])->name('import');
|
||||
Route::get('/import-form', [SlikController::class, 'importForm'])->name('import-form');
|
||||
Route::get('/download-template', [SlikController::class, 'downloadTemplate'])->name('download-template');
|
||||
Route::get('/export', [SlikController::class, 'export'])->name('export');
|
||||
Route::post('/truncate', [SlikController::class, 'truncate'])->name('truncate');
|
||||
});
|
||||
});
|
||||
|
||||
require __DIR__ . '/registrasi.php';
|
||||
|
||||
Reference in New Issue
Block a user