- Tambah routing, breadcrumbs, menu, dan views (index + detail) - Controller: index/show, datatables (filter multi-kolom, sorting, pagination), impor Excel (transaksi + logging) - Import: updateOrCreate by nomor_tiket, normalisasi tanggal & numerik, statistik impor - Migrasi: semua kolom bisnis → string untuk konsistensi input Excel; nomor_tiket unique + index - UX: DataTable dengan filter (tahun, bulan, cost center, status), tombol import, detail tiket BREAKING CHANGE: - Semua kolom bisnis kini bertipe string → perlu sesuaikan casts di model Bucok & filter tanggal/numerik di controller
342 lines
12 KiB
PHP
342 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Modules\Lpj\Imports;
|
|
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Maatwebsite\Excel\Concerns\ToCollection;
|
|
use Maatwebsite\Excel\Concerns\WithStartRow;
|
|
use Maatwebsite\Excel\Concerns\WithValidation;
|
|
use Maatwebsite\Excel\Concerns\WithBatchInserts;
|
|
use Maatwebsite\Excel\Concerns\WithChunkReading;
|
|
use Modules\Lpj\Models\Bucok;
|
|
use Carbon\Carbon;
|
|
use Exception;
|
|
|
|
/**
|
|
* Kelas untuk mengimpor data Excel ke tabel bucoks
|
|
* Menggunakan Laravel Excel dengan validasi dan batch processing
|
|
* Data dimulai dari baris ke-5 tanpa header
|
|
*/
|
|
class BucokImport implements ToCollection, WithStartRow, WithValidation, WithBatchInserts, WithChunkReading
|
|
{
|
|
private $importedCount = 0;
|
|
private $skippedCount = 0;
|
|
private $createdCount = 0;
|
|
private $updatedCount = 0;
|
|
private $errors = [];
|
|
|
|
/**
|
|
* Menentukan baris mulai membaca data (baris ke-5)
|
|
*
|
|
* @return int
|
|
*/
|
|
public function startRow(): int
|
|
{
|
|
return 5;
|
|
}
|
|
|
|
/**
|
|
* Memproses koleksi data dari Excel
|
|
*
|
|
* @param Collection $collection
|
|
* @return void
|
|
*/
|
|
public function collection(Collection $collection)
|
|
{
|
|
DB::beginTransaction();
|
|
|
|
try {
|
|
foreach ($collection as $rowIndex => $row) {
|
|
// Log setiap baris yang diproses
|
|
Log::info('Processing Bucok import row', [
|
|
'row_number' => $rowIndex + 5, // +5 karena mulai dari baris 5
|
|
'row_data' => $row->toArray()
|
|
]);
|
|
|
|
// Konversi row ke array dengan indeks numerik
|
|
$rowArray = $row->toArray();
|
|
|
|
// Skip baris kosong
|
|
if (empty(array_filter($rowArray))) {
|
|
continue;
|
|
}
|
|
|
|
// Validasi data baris
|
|
$mappedData = $this->mapRowToBucok($rowArray, $rowIndex + 5);
|
|
|
|
// Update atau create berdasarkan nomor_tiket
|
|
if (!empty($mappedData['nomor_tiket'])) {
|
|
// Update atau create berdasarkan nomor_tiket
|
|
$bucok = Bucok::updateOrCreate(
|
|
['nomor_tiket' => $mappedData['nomor_tiket']], // Kondisi pencarian
|
|
array_merge($mappedData, ['updated_by' => auth()->id()]) // Data yang akan diupdate/create
|
|
);
|
|
|
|
// Log dan tracking apakah data di-update atau di-create
|
|
if ($bucok->wasRecentlyCreated) {
|
|
$this->createdCount++;
|
|
Log::info('Bucok created successfully', [
|
|
'row_number' => $rowIndex + 5,
|
|
'nomor_tiket' => $mappedData['nomor_tiket'],
|
|
'action' => 'created'
|
|
]);
|
|
} else {
|
|
$this->updatedCount++;
|
|
Log::info('Bucok updated successfully', [
|
|
'row_number' => $rowIndex + 5,
|
|
'nomor_tiket' => $mappedData['nomor_tiket'],
|
|
'action' => 'updated'
|
|
]);
|
|
}
|
|
}
|
|
|
|
$this->importedCount++;
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
// Log summary
|
|
Log::info('Bucok import completed', [
|
|
'imported' => $this->importedCount,
|
|
'skipped' => $this->skippedCount,
|
|
'total_errors' => count($this->errors)
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
DB::rollback();
|
|
Log::error('Bucok import failed', ['error' => $e->getMessage()]);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mapping data Excel berdasarkan indeks kolom ke struktur model Bucok
|
|
* Kolom dimulai dari indeks 0 (A=0, B=1, C=2, dst.)
|
|
*
|
|
* @param array $row
|
|
* @param int $rowNumber
|
|
* @return array
|
|
*/
|
|
private function mapRowToBucok(array $row, int $rowNumber): array
|
|
{
|
|
return [
|
|
'no' => $row[0] ?? null, // Kolom A
|
|
'tanggal' => !empty($row[1]) ? $this->parseDate($row[1]) : null, // Kolom B
|
|
'bulan' => $row[2] ?? null, // Kolom C
|
|
'tahun' => $row[3] ?? null, // Kolom D
|
|
'tanggal_penuh' => !empty($row[4]) ? $this->parseDate($row[4]) : null, // Kolom E
|
|
'nomor_categ' => $row[5] ?? null, // Kolom F
|
|
'coa_summary' => $row[6] ?? null, // Kolom G
|
|
'nomor_coa' => $row[7] ?? null, // Kolom H
|
|
'nama_coa' => $row[8] ?? null, // Kolom I
|
|
'nomor_tiket' => $row[9] ?? null, // Kolom J - Auto-generate jika kosong
|
|
'deskripsi' => $row[10] ?? null, // Kolom K
|
|
'nominal' => $this->parseNumeric($row[11] ?? 0), // Kolom L
|
|
'penyelesaian' => $row[12] ?? 'Belum Selesai', // Kolom M
|
|
'umur_aging' => $this->parseNumeric($row[13] ?? 0), // Kolom N
|
|
'cost_center' => $row[14] ?? null, // Kolom O
|
|
'nama_sub_direktorat' => $row[15] ?? null, // Kolom P
|
|
'nama_direktorat_cabang' => $row[16] ?? null, // Kolom Q
|
|
'tanggal_penyelesaian' => !empty($row[17]) ? $this->parseDate($row[17]) : null, // Kolom R
|
|
'nominal_penyelesaian' => $this->parseNumeric($row[18] ?? 0), // Kolom S
|
|
'nominal_berjalan' => $this->parseNumeric($row[19] ?? 0), // Kolom T
|
|
'amortisasi_berjalan' => $this->parseNumeric($row[20] ?? 0), // Kolom U
|
|
'sistem_berjalan' => $this->parseNumeric($row[21] ?? 0), // Kolom V
|
|
'lainnya_berjalan' => $this->parseNumeric($row[22] ?? 0), // Kolom W
|
|
'nominal_gantung' => $this->parseNumeric($row[23] ?? 0), // Kolom X
|
|
'aset_gantung' => $this->parseNumeric($row[24] ?? 0), // Kolom Y
|
|
'keterangan_gantung' => $row[25] ?? null, // Kolom Z
|
|
'lainnya_satu' => $row[26] ?? null, // Kolom AA
|
|
'lainnya_dua' => $row[27] ?? null, // Kolom AB
|
|
'created_by' => auth()->id(),
|
|
'updated_by' => auth()->id()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Parse tanggal dari berbagai format
|
|
*
|
|
* @param mixed $dateValue
|
|
* @return Carbon|null
|
|
*/
|
|
private function parseDate($dateValue)
|
|
{
|
|
if (empty($dateValue)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Jika berupa angka Excel date serial
|
|
if (is_numeric($dateValue)) {
|
|
return Carbon::createFromFormat('Y-m-d', \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($dateValue)->format('Y-m-d'));
|
|
}
|
|
|
|
// Jika berupa string tanggal
|
|
return Carbon::parse($dateValue);
|
|
} catch (Exception $e) {
|
|
Log::warning('Failed to parse date', ['value' => $dateValue, 'error' => $e->getMessage()]);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse nilai numerik dari berbagai format
|
|
*
|
|
* @param mixed $numericValue
|
|
* @return float
|
|
*/
|
|
private function parseNumeric($numericValue): float
|
|
{
|
|
if (empty($numericValue)) {
|
|
return 0;
|
|
}
|
|
|
|
// Hapus karakter non-numerik kecuali titik dan koma
|
|
$cleaned = preg_replace('/[^0-9.,\-]/', '', $numericValue);
|
|
|
|
// Ganti koma dengan titik untuk decimal
|
|
$cleaned = str_replace(',', '.', $cleaned);
|
|
|
|
return (float) $cleaned;
|
|
}
|
|
|
|
/**
|
|
* Validasi data yang sudah dimapping
|
|
*
|
|
* @param array $data
|
|
* @return \Illuminate\Validation\Validator
|
|
*/
|
|
private function validateMappedData(array $data)
|
|
{
|
|
return Validator::make($data, [
|
|
'no' => 'nullable|integer',
|
|
'tanggal' => 'nullable|date',
|
|
'bulan' => 'nullable|integer|between:1,12',
|
|
'tahun' => 'nullable|integer|min:2000|max:2099',
|
|
'tanggal_penuh' => 'nullable|date',
|
|
'nomor_categ' => 'nullable|string|max:50',
|
|
'coa_summary' => 'nullable|string|max:255',
|
|
'nomor_coa' => 'nullable|string|max:50',
|
|
'nama_coa' => 'nullable|string|max:255',
|
|
'nomor_tiket' => 'nullable|string|max:50',
|
|
'deskripsi' => 'nullable|string',
|
|
'nominal' => 'nullable|numeric|min:0',
|
|
'penyelesaian' => 'nullable|in:Selesai,Belum Selesai,Dalam Proses',
|
|
'umur_aging' => 'nullable|integer|min:0',
|
|
'cost_center' => 'nullable|string|max:100',
|
|
'nama_sub_direktorat' => 'nullable|string|max:255',
|
|
'nama_direktorat_cabang' => 'nullable|string|max:255',
|
|
'tanggal_penyelesaian' => 'nullable|date',
|
|
'nominal_penyelesaian' => 'nullable|numeric|min:0',
|
|
'nominal_berjalan' => 'nullable|numeric|min:0',
|
|
'amortisasi_berjalan' => 'nullable|numeric|min:0',
|
|
'sistem_berjalan' => 'nullable|numeric|min:0',
|
|
'lainnya_berjalan' => 'nullable|numeric|min:0',
|
|
'nominal_gantung' => 'nullable|numeric|min:0',
|
|
'aset_gantung' => 'nullable|numeric|min:0',
|
|
'keterangan_gantung' => 'nullable|string',
|
|
'lainnya_satu' => 'nullable|string',
|
|
'lainnya_dua' => 'nullable|string'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Aturan validasi untuk seluruh file Excel (tidak digunakan karena tanpa header)
|
|
*
|
|
* @return array
|
|
*/
|
|
public function rules(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Ukuran batch untuk insert
|
|
*
|
|
* @return int
|
|
*/
|
|
public function batchSize(): int
|
|
{
|
|
return 100;
|
|
}
|
|
|
|
/**
|
|
* Ukuran chunk untuk membaca file
|
|
*
|
|
* @return int
|
|
*/
|
|
public function chunkSize(): int
|
|
{
|
|
return 100;
|
|
}
|
|
|
|
/**
|
|
* Mendapatkan jumlah data yang berhasil diimpor
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getImportedCount(): int
|
|
{
|
|
return $this->importedCount;
|
|
}
|
|
|
|
/**
|
|
* Mendapatkan jumlah data yang berhasil dibuat
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getCreatedCount(): int
|
|
{
|
|
return $this->createdCount;
|
|
}
|
|
|
|
/**
|
|
* Mendapatkan jumlah data yang berhasil diupdate
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getUpdatedCount(): int
|
|
{
|
|
return $this->updatedCount;
|
|
}
|
|
|
|
/**
|
|
* Mendapatkan statistik lengkap import
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getImportStatistics(): array
|
|
{
|
|
return [
|
|
'total_processed' => $this->importedCount,
|
|
'created' => $this->createdCount,
|
|
'updated' => $this->updatedCount,
|
|
'skipped' => $this->skippedCount,
|
|
'errors' => count($this->errors)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Mendapatkan jumlah data yang dilewati
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getSkippedCount(): int
|
|
{
|
|
return $this->skippedCount;
|
|
}
|
|
|
|
/**
|
|
* Mendapatkan daftar error yang terjadi
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getErrors(): array
|
|
{
|
|
return $this->errors;
|
|
}
|
|
}
|