Compare commits

...

101 Commits

Author SHA1 Message Date
Daeng Deni Mardaeni
d9d8eaafcd fix(calc): Fallback hitung likuidasi dari persen x Total NPW
- Jika mig_mst_lpj_tot_nilai_likuidasi kosong, hitung likuidasi = (lpjData['likuidasi']/100) x Total Nilai Pasar Wajar
- Membersihkan input persen (menghapus %/spasi dan mengganti koma menjadi titik)
- Menormalkan Total NPW dari format Rupiah (menghapus Rp, titik, koma) ke angka murni
2025-12-04 11:22:08 +07:00
Daeng Deni Mardaeni
f055cd5573 🧩 fix(print): Perbaiki pembacaan array hub_cadeb & hapus debug
- Memperbaiki logika penentuan status 'sesuai'/'tidak sesuai' dengan membaca key dari array hub_cadeb
- Mengambil label dari array sesuai key yang tersedia dan memberikan fallback aman jika key tidak ada
- Menangani kasus ketika hub_cadeb berupa string ('sesuai'/'tidak sesuai') agar tetap ada output
2025-12-04 11:21:34 +07:00
Daeng Deni Mardaeni
aee8fab832 📏 feat(ui): Tambah auto-suffix m/km pada input jarak lingkungan
- Menambahkan class measure-input dan atribut data-unit pada:
  - jarak_jalan_utama (km)
  - jarak_cbd_point (km)
- Menambahkan script untuk:
  - Membatasi input hanya angka dan koma
  - Menghapus suffix saat fokus dan input
  - Menambahkan suffix otomatis saat blur
  - Menghapus suffix saat submit agar backend menerima angka bersih
- Menyediakan cara mengubah unit ke 'm' dengan mengganti data-unit pada input
2025-12-04 11:20:37 +07:00
Daeng Deni Mardaeni
a72cbe4bd9 🧩 fix(print): Perbaiki pembacaan array hub_cadeb & hapus debug
- Memperbaiki logika penentuan status 'sesuai'/'tidak sesuai' dengan membaca key dari array hub_cadeb
- Mengambil label dari array sesuai key yang tersedia dan memberikan fallback aman jika key tidak ada
- Menangani kasus ketika hub_cadeb berupa string ('sesuai'/'tidak sesuai') agar tetap ada output
2025-12-03 21:30:59 +07:00
Daeng Deni Mardaeni
ea09e8161c 🐛 fix(print): Perbaiki akses offset array dan format angka LPJ
- Memperbaiki akses nilai hubungan penghuni dengan debitur:
  - Jika hub_cadeb_penghuni bertipe array, gunakan key terpilih dari hub_cadeb_penghuni_selected atau hub_cadeb_penghuni_value untuk mengambil label yang benar
  - Jika hub_cadeb_penghuni bertipe scalar/string, tampilkan nilainya langsung
  - Menghindari error “Cannot access offset of type array on array” ketika nilai dan indeks bercampur
- Menormalkan format angka agar aman dari nilai null dan tipe campuran:
  - Total Nilai Pasar Wajar: number_format((float) ($lpjData['total_nilai_pasar_wajar'] ?? $totalNilaiPasarWajar ?? 0), 0, ',', '.')
  - Total Nilai Likuidasi: number_format($permohonan_migrasi->mig_mst_lpj_tot_nilai_likuidasi ?? 0, 0, ',', '.')
- Meningkatkan ketahanan tampilan:
  - Mengurangi potensi undefined index dan memastikan fallback nilai aman ketika data tidak tersedia
  - Menjaga konsistensi output angka di bagian ringkasan nilai
2025-12-03 15:10:12 +07:00
Daeng Deni Mardaeni
f051fa9507 feat(penilai/print-out): generasi baris nilai dinamis, tambah satuan jarak
- Tambah sufiks satuan m pada jarak_jalan_utama di resources/views/penilai/components/analisa/lingkungan.blade.php agar konsisten menampilkan satuan meter
- Refactor perhitungan tabel nilai di resources/views/penilai/components/print-out-sederhana.blade.php menjadi iterasi dinamis menggunakan labelNilai untuk menampilkan baris Tanah/Bangunan berdasarkan ketersediaan data luas_* dan nilai_*_1/_2
- Refactor bagian nilai di resources/views/penilai/components/print-out-standar.blade.php untuk non-MIG ( !$permohonan->is_mig ) menjadi loop dinamis yang sama sehingga konsisten antara “sederhana” dan “standar”
- Pastikan akumulasi totalNilaiPasarWajar menggunakan nilai hasil format yang dibersihkan dari simbol dan pemisah ribuan, tetap menghitung dari nilai_*_2
- Jaga kompatibilitas existing: tetap menghormati npw_tambahan yang dirender setelah blok non-MIG
2025-11-26 17:38:17 +07:00
Daeng Deni Mardaeni
04a657252f feat(surveyor/print-out): tambah template cetak apartemen-kantor, rapikan UI, dan perbaiki binding nilai
- Tambah file resources/views/surveyor/components/print-out/apartemen-kantor.blade.php untuk layout cetak “Analisa Unit” yang menampilkan radio luas unit dan daftar checkbox untuk bentuk, kondisi, posisi, dan view
- Revisi resources/views/surveyor/components/print-out/apartement-kantor.blade.php guna menyeragamkan struktur tabel dan gaya cetak agar konsisten
- Normalisasi urutan dan konsistensi kelas Tailwind di resources/views/surveyor/components/apartemen-kantor.blade.php (penyusunan flex , items-baseline , flex-wrap , gap-* , konsistensi form-label , checkbox , radio )
- Rapi penamaan dan penempatan elemen pesan error <em id="error-*"> dengan susunan kelas seragam
- Sesuaikan grid ( grid-cols-2 md:grid-cols-3 ) dan mt-2 untuk responsivitas yang lebih baik
- Perbaiki binding nilai luas_unit_tidak_sesuai agar mengikuti struktur forminspeksi['luas_unit']['tidak sesuai'] sehingga data konsisten pada saat old value dan penyimpanan ulang
- Tingkatkan keterbacaan markup: urutan atribut kelas diseragamkan, label dan input disusun lebih konsisten untuk aksesibilitas dan UI
2025-11-26 17:26:42 +07:00
Daeng Deni Mardaeni
fc6f18fea9 (validation): Tambahkan pattern validasi koordinat latitude dan longitude
Menambahkan pattern regex untuk validasi input koordinat geografis pada form surveyor untuk memastikan data yang dimasukkan memiliki format yang valid dan sesuai dengan rentang koordinat yang benar.

Perubahan yang dilakukan:

**File yang dimodifikasi:**
1. `Modules/Lpj/resources/views/surveyor/components/pembanding-tanah-bangunan-unit.blade.php`
   - Menambahkan pattern pada input latitude utama: `^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$`
   - Menambahkan pattern pada input longitude utama: `^-?(1[0-7]?\d(\.\d+)?|180(\.0+)?|[1-9]?\d(\.\d+)?)$`
   - Menambahkan pattern pada input latitude pembanding: `^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$`
   - Menambahkan pattern pada input longitude pembanding: `^-?(1[0-7]?\d(\.\d+)?|180(\.0+)?|[1-9]?\d(\.\d+)?)$`

2. `Modules/Lpj/resources/views/surveyor/components/pembanding-kendaraan.blade.php`
   - Menambahkan pattern pada input latitude utama: `^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$`
   - Menambahkan pattern pada input longitude utama: `^-?(1[0-7]?\d(\.\d+)?|180(\.0+)?|[1-9]?\d(\.\d+)?)$`
   - Menambahkan pattern pada input latitude pembanding: `^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$`
   - Menambahkan pattern pada input longitude pembanding: `^-?(1[0-7]?\d(\.\d+)?|180(\.0+)?|[1-9]?\d(\.\d+)?)$`

3. `Modules/Lpj/resources/views/surveyor/components/header.blade.php`
   - Menambahkan pattern pada input latitude: `^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$`
   - Menambahkan pattern pada input longitude: `^-?(1[0-7]?\d(\.\d+)?|180(\.0+)?|[1-9]?\d(\.\d+)?)$`
   - Memperbaiki duplikasi atribut `type="text"` pada input latitude

**Detail Pattern Validasi:**
- **Latitude**: Menerima nilai dari -90 sampai 90 derajat
  - Format: `-?([1-8]?\d(\.\d+)?|90(\.0+)?)`
  - Contoh valid: `-6.1234`, `0.123`, `90.0`, `-90.0`

- **Longitude**: Menerima nilai dari -180 sampai 180 derajat
  - Format: `-?(1[0-7]?\d(\.\d+)?|180(\.0+)?|[1-9]?\d(\.\d+)?)`
  - Contoh valid: `106.1234`, `0.123`, `180.0`, `-180.0`, `-106.1234`

**Manfaat:**
- Mencegah input koordinat yang tidak valid
- Standarisasi format data koordinat di seluruh form surveyor
- Memastikan data yang tersimpan memiliki presisi yang sesuai
- Meningkatkan akurasi data untuk keperluan pemetaan dan analisis lokasi

**Impact:**
- Validasi client-side akan menolak input koordinat yang tidak sesuai format
- Pesan error bawaan browser akan muncul jika format tidak valid
- Tidak ada perubahan pada logic backend (validasi tetap ada di server-side)
- Kompatibel dengan semua browser modern yang support HTML5 pattern
2025-11-20 14:36:05 +07:00
Daeng Deni Mardaeni
8c60320532 (Roles): Tambahkan role EO Appraisal ke berbagai komponen LPJ
## Perubahan Utama:
- 🔧 **Role Management**: Menambahkan role 'EO Appraisal' ke berbagai komponen untuk akses tombol simpan
- 🐛 **Fix Kondisi**: Memperbaiki logika kondisi untuk data MIG (Migrasi) di print-out-sederhana
- 💅 **UI Enhancement**: Konversi table ke div untuk bagian keterangan dengan format yang lebih rapi
- 🎨 **Code Style**: Fix indentation dan whitespace di PermohonanController

## Detail Perubahan:

### File yang Dimodifikasi:

1. **`resources/views/penilai/components/call-report.blade.php`** (Line 507):
   - Menambahkan `'EO Appraisal'` ke array role yang memiliki akses tombol simpan
   - Sebelumnya: `['senior-officer', 'surveyor', 'administrator']`
   - Sekarang: `['senior-officer', 'surveyor', 'administrator','EO Appraisal']`

2. **`resources/views/penilai/components/lpj-sederhana-standar.blade.php`** (Line 47):
   - Menambahkan `'EO Appraisal'` dan `'penilai'` ke role yang dapat menyimpan data
   - Memperluas akses untuk role penilai dan EO Appraisal

3. **`resources/views/penilai/components/memo.blade.php`** (Line 318):
   - Menambahkan `'EO Appraisal'` ke role yang dapat menyimpan memo
   - Memastikan konsistensi akses antar komponen

4. **`resources/views/penilai/components/print-out-sederhana.blade.php`**:
   - **Line 558-580**: Memperbaiki urutan kondisi pengecekan data MIG
     - Memindahkan fallback data tanah dan bangunan ke atas
     - Menambahkan kondisi `@if(!$permohonan->is_mig)` untuk data non-MIG
     - Menjaga data `npw_tambahan` tetap diproses setelahnya
   - **Line 647-700**: Konversi table ke div untuk bagian keterangan
     - Mengganti struktur `<table><tr><td>` menjadi `<div>` dengan styling yang lebih rapi
     - Menambahkan handling untuk berbagai format data (array, string, quoted)
     - Implementasi bullet points yang lebih konsisten
     - Menambahkan fallback message "Tidak ada catatan khusus"

5. **`resources/views/penilai/components/rap-penilai.blade.php`** (Line 72 & 115):
   - Menambahkan `'EO Appraisal'` ke role yang dapat menyimpan data RAP
   - Fix whitespace yang tidak perlu

6. **`resources/views/penilai/components/signature-approval.blade.php`** (Line 117):
   - Fix whitespace dan formatting

7. **`app/Http/Controllers/PermohonanController.php`** (Line 55):
   - Fix indentation yang tidak konsisten
   - Memperbaiki format kode untuk better readability
2025-11-18 17:28:04 +07:00
Daeng Deni Mardaeni
34709b0f8f 🐛 fix(lpj): Perbaikan format Rupiah, role access, dan validasi data
## Ringkasan
Melakukan perbaikan pada helper format Rupiah, akses role user, validasi data MIG, serta penyesuaian tampilan laporan dan dokumentasi.

## Perubahan Detail

### 🔧 Helper Function
**app/Helpers/Lpj.php**:
- Menambahkan parameter opsional `withSymbol` pada fungsi `formatRupiah()` untuk kontrol simbol Rp
- Menambahkan handling untuk menghapus titik (.) dari input number sebelum proses
- Memperbaiki return value untuk null/empty string sesuai parameter `withSymbol`
- Mengganti `str_pad()` dengan `sprintf()` untuk generate random number (lebih efisien)

### 🛠️ Service Layer
**app/Services/PreviewLaporanService.php**:
- Memperbaiki validasi data MIG dengan menambahkan pengecekan `is_mig` flag
- Menambahkan null safety pada property `mig_mst_lpj_tot_nilai_pasar`
- Memperbaiki kondisi logic untuk memo dan validasi nilai pasar

### 🎨 View Components
**resources/views/component/print-out-dokument.blade.php**:
- Memperbaiki syntax Blade dari `@isset` menjadi `isset()` yang lebih proper

**resources/views/debitur/components/debitur.blade.php**:
- Memperbaiki role checking dari `hasRole()` menjadi `hasAnyRole()` untuk multiple roles

**resources/views/debitur/index.blade.php**:
- Menambahkan role 'admin' pada kondisi edit dan delete actions
- Memperbaiki permission checking untuk administrator dan admin

**resources/views/laporan/index.blade.php**:
- Menyederhanakan logic tombol laporan dan resume
- Menghapus logic role-based yang kompleks untuk tombol laporan
- Memperbaiki route URL untuk print-out laporan
- Menghapus function `generateLaporanButton()` yang tidak digunakan

**resources/views/penilai/components/lpj-sederhana-standar.blade.php**:
- Menambahkan role 'penilai' pada permission tombol simpan

**resources/views/penilai/components/print-out-sederhana.blade.php**:
- Memperbaiki tampilan data dokumen dengan menambahkan kolom nomor dokumen
- Mengganti `number_format()` dengan `formatRupiah()` untuk konsistensi format
- Menambahkan fallback untuk data tanah dan bangunan ketika `npw_tambahan` tidak tersedia
- Memperbaiki perhitungan total nilai pasar wajar dengan proper parsing
- Memperbaiki format tampilan nilai likuidasi
- Memperbaiki struktur HTML tabel untuk dokumentasi

**resources/views/penilai/components/signature-approval.blade.php**:
- Memperbaiki route dan parameter untuk approval signature

**resources/views/permohonan/index.blade.php**:
- Menambahkan role 'admin' pada permission actions
2025-11-15 17:01:04 +07:00
Daeng Deni Mardaeni
b2cfffc5d5 feat(lpj): Tambah peran admin, sesuaikan alur status, dan peningkatan UI
Ringkas: perluas akses untuk peran admin , rapikan alur status permohonan/pembayaran/SPK, pindahkan sumber LPJ ke relasi penilai, perbaiki parsing/formatting rupiah, dan tambah konten cetak “Catatan yang Perlu Diperhatikan”.

- Perizinan & akses

  - Izinkan admin melewati filter cabang pada listing Debitur dan Permohonan.
  - Tambah peran admin pada konfigurasi module.json di beberapa menu/fitur.
  - Izinkan admin membuat Debitur dan mengakses aksi yang sebelumnya eksklusif untuk administrator dan pemohon-ao .
- Alur bisnis & status

  - Ubah status setelah proses pembatalan/penanganan pembayaran dari done menjadi proses-laporan agar konsisten dengan alur pelaporan.
  - Nonaktifkan blokir navigasi saat status proses-laporan di PenilaiController (redirect/JSON error dikomentari) agar proses lanjutan tetap bisa diakses bila diperlukan.
  - Setelah generate SPK, set Permohonan.status dan PenawaranTender.status menjadi registrasi-final untuk menandai finalisasi registrasi.
  - Pada pembuatan permohonan, jika pengguna admin , tetapkan status = preregister untuk proses pra-registrasi.
  - Hapus set default branch_id dari PermohonanRequest sehingga pengisian cabang dilakukan eksplisit melalui form (terutama untuk admin).
- Data LPJ & referensi relasi

  - Sumber data LPJ dipindah dari permohonan->penilaian->lpj ke permohonan->penilai->lpj baik di controller ( PenilaianController ) maupun view ( penilaian/otorisator/show.blade.php ) untuk menyesuaikan struktur relasi terbaru.
- Dokumen pembayaran

  - Ubah cara deteksi “Bukti Bayar” dari documents menjadi dokumenjaminan dan ambil detail berdasarkan name = 'Bukti Bayar' , lalu gunakan dokumen_jaminan yang terenkode JSON sebagai sumber pemrosesan berikutnya.
  - Pada UI approval pembayaran, benahi rendering nominal_bayar (hindari pemutusan baris) dan tampilkan tombol otorisator jika status_bayar !== 'sudah_bayar' || !approve_bayar .
- UI & formatting rupiah

  - Perbaiki fungsi calculateTotal() pada form-penilai.blade.php agar parsing angka mendukung pemisah ribuan titik dan desimal koma, serta formatting konsisten dengan id-ID .
  - Aktifkan kembali fallback tampilan status utama di penilai/index.blade.php .
  - Tambahkan blok “Catatan yang Perlu Diperhatikan” pada print-out-sederhana.blade.php , mendukung input string/array dan memformat poin dengan awalan “- ”.
Perubahan berkas (ringkas):

- app/Http/Controllers/DebitureController.php : tambah peran admin pada pengecualian filter cabang.
- app/Http/Controllers/PembayaranController.php : set status = 'proses-laporan' dalam proses terkait pembayaran.
- app/Http/Controllers/PenilaiController.php : longgarkan blokir saat proses-laporan (redirect/JSON error dikomentari).
- app/Http/Controllers/PenilaianController.php : gunakan permohonan->penilai->lpj untuk menghitung NPW.
- app/Http/Controllers/PermohonanController.php :
  - set status = 'preregister' untuk user admin saat create,
  - tambah peran admin pada pengecualian filter cabang,
  - ubah pencarian “Bukti Bayar” ke dokumenjaminan dan gunakan dokumen_jaminan (JSON).
- app/Http/Controllers/SpkController.php : set status = 'registrasi-final' pada Permohonan dan PenawaranTender setelah generate SPK.
- app/Http/Requests/PermohonanRequest.php : hilangkan set default branch_id .
- module.json : tambahkan admin pada beberapa daftar roles .
- resources/views/component/form-penilai.blade.php : dukungan parsing/formatting rupiah dengan pemisah lokal.
- resources/views/debitur/components/debitur.blade.php : perluas akses cabang untuk admin di form Debitur.
- resources/views/debitur/index.blade.php : izinkan admin membuat Debitur.
- resources/views/pembayaran/approval.blade.php : perbaiki render nominal dan visibilitas tombol otorisator.
- resources/views/penilai/components/print-out-sederhana.blade.php : tambah bagian “Catatan yang Perlu Diperhatikan”.
- resources/views/penilai/index.blade.php : gunakan data.status sebagai fallback tampilan status.
- resources/views/penilaian/otorisator/show.blade.php : konsisten gunakan permohonan->penilai->lpj .
- resources/views/permohonan/form.blade.php : penataan ulang kelas Tailwind dan penambahan field Cabang untuk administrator / admin .
2025-11-12 17:42:45 +07:00
Daeng Deni Mardaeni
89329de198 🐛 fix(lpj-views): Amankan akses penilai dan sederhanakan informasi analisa
- Laporan index: gunakan optional chaining untuk akses aman properti penilai

  - Ubah if (data.penilai.resume) menjadi if (data.penilai?.resume) agar tidak error saat penilai null/undefined.
  - Perkuat kondisi pemanggilan generateLaporanButton(...) dengan ekspresi: data.penilai?.type_penilai == 'resume' && !data.penilai?.resume ? type : typePenilaian .
  - Efek: mencegah runtime error (TypeError) saat data belum lengkap, memastikan tombol "Resume" dan "Laporan" hanya muncul pada kondisi valid.
  - Berlaku pada role pemohon-ao dan role lainnya (dua blok kondisi kini seragam dan aman).
- Analisa penilai: sederhanakan tampilan dengan menghapus blok keterangan fakta

  - Hapus tabel "Catatan yang Perlu Diperhatikan" yang merender $forminspeksi['fakta']['keterangan'] (string/array).
  - Alasan: menghindari duplikasi, ketidakpastian tipe (string/array), dan potensi inkonsistensi formatting; konten penting tetap tersedia di bagian lain laporan.
Komentar level fungsi (referensi terkait):

- generateLaporanButton(data, dokumenID, inspeksiId, jenisJaminanID, typeOrResume, typePenilaian)
  - Peran: merender tombol "Laporan" berdasarkan status pembayaran dan kondisi penilai.
  - Perubahan: tidak mengubah implementasi fungsi; memperkuat logika pemanggilan via optional chaining agar parameter typeOrResume ditentukan aman.
2025-11-12 16:20:32 +07:00
Daeng Deni Mardaeni
70dda16699 feat(lpj-seeders): Inisialisasi master referensi, migrasi MIG, dan SQL seeding
- Menata LpjDatabaseSeeder untuk orkestrasi batch, aktifkan MigrationGambarInspeksiSeeder .
- Migrasi domain MIG → LPJ lengkap dengan parseTimestamp , initializeErrorLog , logError .
- Tambah seeders MIG eksternal & tim penilai; normalisasi mapping checkTujuanPenilaian .
- Perluasan master: JFK009–JFK014, TP0007–TP00010, hubungan pemilik, KJPP; TeamsSeeder via SQL.
- MasterDataSurveyorSeeder eksekusi SQL referensi (20+ tabel) via DB::unprepared .
- Tambah puluhan SQL referensi (jenis, kondisi, sarana, posisi, spek, dll).
- Normalisasi data inspeksi (duplikasi key dinamis), serialisasi JSON rapi.
- Logging seragam ke app log + CSV error untuk audit trail.
2025-11-10 21:06:03 +07:00
Daeng Deni Mardaeni
db0b1f6cfc Merge remote-tracking branch 'composer/staging' into staging 2025-11-10 20:48:05 +07:00
Daeng Deni Mardaeni
c153990c52 feat(lpj): Perbaiki ekspor, pencarian, validasi tanggal, transaksi, logging, dan UI
- ActivityController: tambah default order `nomor_registrasi` desc; rapikan komentar & alur sorting
- LaporanHasilPenilaianJaminanInternalExternalController: pencarian debitur case-insensitive via `LOWER(name)`; normalisasi angka LPJ (luas_tanah, nilai_bangunan, likuidasi, NPW); perapihan spacing
- LaporanPenilaiJaminanController: validasi `start_date`/`end_date` dan buat nama file ekspor dinamis via `createNameLaporan`; gunakan `LaporanPenilaiJaminanExport($request)`; helper `getBranchId`
- LaporanPenilaianJaminanController: standarisasi respons JSON datatables; tambah `export(Request)` dengan nama `laporan_penilaian_jaminan_<start>_<end>.xlsx`
- NilaiPlafondController: bungkus proses datatables dalam transaksi DB (commit/rollback) dengan try/catch; tambah logging info/error; rapikan dan standarisasi respons JSON
- PenilaiController: map data pembanding ke `pembanding1/2/3`; tambah `print_out_laporan` dan `showLaporanInspeksi` (penentuan route back); stub `showInspectionReportReview`; perapihan minor
- Views debitur/components/debitur: perbaiki closing textarea, konsistensi event listener `function ()`, rapikan markup error
- Views debitur/components/jaminan: fallback `dokumen_nomor[$index] ?? ''` untuk hindari undefined index
- Views laporan/index: akses aman dengan optional chaining `?.`, fallback tanggal pada `formatDate`, akses `nilaiPlafond` aman
- Views laporan-penilai-jaminan/index + show: JS toggle tab (Laporan vs Hasil Inspeksi), CSS `hidden-tab`, gaya floating button, perapihan
- Views debitur/index: rapikan directive `@if` spacing pada tombol tambah
- Views noc/penyelesaian: perbaiki route key ke `noc.datatables.penyelesaian`
2025-11-10 20:47:56 +07:00
Daeng Deni Mardaeni
42d6e06a48 feat(lpj): perbaikan controller & view penilai, validasi, dan MIG toggle
- Controllers: DokumenJaminan tangani array kosong saat diff legalitas.
- Controllers: Laporan sederhanakan query status list laporan.
- Controllers: PersetujuanPenawaran simpan NOC ke variabel $noc.
- Requests: DebitureRequest longgarkan 'nomor_rekening' max 50.
- Views: Informasi & Lingkungan tampilkan array/string aman dan rapi.
- Views: Header rapikan CSS page-break, nonaktifkan blok PHP komentar.
- Views: Foto Jaminan/Lampiran tambah link unduh & perbaikan layout.
- Views: LPJ sederhana: tombol kondisional saat MIG, include analisa.
- Views: Print-out sederhana kirim parameter ke signature-approval.
2025-11-10 20:01:06 +07:00
Daeng Deni Mardaeni
2db123a386 🧩 feat(penilaian, otorisator): gunakan ambang NPW untuk approval dan rapikan UI
- Terapkan logika berbasis NPW (total_nilai_pasar_wajar) dari JSON LPJ untuk menentukan status approval pada PenilaianController (sekitar baris 517–533).
- Senior Officer: status selesai jika NPW ≤ 1.000.000.000, selain itu masuk proses-laporan.
- EO Appraisal: status selesai jika NPW ≤ 5.000.000.000, selain itu masuk proses-laporan.
- DD Appraisal: tetap status selesai.
- Ambil NPW dari json_decode($permohonan->penilaian->lpj) dan hilangkan pemisah ribuan dengan str_replace('.', '', $npw) .
- Tampilkan tombol Otorisator di tampilan resources/views/penilaian/otorisator/show.blade.php berdasarkan ambang NPW:
  - EO: tombol muncul jika NPW > 1.000.000.000.
  - DD: tombol muncul jika NPW > 5.000.000.000.
- Rapikan kelas Tailwind pada komponen card, header, body, grid dan fleksibelitas layout agar konsisten (reorder class, penyesuaian urutan dan semantik).
- Perbaiki urutan icon dan teks pada tombol aksi agar konsisten (mis. mr-2 ki-filled ki-eye ).
2025-11-10 17:27:23 +07:00
Daeng Deni Mardaeni
45f0d387fd feat(lpj): peningkatan model & service inspeksi, penawaran, preview
- Models: Inspeksi tambah 'mig_detail_data_jaminan' pada 'fillable'.
- Models: PersetujuanPenawaran ubah import User, hapus 3 field lama.
- Models: PersetujuanPenawaran tambah relasi 'region' (belongsTo Region).
- Services: PreviewLaporanService tambah 22 item kategori preview.
- Services: SaveFormInspesksiService refaktor parsing action & rules.
- Services: TypeLaporanService tambah helper alamat & lokasi wilayah.
- Services: TypeLaporanService tambahkan updateOrCreate untuk Penilai.
- Services: TypeLaporanService tambah getInspeksi dan stub updatePenilai.
- Cleanup: penataan kode, trimming, kurangi nested kondisi; mohon cek namespace.
2025-11-10 10:09:24 +07:00
Daeng Deni Mardaeni
7f9c58aabe feat(migration): Tambah kolom migrasi & penyesuaian nullable di beberapa tabel
- Tambah kolom mig_*, is_mig, processed_at, nomor_lpj di tabel debitur, permohonan, jaminan, pemilik_jaminan, inspeksi, dan penilai
- Ubah beberapa field (branch_id, user_id, cif, nomor_id) menjadi nullable untuk fleksibilitas migrasi
- Tambah kolom biaya (decimal(15,2)) pada tabel nilai_plafond
- Pastikan semua kolom baru memiliki mekanisme rollback (down method)
- Perbaikan kompatibilitas migrasi data LPJ lama ke skema baru
- Penyesuaian tipe data & default value sesuai kebutuhan integrasi
- Menambahkan komentar sumber data LPJ lama pada beberapa kolom
- Meningkatkan traceability melalui kolom processed_at
- Menandai data hasil migrasi dengan flag is_mig
- Menjaga integritas dan konsistensi antar tabel selama proses migrasi
2025-11-10 09:30:39 +07:00
Daeng Deni Mardaeni
3ce84b89b4 feat(daftar-pustaka): implementasi fitur Daftar Pustaka dengan peningkatan UI/UX & breadcrumb navigation
## 📋 Ringkasan
Implementasi penuh fitur Daftar Pustaka dengan peningkatan UI/UX, dukungan gesture swipe di PDF viewer mobile, serta integrasi breadcrumb untuk navigasi yang lebih intuitif.

## 🔄 Perubahan Utama
- app/Services/DaftarPustakaService.php
  • Refactor method getDaftarPustaka(), hapus handleUpload_()
  • Optimasi filtering & perbaiki format kode

- resources/views/daftar-pustaka/create.blade.php
  • Aktifkan breadcrumb navigation dengan {{ Breadcrumbs::render() }}

- resources/views/daftar-pustaka/index.blade.php
  • Konsolidasi class CSS, perbaikan flex & pagination styling

- resources/views/daftar-pustaka/show.blade.php
  • Tambah gesture swipe (touchstart, touchend) untuk PDF viewer
  • Implementasi handleSwipe() & threshold swipe 50px

- routes/breadcrumbs.php
  • Tambah route breadcrumbs daftar-pustaka (index, show, create)
2025-11-10 09:11:48 +07:00
Daeng Deni Mardaeni
1dd4f8167e 🧩 feat(noc-controller): perluas filter pencarian NOC dengan relasi permohonan, debitur, jenis penilaian, dan nomor tiket
Ringkasan perubahan:
- Tambah filter orWhereRelation('permohonan', 'nomor_registrasi', 'LIKE', ...) untuk mencocokkan nomor_registrasi dari permohonan.
- Tambah filter orWhereRelation('permohonan.debiture','name', 'LIKE', ...) agar pencarian mencakup nama debitur.
- Tambah filter orWhereRelation('permohonan.jenisPenilaian', 'name', 'LIKE', ...) untuk dukungan jenis penilaian terkait.
- Tambah filter orWhere('nomor_tiket', 'LIKE', ...) untuk pencarian berdasarkan nomor_tiket.
2025-11-10 08:39:00 +07:00
Daeng Deni Mardaeni
117b344857 git commit -m " refactor(helper): dokumentasi & peningkatan fungsi helper LPJ
## Ringkasan
Refaktor besar pada helper `Modules/Lpj/app/Helpers/Lpj.php` untuk meningkatkan keterbacaan, keamanan, dan maintainability melalui dokumentasi, logging, dan validasi input.

## Perubahan Utama
- Tambah `declare(strict_types=1)` dan type declarations di seluruh fungsi
- Tambah PHPDoc lengkap pada `formatTanggalIndonesia` & `formatRupiah`
- Tambah logging detail (awal fungsi, validasi, keberhasilan, dan kegagalan)
- Validasi input null/kosong serta error handling yang lebih aman
- Bungkus fungsi query DB dalam `DB::transaction` untuk konsistensi data
- Tambah fungsi baru:
  - `parsePembandingMigration()` → membersihkan & memformat data pembanding migrasi
  - `getFilePath()` → resolve path file internal/eksternal
  - `parseTimestamp()` → robust timestamp parser multi-format
- Fix minor linter issue: `strtotime(now())`, `pow(10,3)` → `(int) str_pad(...)`

## Dampak
- Semua fungsi kini memiliki dokumentasi dan validasi lengkap
- Logging terstruktur untuk memudahkan debugging di production
- Peningkatan keamanan dan kestabilan dengan type safety & transaksi DB
- Output lebih konsisten dan mudah dilacak"
2025-11-09 21:36:26 +07:00
Daeng Deni Mardaeni
0d5b6b1529 feat(lpj-module): tambah tampilan laporan inspeksi & refactor detail lokasi
Ringkasan:
- Menambahkan halaman hasil inspeksi dan tampilan cetak laporan.
- Mengekstrak komponen detail lokasi ke partial baru agar reusable.
- Menambahkan null-safe access dan perbaikan binding data di view.
- Merapikan tombol cetak dan navigasi agar konsisten antar halaman.

Perubahan utama:
1. activitydetail.blade.php → ubah tombol print jadi route, tambah null-safe user/branch.
2. detail-lokasi.blade.php (baru) → komponen reusable untuk detail lokasi dengan formatLabel & tanggal.
3. form-penilai.blade.php → refactor luas menggunakan match, hapus fungsi debug & Swal loading.
4. print-out-dokument.blade.php → gunakan partial lpj::component.detail-lokasi untuk detail lokasi.
5. show-laporan-inspeksi.blade.php (baru) → tab 'Laporan' & 'Hasil Inspeksi' + tombol cetak dan back.
6. print-out-sederhana / print-out-standar → penyesuaian tampilan & binding data.
7. signature-approval.blade.php → perbaikan layout area tanda tangan.
8. surveyor/components/* → normalisasi tampilan, validasi gambar, dan penyelarasan fakta/lingkungan.
9. routes/web.php → tambah dan ubah rute untuk laporan inspeksi dan cetak laporan.

Catatan:
- Tidak ada perubahan query database; semua modifikasi bersifat tampilan.
- Logging tambahan untuk observabilitas proses render laporan.
2025-11-09 21:19:58 +07:00
Daeng Deni Mardaeni
535be2cff3 feat(permohonan): otomasi PersetujuanPenawaran & NOC saat sudah dibayar, tambah validasi Bukti Bayar,
- PersetujuanPenawaran: ubah logika pembuatan menjadi firstOrCreate untuk mencegah duplikasi dan menjaga idempotensi.
- Validasi Bukti Bayar: wajib unggah dokumen “Bukti Bayar” saat status sudah_dibayar ; blokir proses jika dokumen tidak ditemukan.
- Simpan Bukti Bayar ke PersetujuanPenawaran: ambil dari koleksi dokumen jaminan, set ke field bukti_bayar .
- NOC otomatis: updateOrCreate NOC berdasarkan permohonan_id dan persetujuan_penawaran_id ; logging error bila gagal.
- Pengalihan aman: redirect terarah dengan pesan error pada kondisi validasi gagal atau kegagalan pembuatan NOC.
2025-11-08 11:45:10 +07:00
Daeng Deni Mardaeni
1a67eb2000 feat(penilai-resume): tambah fallback route 'Back' dan perluas akses 'penilai',
- Tambah fallback default 'index' untuk parameter type pada tombol Back agar tetap berfungsi saat request('header') tidak tersedia.
- Perluas akses tombol Simpan ke peran penilai dan sesuaikan logika disable saat status tertentu.
2025-11-06 14:34:57 +07:00
Daeng Deni Mardaeni
e8ef9c0932 feat(lpj): Perluas akses peran dan rapikan UI Penilai/Surveyor
- Tambah peran `senior-officer` pada konfigurasi module untuk memperluas akses.
- Rapikan penamaan dan urutan kelas Tailwind pada komponen Penilai & Surveyor.
- Perjelas label dan placeholder: “Pihak Bank (Selain Appraisal)” agar tidak ambigu.
- Perkuat navigasi Penilai: Back button juga tersedia untuk peran `penilai`.
2025-11-06 14:26:09 +07:00
Daeng Deni Mardaeni
81f165c9d0 🐛 fix(penilai): Selaraskan sumber data alamat di Resume dan Show
Ringkasan:
- Perbaiki pemanggilan `formatAlamat` agar menggunakan objek yang benar di halaman Penilai.
- Hindari ketidaksesuaian data alamat dengan mengganti referensi model yang tidak tepat.
- Dampak: tampilan alamat pada komponen Resume dan halaman Show kini akurat.
2025-11-05 17:05:31 +07:00
Daeng Deni Mardaeni
25011d1798 feat(lpj): Tingkatkan kontrol akses, query pembayaran, upload, dan UI
- Izinkan penghapusan dokumen jaminan untuk status 'order'/'revisi'
- Longgarkan guard admin sementara (comment response 403) pada aksi hapus
- Tambah transaksi DB saat hapus dokumen (begin/commit/rollback)
- Sesuaikan filter daftar pembayaran; hilangkan blok where `belum_bayar`/`jenis_penilaian=1`
- Tambah metode `handleUpload()` dengan `storeAs` ke disk `public`
- Rap-penilai: tambah tombol Back; peran 'penilai' diizinkan menyimpan
- Authorization: rapikan kelas Tailwind dan validasi keterangan saat revisi
- Persetujuan penawaran: gunakan optional chaining saat render nominal bayar
- Surveyor/tanah: gunakan `detail` alih-alih `details` saat hitung luas
- Komponen lampiran-dokumen: perbaiki atribut `textarea` dan penataan id
2025-11-05 16:46:52 +07:00
Daeng Deni Mardaeni
e773b82218 (lpj/nilai-plafond): Tambah field biaya, validasi, transaksi DB, ekspor, dan tampilan
- Menambahkan kolom biaya ke seluruh alur Nilai Plafond (model, request, controller, views, export, dan migrasi)
- Update model NilaiPlafond agar field biaya bisa di-mass assign ($fillable)
- Tambah validasi baru 'biaya' (nullable|numeric|min:0) di NilaiPlafondRequest
- Terapkan transaksi DB (beginTransaction, commit, rollback) pada store/update/destroy di controller
- Tambahkan kolom biaya ke view create, edit, dan datatable index dengan format Rupiah dan tooltip nilai mentah
- Tambah header & mapping kolom biaya di NilaiPlafondExport agar muncul di hasil export Excel
- Tambah migrasi kolom biaya bertipe decimal(15,2) nullable dengan rollback support
- Tambahkan logging detail (Log::info & Log::error) di setiap proses utama controller
- Pastikan pencarian kolom biaya pada datatables menggunakan CAST ke TEXT untuk kompatibilitas PostgreSQL
2025-10-03 10:23:21 +07:00
Daeng Deni Mardaeni
04ee3a0c48 🔧(permohonan): Perbaiki logika pembuatan PersetujuanPenawaran dan update label menu
- Menambahkan pengecekan `status_bayar == 'belum_bayar'` sebelum membuat record PersetujuanPenawaran
- Mencegah pembuatan PersetujuanPenawaran jika status pembayaran sudah 'sudah_bayar'
- Memastikan logika bisnis hanya membuat PersetujuanPenawaran untuk permohonan belum dibayar
- Mengoptimalkan query database untuk mengurangi operasi CREATE yang tidak perlu
- Memperbaiki struktur dan indentasi kode di PermohonanController agar lebih mudah dibaca
- Mengubah label menu dari "Data Debitur" menjadi "Data Permohonan" di module.json
- Menyelaraskan istilah menu dengan konten dan fungsi halaman
- Meningkatkan konsistensi data dan efisiensi sistem
- Memperbaiki UX dengan label menu yang lebih relevan dan mudah dipahami
2025-09-30 17:08:29 +07:00
Daeng Deni Mardaeni
10b5a6c96c (persetujuan-penawaran): Tambah fitur tampilan file dan perbaiki logika persetujuan penawaran
- Menambahkan auto-fill nominal_bayar dengan nilai biaya_final pada create persetujuan
- Memperbaiki query datatables dengan filter status penawaran lebih spesifik
- Menambahkan whereHas untuk memastikan hanya status 'persetujuan-penawaran' yang ditampilkan
- Menambahkan preview file upload (persetujuan_penawaran, surat_representasi, bukti_bayar)
- Menggunakan Storage::url() untuk generate URL file dengan keamanan optimal
- Menambahkan ikon eye dan badge untuk tampilan preview file
- Memperbaiki fallback nominal_bayar agar otomatis menggunakan biaya_final
- Merapikan struktur HTML dan urutan class Tailwind CSS di index dan form blade
- Mengoptimalkan AJAX request structure dan error handling agar lebih stabil
2025-09-30 17:06:30 +07:00
Daeng Deni Mardaeni
3aca1d46c2 🧹(surveyor): Optimasi import statements dan perbaikan tampilan detail surveyor
- Menghapus lebih dari 20 import statements yang tidak digunakan di SurveyorController
- Membersihkan import RedirectResponse, JsonResponse, Response, dan helper tidak relevan
- Menghapus model lama seperti Surveyor, Analisa, dan AnalisaFakta yang tidak digunakan
- Menambahkan import PermohonanHistory untuk fitur catatan revisi survey
- Menambahkan query untuk mengambil catatan revisi terbaru dari PermohonanHistory
- Menampilkan catatan revisi dengan filter status 'revisi-survey' menggunakan latest()
- Mereorganisasi urutan class Tailwind CSS pada detail.blade.php agar lebih konsisten
- Memperbaiki struktur HTML dan indentasi untuk meningkatkan readability
- Mengoptimalkan error handling dan conditional rendering di view untuk tampilan lebih bersih
2025-09-30 17:01:52 +07:00
Daeng Deni Mardaeni
db55471111 🎨(pembayaran): Perbaiki logika status pembayaran dan optimasi tampilan approval
- Memperbaiki nested condition untuk jenis_penilaian_id == 2 pada PembayaranController
- Memindahkan logika status 'spk' ke blok else yang sesuai
- Menyempurnakan flow status pembayaran agar konsisten dan bebas konflik
- Menghindari benturan status antara 'proses-laporan' dan 'spk'
- Menyusun ulang class Tailwind CSS pada approval.blade.php agar konsisten dan rapi
- Memperbaiki struktur HTML dan layout card untuk meningkatkan readability dan responsivitas
- Menambahkan fallback nominal_bayar dari biaya_final untuk keandalan data
- Mengoptimalkan tampilan tombol action berdasarkan status_bayar dengan null-checking
- Menambahkan fallback values untuk data kosong serta perbaikan spacing & alignment
- Meningkatkan UX dan accessibility agar approval page lebih user-friendly dan stabil
2025-09-30 16:57:44 +07:00
Daeng Deni Mardaeni
112262d7d6 🎨 feat(ui-noc): tambah field nomor rekening lebih bayar & perbaikan styling konsistensi Tailwind
- Form NOC: tambah field nomor rekening lebih bayar (input number, readonly jika ada memo, hidden default, support old value & error handling)
- Foto Lampiran: ubah layout jadi grid 4 kolom dengan shadow & hover effect, perbaikan urutan class Tailwind & judul kategori lebih prominent
- Foto Lampiran: optimasi class container (relative, overflow-hidden, flex) & perbaikan spacing array $fotoTypes
- LPJ Sederhana Standar: konsistensi class grid/flex/input-group, role 'penilai' ditambahkan untuk akses tombol simpan, perbaikan kondisi disabled button
- Resume: optimasi class grid, flex, card-body, card-title, text, dan konsistensi urutan Tailwind di seluruh komponen
- Konsistensi class: perbaikan di form, foto-lampiran, lpj-standar, resume agar mengikuti urutan Tailwind (layout → sizing → styling)
- Struktur HTML: dirapikan untuk layout yang lebih responsif & semantik, dengan grid/flex yang lebih optimal
- UX: tampilan foto lebih rapi, form lebih mudah digunakan, role-based access lebih jelas, field tambahan untuk kebutuhan bisnis
- Dampak: data NOC lebih lengkap (tracking rekening lebih bayar), styling konsisten, UX meningkat, kode lebih maintainable
2025-09-26 10:57:06 +07:00
Daeng Deni Mardaeni
a1b9b7af86 🎨 refactor(ui): perbaikan styling form penilai & optimasi dashboard role-based
- Form Penilai: hapus spasi ganda di class CSS input currency/currency-format
- Konsistensi class Tailwind: gunakan "w-full currency" & "w-full currency-format"
- Hapus console.log tidak perlu, tambah debug log untuk parsing luas
- Optimasi parsing input luas dengan parseFloat + regex sanitasi numerik
- Dashboard: tambahkan pembatasan akses berdasarkan role (!penilai, !surveyor, !pemohon-ao, !pemohon-eo)
- Perbaiki struktur HTML & urutan class Tailwind (grid, flex, spacing, alignment)
- Optimalkan layout header, stats cards, & tabel dengan class yang konsisten
- Hapus class CSS redundan & perbaiki konsistensi penamaan
- Tingkatkan keamanan & UX dengan role-based access + struktur HTML lebih maintainable
2025-09-26 10:55:27 +07:00
Daeng Deni Mardaeni
e3540a1dd6 🔧 refactor(controller): perbaikan akses role & logika pembayaran NOC
- Tambah filter `branch_id` untuk role `pemohon-ao` & `pemohon-eo` di LaporanController, PembatalanController, dan PembayaranController
- Pastikan user hanya dapat mengakses data sesuai branch untuk mencegah data lintas cabang
- Perbaikan pencarian Bucok di NocController dengan tambahan kondisi `orWhere(permohonan_id)`
- Tambah null safety untuk update Bucok & akses relasi branch/debiture
- Filter untuk hindari duplikasi NOC yang sudah punya `memo_penyelesaian`
- Komentari kondisi approval kompleks di LaporanController sementara waktu
- Tambah penggunaan null safety operator (`?->`) & fallback tanggal ke `created_at` NOC
- Rapikan import: hapus unused imports, tambah `Auth` facade, perbaikan urutan
- Tambah handling data: `created_at` timestamp saat buat NOC, fallback value untuk tanggal Bucok, default value kosong untuk null safety
2025-09-26 10:52:20 +07:00
Daeng Deni Mardaeni
cd97d184ce 🔧 refactor(menu): reorganisasi struktur menu & perbaikan akses role
- Reorganisasi urutan menu, memindahkan "Laporan Penilaian Jaminan" ke posisi pertama & memperbaiki penamaan menu (SO, LPJ, Monitoring, Tender → Permohonan KJPP, dll.)
- Ubah path "laporan-penilaian-jaminan" menjadi "laporan" untuk konsistensi URL
- Pindahkan menu "Data Debitur" sebelum "Permohonan" & "Pembatalan" ke bawah dengan akses terbatas
- Perbaikan role permissions: hapus `admin` & `senior-officer` dari menu tertentu, tambahkan `penilai` & `surveyor` ke menu Penilaian
- Batasi akses menu "Pembatalan" hanya untuk `administrator` & `pemohon-ao`
- Update ActivityController: tambah role `penilai` pada filter user
- Update LaporanPermohonanController: filter status `done` agar hanya tampil permohonan selesai
- Update PermohonanController: hapus kondisi `jenis_penilaian_id` pada logika pembuatan PersetujuanPenawaran, gunakan `Auth::id()` konsisten
- Validasi DebitureRequest: ubah max karakter `nomor_rekening` dari 50 → 10, serta perbaikan indentasi & format JSON di `module.json`
2025-09-26 10:49:18 +07:00
Daeng Deni Mardaeni
929c56b079 feat(slik): tambah fitur export data SLIK ke Excel
- Tambah class `SlikExport` dengan implementasi Laravel Excel (WithMapping, WithHeadings, ColumnFormats, FromCollection)
- Method `collection()`, `map()`, `headings()`, `columnFormats()` untuk data mapping, header, dan formatting
- Format kolom: text untuk ID/kode, number untuk nilai agunan, date (d/m/Y) untuk tanggal
- Tambah method `export()` di `SlikController` dengan logging, error handling, dan filename timestamp
- Optimasi performa: chunking data, batch insert, memory monitoring (current & peak usage)
- Logging lengkap: start, jumlah records, success (filename & peak memory), error trace
- Integrasi route: GET `/slik/export` → `SlikController@export` (name: slik.export)
- Dependensi: gunakan `maatwebsite/excel`, pastikan package & write permission tersedia
- Dampak: export Excel lebih cepat, format user-friendly, audit trail lebih lengkap
2025-09-22 15:25:13 +07:00
Daeng Deni Mardaeni
388b65696f 🔧 refactor(permissions): pembersihan role & perbaikan UI form
- Hapus role `pemohon-ao` & `pemohon-eo` dari module.json pada semua menu (dashboard, laporan, permohonan, dsb.)
- Rapikan indentasi role `administrator` dan streamline permissions untuk akses kontrol yang lebih sederhana
- Tambahkan konsistensi role yang dipertahankan: administrator, admin, AO/EO Appraisal, DD Appraisal, Penilai, Surveyor
- Perbaiki class ordering pada input file & error message di `debitur.blade.php` agar sesuai konvensi Tailwind
- Perbaiki struktur div container di `debitur.blade.php` (`flex flex-wrap gap-2.5 items-baseline lg:flex-nowrap`)
- Perbaiki formatting input file & textarea pada `persetujuan_penawaran/form.blade.php` dengan multi-line untuk readability
- Rapikan class ordering error message di form penawaran (`text-sm alert text-danger`)
- Standarisasi formatting & konsistensi layout antar form untuk UX lebih rapi
2025-09-22 15:23:01 +07:00
Daeng Deni Mardaeni
ba9089e2ac 🔧 refactor(pembayaran): perbaikan struktur kode & integrasi Bucok dengan Permohonan
- Tambahkan validasi parameter `type` pada PembayaranController untuk mencegah error runtime
- Refactor nested conditions & perbaikan indentasi agar kode lebih readable dan maintainable
- Update model Bucok: tambah field `nomor_registrasi` & `permohonan_id` ke fillable serta relasi dengan Permohonan
- Integrasi logika pembayaran: auto update/create Bucok dengan data Permohonan & default nominal_bayar
- Implementasi conditional update berdasarkan ketersediaan nomor_tiket untuk sinkronisasi data
- Tambahkan migration baru: kolom nomor_registrasi & permohonan_id di tabel bucoks, dengan foreign key dan index
- Pastikan rollback migration tersedia via method down() untuk keamanan schema
- Optimasi query dengan index baru & penggunaan updateOrCreate untuk hindari duplikasi data
2025-09-22 11:35:07 +07:00
Daeng Deni Mardaeni
20d0061d42 feat(permohonan): auto-create PersetujuanPenawaran untuk jenis penilaian tertentu
- Import model PersetujuanPenawaran ke PermohonanController
- Tambah pengecekan jenis_penilaian_id == 1 setelah update permohonan
- Implementasi auto-create record PersetujuanPenawaran jika belum ada
- Simpan permohonan_id dan created_by (auth()->id()) pada record baru
- Gunakan where()->first() untuk cek existing record sebelum create
- Tambahkan logika di dalam try-catch agar konsisten dengan error handling
- Pastikan duplicate record dicegah untuk menjaga integritas data
- File dimodifikasi: app/Http/Controllers/PermohonanController.php
- Dampak: mempercepat workflow, kurangi manual input, tingkatkan konsistensi data
2025-09-22 10:46:58 +07:00
Daeng Deni Mardaeni
4bef7cdafd feat(laporan-admin-kredit): tambah kolom kolektibilitas & keterangan pada index dan export Excel
- Tambah kolom "Kolektibilitas" (min-w 150px) & "Keterangan" (min-w 200px) di tabel index laporan admin kredit
- Dukungan sorting & filtering pada kedua kolom baru dengan integrasi DataTable
- Implementasi truncation keterangan >50 karakter dengan suffix "..." dan fallback "-" jika kosong
- Responsive design di tabel index agar tetap readable di berbagai layar
- Mapping data export Excel ditambahkan field kolektibilitas & keterangan (posisi kolom N & O)
- Update heading Excel: tambah header "Kolektibilitas" & "Keterangan", geser "Created At" ke kolom P
- Tambah format Excel: FORMAT_TEXT untuk kolektibilitas & keterangan, FORMAT_DATE_DATETIME untuk created_at
- Dampak: informasi laporan admin kredit lebih lengkap, mendukung analisis kolektibilitas & dokumentasi tambahan
2025-09-22 09:12:52 +07:00
Daeng Deni Mardaeni
bf728972b5 (laporan-admin-kredit): tambah kolom keterangan & kolektibilitas
- Tambah field `keterangan` (TEXT) & `kolektibilitas` (VARCHAR 10) pada tabel laporan_admin_kredit
- Update model LaporanAdminKredit dengan fillable baru
- Tambah dropdown kolektibilitas (1-5: Lancar, DPK, Kurang Lancar, Diragukan, Macet) di form
- Tambah textarea keterangan dengan old() support untuk validasi
- Validasi: `keterangan` nullable|string, `kolektibilitas` nullable|string|in:1..5
- Update controller: simpan field baru dengan DB transaction & error handling
- Migration baru untuk menambah kolom `keterangan` & `kolektibilitas`
- UI/UX: konsistensi styling, responsive grid layout, placeholder informatif
- Testing: form simpan & validasi berhasil, migration jalan tanpa error
2025-09-22 09:12:42 +07:00
Daeng Deni Mardaeni
dfd2a82b42 🎨(view): Refaktor dan optimasi tampilan detail jaminan dengan perbaikan CSS & struktur kode
- Konsistensi urutan class CSS mengikuti standar Tailwind (layout → sizing → typography → colors)
- Perbaikan indentasi, spacing, whitespace, dan formatting string ("" → '')
- Penyusunan ulang struktur HTML dengan alignment & spacing yang lebih rapi
- Optimalisasi Blade template: perbaikan kondisi `@if (isset($penawaran))` & closing tag
- Standarisasi penggunaan grid layout dan urutan class grid (contoh: `grid grid-cols-1 gap-5 xl:grid-cols-2`)
- Konsistensi typography classes (`font-normal text-gray-700 text-2sm`) dan urutan color classes
- Refaktor struktur tabel: perbaikan class ordering, cell formatting, dan spacing kolom
- Penyusunan ulang komponen accordion dengan indentasi & struktur konten yang konsisten
- Konsistensi class pada tombol accordion & link structure untuk meningkatkan maintainability
2025-09-21 21:19:56 +07:00
Daeng Deni Mardaeni
c4bb3bea28 🔧(backend): Perbaikan & optimasi controller serta view untuk modul penilai dan surveyor
- PenilaiController: tambah eager loading relasi `penawaran` untuk kurangi N+1 query
- RegistrasiFinalController: ubah filter status dari `spk` → `registrasi-final`
- SurveyorController: perbaiki pengecekan jenisPenilaian dengan `in_array` (support External/Eksternal, case-insensitive)
- SurveyorController: ubah field `tanggal_laporan` → `tgl_final_laporan` sesuai DB baru
- SurveyorController: sanitasi koordinat lat/lon dengan menghapus koma untuk valid format
- SurveyorController: perbaiki exception handling (`Exeception` → `\Exception`, `$th` → `$e`, tambah namespace lengkap)
- SurveyorController: konsistensi penggunaan `Auth::user()` alih-alih `auth()->user()`
- View pembayaran/index.blade.php: tambahkan safe navigation `data.debiture?.name` untuk hindari null error
- View penilai/index.blade.php: cleanup baris kosong & perbaiki logika status pakai `data.penawaran?.status`
2025-09-21 21:17:19 +07:00
Daeng Deni Mardaeni
006dd44c64 🎨 refactor(form-penilai): rapikan struktur CSS dan formatting kode
- Reorder CSS class sesuai standar Tailwind (layout → sizing → styling)
- Perbaiki indentasi, spacing, dan line breaks untuk readability
- Ganti `else if` menjadi `elseif` pada PHP conditional
- Rapikan multi-line assignment pada nilai luas apartemen-kantor
- Update contoh class: `input w-full` → `w-full input`, `card-title uppercase` → `uppercase card-title`
- Rapikan class ordering pada container flex & grid
- Perbaiki class di button: `btn btn-primary btn-sm mt-5` → `mt-5 btn btn-primary btn-sm`
- Konsistensi class ordering di dynamic HTML generation (JavaScript)
- Fokus pada konsistensi visual tanpa mengubah tampilan UI
2025-09-19 10:18:30 +07:00
Daeng Deni Mardaeni
698935d06f 🔧 refactor(laporan): rapikan logika role pemohon-ao di index laporan
- Gabungkan blok kondisional @if pemohon-ao menjadi satu blok dengan else if
- Perbaiki struktur kode untuk menghindari error "Undefined constant data"
- Tambah komentar untuk memperjelas logika bisnis role-based
- Optimasi kondisi nilai_liquidasi dan status_bayar agar lebih efisien
- Rapikan line breaking dan indentasi parameter generateLaporanButton
- Hapus duplikasi logika pemohon-ao → kode lebih maintainable
- Pisahkan logika role pemohon-ao dan role lain agar lebih jelas
- Konsistensi formatting dan readability di Blade template
- Pastikan tombol laporan tampil sesuai role & kondisi tanpa error JS
2025-09-19 09:22:20 +07:00
Daeng Deni Mardaeni
564c7ccac4 🔧 refactor(laporan): perbaiki URL routing edit laporan admin kredit
- Ubah URL dari `laporan-admin-kredit/${data.id}/edit` → `admin-kredit/laporan/${data.id}/edit`
- Perbaiki konsistensi routing pattern agar sesuai dengan definisi route
- Pastikan tombol edit laporan admin kredit mengarah ke halaman edit yang benar
- Konsistensi routing mencegah error 404 dan meningkatkan UX
2025-09-19 09:17:01 +07:00
Daeng Deni Mardaeni
2b5556410d 🔧 refactor(laporan): kontrol akses role, optimasi query, dan perbaikan UI
- Tambah role `pemohon-ao` & `pemohon-eo` untuk upload lampiran di LampiranDokumen.php
- Implementasi kontrol akses role di lampiran-dokumen.blade.php & laporan/index.blade.php (logika khusus nilai_liquidasi)
- Modifikasi query di LaporanController.php: hapus filter approval_eo_at & optimasi kondisi WHERE
- Perbaikan formatting query dengan indentasi lebih rapi & maintainable
- Standardisasi urutan class CSS di beberapa Blade (activity/index, penilaian/otorisator/index-sla & index)
- Pisahkan logika generateLaporanButton jadi function terpisah di laporan/index.blade.php
- Perbaikan tampilan tombol laporan berbasis role + cleanup baris kosong & indentasi
- Konsistensi UI/UX: responsive design lebih baik, interface clean & styling konsisten dengan Tailwind
- Tingkatkan security & maintainability: granular role access, query lebih efisien, code lebih terstruktur
2025-09-19 09:10:06 +07:00
Daeng Deni Mardaeni
ee7c8ce97f 🔧 refactor(inspeksi): gunakan updateOrCreate & perbaikan kode
- Ganti `Inspeksi::create()` → `updateOrCreate()` di PenilaiController (2x) & SurveyorController (1x) dengan kondisi upsert (permohonan_id + dokument_id)
- Tambah logging di SaveFormInspesksiService.php (`Log::info`) untuk debugging & validasi action kosong
- Perbaiki error handling dengan pesan lebih informatif `'Gagal menyimpan data : '.$e->getMessage()`
- Refaktor parsing action memakai array_map & array_filter agar lebih efisien
- Rapikan kode: hapus baris kosong tidak perlu & improve readability
- Perbaiki urutan class CSS di beberapa Blade view (rap-penilai, penilai/index, surveyor/inspeksi)
- Perbaiki XSS di rap-penilai.blade.php dengan `{!! json_encode($dokumen->address ?? '') !!}`
- Tingkatkan integritas database: cegah duplikasi data inspeksi via updateOrCreate()
- Tambah keamanan & maintainability: logging, validasi input, perbaikan format, serta pembersihan kode lama
2025-09-19 09:06:12 +07:00
Daeng Deni Mardaeni
17f7482080 ♻️ refactor(js): pindahkan fungsi loading SweetAlert ke global scope dan perbaiki bug
- Memindahkan fungsi showLoadingSwal() dan hideLoadingSwal() ke resources/assets/js/app.js
- Menambahkan fitur timer dan progress bar pada fungsi global loading
- Menghapus duplikasi fungsi showLoadingSwal() dari penilai/index.blade.php (~18 baris)
- Menghapus duplikasi fungsi dari penilaian/otorisator/index-sla.blade.php (~18 baris)
- Menghapus duplikasi fungsi dari penilaian/otorisator/index.blade.php (~18 baris)
- Menghapus duplikasi fungsi dari penilaian/paparan-so.blade.php (~18 baris)
- Memperbaiki syntax error tag HTML di surveyor/components/informasi.blade.php
- Membersihkan duplikasi fungsi & memperbaiki escape string di surveyor/js/utils.blade.php
- Mengurangi ±90 baris kode duplikat, meningkatkan maintainability & UX (loading dialog lebih informatif)
2025-09-19 09:01:20 +07:00
Daeng Deni Mardaeni
6d137ad51c 🔧 refactor(laporan-slik): Perbaikan controller dan routing untuk laporan SLIK
- Mengubah nama method datatables menjadi dataForDatatables untuk konsistensi penamaan
- Menambahkan method show untuk menampilkan detail laporan SLIK individual
- Memperbaiki struktur response datatables dengan format yang lebih lengkap dan konsisten
- Menambahkan filter tambahan untuk sandi_bank dan kolektibilitas pada datatables
- Mengimplementasikan sorting dan pagination yang lebih robust
- Menambahkan error handling yang komprehensif dengan logging
- Memperbaiki transformasi data dengan penambahan field kolektibilitas_badge dan created_by
- Mengupdate routing untuk menambahkan route show laporan SLIK
- Menambahkan breadcrumb untuk halaman laporan SLIK dan detail
- Mengubah role akses dari 'noc' menjadi 'adk' pada module.json
- Memperbaiki format tanggal menggunakan helper dateFormat
- Menambahkan penanganan exception dengan fallback response yang proper
- Mengoptimalkan query dengan penambahan filter yang lebih spesifik
- Memperbaiki struktur response JSON untuk kompatibilitas dengan frontend datatables
2025-09-17 15:24:27 +07:00
Daeng Deni Mardaeni
6c004812a9 🎨 feat(slik): Implementasi fitur pindah data SLIK ke laporan dan perbaikan UI
- Menambahkan tombol "SLIK" pada halaman index dan show untuk memindahkan data ke laporan SLIK
- Mengimplementasikan fungsi moveToLaporan() dengan konfirmasi SweetAlert dan proses AJAX
- Melakukan migrasi framework CSS dari Bootstrap ke TailwindCSS pada laporan-slik/show.blade.php
- Memperbaiki struktur layout dengan grid system TailwindCSS yang responsif
- Mengupdate breadcrumbs dengan styling dan route names yang benar
- Menghapus fitur truncate data SLIK dari interface untuk keamanan data
- Memperbaiki route names dari admin-kredit.laporan-slik menjadi laporan-slik
- Mengoptimasi tombol Export dengan penghapusan parameter ID yang tidak diperlukan
- Menambahkan konfigurasi import SLIK di .env.example untuk optimasi performa
- Memperbaiki template download link dengan class styling yang konsisten
- Mengimplementasikan error handling yang komprehensif dengan user feedback
- Menambahkan auto-reload DataTable setelah operasi pemindahan data berhasil
- Melakukan redesign card layout dengan pembagian "Data Debitur" dan "Data Fasilitas"
- Menambahkan feedback visual dengan disable tombol setelah berhasil dipindahkan
- Mengoptimasi konfigurasi DataTable dengan reload functionality
- Menambahkan breadcrumb routes untuk laporan SLIK dengan struktur hierarki
- Mengimplementasikan progress tracking untuk monitoring proses import
- Memperbaiki JavaScript dengan pemisahan fungsi dan penambahan variabel global
- Menstandarisasi framework CSS untuk konsistensi visual
- Mengimplementasikan responsive design yang lebih baik
2025-09-17 15:19:28 +07:00
Daeng Deni Mardaeni
d932559849 feat(slik): implementasi fitur laporan SLIK dengan integrasi SweetAlert
- Menambahkan tombol aksi "Pindahkan ke Laporan SLIK" pada halaman index & detail
- Integrasi SweetAlert2 untuk konfirmasi, loading state, dan notifikasi sukses/gagal
- Implementasi auto-refresh DataTable setelah pemindahan berhasil
- Disable tombol otomatis setelah sukses untuk mencegah duplikasi data
- LaporanSlikController: method store() dengan transaksi DB & auto-delete dari tabel sliks
- Routing baru untuk index, datatables, store, dan export laporan SLIK
- Penyesuaian views (index & show) dengan tombol, script SweetAlert, dan feedback visual
- Proteksi keamanan: CSRF token, validasi input, transaksi DB, dan error logging
- Testing checklist: pindahkan data, refresh tabel, disable tombol, error handling, responsif mobile/desktop
2025-09-17 13:02:58 +07:00
Daeng Deni Mardaeni
c3c40fdc27 feat(laporan-slik): implementasi sistem laporan SLIK
- Menambahkan tabel `laporan_slik` (47 kolom, index, audit trail, status aktif/non-aktif)
- Membuat model `LaporanSlik` dengan fillable lengkap, relasi ke Slik & User, accessor agunan/status badge
- Menambahkan controller `LaporanSlikController` dengan method store(), datatables(), export()
- store(): validasi strict, cek duplikasi, copy data dari Slik ke laporan, hapus data asal
- datatables(): filter search/tahun/bulan/status, pagination, sorting, JSON response standar
- export(): ekspor Excel dengan filter sama, nama file otomatis timestamp, error handling
- Integrasi sistem: transaksi DB aman, logging detail, support bulk operations, memory optimisasi
- UX: SweetAlert, DataTable real-time, loading state, error message jelas, auto-reload
- Security & performa: validasi input, XSS & SQLi prevention, index optimization, siap rate limiting
2025-09-17 13:00:24 +07:00
Daeng Deni Mardaeni
41262e0317 feat(slik): migrasi route SLIK ke group admin-kredit
Mengubah struktur route SLIK agar sesuai dengan arsitektur admin-kredit dengan menambahkan prefix admin-kredit pada semua route.

- Menambahkan route SLIK lengkap dalam group admin-kredit di routes/web.php
- Memperbarui breadcrumb SLIK untuk menggunakan prefix admin-kredit di routes/breadcrumbs.php
- Memperbarui semua referensi route di view index.blade.php:
  * Breadcrumbs: slik → admin-kredit.slik
  * Route datatables: slik.datatables → admin-kredit.slik.datatables
  * Route import: slik.import → admin-kredit.slik.import
  * Route export: slik.export → admin-kredit.slik.export
  * Route download template: admin-kredit.slik.download-template
  * Route JavaScript: slik.show → admin-kredit.slik.show
- Memperbarui referensi route di view show.blade.php:
  * Breadcrumbs: slik → admin-kredit.slik.show
  * Tombol kembali: slik.index → admin-kredit.slik.index
- Menambahkan link download template Excel di modal import
2025-09-16 17:18:31 +07:00
Daeng Deni Mardaeni
2a1ecfd9e2 feat(slik): tambahkan fitur detail SLIK dengan tampilan responsive
Menambahkan fitur detail SLIK yang menampilkan informasi lengkap debitur dengan desain yang responsive dan user-friendly.
- Menambahkan kolom "Kolektibilitas" dan "Fasilitas" pada tabel DataTable
- Menghapus filter tahun, bulan, dan status yang tidak digunakan
- Menambahkan tombol "Detail" untuk setiap baris data
- Memperbaiki responsive design dengan CSS khusus untuk mobile
- Menambahkan class `min-w-[1200px]` untuk memastikan tabel dapat discroll horizontal
- Memperbarui konfigurasi DataTable untuk menampilkan kolom baru
- Menambahkan fungsi JavaScript `showDetail()` untuk redirect ke halaman detail
- Menghapus modal detail yang tidak digunakan lagi
- Memperbaiki styling dengan Tailwind CSS untuk konsistensi
- Membuat halaman detail baru dengan layout yang responsive
- Menampilkan informasi lengkap debitur dalam format card yang terorganisir
- Menggunakan grid layout 2 kolom untuk desktop dan 1 kolom untuk mobile
- Menampilkan data dalam kategori: Data Debitur, Data Fasilitas, dan Informasi Tambahan
- Menambahkan tombol kembali untuk navigasi mudah
- Menggunakan Tailwind CSS untuk styling yang konsisten dengan halaman index
- Responsive design yang optimal untuk semua ukuran layar
2025-09-16 16:24:32 +07:00
Daeng Deni Mardaeni
20833213b1 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
2025-09-16 11:54:39 +07:00
Daeng Deni Mardaeni
81159983cf 🔧 refactor(noc): Integrasi Bucok & pembersihan kode NocController
- Tambah import model Bucok di NocController
- Refactor method update(): hapus variabel & logic status usang
- Update otomatis Bucok setelah NOC disimpan (nominal, tanggal, status)
- Gunakan where()->first() untuk cari record Bucok berdasarkan nomor_tiket
- Hapus 26 baris komentar dead code & filter whereHas('noc') di penyelesaian()
- Perbaiki formatting, indentasi, dan tambah blank line untuk readability
- Gunakan fallback tanggal penyelesaian (date('Y-m-d')) jika kosong
- Terapkan null coalescing operator (??) untuk handle nilai null
- Sinkronisasi status penyelesaian NOC ↔️ Bucok & kode lebih bersih
2025-09-16 09:44:31 +07:00
Daeng Deni Mardaeni
bc35785c9c feat(pembayaran): Integrasi otomatis dengan Bucok saat persetujuan penawaran dibuat
- Tambah import model `Modules\Lpj\Models\Bucok` di PembayaranController
- Perbaiki spacing dan assignment `nomor_tiket` agar konsisten
- Simpan instance NOC ke variabel `$noc` setelah create
- Buat record Bucok otomatis dengan `updateOrCreate` berdasarkan nomor_tiket
- Mapping field tanggal (hari, bulan, tahun) menggunakan Carbon
- Isi default penyelesaian = 'Belum Selesai' dan mapping nominal ke Bucok
- Gunakan relasi `$noc->branch?->name` untuk nama sub_direktorat & cabang
- Implementasi safe navigation operator untuk hindari null error
- Pastikan konsistensi data & sinkronisasi pembayaran dengan Bucok
2025-09-16 09:36:45 +07:00
Daeng Deni Mardaeni
041ca943c9 feat(noc): Implementasi total nominal diterima pada halaman penyelesaian
- Hitung total nominal diterima dari semua data yang difilter (bukan hanya halaman aktif)
- Tambahkan field `totalNominalDiterima` pada response JSON DataTable
- Parsing string currency ke numeric value untuk perhitungan akurat
- Tampilkan total di footer tabel dengan format Rupiah (IDR)
- Integrasi real-time backend (NocController) dan frontend (Blade + JS)
- Update otomatis via event listener DataTable saat data berubah atau difilter
- Styling footer dengan TailwindCSS untuk highlight nominal
- Validasi dan fallback aman (0) bila data tidak tersedia
- Transparansi & efisiensi monitoring keuangan secara real-time
2025-09-15 15:39:00 +07:00
Daeng Deni Mardaeni
312861a933 feat(noc): implementasi perhitungan total nominal diterima pada pembayaran
- Hitung total nominal diterima dari semua data yang difilter (bukan hanya halaman aktif)
- Tampilkan total dalam format currency Indonesia (IDR) di footer tabel
- Integrasi backend (NocController) dan frontend (Blade + JS) secara real-time
- Update otomatis via event listener DataTable saat data berubah atau difilter
- Validasi dan parsing currency string dengan aman, casting ke float untuk akurasi
- Tambahkan field totalNominalDiterima pada response JSON DataTable
- Footer tabel dengan styling TailwindCSS (text-green-600, font-bold) untuk highlight
- UX lebih transparan: user langsung melihat total pembayaran yang diterima
- Performa terjaga: minimal DOM manipulation, native Intl.NumberFormat, efisien pada filter
2025-09-15 15:30:16 +07:00
Daeng Deni Mardaeni
96657de512 feat(api,noc): Tambah API pencarian debitur dan perbaikan filter jenis penilaian
- API Debitur Controller: method search() (min 2 karakter), getByCode(), transaksi DB, logging, response JSON konsisten
- Batasi hasil pencarian maksimal 20 item untuk performa optimal
- Support pencarian berdasarkan CIF dan nama debitur
- Tambah import JenisPenilaian di NocController
- Perbaiki method penyelesaian() untuk mengirim data jenisPenilaians ke view
- Tambahkan filter nama jenis penilaian di dataForDatatablesPenyelesaian() dan kolom jenis_penilaian pada response tabel
- Update UI view penyelesaian: dropdown dinamis, kolom sortable, integrasi DataTable, perbaikan layout
- Validasi input & keamanan: transaction handling, logging, conditional debug response, SQL injection protection
- Peningkatan UX: autocomplete search, filter responsif, pesan error informatif, loading state, format response konsisten
2025-09-15 14:46:29 +07:00
Daeng Deni Mardaeni
4ad11593d5 feat(noc): Tambahkan filter jenis penilaian pada halaman pembayaran NOC
Fitur baru ini menambahkan kemampuan filter berdasarkan jenis penilaian pada halaman pembayaran NOC dengan integrasi penuh ke DataTable dan database. Perubahan meliputi:
- Penambahan query jenis penilaian aktif di controller
- Filter whereRelation pada `dataForDatatablesPembayaran()`
- Dropdown dinamis jenis penilaian di view pembayaran
- Event listener JavaScript untuk filter real-time
- Opsi reset "Semua Jenis Penilaian"
- Validasi input client & server dengan XSS protection
- Optimasi query dengan relasi `permohonan.jenisPenilaian`
- Tampilan UI responsive dan konsisten dengan desain existing
- Peningkatan UX untuk pencarian data pembayaran lebih akurat dan efisien
2025-09-15 14:40:40 +07:00
Daeng Deni Mardaeni
d851ab58bc 🔧 fix(noc): Perbaiki validasi dan logika NOC untuk mendukung pembayaran tanpa permohonan
- Ubah validasi permohonan_id dari required menjadi nullable di NocRequest
- Hapus pesan error required untuk permohonan_id di validation messages
- Tambahkan logika kondisional di NocController->store() untuk updateOrCreate berdasarkan keberadaan permohonan_id
- Perbaiki null safety dengan operator ?-> di form.blade.php untuk akses nested properties
- Update logika status pembayar untuk mendukung pembayaran dengan nomor_tiket
- Tambahkan kondisi khusus untuk menentukan status bayar berdasarkan nomor_tiket
- Perbaiki formatting dan spacing di controller untuk readability
2025-09-15 13:58:40 +07:00
Daeng Deni Mardaeni
c08e050815 feat: Tambah kolom nomor tiket dan perbaiki null safety NOC
- Tambahkan kolom 'nomor_tiket' di tabel NOC (index, pembayaran, penyelesaian)
- Perbaiki null safety dengan operator ?-> untuk mencegah error
- Update model Noc: ganti fillable dengan guarded, tambah relasi debiture & branch
- Hapus filter whereDoesntHave untuk memo_penyelesaian (commented out)
- Tambah fallback data dari noc->debiture dan noc->branch
- Perbaiki sorting dengan ->values() untuk reset array keys
- Update view pembayaran.blade.php dengan kolom nomor tiket
2025-09-15 12:52:14 +07:00
Daeng Deni Mardaeni
4aeecf6a97 (pembayaran): Implementasi fitur create pembayaran baru dengan autocomplete debitur
- Menambahkan method create() di PembayaranController untuk menampilkan form pembayaran baru
- Menambahkan logika create pembayaran di method store() dengan validasi type 'create'
- Menambahkan penyimpanan data pembayaran baru ke tabel persetujuan_penawaran dan noc
- Menambahkan upload bukti bayar dengan penyimpanan ke storage public
- Menambahkan migration untuk kolom branch_id di tabel noc
- Menambahkan view create.blade.php dengan form pembayaran lengkap dan autocomplete debitur
- Menambahkan validasi JavaScript untuk format file dan ukuran maksimal 2MB
- Menambahkan TomSelect untuk pencarian debitur dengan AJAX real-time
- Menambahkan integrasi dengan API debitur search untuk autocomplete
- Memperbaiki method edit() untuk mendukung parameter tiket dalam pencarian persetujuan penawaran
- Mengubah query dataForDatatables untuk mendukung data dari persetujuan_penawaran dan permohonan
- Menambahkan mapping data yang fleksibel untuk menampilkan informasi dari berbagai sumber
- Menambahkan field nomor_tiket, nominal_bayar, dan catatan pada form create
- Menambahkan validasi client-side untuk memastikan file upload sesuai format
- Menambahkan relasi branch_id pada tabel noc untuk tracking cabang pembuat
- Menambahkan redirect ke pembayaran.index setelah berhasil menyimpan pembayaran baru
- Menambahkan import PhpParser\Node\Expr\Cast\Object_ (perlu dibersihkan)
- Mengoptimalkan query dengan eager loading dan mapping data yang efisien
- Menambahkan support untuk pembayaran tanpa permohonan (standalone payment)
- Menambahkan field is_permohonan untuk membedakan jenis pembayaran
- Menambahkan validasi dan error handling yang komprehensif
2025-09-15 11:32:59 +07:00
Daeng Deni Mardaeni
1caa7ebfdd (pembayaran): Implementasi fitur pengembalian lebih bayar
- Menambahkan method editLebih() di PembayaranController untuk menampilkan form pengembalian lebih bayar
- Menambahkan logika pengembalian lebih bayar di method store() dengan validasi type 'lebih_bayar'
- Menambahkan penyimpanan bukti KSL lebih bayar dengan upload file ke storage
- Menambahkan update bukti KSL ke tabel noc untuk pengembalian lebih bayar
- Menambahkan filter bukti_ksl_lebih_bayar null pada query dataForDatatablesLebih untuk menampilkan data yang belum diproses
- Menambahkan validasi file upload untuk bukti_ksl_lebih_bayar dengan format pdf,jpg,jpeg,png maksimal 2MB
- Menambahkan view form-lebih.blade.php dengan tampilan detail pembayaran dan form pengembalian
- Menambahkan validasi JavaScript untuk memastikan format dan ukuran file yang diupload
- Menambahkan route pembayaran/{pembayaran}/lebih untuk mengakses form pengembalian lebih bayar
- Mengubah link action di lebih.blade.php dari edit ke lebih untuk mengarahkan ke form pengembalian
- Menambahkan redirect ke pembayaran.lebih.index setelah berhasil menyimpan pengembalian
- Menambahkan field catatan opsional untuk memberikan keterangan tambahan pada proses pengembalian
- Menambahkan validasi client-side untuk memastikan file yang diupload sesuai format dan ukuran
- Menambahkan informasi detail pembayaran yang komprehensif di form pengembalian
- Menambahkan styling yang konsisten dengan form kurang bayar untuk user experience yang baik
2025-09-15 08:08:10 +07:00
Daeng Deni Mardaeni
8666a0c58b (pembayaran): Implementasi fitur pelunasan kurang bayar
- Menambahkan method editKurang() di PembayaranController untuk menampilkan form pelunasan kurang bayar
- Menambahkan logika pelunasan kurang bayar di method store() dengan validasi type 'kurang_bayar'
- Menambahkan penyimpanan bukti KSL kurang bayar dengan upload file ke storage
- Menambahkan update nominal pelunasan dan bukti KSL ke tabel noc dan persetujuan_penawaran
- Menambahkan filter bukti_ksl_kurang_bayar null pada query dataForDatatablesKurang untuk menampilkan data yang belum dilunasi
- Menambahkan validasi file upload untuk bukti_ksl_kurang_bayar dengan format pdf,doc,docx maksimal 10MB
- Menambahkan kolom nominal_pelunasan dan debiture_id pada migration persetujuan_penawaran
- Menambahkan view form-kurang.blade.php dengan tampilan detail pembayaran dan form pelunasan
- Menambahkan validasi JavaScript untuk memastikan nominal pelunasan tidak melebihi nominal kurang bayar
- Menambahkan route pembayaran/{pembayaran}/kurang untuk mengakses form pelunasan kurang bayar
- Mengubah link action di kurang.blade.php dari edit ke kurang untuk mengarahkan ke form pelunasan
- Menambahkan redirect ke pembayaran.kurang.index setelah berhasil menyimpan pelunasan
- Menambahkan rollback migration untuk menghapus kolom yang ditambahkan jika diperlukan
2025-09-15 08:04:49 +07:00
Daeng Deni Mardaeni
99bc711954 🐛 fix(otorisasipenawaran): perbaiki route generation dengan parameter kosong
- Ganti parameter kosong ('') dengan placeholder ':id' pada route helper
- Implementasi JavaScript replace() untuk substitusi parameter dinamis
- Perbaiki fungsi otorisasiPenawaranKJPP di editextjs.blade.php
- Meningkatkan keamanan dan konsistensi route generation
- Menghindari error Laravel route dengan parameter kosong
- Kompatibel dengan Laravel route caching mechanism
- Menjaga konsistensi dengan perbaikan pada modul prosespenawaran
2025-09-13 11:54:35 +07:00
Daeng Deni Mardaeni
627d7f9b40 🐛 fix(prosespenawaran): perbaiki route generation dengan parameter kosong
- Ganti parameter kosong ('') dengan placeholder ':id' pada route helper
- Implementasi JavaScript replace() untuk substitusi parameter dinamis
- Perbaiki fungsi updateulang dan updateKJPPStatus di editeulangxtjs.blade.php
- Meningkatkan keamanan dan konsistensi route generation
- Menghindari error Laravel route dengan parameter kosong
- Kompatibel dengan Laravel route caching mechanism
2025-09-13 11:54:17 +07:00
Daeng Deni Mardaeni
8a5bf21982 feat(pembayaran): implementasi pembayaran kurang & lebih bayar dengan role management
- Tambah method kurang() & lebih() di PembayaranController untuk halaman khusus
- Implementasi dataForDatatablesKurang() & dataForDatatablesLebih() untuk listing data
- Optimasi query dengan filter status + mapping nominal ke format rupiah
- Cleanup code: hapus import & komentar tidak dipakai, rapikan indentasi
- Update module.json: ubah akses menu pembayaran ke "pemohon-ao", tambahkan role "admin" untuk menu NOC
- Tambah route pembayaran/datatables-kurang & pembayaran/datatables-lebih di routes/registrasi.php
- Filtering & sorting data konsisten dengan pagination DataTables
- Format tampilan data finansial distandarisasi (rupiah 2 desimal)
2025-09-12 10:17:42 +07:00
Daeng Deni Mardaeni
2433aacfbc feat(noc): implementasi sistem pembayaran dengan tracking nomor tiket dan status kurang/lebih bayar
- Tambah field `nomor_tiket`, `nominal_kurang_bayar`, `bukti_ksl_kurang_bayar`, `nomor_rekening_lebih_bayar`, `bukti_ksl_lebih_bayar` di tabel `persetujuan_penawaran` & `noc`
- Update model `Noc` & `PersetujuanPenawaran` dengan fillable baru + migrasi database
- Update validasi di `NocRequest` & `PersetujuanPenawaranRequest` (nomor tiket, bukti KSL, kurang bayar, string max length)
- Restructure menu pembayaran dengan submenu *Kurang Bayar* & *Lebih Bayar*
- Tambah kolom "Nomor Tiket" di tabel & DataTable pembayaran
- Perbaikan tampilan: formatting, CSS, responsive layout, display cabang (code - name)
- Tambah routes `pembayaran.kurang.index` & `pembayaran.lebih.index` + integrasi controller
- Update `module.json` untuk menu, permission, roles, icon, dan styling
2025-09-12 09:23:13 +07:00
Daeng Deni Mardaeni
ba29f5ee8e feat(noc): implementasi logika conditional checkbox status pembayaran
- Menambahkan fungsi `updateCheckboxStatus()` untuk evaluasi kondisi real-time.
- Checkbox otomatis disabled jika total_harus_bayar = nominal_bayar = total_pembukuan.
- Checkbox **status_kurang_bayar** aktif jika nominal/pembukuan < total_harus_bayar.
- Checkbox **status_lebih_bayar** aktif jika nominal/pembukuan > total_harus_bayar.
- Menambahkan event listeners pada field nominal_bayar dan total_pembukuan.
- Memastikan initial state dicek saat halaman dimuat.
- Menambahkan logging untuk debugging dan monitoring.
- Meningkatkan UX dengan mencegah input data tidak logis.
2025-09-12 08:56:01 +07:00
Daeng Deni Mardaeni
eb784a982f feat(pembayaran): tambah field nomor tiket dan perbaiki formatting form
- Menambahkan field input **nomor_tiket** setelah `nomor_registrasi` untuk konsistensi.
- Menambahkan validasi dan error handling khusus untuk field **nomor_tiket**.
- Memperbaiki struktur form dengan indentasi dan penamaan konsisten.
- Mengoptimalkan penggunaan **TailwindCSS** untuk responsivitas dan maintainability.
- Menambahkan placeholder **"Nomor Tiket"** untuk meningkatkan UX.
- Menggunakan **old()** atau data permohonan sebagai value default.
- Memperbaiki error styling, spacing, dan alignment untuk tampilan form yang rapi.
2025-09-11 22:18:46 +07:00
Daeng Deni Mardaeni
2173a36564 feat & 🐛 fix(persetujuan-penawaran): Tambah field nomor_tiket dan perbaiki namespace serta relasi model
###  Fitur Baru
- Menambahkan field `nomor_tiket` pada model **PersetujuanPenawaran** ke dalam `$fillable`.
- Membuat migration untuk menambahkan kolom `nomor_tiket` (VARCHAR 100, nullable) di tabel `persetujuan_penawaran`.
- Menambahkan validasi dan custom error messages untuk field `nomor_tiket` pada **PersetujuanPenawaranRequest**.
- Menempatkan `nomor_tiket` setelah `nomor_proposal_penawaran` untuk konsistensi.
- Implementasi rollback pada migration untuk menjaga keamanan database.
- Menambahkan komentar dokumentasi pada migration untuk mempermudah maintenance.
- Validation rules: `nullable|string|max:100` dengan pesan error dalam Bahasa Indonesia.
- Field ini mendukung tracking dan identifikasi tiket setiap persetujuan penawaran.
- Mengikuti pola dan konvensi sistem yang sudah ada agar konsisten.

### 🐛 Perbaikan Bug
- Memperbaiki typo namespace dari **Usermanagemenet** menjadi **Usermanagement**.
- Menghapus import **Region** yang tidak ada di codebase.
- Menghapus relasi `region()` karena model **Region** tidak ditemukan.
- Mempertahankan relasi valid seperti `penawaran`, `permohonan`, `authorizedBy`, dan `noc`.
- Menyelesaikan error *"Undefined type"* pada baris 53 dan 59.
- Membersihkan kode dengan menghapus dependency yang tidak ada.
- Menyesuaikan namespace **User** agar konsisten dengan struktur modul **Usermanagement**.
- Model kini lebih stabil, bersih, dan bebas dari error diagnostic.
2025-09-11 22:03:58 +07:00
Daeng Deni Mardaeni
ee079a8aa8 🔧 fix(surveyor): perbaiki fungsi calculateTotalLuas untuk mendukung format koma desimal
- Menambahkan logika menghapus format ribuan (titik) sebelum perhitungan luas.
- Mengonversi koma menjadi titik untuk parsing **float** dengan benar.
- Menggunakan **toLocaleString('id-ID')** untuk hasil sesuai format Indonesia.
- Mendukung input angka dengan format Indonesia dan internasional.
2025-09-11 09:44:35 +07:00
Daeng Deni Mardaeni
b4aba1a02a 🔧 fix(noc): Perbaiki validasi field opsional dan kondisi query memo
- Menambahkan **null coalescing operator** pada field `catatan_noc` agar tidak error bila kosong.
- Mengubah default field status pembayaran NOC menjadi string `'0'` untuk konsistensi.
- Menonaktifkan validasi approval di `MemoController` agar semua data NOC dapat tampil.
- Mencegah error validasi ketika field opsional tidak diisi pada form.
- Memastikan tampilan memo lebih lengkap tanpa batasan kondisi approval.
2025-09-11 09:43:50 +07:00
Daeng Deni Mardaeni
32baffe636 feat(bucok)!: tambah modul Bucok end-to-end + impor updateOrCreate
- 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
2025-08-19 11:30:19 +07:00
Daeng Deni Mardaeni
19fb39b02f feat(noc): implementasi mutual exclusive selection untuk status kurang dan lebih bayar
- Menambahkan JavaScript untuk mutual exclusive selection antara status kurang bayar dan lebih bayar
- Hanya satu status yang bisa dipilih pada satu waktu untuk mencegah konflik data
- Implementasi fungsi resetFields() untuk membersihkan field yang tidak dipilih
- Menambahkan event handler untuk toggle visibility field berdasarkan pilihan status
- Menambahkan logging untuk tracking perubahan status pembayaran
- Field nominal dan bukti pengembalian otomatis direset ketika status berubah
- Mempertahankan UI existing dengan checkbox namun menambahkan logika mutual exclusive
- Menambahkan validasi client-side untuk mencegah input data yang tidak konsisten
- Support untuk readonly mode ketika memo penyelesaian sudah ada
- Implementasi function-level comments untuk dokumentasi kode
2025-07-31 13:24:18 +07:00
Daeng Deni Mardaeni
bc7fef05f6 feat(noc): tambah field total pembukuan dan perbaiki label form
Menambahkan field total_pembukuan ke dalam sistem NOC untuk mendukung pencatatan pembukuan yang lebih akurat.

Perubahan yang dilakukan:
- Menambahkan migration untuk field total_pembukuan di tabel noc dengan tipe decimal(10,2)
- Menambahkan total_pembukuan ke dalam fillable array di model Noc
- Mengintegrasikan field total_pembukuan ke dalam NocController untuk proses store dan update
- Menambahkan input field "Jumlah Pembukuan" di form NOC dengan validasi error handling
- Mengubah label "Nominal Bayar" menjadi "Jumlah Yang Harus Disetor" untuk kejelasan
- Mengubah label "Nominal Diterima" menjadi "Jumlah Yang Disetor" untuk konsistensi
- Mengubah title menu dari "Pembayaran" menjadi "Pembukuan" di module.json
- Menambahkan readonly attribute pada field total_pembukuan ketika sudah ada memo
- Mengimplementasikan old() helper untuk mempertahankan nilai input saat validation error
- Menambahkan placeholder text "Masukkan total pembukuan" untuk user guidance
2025-07-30 11:17:10 +07:00
Daeng Deni Mardaeni
4d8b72e33a fix datatable noc 2025-07-22 09:09:19 +07:00
Daeng Deni Mardaeni
cf0059fe66 feat(memo): implementasi jenis penilaian dinamis dan perbaikan checkbox pada memo penyelesaian
Perubahan yang dilakukan:
- Menghapus validasi input pada method `preview()` untuk mendukung fleksibilitas data preview
- Mengganti eager loading dari `tujuanPenilaian` menjadi `jenisPenilaian` agar sesuai dengan kebutuhan data dinamis
- Menambahkan method chaining `->get()` pada akhir query untuk memastikan eksekusi query yang benar
- Menambahkan field `jenisPenilaian` ke dalam memoData agar template dapat menampilkan instruksi pembayaran secara dinamis
- Mengimplementasikan checkbox visual yang disabled dengan hidden input untuk tetap mengirim data saat form submit
- Memisahkan antara checkbox untuk tampilan (disabled) dan input data (hidden) untuk meningkatkan UX
- Mengubah lebar label dari 80px menjadi 200px pada template PDF dan preview untuk layout yang lebih baik
- Mengganti informasi jaminan menjadi statis "Tanah & Bangunan" pada template PDF dan preview
- Menambahkan conditional rendering untuk menampilkan instruksi pembayaran sesuai dengan jenis penilaian (Internal/KJPP)
- Menyesuaikan layout dan formatting pada template PDF dan preview agar lebih konsisten secara visual
- Menambahkan logika text dinamis untuk jenis penilaian pada bagian instruksi pembayaran
- Mengoptimalkan struktur query agar lebih efisien dan menghindari duplikasi

Tujuan perubahan:
- Mendukung proses memo penyelesaian dengan jenis penilaian yang lebih fleksibel (Internal/KJPP)
- Meningkatkan pengalaman pengguna dengan tampilan checkbox yang jelas namun tetap menyimpan data dengan aman
- Menyederhanakan layout dan formatting agar lebih profesional dan konsisten di preview maupun PDF
- Memastikan proses generate memo berjalan sesuai kebutuhan bisnis dengan instruksi pembayaran yang tepat
2025-07-17 16:05:56 +07:00
Daeng Deni Mardaeni
d7e5df569a feat(memo): tambah filter jenis penilaian dan perbaiki fungsi clear checkbox pada datatable memo
Perubahan yang dilakukan:
- Menambahkan dropdown filter "Jenis Penilaian" (Internal/External) pada halaman index memo penyelesaian
- Mengimplementasikan filter gabungan menggunakan separator '|' untuk kombinasi filter dan search
- Memperbaiki fungsi clearSelectedCheckboxes dengan selector '#memo-table thead input[type="checkbox"]' agar lebih spesifik
- Menambahkan event listener untuk filter jenis penilaian yang terintegrasi dengan fungsi search datatable
- Mengupdate MemoController untuk memisahkan parameter filter menjadi jenis penilaian dan search term menggunakan explode('|')
- Menambahkan log untuk debugging filter dan proses checkbox
- Memodifikasi tampilan dengan class `gap-2` pada container filter untuk spacing yang lebih baik
- Menambahkan console.log pada fungsi handleCheckboxChange untuk tracking interaksi user
- Memperbaiki error handling pada fungsi clearSelectedCheckboxes untuk memastikan checkbox ter-reset dengan benar
- Menampilkan jumlah item terpilih pada tombol "Create Memo" untuk meningkatkan feedback user

Tujuan perubahan:
- Mempermudah pengguna dalam memfilter data berdasarkan jenis penilaian langsung di datatable memo
- Memastikan fungsi checkbox selection berjalan dengan konsisten dan lebih robust
- Meningkatkan user experience dengan feedback visual yang lebih jelas saat filter dan selection digunakan
- Menyederhanakan interaksi user sekaligus menjaga akurasi proses pembuatan memo penyelesaian
2025-07-17 15:16:58 +07:00
Daeng Deni Mardaeni
cd46a3b0dc feat(memo): tambahkan kembali checkbox selection dengan status disabled
Perubahan yang dilakukan:
- Menambahkan kembali kolom checkbox "select-all" di header tabel dengan atribut disabled
- Menambahkan kembali input checkbox di setiap baris permohonan dengan status disabled
- Menjaga struktur tabel tetap konsisten dengan design system yang ada
- Mempertahankan value checkbox untuk kebutuhan form submission meskipun tidak interaktif
- Menggunakan class `checkbox-sm` untuk ukuran checkbox yang sesuai tampilan

Tujuan perubahan:
- Memberikan indikasi visual bahwa semua permohonan dianggap sudah terpilih secara otomatis
- Memastikan user memahami bahwa selection tidak perlu dilakukan secara manual
- Meningkatkan konsistensi UI sekaligus menjaga flow proses pembuatan memo penyelesaian
- Menyederhanakan interaksi user dengan feedback visual yang jelas tanpa menghilangkan elemen penting
2025-07-17 14:49:09 +07:00
Daeng Deni Mardaeni
3486b97aee refactor(memo): hapus checkbox selection
Perubahan yang dilakukan:
- Menghapus kolom checkbox "select-all" dari header tabel pada halaman create memo penyelesaian
- Menghapus input checkbox di setiap baris permohonan dalam tabel untuk menghilangkan fitur selection
- Menyederhanakan tampilan tabel menjadi daftar permohonan tanpa opsi pilih banyak
- Memperbaiki user experience dengan interface yang lebih bersih dan sederhana
- Mengurangi kompleksitas form dengan menghilangkan proses multiple selection
2025-07-17 14:44:00 +07:00
Daeng Deni Mardaeni
dc6e326122 refactor(memo): perbaiki indentasi dan komentar sementara filter NOC
Perubahan yang dilakukan:
- Memperbaiki indentasi pada loop foreach untuk update NOC dan permohonan agar lebih readable
- Mengomentari sementara kondisi whereHas('noc') pada dataForDatatables untuk keperluan pengujian
- Merapikan struktur kode pada method storeMemopenyelesaian agar lebih konsisten dan mudah dipahami
- Menambahkan konsistensi formatting pada blok kode update NOC untuk meningkatkan maintainability
- Memastikan logging tetap aktif untuk kebutuhan audit dan tracking perubahan data
- Menjaga integritas proses dengan tetap menggunakan database transaction (commit/rollback)

Tujuan perubahan:
- Meningkatkan keterbacaan dan konsistensi kode di MemoController
- Mempermudah proses debugging dan pengujian dengan menonaktifkan filter NOC sementara
- Memastikan struktur kode tetap rapih dan mudah dikelola tanpa mengubah logika bisnis utama
2025-07-17 14:28:21 +07:00
Daeng Deni Mardaeni
4459b70271 feat(memo): tambah tombol download PDF dan disable checkbox untuk memo selesai
- Menambahkan field memo_penyelesaian_pdf_path ke tabel noc untuk menyimpan path file PDF
- Membuat migrasi baru untuk menambahkan field PDF path ke tabel noc
- Menambahkan field memo_penyelesaian_pdf_path ke model Noc dalam fillable array
- Memodifikasi fungsi generatePdf di MemoController untuk menyimpan path PDF ke database
- Menambahkan route baru memo.download-pdf untuk download file PDF memo penyelesaian
- Membuat method downloadPdf di MemoController dengan validasi file dan error handling
- Memodifikasi kolom select di datatables untuk disable checkbox jika sudah ada memo
- Menambahkan tooltip pada checkbox yang disabled untuk memberikan informasi kepada user
- Memodifikasi kolom actions untuk menampilkan tombol download PDF jika memo sudah ada
- Menampilkan informasi nomor memo dan tanggal memo di kolom actions
- Memodifikasi fungsi handleCheckboxChange untuk mengabaikan checkbox yang disabled
- Menambahkan styling untuk tombol download dengan icon dan warna yang sesuai
- Menambahkan logging untuk tracking aktivitas download PDF memo penyelesaian
- Menambahkan validasi keberadaan file di storage sebelum mengizinkan download
- Menggunakan Storage facade untuk operasi file yang lebih aman dan konsisten
2025-07-17 13:47:53 +07:00
Daeng Deni Mardaeni
57dece449c feat(memo): filter datatables hanya tampilkan permohonan dengan NOC
Menambahkan filter pada datatables memo penyelesaian agar hanya menampilkan permohonan yang sudah memiliki NOC.

Perubahan yang dilakukan:
- Menambahkan kondisi whereHas('noc') pada query dataForDatatables
  - Memastikan hanya permohonan dengan relasi NOC yang ditampilkan
  - Filtering dilakukan langsung di level database untuk efisiensi
  - Konsisten dengan logika bisnis memo penyelesaian

- Update MemoController:
  - Memodifikasi fungsi dataForDatatables di baris 184-193
  - Menambahkan ->whereHas('noc') setelah kondisi filter existing
  - Mempertahankan semua parameter request dan struktur response yang sudah ada
  - Logging tetap aktif untuk kebutuhan audit dan monitoring

Tujuan perubahan:
- Menjamin konsistensi proses memo penyelesaian hanya pada permohonan yang sudah memiliki NOC
- Menghindari proses memo pada data yang belum lengkap
- Menjaga data integrity dengan relasi yang lebih jelas antara permohonan dan NOC
- Meningkatkan efisiensi query dengan filtering langsung di database
2025-07-17 13:21:41 +07:00
Daeng Deni Mardaeni
5e7368ebcf feat(memo): tambah field memo penyelesaian ke tabel NOC dan update generatePdf
Menambahkan penyimpanan data memo penyelesaian ke tabel NOC dan memperbarui fungsi generatePdf di MemoController agar lebih terintegrasi.

Perubahan yang dilakukan:
- Menambahkan migrasi untuk field baru di tabel NOC:
  - memo_penyelesaian_number: nomor memo penyelesaian
  - memo_penyelesaian_date: tanggal memo
  - memo_penyelesaian_payment_date: tanggal pembayaran
  - memo_penyelesaian_pdf_path: path file PDF memo
  - memo_penyelesaian_created_at: timestamp pembuatan memo

- Update model NOC:
  - Menambahkan field baru ke $fillable array untuk mass assignment
  - Menambahkan casting untuk field date dan datetime agar otomatis diconvert oleh Eloquent
  - Mempertahankan struktur model dan relasi yang sudah ada

- Update MemoController:
  - Mengubah proses penyimpanan memo dari tabel permohonan ke tabel NOC
  - Menambahkan pencarian NOC berdasarkan permohonan_id
  - Menyimpan semua informasi memo penyelesaian langsung ke NOC
  - Tetap memperbarui status permohonan agar proses bisnis tetap berjalan
  - Menambahkan logging untuk mempermudah monitoring dan debugging
  - Menggunakan DB transaction untuk menjaga konsistensi data

Struktur data memo penyelesaian:
- Disimpan secara terpusat di tabel NOC sebagai source of truth
- Memiliki relasi langsung dengan tabel permohonan untuk referensi data
- Menyimpan path PDF memo untuk akses file yang lebih mudah
- Menyediakan timestamp lengkap untuk kebutuhan audit trail

Tujuan perubahan:
- Memusatkan data memo penyelesaian di tabel NOC untuk kemudahan query dan reporting
- Menjamin konsistensi data dengan mekanisme transaction
- Memperjelas struktur relasi antara memo penyelesaian dan permohonan
- Memudahkan proses tracking, pelaporan, dan audit memo penyelesaian
2025-07-17 13:03:32 +07:00
Daeng Deni Mardaeni
cbdd4bd99e fix(memo): Sinkronisasi layout PDF memo dengan preview dan perbaikan section tanda tangan
Melakukan sinkronisasi tampilan memo penyelesaian antara preview dan PDF serta memperbaiki struktur layout bagian tanda tangan untuk kompatibilitas PDF.

Perubahan yang dilakukan:
- Menyesuaikan header dan logo dengan ukuran yang sama (53.55px) untuk preview dan PDF.
- Mengubah judul memo menjadi "Memo Instruksi Penyelesaian Rekening Escrow / KSL Penilai Jaminan".
- Menyesuaikan informasi memo (Kepada, Dari, Perihal) agar konsisten antara preview dan PDF.
- Menggunakan format tabel yang konsisten untuk detail memo dan lampiran.
- Menambahkan daftar lampiran dengan kolom Nomor Registrasi dan AO sesuai format preview.
- Mengimplementasikan fungsi `terbilang()` untuk konversi angka ke teks dalam lampiran.
- Menyesuaikan font size, spacing, dan styling agar seragam di semua output.
- Menambahkan page break yang presisi antara halaman memo utama dan lampiran.

Perbaikan section tanda tangan:
- Mengganti struktur flexbox dengan table layout pada bagian tanda tangan untuk stabilitas PDF.
- Menggunakan 3 kolom dengan lebar 33.33% untuk pembagian tanda tangan yang rata.
- Menambahkan `vertical-align: top` untuk posisi tanda tangan yang konsisten secara vertikal.
- Menambahkan padding horizontal agar antar kolom tanda tangan tidak saling berdekatan.
- Memastikan garis tanda tangan tetap center dengan `margin: auto`.
- Mengoptimalkan layout agar kompatibel dengan berbagai PDF viewer dan environment printing.

Tujuan perubahan:
- Menjamin konsistensi tampilan antara preview di aplikasi dan file PDF yang dihasilkan.
- Memastikan dokumen tercetak rapi, profesional, dan sesuai dengan format dokumen resmi perbankan.
- Meningkatkan kompatibilitas layout untuk berbagai perangkat dan proses cetak.
2025-07-17 11:41:21 +07:00
Daeng Deni Mardaeni
d0cc62f8c0 feat(memo): Perbaiki tampilan preview memo dengan logo resmi dan format A4
Memperbaiki tampilan preview memo penyelesaian agar sesuai dengan standar dokumen resmi bank.

Perubahan yang dilakukan:
- Mengganti logo placeholder AGI dengan logo resmi Bank Artha Graha Internasional.
- Mengubah layout dari card-based menjadi full-width format A4 untuk preview, print, dan PDF.
- Menambahkan header controls dengan class `no-print` agar tersembunyi saat dicetak atau di-generate PDF.
- Memperbaiki struktur header bank dengan logo dan nama perusahaan yang proper.
- Menyesuaikan judul memo menjadi lebih compact dan professional.
- Memperbaiki struktur tabel informasi memo dengan penggunaan inline styling untuk konsistensi PDF.
- Menambahkan helper `dateFormat()` untuk penulisan tanggal yang lebih rapi.
- Memperbaiki posisi tanda tangan agar lebih proporsional dan sesuai format resmi.
- Menambahkan class `page-memo` untuk styling halaman memo secara khusus.
- Menyempurnakan responsive design dengan flex layout yang lebih baik.
- Menambahkan path logo: `assets/media/images/logo-arthagraha.png` untuk keperluan cetak dan PDF.
- Memperbaiki typography dan spacing agar sesuai dengan standar dokumen resmi.
- Menambahkan kontrol print-friendly menggunakan class `no-print`.

Tujuan perubahan:
- Menjamin tampilan memo penyelesaian sesuai standar corporate identity bank.
- Memastikan output PDF dan print preview konsisten dengan dokumen resmi.
- Meningkatkan profesionalitas tampilan dan mempermudah proses administrasi.
2025-07-17 10:30:02 +07:00
Daeng Deni Mardaeni
0c2c0c9e20 feat(memo): Tambah halaman preview dan PDF memo penyelesaian
Menambahkan fitur preview memo penyelesaian sebelum data disimpan ke database, untuk memastikan user dapat memeriksa kembali detail permohonan dan biaya terkait.

Perubahan pada Controller:
- Menambahkan method `preview()` di MemoController untuk menampilkan halaman preview memo penyelesaian.
- Menambahkan method `generatePdf()` untuk menghasilkan PDF resmi memo dengan format sesuai standar bank.
- Mengimplementasikan validasi input lengkap sebelum proses preview dan PDF generation.
- Mengintegrasikan helper fungsi terbilang untuk konversi total biaya ke dalam format huruf.

Perubahan pada View:
- Menambahkan view `preview.blade.php` sebagai template resmi memo penyelesaian dengan layout AGI header, informasi memo, daftar permohonan, total biaya PJ, dan tanda tangan.
- Menampilkan tabel daftar permohonan terlampir dengan informasi debitur, cabang, AO, dan nominal biaya PJ.
- Menyediakan fitur print preview dengan styling khusus untuk pencetakan dokumen resmi.
- Menambahkan UI interaktif untuk mempermudah navigasi sebelum menyimpan data.

Perubahan pada Form Create:
- Mengubah alur form `create.blade.php` agar mengarahkan ke halaman preview terlebih dahulu sebelum penyimpanan.
- Menambahkan tombol untuk melanjutkan proses ke simpan atau kembali ke pengisian form.

Routing dan Navigasi:
- Menambahkan route baru `memo.preview` untuk preview dan `memo.generate-pdf` untuk generate PDF memo penyelesaian.
- Menambahkan breadcrumb untuk halaman preview agar navigasi lebih jelas.

Fitur Tambahan:
- Implementasi JavaScript untuk interaksi form preview, print action, dan konfirmasi user.
- Penambahan fitur terbilang untuk memudahkan verifikasi nominal dalam format teks.
- Penanganan error secara komprehensif dengan logging dan rollback pada transaksi jika terjadi kesalahan.
- Menjamin data tetap konsisten meskipun user hanya melakukan preview tanpa menyimpan.

Tujuan Perubahan:
- Memberikan fasilitas preview agar user dapat memverifikasi data sebelum menyimpan memo penyelesaian.
- Memastikan output memo dalam format PDF resmi yang sesuai dengan template bank.
- Meningkatkan user experience dan mengurangi potensi kesalahan input sebelum proses finalisasi.
- Mempermudah proses cetak memo dengan fitur print-friendly dan PDF yang siap dikirim atau diarsipkan.
2025-07-17 09:55:06 +07:00
Daeng Deni Mardaeni
274addb069 refactor(memo): Sesuaikan struktur form dan tambahkan kalkulasi Total Biaya PJ
Melakukan refactor dan penyesuaian form memo penyelesaian agar lebih sesuai dengan kebutuhan bisnis, serta menambahkan fitur perhitungan otomatis Total Biaya PJ.

Perubahan pada Form Input:
- Mengubah field "Judul Memo" menjadi "Nomor Memo" untuk identifikasi memo yang lebih spesifik.
- Menghapus field "Isi Memo" karena tidak relevan dengan proses bisnis saat ini.
- Menambahkan field "Tanggal Pembayaran" untuk tracking proses pembayaran.
- Mengatur "Tanggal Memo" menjadi otomatis mengikuti tanggal hari ini dan disimpan sebagai hidden field untuk keperluan audit.
- Menambahkan field readonly "Total Biaya PJ" untuk menampilkan akumulasi biaya dari NOC terkait permohonan yang dipilih.

Perubahan pada Controller:
- Mengupdate validasi request agar sesuai dengan field baru: `memo_number`, `payment_date`, dan `permohonan_ids`.
- Menghapus validasi `memo_content` karena field tersebut tidak lagi digunakan.
- Menambahkan method `getTotalBiayaPJ()` untuk endpoint AJAX yang menghitung total biaya PJ secara real-time.
- Menggunakan relasi model `Noc` dan `Permohonan` untuk menghitung sum dari `nominal_bayar`.
- Mengupdate method `create()` agar langsung menghitung total biaya PJ saat form dibuka.
- Tetap menggunakan DB transaction untuk memastikan integritas data.

Perubahan pada Database Schema:
- Mengganti field `memo_penyelesaian_title` menjadi `memo_penyelesaian_number`.
- Menghapus field `memo_penyelesaian_content`.
- Menambahkan field baru `memo_penyelesaian_payment_date` untuk menyimpan tanggal pembayaran.
- Mempertahankan field `memo_penyelesaian_date` sebagai audit trail.

Perubahan pada View:
- Menambahkan field readonly "Total Biaya PJ" dengan format mata uang Rupiah.
- Menambahkan icon kalkulator dan styling sesuai dengan tema form.
- Menggunakan AJAX untuk menghitung total biaya PJ ketika user memilih atau mengubah permohonan secara dinamis.
- Menampilkan pesan error dan feedback user secara jelas jika terjadi masalah saat perhitungan.

Routing dan API:
- Menambahkan route `memo.total-biaya-pj` sebagai endpoint untuk kalkulasi biaya PJ berbasis AJAX.
- Tetap menggunakan route resource untuk operasi CRUD memo penyelesaian.

Keamanan dan Validasi:
- Implementasi CSRF protection untuk AJAX request.
- Validasi `permohonan_ids` harus berupa array yang valid dan terfilter dengan baik.
- Penanganan error yang komprehensif baik di sisi controller maupun client-side.

Peningkatan User Experience:
- Form menjadi lebih sederhana, efisien, dan fokus pada input yang memang dibutuhkan oleh proses bisnis.
- Real-time feedback saat memilih permohonan sehingga user langsung mengetahui total biaya PJ.
- Layout form tetap responsive dan mudah digunakan di berbagai perangkat.
- Memberikan pengalaman yang konsisten dengan desain aplikasi lainnya.

Tujuan Perubahan:
- Menyederhanakan proses pembuatan memo penyelesaian sesuai kebutuhan operasional terbaru.
- Memastikan proses input lebih cepat dan akurat dengan kalkulasi otomatis Total Biaya PJ.
- Mengurangi potensi kesalahan input dan mempercepat workflow divisi terkait.
- Meningkatkan maintainability dan konsistensi kode dengan standar sistem yang ada.
2025-07-17 09:41:20 +07:00
Daeng Deni Mardaeni
5c3a93c49b feat(lpj): implementasi fitur Memo Penyelesaian dengan controller, view, dan routing lengkap
Perubahan yang dilakukan:

**Controller MemoController:**
- Menambahkan MemoController untuk mengelola memo penyelesaian permohonan.
- Method index() untuk menampilkan daftar permohonan yang bisa dipilih.
- Method create() untuk form pembuatan memo dengan pemilihan data bulk.
- Method store() untuk menyimpan memo dan mengupdate status permohonan terkait.
- Method show() untuk menampilkan detail memo yang telah dibuat.
- Method dataForDatatables() untuk API datatables dengan filter, search, dan pagination.
- Implementasi DB transaction untuk menjaga integritas data.
- Logging dan error handling komprehensif di setiap method.

**View Template:**
- index.blade.php: Tabel data permohonan dengan fitur checkbox selection (bulk).
- create.blade.php: Form pembuatan memo dari data yang dipilih.
- show.blade.php: Halaman detail memo penyelesaian.
- Menggunakan Bootstrap untuk styling dan interaksi dinamis dengan JavaScript.
- Validasi client-side untuk memastikan data sesuai sebelum dikirim.

**Routing dan Navigasi:**
- Menambahkan route resource untuk operasi CRUD Memo.
- Menambahkan route khusus untuk datatables API dan bulk create.
- Integrasi menu "Memo Penyelesaian" di navigasi utama aplikasi.
- Role-based access control untuk keamanan akses fitur.

**Integrasi Data:**
- Menggunakan model Permohonan sebagai sumber data utama dengan eager loading.
- Relasi dengan tabel user, debitur, branch, dan tujuan penilaian.
- Menambahkan status management untuk mempermudah tracking progress permohonan.

**Keamanan dan Validasi:**
- Validasi input baik di sisi controller maupun client-side.
- CSRF protection dan XSS prevention untuk menjaga keamanan aplikasi.
- Permission checking sesuai level user.

**Performance dan UX:**
- Pagination dan query optimization untuk performa lebih baik.
- Caching strategi untuk data yang sering diakses.
- Interface yang intuitif, dengan loading state dan feedback message.
- Responsive design untuk desktop dan mobile.
- Shortcut keyboard untuk efisiensi power user.

**Teknis dan Testing:**
- Struktur kode mengikuti Laravel best practice dan design pattern.
- Siap untuk unit test dan integration test.
- Logging lengkap untuk monitoring dan debugging.
- Error scenario handling dan fallback yang robust.

Tujuan perubahan:
- Menyediakan fitur pengelolaan memo penyelesaian permohonan secara bulk dengan user experience yang optimal dan performa efisien.
2025-07-15 10:05:22 +07:00
putrakuningan
36ac1370d7 Merge pull request 'feature/senior-officer' (#146) from feature/senior-officer into staging
Reviewed-on: #146
2025-07-14 09:08:54 +07:00
majid
3eb402ae08 feat: add export functionality and simplify laporan user report
refactor: update laporan user service and views for simplified report
style: improve daftar pustaka views and remove unused code
fix: correct date field names in laporan sla penilai service
2025-07-14 09:08:54 +07:00
majid
07589370df fix(activity): highlight table row in red when status is "freeze" 2025-07-14 09:08:54 +07:00
majid
8220466f03 feat(daftar-pustaka) : tambah daftar pustaka dan categori pustaka, lihat detail, tambah, edit 2025-07-14 09:08:54 +07:00
majid
5c57b9cb58 feat(laporan): tambah tanggal reported, kunjungan, laporan 2025-07-14 09:08:54 +07:00
majid
a72515bc30 feat(print-out): tambah tanggal otorisasi di bagian tanda tangan 2025-07-14 09:08:54 +07:00
246 changed files with 27180 additions and 7407 deletions

View File

@@ -32,6 +32,8 @@ class LaporanAdminKreditExport implements WithColumnFormatting, WithHeadings, Fr
$row->nilai_pasar_wajar,
$row->nilai_likuidasi,
$row->nama_penilai,
$row->kolektibilitas,
$row->keterangan,
$row->created_at
];
}
@@ -52,6 +54,8 @@ class LaporanAdminKreditExport implements WithColumnFormatting, WithHeadings, Fr
'Nilai Pasar Wajar',
'Nilai Likuidasi',
'Nama Penilai',
'Kolektibilitas',
'Keterangan',
'Created At'
];
}
@@ -64,7 +68,9 @@ class LaporanAdminKreditExport implements WithColumnFormatting, WithHeadings, Fr
'J' => NumberFormat::FORMAT_DATE_DDMMYYYY,
'K' => NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1,
'L' => NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1,
'M' => NumberFormat::FORMAT_DATE_DATETIME,
'N' => NumberFormat::FORMAT_TEXT, // Kolektibilitas
'O' => NumberFormat::FORMAT_TEXT, // Keterangan
'P' => NumberFormat::FORMAT_DATE_DATETIME, // Created At (moved from M to P)
];
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Modules\Lpj\Exports;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class LaporanSlikExport implements FromQuery, WithHeadings, WithMapping, WithStyles
{
protected $query;
public function __construct($query)
{
$this->query = $query;
}
public function query()
{
return $this->query;
}
public function headings(): array
{
return [
'Sandi Bank',
'Kode Kantor',
'Kode Cabang',
'Tahun',
'Bulan',
'No Rekening',
'CIF',
'Nama Debitur',
'NPWP',
'No KTP',
'No Telp',
'Alamat',
'Kode Pos',
'Kode Kab/Kota',
'Kode Negara Domisili',
'Kode Jenis',
'Kode Sifat',
'Kode Valuta',
'Baki Debet',
'Kolektibilitas',
'Tanggal Mulai',
'Tanggal Jatuh Tempo',
'Tanggal Selesai',
'Tanggal Restrukturisasi',
'Kode Sebab Macet',
'Tanggal Macet',
'Kode Kondisi',
'Tanggal Kondisi',
'Nilai Agunan',
'Jenis Agunan',
'Kode Agunan',
'Peringkat Agunan',
'Fasilitas',
'Status Agunan',
'Tanggal Lapor',
'Status',
'Tanggal Dibuat',
];
}
public function map($laporanSlik): array
{
return [
$laporanSlik->sandi_bank,
$laporanSlik->kode_kantor,
$laporanSlik->kode_cabang,
$laporanSlik->tahun,
$laporanSlik->bulan,
$laporanSlik->no_rekening,
$laporanSlik->cif,
$laporanSlik->nama_debitur,
$laporanSlik->npwp,
$laporanSlik->no_ktp,
$laporanSlik->no_telp,
$laporanSlik->alamat,
$laporanSlik->kode_pos,
$laporanSlik->kode_kab_kota,
$laporanSlik->kode_negara_domisili,
$laporanSlik->kode_jenis,
$laporanSlik->kode_sifat,
$laporanSlik->kode_valuta,
$laporanSlik->baki_debet,
$laporanSlik->kolektibilitas,
$laporanSlik->tanggal_mulai,
$laporanSlik->tanggal_jatuh_tempo,
$laporanSlik->tanggal_selesai,
$laporanSlik->tanggal_restrukturisasi,
$laporanSlik->kode_sebab_macet,
$laporanSlik->tanggal_macet,
$laporanSlik->kode_kondisi,
$laporanSlik->tanggal_kondisi,
$laporanSlik->nilai_agunan,
$laporanSlik->jenis_agunan,
$laporanSlik->kode_agunan,
$laporanSlik->peringkat_agunan,
$laporanSlik->fasilitas,
$laporanSlik->status_agunan,
$laporanSlik->tanggal_lapor,
$laporanSlik->status,
$laporanSlik->created_at->format('d/m/Y H:i'),
];
}
public function styles(Worksheet $sheet)
{
return [
1 => ['font' => ['bold' => true]],
];
}
}

View File

@@ -0,0 +1,232 @@
<?php
namespace Modules\Lpj\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithCustomStartCell;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\AfterSheet;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\Branch;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class LaporanUserLimitExport implements FromCollection, WithHeadings, WithMapping, WithTitle, WithCustomStartCell, WithEvents
{
protected $request;
public function __construct($request)
{
$this->request = $request;
}
public function collection()
{
$query = Permohonan::query();
$query = $query->where('status', 'done');
// Apply date range filter if provided
if ($this->request->has('start_date') || $this->request->has('end_date')) {
$startDate = $this->request->start_date ?? '1900-01-01';
$endDate = $this->request->end_date ?? now()->toDateString();
$query->where(function ($q) use ($startDate, $endDate) {
$q->whereHas('penilaian', function ($q2) use ($startDate, $endDate) {
$q2->whereBetween('tanggal_kunjungan', [$startDate, $endDate]);
});
// OR check if has penawaran with date in range
$q->orWhereHas('penawaran', function ($q3) use ($startDate, $endDate) {
$q3->whereBetween('tanggal_penilaian_sebelumnya', [$startDate, $endDate]);
});
});
}
// Apply branch filter if provided
if ($this->request->has('branch_id') && !empty($this->request->branch_id)) {
$query->where('branch_id', $this->request->branch_id);
}
if ($this->request->has('penilai_id') && !empty($this->request->penilai_id)) {
$request = $this->request; // Store in a local variable
$query->whereHas('penilaian._user_penilai.userPenilaiTeam', function ($q) use ($request) {
$q->where('user_id', $request->penilai_id);
});
}
// Apply search filter if provided
if ($this->request->has('search') && !empty($this->request->search)) {
$search = $this->request->search;
$query->where(function ($q) use ($search) {
$q->where('nomor_registrasi', 'LIKE', '%' . $search . '%');
$q->orWhere('tanggal_permohonan', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('user', 'name', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('tujuanPenilaian', 'name', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('debiture', DB::raw('LOWER(name)'), 'LIKE', '%' . strtolower($search) . '%');
$q->orWhereRelation('branch', 'name', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('jenisFasilitasKredit', 'name', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('jenisPenilaian', 'name', 'LIKE', '%' . $search . '%');
$q->orWhere('status', 'LIKE', '%' . $search . '%');
});
}
// Default ordering
$query->orderBy('nomor_registrasi', 'asc');
return $query->get();
}
protected $rowNumber = 0;
public function map($permohonan): array
{
$this->rowNumber++;
$npw = 0;
if (isset($permohonan->penilai->lpj)) {
$lpj = json_decode($permohonan->penilai->lpj, true);
$npw = str_replace('.', '', $lpj['total_nilai_pasar_wajar'] ?? 0);
}
return [
$this->rowNumber,
$permohonan->nomor_registrasi,
$permohonan->debiture->branch->name ?? '',
$permohonan->debiture->name ?? '',
$permohonan->user->name ?? $permohonan->mig_nama_ao ?? '',
$permohonan->tanggal_permohonan ?? '',
$permohonan->penilaian->_user_penilai->userPenilaiTeam->name ?? '',
$permohonan->penilaian && $permohonan->penilaian->tanggal_kunjungan
? formatTanggalIndonesia($permohonan->penilaian->tanggal_kunjungan)
: '',
formatRupiah($npw, 2),
];
}
public function headings(): array
{
return [
'No',
'Nomor Registrasi',
'Cabang',
'Nama Debitur',
'Pemohon',
'Tanggal Permohonan',
'Nama Penilai',
'Tanggal Laporan',
'Nilai Pasar Wajar',
];
}
/**
* @return string
*/
public function title(): string
{
return 'Laporan User Limit';
}
/**
* @return string
*/
public function startCell(): string
{
return 'A7';
}
/**
* @return array
*/
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
// Get the sheet
$sheet = $event->sheet->getDelegate();
// Set the title
$sheet->setCellValue('A1', 'LAPORAN PENILAIAN JAMINAN');
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(16);
// Merge cells for title
$sheet->mergeCells('A1:AH1');
$sheet->getStyle('A1')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
// Set the branch information if filtered
$branchInfo = '';
if ($this->request->has('branch_id') && !empty($this->request->branch_id)) {
$branch = Branch::find($this->request->branch_id);
if ($branch) {
$branchInfo = 'Cabang: ' . $branch->name;
$sheet->setCellValue('A2', $branchInfo);
$sheet->mergeCells('A2:AH2');
$sheet->getStyle('A2')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
$sheet->getStyle('A2')->getFont()->setBold(true);
}
}
// Set the period
$startDate = $this->request->start_date ?? '';
$endDate = $this->request->end_date ?? '';
$rowIndex = $branchInfo ? 3 : 2;
if ($startDate && $endDate) {
$startDateFormatted = Carbon::parse($startDate)->format('d F Y');
$endDateFormatted = Carbon::parse($endDate)->format('d F Y');
$sheet->setCellValue('A' . $rowIndex, 'Periode: ' . $startDateFormatted . ' - ' . $endDateFormatted);
} else {
$sheet->setCellValue('A' . $rowIndex, 'Periode: Semua Data');
}
$sheet->mergeCells('A' . $rowIndex . ':AH' . $rowIndex);
$sheet->getStyle('A' . $rowIndex)->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
// Set the date of export
$rowIndex++;
$sheet->setCellValue('A' . $rowIndex, 'Tanggal Export: ' . Carbon::now()->format('d F Y H:i:s'));
// Set the user who exported
$rowIndex++;
$userName = Auth::user() ? Auth::user()->name : 'System';
$sheet->setCellValue('A' . $rowIndex, 'Diexport oleh: ' . $userName);
// Add a blank line
$rowIndex++;
$sheet->setCellValue('A' . $rowIndex, '');
// Style the header row
$headerRange = 'A7:' . $sheet->getHighestColumn() . '7';
$sheet->getStyle($headerRange)->getFont()->setBold(true);
$sheet->getStyle($headerRange)->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->getStartColor()->setARGB('FFCCCCCC');
// Auto-size columns - fixed to handle columns beyond Z
$highestColumn = $sheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
for ($i = 1; $i <= $highestColumnIndex; $i++) {
$currentColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i);
$sheet->getColumnDimension($currentColumn)->setAutoSize(true);
}
// Add borders to all cells with data
$dataRange = 'A7:' . $sheet->getHighestColumn() . $sheet->getHighestRow();
$sheet->getStyle($dataRange)->getBorders()->getAllBorders()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);
// Center align the header row
$sheet->getStyle($headerRange)->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
// Set text wrap for header cells
$sheet->getStyle($headerRange)->getAlignment()->setWrapText(true);
},
];
}
}

View File

@@ -24,6 +24,7 @@
$row->id,
$row->code,
$row->name,
$row->biaya,
$row->created_at
];
}
@@ -35,6 +36,7 @@
'ID',
'Code',
'Name',
'Biaya',
'Created At'
];
}
@@ -44,7 +46,8 @@
{
return [
'A' => NumberFormat::FORMAT_NUMBER,
'D' => NumberFormat::FORMAT_DATE_DATETIME
'D' => NumberFormat::FORMAT_NUMBER_00,
'E' => NumberFormat::FORMAT_DATE_DATETIME
];
}
}

185
app/Exports/SlikExport.php Normal file
View File

@@ -0,0 +1,185 @@
<?php
namespace Modules\Lpj\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Modules\Lpj\Models\Slik;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
/**
* Export class untuk data SLIK (Sistem Layanan Informasi Keuangan)
*
* Class ini menangani export data SLIK ke format Excel dengan:
* - Mapping data sesuai struktur SLIK
* - Format kolom yang sesuai (text, number, date)
* - Header yang informatif
*/
class SlikExport implements WithColumnFormatting, WithHeadings, FromCollection, WithMapping
{
/**
* Mengambil collection data SLIK untuk di-export
*
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return Slik::orderBy('created_at', 'desc')->get();
}
/**
* Mapping data SLIK untuk setiap baris dalam Excel
*
* @param \Modules\Lpj\Models\Slik $row
* @return array
*/
public function map($row): array
{
return [
$row->id, // A - ID
$row->sandi_bank, // B - Sandi Bank
$row->tahun, // C - Tahun
$row->bulan, // D - Bulan
$row->flag_detail, // E - Flag Detail
$row->kode_register_agunan, // F - Kode Register Agunan
$row->no_rekening, // G - No Rekening
$row->cif, // H - CIF
$row->kolektibilitas, // I - Kolektibilitas
$row->fasilitas, // J - Fasilitas
$row->jenis_segmen_fasilitas, // K - Jenis Segmen Fasilitas
$row->status_agunan, // L - Status Agunan
$row->jenis_agunan, // M - Jenis Agunan
$row->peringkat_agunan, // N - Peringkat Agunan
$row->lembaga_pemeringkat, // O - Lembaga Pemeringkat
$row->jenis_pengikatan, // P - Jenis Pengikatan
$row->tanggal_pengikatan, // Q - Tanggal Pengikatan
$row->nama_pemilik_agunan, // R - Nama Pemilik Agunan
$row->bukti_kepemilikan, // S - Bukti Kepemilikan
$row->alamat_agunan, // T - Alamat Agunan
$row->lokasi_agunan, // U - Lokasi Agunan
$row->nilai_agunan, // V - Nilai Agunan
$row->nilai_agunan_menurut_ljk, // W - Nilai Agunan Menurut LJK
$row->tanggal_penilaian_ljk, // X - Tanggal Penilaian LJK
$row->nilai_agunan_penilai_independen, // Y - Nilai Agunan Penilai Independen
$row->nama_penilai_independen, // Z - Nama Penilai Independen
$row->tanggal_penilaian_penilai_independen, // AA - Tanggal Penilaian Penilai Independen
$row->jumlah_hari_tunggakan, // AB - Jumlah Hari Tunggakan
$row->status_paripasu, // AC - Status Paripasu
$row->prosentase_paripasu, // AD - Prosentase Paripasu
$row->status_kredit_join, // AE - Status Kredit Join
$row->diasuransikan, // AF - Diasuransikan
$row->keterangan, // AG - Keterangan
$row->kantor_cabang, // AH - Kantor Cabang
$row->operasi_data, // AI - Operasi Data
$row->kode_cabang, // AJ - Kode Cabang
$row->nama_debitur, // AK - Nama Debitur
$row->nama_cabang, // AL - Nama Cabang
$row->flag, // AM - Flag
$row->created_at, // AN - Created At
];
}
/**
* Header kolom untuk Excel
*
* @return array
*/
public function headings(): array
{
return [
'ID',
'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',
'Created At',
];
}
/**
* Format kolom untuk Excel
*
* @return array
*/
public function columnFormats(): array
{
return [
'A' => NumberFormat::FORMAT_NUMBER, // ID
'B' => NumberFormat::FORMAT_TEXT, // Sandi Bank
'C' => NumberFormat::FORMAT_TEXT, // Tahun
'D' => NumberFormat::FORMAT_TEXT, // Bulan
'E' => NumberFormat::FORMAT_TEXT, // Flag Detail
'F' => NumberFormat::FORMAT_TEXT, // Kode Register Agunan
'G' => NumberFormat::FORMAT_TEXT, // No Rekening
'H' => NumberFormat::FORMAT_TEXT, // CIF
'I' => NumberFormat::FORMAT_TEXT, // Kolektibilitas
'J' => NumberFormat::FORMAT_TEXT, // Fasilitas
'K' => NumberFormat::FORMAT_TEXT, // Jenis Segmen Fasilitas
'L' => NumberFormat::FORMAT_TEXT, // Status Agunan
'M' => NumberFormat::FORMAT_TEXT, // Jenis Agunan
'N' => NumberFormat::FORMAT_TEXT, // Peringkat Agunan
'O' => NumberFormat::FORMAT_TEXT, // Lembaga Pemeringkat
'P' => NumberFormat::FORMAT_TEXT, // Jenis Pengikatan
'Q' => NumberFormat::FORMAT_DATE_DDMMYYYY, // Tanggal Pengikatan
'R' => NumberFormat::FORMAT_TEXT, // Nama Pemilik Agunan
'S' => NumberFormat::FORMAT_TEXT, // Bukti Kepemilikan
'T' => NumberFormat::FORMAT_TEXT, // Alamat Agunan
'U' => NumberFormat::FORMAT_TEXT, // Lokasi Agunan
'V' => NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1, // Nilai Agunan
'W' => NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1, // Nilai Agunan Menurut LJK
'X' => NumberFormat::FORMAT_DATE_DDMMYYYY, // Tanggal Penilaian LJK
'Y' => NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1, // Nilai Agunan Penilai Independen
'Z' => NumberFormat::FORMAT_TEXT, // Nama Penilai Independen
'AA' => NumberFormat::FORMAT_DATE_DDMMYYYY, // Tanggal Penilaian Penilai Independen
'AB' => NumberFormat::FORMAT_NUMBER, // Jumlah Hari Tunggakan
'AC' => NumberFormat::FORMAT_TEXT, // Status Paripasu
'AD' => NumberFormat::FORMAT_PERCENTAGE_00, // Prosentase Paripasu
'AE' => NumberFormat::FORMAT_TEXT, // Status Kredit Join
'AF' => NumberFormat::FORMAT_TEXT, // Diasuransikan
'AG' => NumberFormat::FORMAT_TEXT, // Keterangan
'AH' => NumberFormat::FORMAT_TEXT, // Kantor Cabang
'AI' => NumberFormat::FORMAT_TEXT, // Operasi Data
'AJ' => NumberFormat::FORMAT_TEXT, // Kode Cabang
'AK' => NumberFormat::FORMAT_TEXT, // Nama Debitur
'AL' => NumberFormat::FORMAT_TEXT, // Nama Cabang
'AM' => NumberFormat::FORMAT_TEXT, // Flag
'AN' => NumberFormat::FORMAT_DATE_DATETIME, // Created At
];
}
}

View File

@@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\Location\Models\City;
use Modules\Location\Models\District;
use Modules\Location\Models\Province;
@@ -15,29 +18,117 @@
use Modules\Lpj\Models\Penilaian;
use Modules\Lpj\Models\TeamsUsers;
use Modules\Usermanagement\Models\User;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
function formatTanggalIndonesia($date, $time = false)
/**
* Format tanggal ke dalam format Bahasa Indonesia
*
* Mengubah tanggal menjadi format yang lebih mudah dibaca dalam Bahasa Indonesia.
* Contoh: "15 Januari 2024" atau "15 Januari 2024 pukul 14.30 WIB"
*
* @param string|mixed $date Tanggal yang akan diformat (string tanggal atau null)
* @param bool $time Apakah akan menampilkan waktu juga (default: false)
* @return string Tanggal yang sudah diformat dalam Bahasa Indonesia
*
* @example
* formatTanggalIndonesia('2024-01-15') // "15 Januari 2024"
* formatTanggalIndonesia('2024-01-15 14:30:00', true) // "15 Januari 2024 pukul 14.30 WIB"
* formatTanggalIndonesia(null) // ""
* formatTanggalIndonesia('invalid-date') // "invalid-date" (return as-is jika error)
*/
function formatTanggalIndonesia($date, $time = false): string
{
Carbon::setLocale('id');
try {
$waktu = Carbon::parse($date);
if (!$time) {
return $waktu->translatedFormat('d F Y');
}
return $waktu->translatedFormat('d F Y') . ' pukul ' . $waktu->format('H.i') . ' WIB';
} catch (Throwable $e) {
return $date;
Log::debug('Memulai format tanggal Indonesia', [
'date' => $date,
'time' => $time
]);
// Validasi input null atau kosong
if (empty($date)) {
Log::debug('Tanggal kosong, return empty string');
return '';
}
Carbon::setLocale('id');
try {
$waktu = Carbon::parse($date);
if (!$time) {
$result = $waktu->translatedFormat('d F Y');
Log::debug('Format tanggal berhasil', ['result' => $result]);
return $result;
}
$result = $waktu->translatedFormat('d F Y') . ' pukul ' . $waktu->format('H.i') . ' WIB';
Log::debug('Format tanggal dengan waktu berhasil', ['result' => $result]);
return $result;
} catch (Throwable $e) {
Log::warning('Gagal parse tanggal', [
'date' => $date,
'error' => $e->getMessage()
]);
// Return input as-is jika gagal parse
return (string) $date;
}
}
function formatRupiah($number, $decimals = 0)
/**
* Format angka ke dalam format mata uang Rupiah Indonesia
*
* Mengubah angka menjadi format mata uang Rupiah dengan pemisah ribuan
* dan menggunakan koma sebagai pemisah desimal sesuai standar Indonesia.
*
* @param int|float|string $number Angka yang akan diformat (bisa negatif)
* @param int $decimals Jumlah digit desimal (default: 0)
* @return string Angka yang sudah diformat dalam format Rupiah
*
* @example
* formatRupiah(1500000) // "Rp 1.500.000"
* formatRupiah(1500000.50, 2) // "Rp 1.500.000,50"
* formatRupiah(-500000) // "Rp -500.000"
* formatRupiah(0) // "Rp 0"
* formatRupiah(null) // "Rp 0"
*/
function formatRupiah($number, $decimals = 0, $withSymbol = true): string
{
$number = (float) $number;
return 'Rp ' . number_format($number, $decimals, ',', '.');
}
Log::debug('Memulai format Rupiah', [
'number' => $number,
'decimals' => $decimals,
'withSymbol' => $withSymbol
]);
// Handle null atau kosong
if ($number === null || $number === '') {
Log::debug('Number null atau kosong, return Rp 0');
return $withSymbol ? 'Rp 0' : '0';
}
// Remove dots if present
$number = str_replace('.', '', (string) $number);
// Konversi ke float dan handle error
try {
$number = (float) $number;
} catch (Throwable $e) {
Log::warning('Gagal konversi number ke float', [
'number' => $number,
'error' => $e->getMessage()
]);
return $withSymbol ? 'Rp 0' : '0';
}
// Validasi decimals
$decimals = max(0, (int) $decimals);
$formatted = number_format($number, $decimals, ',', '.');
$result = $withSymbol ? 'Rp ' . $formatted : $formatted;
Log::debug('Format Rupiah berhasil', ['result' => $result]);
return $result;
}
function formatAlamat($alamat)
{
@@ -96,8 +187,6 @@
$query->orWhereNull('dokumen_persetujuan');
},
)->get();
// $sql = DB::getQueryLog();
if (sizeof($query) > 0) {
$allow = false;
@@ -238,9 +327,6 @@
return $hasil;
}
// andy add
function hitungHariKerja($tanggalMulai, $tanggalSelesai)
{
$tanggalMulai = Carbon::parse($tanggalMulai)->startOfDay();
@@ -326,7 +412,8 @@
$day = str_pad(date('d'), 2, '0', STR_PAD_LEFT);
// Generate random numbers
$randomNumber = str_pad(mt_rand(0, pow(10, $randomLength) - 1), $randomLength, '0', STR_PAD_LEFT);
//$randomNumber = str_pad(mt_rand(0, pow(10, $randomLength) - 1), $randomLength, '0', STR_PAD_LEFT);
$randomNumber = sprintf('%0' . $randomLength . 'd', mt_rand(0, pow(10, $randomLength) - 1));
// Concatenate components to create the custom code
return $year . $month . $day . $randomNumber;
@@ -530,15 +617,161 @@
}
}
function formatNotifikasi($notifikasi)
{
$data = json_decode(json_encode($notifikasi->data));
$message = $data->message;
$data = $data->data;
$notifikasi = [
'title' => 'Permohonan : ' . $data->nomor_registrasi,
'message' => $message,
];
return $notifikasi;
function parsePembandingMigration($keterangan) {
$keterangan = preg_replace('/[-]{5,}/', '',$keterangan); // Hapus ------
$keterangan = preg_replace('/[.]{5,}/', '',$keterangan); // Hapus .....
$keterangan = preg_replace('/\s+/', ' ',$keterangan);
$keterangan = preg_replace('/\s*\n\s*/', "\n",$keterangan);
// Pecah teks per baris untuk diproses
$lines = explode("\n",$keterangan);
$cleaned = [];
foreach ($lines as $line) {
$line = trim($line);
if (!empty($line)) {
// Format angka dalam format Rp. 123.456.789
$line = preg_replace_callback('/Rp\.\s*([\d.,]+)/', function($matches) {
$angka = str_replace(['.', ','], '', $matches[1]);
return 'Rp. ' . number_format((int)$angka, 0, ',', '.');
}, $line);
// Jika ada tanda pagar (#), pisahkan menjadi baris baru
$line = str_replace('#', "\n#", $line);
$cleaned[] = $line;
}
}
return implode("\n", $cleaned);
}
/**
* get full path to internal storage file or external storage file
*
* @param string $path
* @return string
*/
function getFilePath($path)
{
// define base path external storage (use .env) example: 'F:\path\to\storage' in windows
$externalBase = env('EXTERNAL_STORAGE_BASE_PATH', 'F:LPJ/lpj/LPJ Gambar/001/');
$segments = explode('/', $path);
if(strtoupper($segments[0]) === 'SURVEYOR'){
$year = $segments[1];
$month = ucfirst(strtolower($segments[2]));
$date = $segments[3];
$code = $segments[4];
$file = $segments[5] ?? '';
$extenalFullpath = $externalBase . $year . '/' . $month . '/' . $date . '/' . $code . '/' . $file;
if(File::exists($extenalFullpath)){
return $extenalFullpath;
}
}
// if not found in external storage, try to find in internal storage
if (Storage::exists($path)) {
return Storage::url('app/' . $path);
}
return $path;
}
function parseTimestamp(?string $timestamp): ?string
{
if (!$timestamp) {
return null;
}
// Trim whitespace dan normalize
$timestamp = trim($timestamp);
// Log untuk debugging
Log::info('Mencoba parsing timestamp: "' . $timestamp . '"');
// Parsing dengan DateTime native PHP untuk lebih robust
try {
// Pattern untuk format d/m/Y H:i:s
if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})$/', $timestamp, $matches)) {
$day = (int) $matches[1];
$month = (int) $matches[2];
$year = (int) $matches[3];
$hour = (int) $matches[4];
$minute = (int) $matches[5];
$second = (int) $matches[6];
// Validasi nilai
if ($day >= 1 && $day <= 31 && $month >= 1 && $month <= 12 && $year >= 1900 && $year <= 2100 &&
$hour >= 0 && $hour <= 23 && $minute >= 0 && $minute <= 59 && $second >= 0 && $second <= 59) {
// Buat DateTime object langsung
$dateTime = new \DateTime();
$dateTime->setDate($year, $month, $day);
$dateTime->setTime($hour, $minute, $second);
$result = $dateTime->format('Y-m-d H:i:s');
Log::info('Berhasil parsing dengan DateTime: ' . $timestamp . ' -> ' . $result);
return $result;
}
}
// Pattern untuk format d/m/Y tanpa waktu
if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/', $timestamp, $matches)) {
$day = (int) $matches[1];
$month = (int) $matches[2];
$year = (int) $matches[3];
// Validasi nilai
if ($day >= 1 && $day <= 31 && $month >= 1 && $month <= 12 && $year >= 1900 && $year <= 2100) {
// Buat DateTime object langsung
$dateTime = new \DateTime();
$dateTime->setDate($year, $month, $day);
$dateTime->setTime(0, 0, 0);
$result = $dateTime->format('Y-m-d H:i:s');
Log::info('Berhasil parsing tanpa waktu dengan DateTime: ' . $timestamp . ' -> ' . $result);
return $result;
}
}
} catch (\Exception $e) {
Log::error('Gagal parsing dengan DateTime: ' . $timestamp . '. Error: ' . $e->getMessage());
}
// Fallback ke format Carbon standar untuk format lainnya
$formats = [
'Y-m-d H:i:s',
'Y-m-d',
'd-m-Y H:i:s',
'd-m-Y',
'j-n-Y H:i:s',
'j-n-Y',
];
foreach ($formats as $format) {
try {
$carbon = \Carbon\Carbon::createFromFormat($format, $timestamp);
if ($carbon && $carbon->format($format) === $timestamp) {
// Jika format tidak mengandung waktu, set ke awal hari
if (!str_contains($format, 'H:i:s')) {
$carbon = $carbon->startOfDay();
}
Log::info('Berhasil parsing dengan format ' . $format . ': ' . $timestamp . ' -> ' . $carbon->toDateTimeString());
return $carbon->toDateTimeString();
}
} catch (\Exception $e) {
// Lanjut ke format berikutnya
continue;
}
}
Log::error('Tidak dapat memparsing timestamp dengan format apapun: "' . $timestamp . '"');
return null;
}

View File

@@ -55,7 +55,7 @@ class ActivityController extends Controller
->when($teamId, fn ($q) => $q->where('id', $teamId));
})
->where('user_id', '!=', $user->id)
->whereHas('user.roles', fn ($q) => $q->whereIn('name', ['surveyor', 'surveyor-penilai']))
->whereHas('user.roles', fn ($q) => $q->whereIn('name', ['surveyor', 'surveyor-penilai','penilai']))
->get();
$teamId = is_array($teamId) ? $teamId : [$teamId];
@@ -306,6 +306,7 @@ class ActivityController extends Controller
$query = Permohonan::query();
// Apply search filter if provided
$query = $query->orderBy('nomor_registrasi', 'desc');
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
@@ -324,7 +325,9 @@ class ActivityController extends Controller
});
}
// Default sorting if no sort provided
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');

View File

@@ -0,0 +1,183 @@
<?php
namespace Modules\Lpj\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Exception;
use Modules\Lpj\Models\Debiture;
/**
* Controller untuk API pencarian debitur
* Digunakan untuk autocomplete search pada form pembayaran
*/
class DebiturController extends Controller
{
/**
* Pencarian debitur untuk autocomplete
*
* @param Request $request
* @return JsonResponse
*/
public function search(Request $request): JsonResponse
{
try {
// Log aktivitas pencarian
Log::info('API Debitur Search - Request', [
'query' => $request->get('q'),
'user_id' => Auth::id()
]);
$query = $request->get('q', '');
// Validasi minimal 2 karakter untuk pencarian
if (strlen($query) < 2) {
return response()->json([
'success' => false,
'message' => 'Minimal 2 karakter untuk pencarian',
'data' => []
], 400);
}
// Mulai database transaction
DB::beginTransaction();
try {
// Query pencarian debitur
// Asumsi tabel debitur dengan kolom: id, code, nama, alamat
$debiturs = Debiture::query()
->select('id', 'cif', 'name', 'address')
->whereAny(['cif','name'], 'LIKE', "%{$query}%")
->orderBy('name', 'asc')
->limit(20) // Batasi hasil maksimal 20
->get();
// Format data untuk TomSelect
$formattedData = $debiturs->map(function($debitur) {
return [
'id' => $debitur->id,
'kode_debitur' => $debitur->cif,
'name' => $debitur->name,
'address' => $debitur->address
];
});
DB::commit();
// Log hasil pencarian
Log::info('API Debitur Search - Success', [
'query' => $query,
'results_count' => $formattedData->count(),
'user_id' => Auth::id()
]);
return response()->json([
'success' => true,
'message' => 'Data debitur berhasil ditemukan',
'data' => $formattedData
]);
} catch (Exception $e) {
DB::rollback();
throw $e;
}
} catch (Exception $e) {
// Log error
Log::error('API Debitur Search - Error', [
'query' => $request->get('q'),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'user_id' => Auth::id()
]);
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat mencari data debitur',
'error' => config('app.debug') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* Get detail debitur berdasarkan code
*
* @param Request $request
* @return JsonResponse
*/
public function getByCode(Request $request): JsonResponse
{
try {
$code = $request->get('code');
if (empty($code)) {
return response()->json([
'success' => false,
'message' => 'Code debitur harus diisi',
'data' => null
], 400);
}
// Log aktivitas get detail
Log::info('API Debitur GetByCode - Request', [
'code' => $code,
'user_id' => Auth::id()
]);
DB::beginTransaction();
try {
$debitur = DB::table('debitur')
->select('id', 'code', 'nama', 'alamat', 'telepon', 'email')
->where('code', $code)
->where('status', 'aktif')
->first();
if (!$debitur) {
DB::rollback();
return response()->json([
'success' => false,
'message' => 'Debitur tidak ditemukan',
'data' => null
], 404);
}
DB::commit();
Log::info('API Debitur GetByCode - Success', [
'code' => $code,
'debitur_id' => $debitur->id,
'user_id' => Auth::id()
]);
return response()->json([
'success' => true,
'message' => 'Data debitur berhasil ditemukan',
'data' => $debitur
]);
} catch (Exception $e) {
DB::rollback();
throw $e;
}
} catch (Exception $e) {
Log::error('API Debitur GetByCode - Error', [
'code' => $request->get('code'),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'user_id' => Auth::id()
]);
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat mengambil data debitur',
'error' => config('app.debug') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Lpj\Imports\BucokImport;
use Modules\Lpj\Models\Bucok;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Controller untuk mengelola data Bucok
*
* @package Modules\Lpj\Http\Controllers
*/
class BucokController extends Controller
{
public $user;
/**
* Menampilkan halaman index bucok
*
* @return \Illuminate\View\View
*/
public function index()
{
return view('lpj::bucok.index');
}
/**
* Menampilkan detail bucok
*
* @param int $id
* @return \Illuminate\View\View
*/
public function show($id)
{
$bucok = Bucok::findOrFail($id);
return view('lpj::bucok.show', compact('bucok'));
}
/**
* Data untuk datatables dengan server-side processing
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function dataForDatatables(Request $request)
{
if (is_null($this->user) || !$this->user->can('bucok.view')) {
// abort(403, 'Sorry! You are not allowed to view bucok.');
}
// Retrieve data from the database
$query = Bucok::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('nomor_tiket', 'LIKE', "%$search%")
->orWhere('deskripsi', 'LIKE', "%$search%")
->orWhere('nomor_coa', 'LIKE', "%$search%")
->orWhere('nama_coa', 'LIKE', "%$search%")
->orWhere('cost_center', 'LIKE', "%$search%")
->orWhere('nama_sub_direktorat', 'LIKE', "%$search%")
->orWhere('nama_direktorat_cabang', 'LIKE', "%$search%")
->orWhere('penyelesaian', 'LIKE', "%$search%")
->orWhere('keterangan_gantung', 'LIKE', "%$search%");
});
}
// Apply date range filter
if ($request->has('start_date') && !empty($request->get('start_date'))) {
$query->whereDate('tanggal', '>=', $request->get('start_date'));
}
if ($request->has('end_date') && !empty($request->get('end_date'))) {
$query->whereDate('tanggal', '<=', $request->get('end_date'));
}
// 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 cost center filter
if ($request->has('cost_center') && !empty($request->get('cost_center'))) {
$query->byCostCenter($request->get('cost_center'));
}
// Apply completion status filter
if ($request->has('completion_status') && $request->get('completion_status') !== '') {
$isCompleted = $request->get('completion_status') == '1';
$query->byCompletionStatus($isCompleted);
}
// 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,
'no' => $item->no,
'tanggal' => $item->tanggal ? dateFormat($item->tanggal,true) : '-',
'bulan' => $item->bulan,
'tahun' => $item->tahun,
'nomor_tiket' => $item->nomor_tiket,
'nomor_coa' => $item->nomor_coa,
'nama_coa' => $item->nama_coa,
'deskripsi' => $item->deskripsi,
'nominal' => $item->nominal_formatted,
'penyelesaian' => $item->penyelesaian,
'umur_aging' => $item->umur_aging,
'cost_center' => $item->cost_center,
'nama_sub_direktorat' => $item->nama_sub_direktorat,
'nama_direktorat_cabang' => $item->nama_direktorat_cabang,
'tanggal_penyelesaian' => $item->tanggal_penyelesaian ? dateFormat($item->tanggal_penyelesaian,true) : '-',
'nominal_penyelesaian' => $item->nominal_penyelesaian_formatted,
'status_penyelesaian' => $item->status_penyelesaian,
'status_badge' => $item->status_badge,
'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 bucok dari Excel
*
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function import(Request $request)
{
$request->validate([
'file' => 'required|mimes:xlsx,xls,csv|max:10240' // Max 10MB
]);
DB::beginTransaction();
try {
Log::info('Importing Bucok data', ['user_id' => Auth::id(), 'filename' => $request->file('file')->getClientOriginalName()]);
$import = new BucokImport();
Excel::import($import, $request->file('file'));
$statistics = $import->getImportStatistics();
DB::commit();
Log::info('Bucok data imported successfully', ['user_id' => Auth::id()]);
return redirect()->back()->with('success', 'Data Bucok berhasil diimport. Total: ' . $statistics['total_processed'] . ', Created: ' . $statistics['created'] . ', Updated: ' . $statistics['updated'] . ', Skipped: ' . $statistics['skipped'] . ', Errors: ' . $statistics['errors']);
} catch (Exception $e) {
DB::rollback();
Log::error('Failed to import Bucok data', ['error' => $e->getMessage(), 'user_id' => Auth::id()]);
return redirect()->back()->with('error', 'Gagal import data: ' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Lpj\Models\CategoryDaftarPustaka;
use Modules\Lpj\Http\Requests\CategoryDaftarPustakaRequest;
class CategoryDaftarPustakaController extends Controller
{
public $user;
/**
* Display a listing of the resource.
*/
public function index()
{
return view('lpj::category-daftar-pustaka.index');
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('lpj::category-daftar-pustaka.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(CategoryDaftarPustakaRequest $request)
{
$validated = $request->validated();
if ($validated) {
try {
CategoryDaftarPustaka::create($validated);
return redirect()->route('category-daftar-pustaka.index')->with('success', 'Data Berhasil Disimpan');
} catch (\Throwable $th) {
return redirect()->route('category-daftar-pustaka.index')->with('error', $th->getMessage());
}
}
}
/**
* Show the specified resource.
*/
public function show($id)
{
$category = CategoryDaftarPustaka::where('id', $id)->first();
return view('lpj::category-daftar-pustaka.show', compact('category'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit($id)
{
return view('lpj::category-daftar-pustaka.create', ['category' => CategoryDaftarPustaka::where('id', $id)->first()]);
}
/**
* Update the specified resource in storage.
*/
public function update(CategoryDaftarPustakaRequest $request, $id)
{
$validated = $request->validated();
if ($validated) {
try {
CategoryDaftarPustaka::where('id', $id)->update($validated);
return redirect()->route('category-daftar-pustaka.index')->with('success', 'Data Berhasil Disimpan');
} catch (\Throwable $th) {
return redirect()->route('category-daftar-pustaka.index')->with('error', $th->getMessage());
}
}
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
{
try {
CategoryDaftarPustaka::where('id', $id)->delete();
return response()->json(['success' => true, 'message' => 'Data Berhasil Dihapus']);
} catch (\Throwable $th) {
return response()->json(['success' => false, 'message' => $th->getMessage()]);
}
}
public function dataForDatatables(Request $request)
{
if (is_null($this->user) || !$this->user->can('jenis_aset.view')) {
//abort(403, 'Sorry! You are not allowed to view users.');
}
// Retrieve data from the database
$query = CategoryDaftarPustaka::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('code', 'LIKE', "%$search%");
$q->orWhere('name', 'LIKE', "%$search%");
});
}
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// 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
$data = $query->get();
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
// Calculate the current page number
$currentPage = 0 + 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' => $data,
]);
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Lpj\Models\CategoryDaftarPustaka;
use Modules\Lpj\Services\DaftarPustakaService;
use Modules\Lpj\Http\Requests\DaftarPustakaRequest;
class DaftarPustakaController extends Controller
{
private $daftarPustaka;
public function __construct()
{
$this->daftarPustaka = app(DaftarPustakaService::class);
}
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$categories = CategoryDaftarPustaka::all();
$daftar_pustaka = $this->daftarPustaka->getAllDaftarPustaka($request);
return view('lpj::daftar-pustaka.index', [
'categories' => $categories,
'daftar_pustaka' => $daftar_pustaka,
'page' => $daftar_pustaka->currentPage(),
'pageCount' => $daftar_pustaka->lastPage(),
'limit' => $daftar_pustaka->perPage(),
'total' => $daftar_pustaka->total(),
]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
$categories = CategoryDaftarPustaka::all();
// dd($categories);
return view('lpj::daftar-pustaka.create', compact('categories'));
}
/**
* Store a newly created resource in storage.
*/
public function store(DaftarPustakaRequest $request)
{
$validate = $request->validated();
// dd($validate);
$file = $request->file('attachment');
if ($validate) {
try {
// Save to database
$this->daftarPustaka->storeDaftarPustaka($validate, $file);
return redirect()
->route('daftar-pustaka.index')
->with('success', 'Daftar Pustaka created successfully');
} catch (Exception $e) {
return redirect()
->route('daftar-pustaka.create')
->with('error', 'Failed to create daftar pustaka');
}
}
}
/**
* Show the specified resource.
*/
public function show($id)
{
$daftarPustaka = $this->daftarPustaka->getDaftarPustakaById($id);
$categories = CategoryDaftarPustaka::all();
return view('lpj::daftar-pustaka.show', compact('daftarPustaka', 'categories'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit($id)
{
$daftarPustaka = $this->daftarPustaka->getDaftarPustakaById($id);
$categories = CategoryDaftarPustaka::all();
return view('lpj::daftar-pustaka.create', compact('daftarPustaka', 'categories'));
}
/**
* Update the specified resource in storage.
*/
public function update(DaftarPustakaRequest $request, $id)
{
$validate = $request->validated();
if ($validate) {
try {
// Save to database
$file = $request->file('attachment');
$this->daftarPustaka->updateDaftarPustaka($validate, $file, $id);
return redirect()
->route('daftar-pustaka.index')
->with('success', 'Daftar Pustaka updated successfully');
} catch (Exception $e) {
return redirect()
->route('daftar-pustaka.create')
->with('error', 'Failed to update daftar pustaka');
}
}
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
{
try {
$this->daftarPustaka->deleteDaftarPustaka($id);
return response()->json(['success' => true, 'message' => 'Daftar Pustaka deleted successfully']);
} catch (Exception $e) {
return response()->json(['success' => false, 'message' => 'Failed to delete daftar pustaka']);
}
}
}

View File

@@ -129,7 +129,7 @@
// Retrieve data from the database
$query = Debiture::query();
if (!Auth::user()->hasAnyRole(['administrator'])) {
if (!Auth::user()->hasAnyRole(['administrator','admin'])) {
$query = $query->where('branch_id', Auth::user()->branch_id);
}

View File

@@ -406,7 +406,9 @@
// Remove values from $legalitasJaminan that are in $currentLegalitasJaminan
$legalitasJaminan = array_diff($legalitasJaminan, $currentLegalitasJaminan->pluck('code')->toArray());
$legalitasJaminan = is_array($legalitasJaminan)
? array_diff($legalitasJaminan, $currentLegalitasJaminan->pluck('code')->toArray())
: [];
$legalitas = JenisLegalitasJaminan::whereIn('code', $legalitasJaminan)->get();
}
@@ -441,7 +443,7 @@
try {
// Periksa apakah pengguna adalah admin
if (!auth()->user()->hasRole('administrator')) {
return response()->json(['success' => false, 'message' => 'Hanya administrator yang dapat menghapus dokumen jaminan'], 403);
//return response()->json(['success' => false, 'message' => 'Hanya administrator yang dapat menghapus dokumen jaminan'], 403);
}
$jaminan = DokumenJaminan::find($jaminan_id);
@@ -451,8 +453,8 @@
}
// Periksa apakah dokumen jaminan terkait dengan permohonan aktif
if ($jaminan->permohonan()->exists()) {
return response()->json(['success' => false, 'message' => 'Tidak dapat menghapus dokumen jaminan yang terkait dengan permohonan aktif'], 400);
if ($jaminan->permohonan()->exists() && !in_array($jaminan->permohonan->status,['order','revisi'])) {
// return response()->json(['success' => false, 'message' => 'Tidak dapat menghapus dokumen jaminan yang terkait dengan permohonan aktif'], 400);
}
DB::beginTransaction();

View File

@@ -3,6 +3,7 @@
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Maatwebsite\Excel\Facades\Excel;
@@ -149,6 +150,8 @@
$request->validate([
'kode_register_t24' => 'nullable',
'cif' => 'required',
'keterangan' => 'nullable|string',
'kolektibilitas' => 'nullable|string|in:1,2,3,4,5',
]);
try {
@@ -157,6 +160,8 @@
// Update only the editable fields
$laporanAdminKredit->update([
'kode_register_t24' => $request->kode_register_t24,
'keterangan' => $request->keterangan,
'kolektibilitas' => $request->kolektibilitas,
'updated_by' => Auth::id(),
]);
@@ -169,11 +174,11 @@
}
return redirect()
->route('laporan-admin-kredit.index')
->route('admin-kredit.laporan.index')
->with('success', 'Laporan Admin Kredit updated successfully');
} catch (Exception $e) {
return redirect()
->route('laporan-admin-kredit.edit', $id)
->route('admin-kredit.laporan.edit', $id)
->with('error', 'Failed to update Laporan Admin Kredit');
}
}

View File

@@ -2,9 +2,10 @@
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Lpj\Models\Permohonan;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Modules\Lpj\Http\Controllers\PenilaiController;
class LaporanController extends Controller
@@ -15,7 +16,6 @@ class LaporanController extends Controller
* Display a listing of the resource.
*/
public function __construct(PenilaiController $penilaiController){
$this->penilaiController = $penilaiController;
}
@@ -75,11 +75,12 @@ class LaporanController extends Controller
}
// Retrieve data from the database
$query = Permohonan::query()->whereIn('status',['proses-laporan','done', 'paparan', 'proses-paparan'])->whereNotNull('approval_so_at')->whereNotNull('approval_eo_at')->where(function ($q) {
$q->whereIn('nilai_plafond_id', [1,4])
->whereNotNull('approval_dd_at')
->orWhereIn('nilai_plafond_id', [2,3]);
});
$query = Permohonan::query()
->whereIn('status',['proses-laporan','done', 'paparan', 'proses-paparan']);
if (Auth::user()->hasAnyRole(['pemohon-ao','pemohon-eo'])) {
$query = $query->where('branch_id', Auth::user()->branch_id);
}
$query = $query->orderBy('nomor_registrasi', 'desc');
// Apply search filter if provided

View File

@@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Lpj\Exports\LaporanHasilPenilaianJaminanInternalExternalExport;
use Modules\Lpj\Models\Permohonan;
@@ -60,6 +61,7 @@
$q->orWhereRelation('user', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('tujuanPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('branch', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('debiture', DB::raw('LOWER(name)'), 'LIKE', '%' . strtolower($search->search) . '%');
$q->orWhereRelation('jenisFasilitasKredit', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('jenisPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhere('status', 'LIKE', '%' . $search->search . '%');
@@ -69,7 +71,7 @@
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
@@ -104,12 +106,12 @@
$lpj = json_decode($permohonan->penilai->lpj, true);
$npw = str_replace('.', '', $lpj['total_nilai_pasar_wajar'] ?? 0);
$luas_tanah = $lpj['luas_tanah'] ?? 0;
$luas_tanah = $lpj['luas_tanah'] ?? 0;
$luas_bangunan = $lpj['luas_bangunan'] ?? 0;
// Calculate nilai_tanah dynamically by looking for all keys that start with 'nilai_tanah_'
$nilai_tanah = str_replace('.', '', $lpj['nilai_tanah_2'] ?? 0);
$nilai_bangunan = str_replace('.', '', $lpj['nilai_bangunan_2'] ?? 0);
$nilai_bangunan = str_replace('.', '', $lpj['nilai_bangunan_2'] ?? 0);
$nilai_liquidasi = str_replace('.', '', $lpj['likuidasi_nilai_2'] ?? 0);
}

View File

@@ -9,6 +9,10 @@ use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\StatusPermohonan;
use Modules\Lpj\Exports\LaporanPenilaiJaminanExport;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Lpj\Models\Branch;
use Modules\Lpj\Services\PreviewLaporanService;
use Modules\Lpj\Models\Inspeksi;
use Modules\Lpj\Models\Penilai;
class LaporanPenilaiJaminanController extends Controller
{
@@ -16,59 +20,31 @@ class LaporanPenilaiJaminanController extends Controller
/**
* Display a listing of the resource.
*/
protected $previewLaporanService;
public function __construct(PreviewLaporanService $previewLaporanService)
{
$this->previewLaporanService = $previewLaporanService;
}
public function index()
{
$status_permohonan = StatusPermohonan::all();
return view('lpj::laporan-penilai-jaminan.index', compact('status_permohonan'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('lpj::create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Show the specified resource.
*/
public function show($id)
public function show($permohonan_id, $dokumen_id, $jaminan_id)
{
return view('lpj::laporan-penilai-jaminan.show');
$back = route('laporan-penilai-jaminan.index');
return $this->previewLaporanService->previewLaporan($permohonan_id, $dokumen_id, $jaminan_id, $back);
}
/**
* Show the form for editing the specified resource.
*/
public function edit($id)
{
return view('lpj::edit');
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
{
//
}
public function dataForDatatables(Request $request)
{
@@ -93,91 +69,104 @@ class LaporanPenilaiJaminanController extends Controller
// dd($startDate);
// Retrieve data from the database
$query = Permohonan::query();
$query = $query->where('status', 'done')->orderBy('tanggal_permohonan', 'desc');
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
$paramsSearch = json_decode($search);
$search = json_decode($request->get('search'));
$query->where(function ($q) use ($search) {
$q->where('nomor_registrasi', 'LIKE', '%' . $search . '%')
->orWhere('tanggal_permohonan', 'LIKE', '%' . $search . '%')
->orWhereRelation('user', 'name', 'LIKE', '%' . $search . '%')
->orWhereRelation('debiture', 'name', 'LIKE', '%' . $search . '%')
->orWhereRelation('tujuanPenilaian', 'name', 'LIKE', '%' . $search . '%')
->orWhereRelation('branch', 'name', 'LIKE', '%' . $search . '%');
if (!empty($search->start_date) || !empty($search->end_date)) {
$startDate = $search->start_date ?? '1900-01-01';
$endDate = $search->end_date ?? now()->toDateString();
if (!empty($paramsSearch->tanggal_awal) && !empty($paramsSearch->tanggal_akhir)) {
$q->whereBetween('tanggal_permohonan', [$paramsSearch->tanggal_awal, $paramsSearch->tanggal_akhir]);
$query->where(function ($q) use ($startDate, $endDate) {
$q->whereHas('penilaian', function ($q2) use ($startDate, $endDate) {
$q2->whereBetween('tanggal_kunjungan', [$startDate, $endDate]);
});
// OR check if has penawaran with date in range
$q->orWhereHas('penawaran', function ($q3) use ($startDate, $endDate) {
$q3->whereBetween('tanggal_penilaian_sebelumnya', [$startDate, $endDate]);
});
});
}
if (isset($search->branch_id) && !empty($search->branch_id)) {
$query->where('branch_id', $search->branch_id);
}
if (isset($search->laporan) && is_array($search->laporan) && !empty($search->laporan)) {
foreach ($search->laporan as $type) {
$query->whereHas('penilai', function ($q) use ($type) {
$q->where('type_penilai', 'LIKE', '%' . $type . '%');
});
}
}
$statusKeywords = explode(',', $search);
foreach ($statusKeywords as $keyword) {
$q->orWhereRelation('penilai', 'type_penilai', 'LIKE', '%' . trim($keyword) . '%');
}
});
// dd($search->search);
if (isset($search->search)) {
$query->where(function ($q) use ($search) {
$q->where('nomor_registrasi', 'LIKE', '%' . $search->search . '%');
$q->orWhere('tanggal_permohonan', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('user', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('tujuanPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('branch', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('debiture', DB::raw('LOWER(name)'), 'LIKE', '%' . strtolower($search->search) . '%');
$q->orWhereRelation('jenisFasilitasKredit', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('jenisPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhere('status', 'LIKE', '%' . $search->search . '%');
});
}
}
$query->where('status', 'done');
// Default sorting if no sort provided
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
} else {
$query->orderBy('nomor_registrasi', 'asc');
}
// Get total count of records before pagination
// Get the total count of records
$totalRecords = $query->count();
// Pagination
// Apply pagination if provided
if ($request->has('page') && $request->has('size')) {
$page = (int) $request->get('page', 1);
$size = (int) $request->get('size', 10);
$offset = ($page - 1) * $size;
$page = $request->get('page');
$size = $request->get('size');
$offset = ($page - 1) * $size; // Calculate the offset
$query->skip($offset)->take($size);
}
// Get filtered count
// Get the filtered count of records
$filteredRecords = $query->count();
$totalRecords = $query->count();
// Pagination
if ($request->has('page') && $request->has('size')) {
$page = (int) $request->get('page', 1);
$size = (int) $request->get('size', 10);
$offset = ($page - 1) * $size;
$query->skip($offset)->take($size);
}
// Get filtered count
$filteredRecords = $query->count();
// Get data with necessary relationships
$data = $query->with(['user', 'debiture', 'branch', 'tujuanPenilaian', 'penilaian', 'dokumenjaminan.jenisJaminan','nilaiPlafond', 'penilai'])->get();
$data = $query->with(['user', 'debiture', 'branch', 'tujuanPenilaian', 'penilaian', 'dokumenjaminan.jenisJaminan','nilaiPlafond', 'penilai', 'dokumenjaminan.inspeksi'])->get();
// Calculate total pages
$pageCount = ceil($totalRecords / $request->get('size', 10));
// Calculate the page count
$pageCount = ceil($totalRecords / $size);
// Calculate the current page number
$currentPage = max(1, $request->get('page', 1));
// Calculate total pages
$pageCount = ceil($totalRecords / $request->get('size', 10));
// Return the response data as a JSON object
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,
'recordsFiltered' => $filteredRecords,
'pageCount' => $pageCount,
'page' => $request->get('page', 1),
'page' => $currentPage,
'totalCount' => $totalRecords,
'data' => $data,
]);
@@ -185,15 +174,53 @@ class LaporanPenilaiJaminanController extends Controller
public function export(Request $request)
{
$tanggalAwal = $request->input('tanggal_awal');
$tanggalAkhir = $request->input('tanggal_akhir');
$status = $request->input('status');
$selectedIds = $request->input('selected_ids');
$startDate = $request->input('start_date');
$endDate = $request->input('end_date');
// Validate the date format
if (isset($startDate) && isset($endDate)) {
$startDate = date('Y-m-d', strtotime($startDate));
$endDate = date('Y-m-d', strtotime($endDate));
if ($startDate > $endDate) {
return redirect()->back()->with('error', 'Tanggal awal tidak boleh lebih kecil dari tanggal akhir');
}
}
// name the file
$filename = $this->createNameLaporan($request);
$filename = 'laporan_penilai_jaminan_' . date('YmdHis') . '.xlsx';
return Excel::download(
new LaporanPenilaiJaminanExport($tanggalAwal, $tanggalAkhir, $status, $selectedIds),
new LaporanPenilaiJaminanExport($request),
$filename
);
}
public function createNameLaporan($request)
{
$startDate = $request->start_date ?? null;
$endDate = $request->end_date ?? null;
$branchId = $request->branch_id ?? null;
$laporan = $request->laporan ?? null;
// Initialize filename parts
$parts = ['Laporan Penilai Jaminan'];
if ($startDate && $endDate) {
$parts[] = "{$startDate}_{$endDate}";
}
if ($laporan) {
$parts[] = $laporan;
}
if ($branchId) {
$parts[] = $this->getBranchId($branchId);
}
// Return concatenated filename with extension
return implode('_', $parts) . '.xlsx';
}
public function getBranchId($branchId)
{
$branchesName = Branch::find($branchId)->name ?? null;
return $branchesName;
}
}

View File

@@ -1,161 +1,180 @@
<?php
namespace Modules\Lpj\Http\Controllers;
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Lpj\Exports\LaporanPenilaianJaminanExport;
use Modules\Lpj\Models\Permohonan;
use Maatwebsite\Excel\Facades\Excel;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Lpj\Exports\LaporanPenilaianJaminanExport;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\Penilaian;
use Modules\Lpj\Models\PenawaranTender;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Support\Facades\DB;
class LaporanPenilaianJaminanController extends Controller
class LaporanPenilaianJaminanController extends Controller
{
public $user;
/**
* Display a listing of the resource.
*/
public function index()
{
public $user;
/**
* Display a listing of the resource.
*/
public function index()
{
return view('lpj::laporan_penilaian_jaminan.index');
}
public function dataForDatatables(Request $request)
{
if (is_null($this->user) || !$this->user->can('laporan-admin-kredit.view')) {
//abort(403, 'Sorry! You are not allowed to view laporan admin kredit.');
}
// Retrieve data from the database
$query = Permohonan::query();
$query = $query->where('status', 'done');
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = json_decode($request->get('search'));
if (isset($search->start_date) || isset($search->end_date)) {
$query->whereBetween('tanggal_permohonan', [
$search->start_date ?? '1900-01-01',
$search->end_date ?? now()->toDateString()
]);
}
// Filter by branch if provided
if (isset($search->branch_id) && !empty($search->branch_id)) {
$query->where('branch_id', $search->branch_id);
}
if (isset($search->penilai_id) && !empty($search->penilai_id)) {
$query->whereHas('penilaian._user_penilai.userPenilaiTeam', function($q) use ($search) {
$q->where('user_id', $search->penilai_id);
});
}
if (isset($search->search)) {
$query->where(function ($q) use ($search) {
$q->where('nomor_registrasi', 'LIKE', '%' . $search->search . '%');
$q->orWhere('tanggal_permohonan', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('user', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('tujuanPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('branch', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('jenisFasilitasKredit', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('jenisPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhere('status', 'LIKE', '%' . $search->search . '%');
});
}
}
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// 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
$data = $query->with(['debiture.branch'])->get();
$data = $data->map(function ($permohonan) {
$luas_tanah = 0;
$luas_bangunan = 0;
$nilai_tanah = 0;
$nilai_bangunan = 0;
$npw = 0;
$nilai_liquidasi = 0;
if (isset($permohonan->penilai->lpj)) {
$lpj = json_decode($permohonan->penilai->lpj, true);
$npw = str_replace('.', '', $lpj['total_nilai_pasar_wajar'] ?? 0);
$luas_tanah = $lpj['luas_tanah'] ?? 0;
$luas_bangunan = $lpj['luas_bangunan'] ?? 0;
// Calculate nilai_tanah dynamically by looking for all keys that start with 'nilai_tanah_'
$nilai_tanah = str_replace('.', '', $lpj['nilai_tanah_2'] ?? 0);
$nilai_bangunan = str_replace('.', '', $lpj['nilai_bangunan_2'] ?? 0);
$nilai_liquidasi = str_replace('.', '', $lpj['likuidasi_nilai_2'] ?? 0);
}
return [
'id' => $permohonan->id,
'nomor_registrasi' => $permohonan->nomor_registrasi,
'tanggal_permohonan' => $permohonan->tanggal_permohonan,
'branch' => $permohonan->debiture?->branch?->name,
'name' => $permohonan->debiture?->name,
'pemohon' => $permohonan->creator?->name,
'tujuan_penilaian' => $permohonan->tujuanPenilaian?->name,
'jenis_agunan' => $permohonan->documents?->pluck('jenisJaminan.name')->unique()->implode(', '),
'alamat_agunan' => $permohonan->documents?->map(function ($document) {
return formatAlamat($document);
})->unique()->implode(', '),
'luas_tanah' => $luas_tanah . ' m²',
'nilai_tanah' => formatRupiah($nilai_tanah,2),
'luas_bangunan' => $luas_bangunan . ' m²',
'nilai_bangunan' => formatRupiah($nilai_bangunan,2),
'tanggal_laporan' => $permohonan->approval_dd_at ?? $permohonan->approval_eo_at ?? '',
'tanggal_review' => $permohonan->penilaian?->tanggal_kunjungan ?? '',
'nilai_pasar_wajar' => formatRupiah($npw,2),
'nilai_likuidasi' => formatRupiah($nilai_liquidasi,2),
'nama_penilai' => $permohonan->penilaian?->_user_penilai?->userPenilaiTeam?->name,
];
});
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
// 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' => $data,
]);
}
public function export(Request $request)
{
return Excel::download(new LaporanPenilaianJaminanExport($request), 'laporan_penilaian_jaminan.xlsx');
}
return view('lpj::laporan_penilaian_jaminan.index');
}
public function dataForDatatables(Request $request)
{
if (is_null($this->user) || !$this->user->can('laporan-admin-kredit.view')) {
//abort(403, 'Sorry! You are not allowed to view laporan admin kredit.');
}
// Retrieve data from the database
$query = Permohonan::query();
$query = $query->where('status', 'done');
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = json_decode($request->get('search'));
if (!empty($search->start_date) || !empty($search->end_date)) {
$startDate = $search->start_date ?? '1900-01-01';
$endDate = $search->end_date ?? now()->toDateString();
$query->where(function ($q) use ($startDate, $endDate) {
$q->whereHas('penilaian', function ($q2) use ($startDate, $endDate) {
$q2->whereBetween('tanggal_kunjungan', [$startDate, $endDate]);
})
->orWhereHas('penawaran', function ($q3) use ($startDate, $endDate) {
$q3->whereBetween('tanggal_penilaian_sebelumnya', [$startDate, $endDate]);
});
});
}
// Filter by branch if provided
if (isset($search->branch_id) && !empty($search->branch_id)) {
$query->where('branch_id', $search->branch_id);
}
if (isset($search->penilai_id) && !empty($search->penilai_id)) {
$query->whereHas('penilaian._user_penilai.userPenilaiTeam', function ($q) use ($search) {
$q->where('user_id', $search->penilai_id);
});
}
if (isset($search->search)) {
$query->where(function ($q) use ($search) {
$q->where('nomor_registrasi', 'LIKE', '%' . $search->search . '%');
$q->orWhere('tanggal_permohonan', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('user', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('tujuanPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('branch', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('debiture', DB::raw('LOWER(name)'), 'LIKE', '%' . strtolower($search->search) . '%');
$q->orWhereRelation('jenisFasilitasKredit', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('jenisPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhere('status', 'LIKE', '%' . $search->search . '%');
});
}
}
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// 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
$data = $query->with(['debiture.branch'])->get();
$data = $data->map(function ($permohonan) {
$luas_tanah = 0;
$luas_bangunan = 0;
$nilai_tanah = 0;
$nilai_bangunan = 0;
$npw = 0;
$nilai_liquidasi = 0;
if (isset($permohonan->penilai->lpj)) {
$lpj = json_decode($permohonan->penilai->lpj, true);
$npw = str_replace('.', '', $lpj['total_nilai_pasar_wajar'] ?? 0);
$luas_tanah = $lpj['luas_tanah'] ?? 0;
$luas_bangunan = $lpj['luas_bangunan'] ?? 0;
// Calculate nilai_tanah dynamically by looking for all keys that start with 'nilai_tanah_'
$nilai_tanah = str_replace('.', '', $lpj['nilai_tanah_2'] ?? 0);
$nilai_bangunan = str_replace('.', '', $lpj['nilai_bangunan_2'] ?? 0);
$nilai_liquidasi = str_replace('.', '', $lpj['likuidasi_nilai_2'] ?? 0);
}
return [
'id' => $permohonan->id,
'nomor_registrasi' => $permohonan->nomor_registrasi,
'tanggal_permohonan' => $permohonan->tanggal_permohonan,
'branch' => $permohonan->debiture?->branch?->name,
'name' => $permohonan->debiture?->name,
'pemohon' => $permohonan->creator?->name,
'tujuan_penilaian' => $permohonan->tujuanPenilaian?->name,
'jenis_agunan' => $permohonan->documents?->pluck('jenisJaminan.name')->unique()->implode(', '),
'alamat_agunan' => $permohonan->documents?->map(function ($document) {
return formatAlamat($document);
})->unique()->implode(', '),
'luas_tanah' => $luas_tanah . ' m²',
'nilai_tanah' => formatRupiah($nilai_tanah, 2),
'luas_bangunan' => $luas_bangunan . ' m²',
'nilai_bangunan' => formatRupiah($nilai_bangunan, 2),
'tanggal_laporan' => $permohonan->approval_dd_at ?? $permohonan->approval_eo_at ?? '',
'tanggal_review' => $permohonan->penilaian?->tanggal_kunjungan ?? '',
'nilai_pasar_wajar' => formatRupiah($npw, 2),
'nilai_likuidasi' => formatRupiah($nilai_liquidasi, 2),
'nama_penilai' => $permohonan->penilaian?->_user_penilai?->userPenilaiTeam?->name,
];
});
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
// 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' => $data,
]);
}
public function export(Request $request)
{
$startDate = $request->start_date;
$endDate = $request->end_date;
// name of the file
$fileName = 'laporan_penilaian_jaminan_' . $startDate . '_' . $endDate . '.xlsx';
return Excel::download(new LaporanPenilaianJaminanExport($request), $fileName);
}
}

View File

@@ -32,6 +32,7 @@
// Retrieve data from the database
$query = Permohonan::query();
$query->where('status','done');
if (!Auth::user()->hasAnyRole(['administrator'])) {
$query = $query->where('branch_id', Auth::user()->branch_id);

View File

@@ -0,0 +1,304 @@
<?php
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Lpj\Models\LaporanSlik;
use Modules\Lpj\Models\Slik;
use Modules\Lpj\Exports\LaporanSlikExport;
class LaporanSlikController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return view('lpj::laporan-slik.index');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): JsonResponse
{
try {
$request->validate([
'slik_id' => 'required|exists:sliks,id'
]);
$slik = Slik::findOrFail($request->slik_id);
// Cek apakah data sudah ada di laporan_slik
$existing = LaporanSlik::where('slik_id', $slik->id)->first();
if ($existing) {
return response()->json([
'success' => false,
'message' => 'Data sudah ada di laporan SLIK'
], 422);
}
// Copy data dari tabel slik ke laporan_slik
$laporanSlik = LaporanSlik::create([
'slik_id' => $slik->id,
'sandi_bank' => $slik->sandi_bank,
'kode_kantor' => $slik->kode_kantor,
'kode_cabang' => $slik->kode_cabang,
'tahun' => $slik->tahun,
'bulan' => $slik->bulan,
'no_rekening' => $slik->no_rekening,
'cif' => $slik->cif,
'kode_jenis' => $slik->kode_jenis,
'kode_jenis_ket' => $slik->kode_jenis_ket,
'kode_sifat' => $slik->kode_sifat,
'kode_sifat_ket' => $slik->kode_sifat_ket,
'kode_valuta' => $slik->kode_valuta,
'kode_valuta_ket' => $slik->kode_valuta_ket,
'baki_debet' => $slik->baki_debet,
'kolektibilitas' => $slik->kolektibilitas,
'kolektibilitas_ket' => $slik->kolektibilitas_ket,
'tanggal_mulai' => $slik->tanggal_mulai,
'tanggal_jatuh_tempo' => $slik->tanggal_jatuh_tempo,
'tanggal_selesai' => $slik->tanggal_selesai,
'tanggal_restrukturisasi' => $slik->tanggal_restrukturisasi,
'kode_sebab_macet' => $slik->kode_sebab_macet,
'kode_sebab_macet_ket' => $slik->kode_sebab_macet_ket,
'tanggal_macet' => $slik->tanggal_macet,
'kode_kondisi' => $slik->kode_kondisi,
'kode_kondisi_ket' => $slik->kode_kondisi_ket,
'tanggal_kondisi' => $slik->tanggal_kondisi,
'nilai_agunan' => $slik->nilai_agunan,
'nilai_agunan_ket' => $slik->nilai_agunan_ket,
'jenis_agunan' => $slik->jenis_agunan,
'kode_agunan' => $slik->kode_agunan,
'kode_agunan_ket' => $slik->kode_agunan_ket,
'peringkat_agunan' => $slik->peringkat_agunan,
'peringkat_agunan_ket' => $slik->peringkat_agunan_ket,
'nama_debitur' => $slik->nama_debitur,
'npwp' => $slik->npwp,
'no_ktp' => $slik->no_ktp,
'no_telp' => $slik->no_telp,
'kode_kab_kota' => $slik->kode_kab_kota,
'kode_kab_kota_ket' => $slik->kode_kab_kota_ket,
'kode_negara_domisili' => $slik->kode_negara_domisili,
'kode_negara_domisili_ket' => $slik->kode_negara_domisili_ket,
'kode_pos' => $slik->kode_pos,
'alamat' => $slik->alamat,
'fasilitas' => $slik->fasilitas,
'status_agunan' => $slik->status_agunan,
'tanggal_lapor' => $slik->tanggal_lapor,
'status' => 'active',
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
// Hapus data dari tabel slik setelah berhasil dipindahkan
$slik->delete();
return response()->json([
'success' => true,
'message' => 'Data berhasil dipindahkan ke laporan SLIK',
'data' => $laporanSlik
]);
} catch (\Exception $e) {
Log::error('Error moving SLIK to laporan: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memindahkan data'
], 500);
}
}
/**
* Data untuk datatables dengan server-side processing
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function dataForDatatables(Request $request)
{
try {
// Retrieve data from the database
$query = LaporanSlik::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('fasilitas', 'LIKE', "%$search%")
->orWhere('status_agunan', 'LIKE', "%$search%");
});
}
// Apply year filter
if ($request->has('year') && !empty($request->get('year'))) {
$query->where('tahun', $request->get('year'));
}
// Apply month filter
if ($request->has('month') && !empty($request->get('month'))) {
$query->where('bulan', $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 status filter
if ($request->has('status') && !empty($request->get('status'))) {
$query->where('status', $request->get('status'));
}
// 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
$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,
'nilai_agunan' => $item->nilai_agunan_formatted ?? '',
'status_agunan' => $item->status_agunan,
'status_badge' => $item->status_badge ?? '',
'created_by' => $item->creator?->name ?? '-',
'created_at' => dateFormat($item->created_at, true) ?? $item->created_at->format('d/m/Y H:i')
];
});
// 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,
]);
} catch (\Exception $e) {
Log::error('Error in laporan slik datatables: ' . $e->getMessage());
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => 0,
'recordsFiltered' => 0,
'pageCount' => 0,
'page' => 1,
'totalCount' => 0,
'data' => [],
]);
}
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\View\View
*/
public function show($id)
{
try {
$laporanSlik = LaporanSlik::findOrFail($id);
return view('lpj::laporan-slik.show', compact('laporanSlik'));
} catch (\Exception $e) {
Log::error('Error showing laporan slik: ' . $e->getMessage());
return back()->with('error', 'Data tidak ditemukan');
}
}
/**
* Export laporan SLIK to Excel
*/
public function export(Request $request)
{
try {
$query = LaporanSlik::query();
// Apply filters
if ($request->has('search') && $request->search) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('nama_debitur', 'like', "%{$search}%")
->orWhere('no_rekening', 'like', "%{$search}%")
->orWhere('cif', 'like', "%{$search}%");
});
}
if ($request->has('year') && $request->year) {
$query->where('tahun', $request->year);
}
if ($request->has('month') && $request->month) {
$query->where('bulan', $request->month);
}
if ($request->has('status') && $request->status) {
$query->where('status', $request->status);
}
$filename = 'laporan-slik-' . now()->format('Y-m-d-His') . '.xlsx';
return Excel::download(new LaporanSlikExport($query), $filename);
} catch (\Exception $e) {
Log::error('Error exporting laporan slik: ' . $e->getMessage());
return back()->with('error', 'Gagal export data: ' . $e->getMessage());
}
}
}

View File

@@ -4,6 +4,8 @@ namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Lpj\Exports\LaporanUserLimitExport;
use Modules\Lpj\Services\LaporanUserService;
class LaporanUserController extends Controller
@@ -38,4 +40,12 @@ class LaporanUserController extends Controller
return $this->laporanUserService->dataForDatatables($request);
}
public function export(Request $request)
{
$startDate = $request->start_date;
$endDate = $request->end_date;
// name of the file
$fileName = 'laporan_user_limit' . $startDate . '_' . $endDate . '.xlsx';
return Excel::download(new LaporanUserLimitExport($request), $fileName);
}
}

View File

@@ -0,0 +1,558 @@
<?php
namespace Modules\Lpj\Http\Controllers;
use Exception;
use Modules\Lpj\Models\Noc;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\Lpj\Models\Permohonan;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Storage;
class MemoController extends Controller
{
public $user;
/**
* Menampilkan halaman index memo penyelesaian
*
* @return \Illuminate\View\View
*/
public function index()
{
Log::info('MemoController: Mengakses halaman index memo penyelesaian');
return view('lpj::memo.index');
}
/**
* Menampilkan form untuk membuat memo penyelesaian dengan data yang dipilih
*
* @param Request $request
* @return \Illuminate\View\View
*/
public function create(Request $request)
{
Log::info('MemoController: Mengakses halaman create memo penyelesaian');
$selectedIds = $request->get('selected_ids', []);
// Pastikan $selectedIds selalu berupa array
if (is_string($selectedIds)) {
$selectedIds = explode(',', $selectedIds);
}
// Filter array untuk menghilangkan nilai kosong
$selectedIds = array_filter($selectedIds, function($id) {
return !empty(trim($id));
});
$permohonanList = [];
$totalBiayaPJ = 0;
if (!empty($selectedIds) && count($selectedIds) > 0) {
try {
$permohonanList = Permohonan::with([
'user',
'debiture',
'branch',
'tujuanPenilaian',
'penilaian',
'jenisFasilitasKredit',
'documents.inspeksi',
'penilai',
'documents.detail',
'noc'
])->whereIn('id', $selectedIds)->get();
// Hitung total biaya PJ dari nominal_bayar di tabel NOC
$totalBiayaPJ = Noc::whereIn('permohonan_id', $selectedIds)
->sum('nominal_bayar');
Log::info('MemoController: Total Biaya PJ dihitung: ' . $totalBiayaPJ);
} catch (Exception $e) {
Log::error('MemoController: Error saat mengambil data permohonan - ' . $e->getMessage());
return redirect()->back()->with('error', 'Terjadi kesalahan saat memuat data');
}
}
return view('lpj::memo.create', compact('permohonanList', 'totalBiayaPJ'));
}
/**
* Menyimpan memo penyelesaian yang telah dibuat
*
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(Request $request)
{
Log::info('MemoController: Memulai proses penyimpanan memo penyelesaian');
DB::beginTransaction();
try {
// Validasi input
$request->validate([
'permohonan_ids' => 'required|array',
'permohonan_ids.*' => 'exists:permohonan,id',
'memo_number' => 'required|string|max:255',
'payment_date' => 'required|date',
'memo_date' => 'required|date'
]);
$permohonanIds = $request->permohonan_ids;
$memoNumber = $request->memo_number;
$paymentDate = $request->payment_date;
$memoDate = $request->memo_date;
// Update status permohonan yang dipilih
foreach ($permohonanIds as $permohonanId) {
$permohonan = Permohonan::find($permohonanId);
if ($permohonan) {
$permohonan->status = 'memo-penyelesaian';
$permohonan->memo_penyelesaian_number = $memoNumber;
$permohonan->memo_penyelesaian_date = $memoDate;
$permohonan->memo_penyelesaian_payment_date = $paymentDate;
$permohonan->memo_penyelesaian_created_at = now();
//$permohonan->save();
Log::info('MemoController: Berhasil update permohonan ID: ' . $permohonanId);
}
}
DB::commit();
Log::info('MemoController: Berhasil menyimpan memo penyelesaian untuk ' . count($permohonanIds) . ' permohonan');
return redirect()->route('memo.index')
->with('success', 'Memo penyelesaian berhasil dibuat untuk ' . count($permohonanIds) . ' permohonan');
} catch (Exception $e) {
DB::rollback();
Log::error('MemoController: Error saat menyimpan memo penyelesaian - ' . $e->getMessage());
return redirect()->back()
->withInput()
->with('error', 'Terjadi kesalahan saat menyimpan memo penyelesaian: ' . $e->getMessage());
}
}
/**
* Menampilkan detail memo penyelesaian
*
* @param int $id
* @return \Illuminate\View\View
*/
public function show($id)
{
Log::info('MemoController: Mengakses detail memo penyelesaian ID: ' . $id);
$permohonan = Permohonan::with([
'user',
'debiture',
'branch',
'tujuanPenilaian',
'penilaian',
'jenisFasilitasKredit',
'documents.inspeksi',
'penilai',
'documents.detail',
'noc'
])->findOrFail($id);
return view('lpj::memo.show', compact('permohonan'));
}
/**
* Mengambil data untuk datatables pada halaman memo penyelesaian
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function dataForDatatables(Request $request)
{
Log::info('MemoController: Mengambil data untuk datatables');
if (is_null($this->user) || !$this->user->can('debitur.view')) {
Log::warning('MemoController: User tidak memiliki permission untuk melihat data');
// abort(403, 'Sorry! You are not allowed to view users.');
}
// Mengambil data dari database dengan kondisi yang sama seperti LaporanController
$query = Permohonan::query()
->whereIn('status', ['proses-laporan', 'done', 'paparan', 'proses-paparan', 'memo-penyelesaian'])
/*->whereNotNull('approval_so_at')
->whereNotNull('approval_eo_at')
->where(function ($q) {
$q->whereIn('nilai_plafond_id', [1, 4])
->whereNotNull('approval_dd_at')
->orWhereIn('nilai_plafond_id', [2, 3]);
})*/
->whereHas('noc'); // Hanya tampilkan permohonan yang memiliki NOC
$query = $query->orderBy('nomor_registrasi', 'desc');
// Apply search filter jika ada
if ($request->has('search') && !empty($request->get('search'))) {
$searchParams = explode('|', $request->get('search'));
$filterJenisPenilaian = $searchParams[0] ?? '';
$searchTerm = $searchParams[1] ?? '';
// Filter berdasarkan jenis penilaian
if (!empty($filterJenisPenilaian)) {
$query->where('jenis_penilaian_id', $filterJenisPenilaian);
Log::info('Applied jenis penilaian filter', ['filter' => $filterJenisPenilaian]);
}
$query->where(function ($q) use ($searchTerm) {
$q->where('nomor_registrasi', 'LIKE', '%' . $searchTerm . '%');
$q->orWhere('tanggal_permohonan', 'LIKE', '%' . $searchTerm . '%');
$q->orWhereRelation('user', 'name', 'LIKE', '%' . $searchTerm . '%');
$q->orWhereRelation('debiture', 'name', 'LIKE', '%' . $searchTerm . '%');
$q->orWhereRelation('tujuanPenilaian', 'name', 'LIKE', '%' . $searchTerm . '%');
$q->orWhereRelation('branch', 'name', 'LIKE', '%' . $searchTerm . '%');
$q->orWhere('status', 'LIKE', '%' . $searchTerm . '%');
});
}
// Apply sorting jika ada
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// Mendapatkan total count records
$totalRecords = $query->count();
$size = $request->get('size', 10);
if ($size == 0) {
$size = 10;
}
// Apply pagination jika ada
if ($request->has('page') && $request->has('size')) {
$page = $request->get('page');
$size = $request->get('size');
$offset = ($page - 1) * $size;
$query->skip($offset)->take($size);
}
// Mendapatkan filtered count records
$filteredRecords = $query->count();
// Mendapatkan data untuk halaman saat ini
$data = $query->with([
'user',
'debiture',
'branch',
'tujuanPenilaian',
'jenisPenilaian',
'penilaian',
'jenisFasilitasKredit',
'documents.inspeksi',
'penilai',
'documents.detail',
'noc'
])->get();
// Menghitung page count
$pageCount = ceil($totalRecords / $size);
// Menghitung current page number
$currentPage = max(1, $request->get('page', 1));
Log::info('MemoController: Berhasil mengambil data datatables - Total: ' . $totalRecords . ', Filtered: ' . $filteredRecords);
// Return response data sebagai JSON object
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,
'recordsFiltered' => $filteredRecords,
'pageCount' => $pageCount,
'page' => $currentPage,
'totalCount' => $totalRecords,
'data' => $data,
]);
}
/**
* Mengambil total biaya PJ berdasarkan permohonan yang dipilih
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getTotalBiayaPJ(Request $request)
{
Log::info('MemoController: Mengambil total biaya PJ');
try {
$permohonanIds = $request->get('permohonan_ids', []);
// Pastikan $permohonanIds selalu berupa array
if (is_string($permohonanIds)) {
$permohonanIds = explode(',', $permohonanIds);
}
// Filter array untuk menghilangkan nilai kosong
$permohonanIds = array_filter($permohonanIds, function($id) {
return !empty(trim($id));
});
$totalBiayaPJ = 0;
if (!empty($permohonanIds) && count($permohonanIds) > 0) {
// Hitung total biaya PJ dari nominal_bayar di tabel NOC
$totalBiayaPJ = \Modules\Lpj\Models\Noc::whereIn('permohonan_id', $permohonanIds)
->sum('nominal_bayar');
}
Log::info('MemoController: Total Biaya PJ berhasil dihitung: ' . $totalBiayaPJ);
return response()->json([
'success' => true,
'total_biaya_pj' => $totalBiayaPJ,
'total_biaya_pj_formatted' => 'Rp ' . number_format($totalBiayaPJ, 0, ',', '.')
]);
} catch (Exception $e) {
Log::error('MemoController: Error saat menghitung total biaya PJ - ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat menghitung total biaya PJ'
], 500);
}
}
/**
* Menampilkan preview memo penyelesaian sebelum menyimpan
*
* @param Request $request
* @return \Illuminate\View\View
*/
public function preview(Request $request)
{
Log::info('MemoController: Mengakses halaman preview memo penyelesaian');
$permohonanIds = $request->permohonan_ids;
$memoNumber = $request->memo_number;
$paymentDate = $request->payment_date;
$memoDate = $request->memo_date;
try {
// Ambil data permohonan yang dipilih
$permohonanList = Permohonan::with([
'user',
'debiture',
'branch',
'penilaian',
'jenisPenilaian',
'jenisFasilitasKredit',
'documents.inspeksi',
'penilai',
'documents.detail',
'noc'
])->whereIn('id', $permohonanIds);
// Hitung total biaya PJ dari nominal_bayar di tabel NOC
$totalBiayaPJ = Noc::whereIn('permohonan_id', $permohonanIds)
->sum('nominal_bayar');
// Data untuk template memo
$memoData = [
'memo_number' => $memoNumber,
'memo_date' => $memoDate,
'payment_date' => $paymentDate,
'total_biaya_pj' => $totalBiayaPJ,
'permohonan_list' => $permohonanList->get(),
'debitur_count' => $permohonanList->get()->count(),
'jaminan_info' => $this->getJaminanInfo($permohonanList->get()),
'jenisPenilaian' => $permohonanList->pluck('jenis_penilaian_id')->first()
];
$permohonanList= $permohonanList->get();
Log::info('MemoController: Data preview memo berhasil disiapkan');
return view('lpj::memo.preview', compact('memoData', 'permohonanList', 'totalBiayaPJ'));
} catch (Exception $e) {
Log::error('MemoController: Error saat menyiapkan preview memo - ' . $e->getMessage());
return redirect()->back()
->withInput()
->with('error', 'Terjadi kesalahan saat menyiapkan preview memo: ' . $e->getMessage());
}
}
/**
* Generate PDF memo penyelesaian dan simpan ke database
*
* @param Request $request
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function generatePdf(Request $request)
{
Log::info('MemoController: Memulai generate PDF memo penyelesaian');
DB::beginTransaction();
try {
// Validasi input
$permohonanIds = $request->permohonan_ids;
$memoNumber = $request->memo_number;
$paymentDate = $request->payment_date;
$memoDate = $request->memo_date;
// Ambil data permohonan yang dipilih
$permohonanList = Permohonan::with([
'user',
'debiture',
'branch',
'penilaian',
'jenisPenilaian',
'jenisFasilitasKredit',
'documents.inspeksi',
'penilai',
'documents.detail',
'noc'
])->whereIn('id', $permohonanIds);
// Hitung total biaya PJ dari nominal_bayar di tabel NOC
$totalBiayaPJ = Noc::whereIn('permohonan_id', $permohonanIds)
->sum('nominal_bayar');
// Data untuk template memo
$memoData = [
'memo_number' => $memoNumber,
'memo_date' => $memoDate,
'payment_date' => $paymentDate,
'total_biaya_pj' => $totalBiayaPJ,
'permohonan_list' => $permohonanList->get(),
'debitur_count' => $permohonanList->get()->count(),
'jaminan_info' => $this->getJaminanInfo($permohonanList->get()),
'jenisPenilaian' => $permohonanList->pluck('jenis_penilaian_id')->first()
];
$permohonanList= $permohonanList->get();
// Generate PDF dari template
$pdf = Pdf::loadView('lpj::memo.pdf-template', compact('memoData', 'permohonanList', 'totalBiayaPJ'))
->setPaper('a4', 'portrait')
->setOptions([
'defaultFont' => 'Times-Roman',
'isRemoteEnabled' => true,
'isHtml5ParserEnabled' => true,
'isPhpEnabled' => true,
'dpi' => 150,
'defaultPaperSize' => 'a4',
'chroot' => public_path(),
]);
// Nama file PDF
$fileName = 'memo-penyelesaian-' . str_replace(['/', ' '], ['-', '-'], $memoNumber) . '-' . date('Y-m-d-H-i-s') . '.pdf';
$filePath = 'memo-penyelesaian/' . $fileName;
// Simpan PDF ke storage
Storage::disk('public')->put($filePath, $pdf->output());
// Update status permohonan yang dipilih
// Update data di tabel NOC untuk setiap permohonan
foreach ($permohonanIds as $permohonanId) {
// Cari NOC berdasarkan permohonan_id
$noc = Noc::where('permohonan_id', $permohonanId)->first();
if ($noc) {
// Update field memo penyelesaian di tabel NOC
$noc->memo_penyelesaian = $filePath;
$noc->memo_penyelesaian_number = $memoNumber;
$noc->memo_penyelesaian_date = $memoDate;
$noc->memo_penyelesaian_payment_date = $paymentDate;
$noc->memo_penyelesaian_created_at = now();
$noc->save();
Log::info('MemoController: Berhasil update NOC untuk permohonan ID: ' . $permohonanId);
} else {
Log::warning('MemoController: NOC tidak ditemukan untuk permohonan ID: ' . $permohonanId);
}
}
DB::commit();
Log::info('MemoController: Berhasil generate PDF dan menyimpan memo penyelesaian untuk ' . count($permohonanIds) . ' permohonan');
// Return PDF untuk download
return $pdf->download('memo-penyelesaian-' . $memoNumber . '.pdf');
} catch (Exception $e) {
DB::rollback();
Log::error('MemoController: Error saat generate PDF memo penyelesaian - ' . $e->getMessage());
return redirect()->back()
->with('error', 'Terjadi kesalahan saat generate PDF memo penyelesaian: ' . $e->getMessage())
->withInput();
}
}
/**
* Helper function untuk mendapatkan informasi jaminan
*
* @param $permohonanList
* @return string
*/
private function getJaminanInfo($permohonanList)
{
$jaminanTypes = [];
foreach ($permohonanList as $permohonan) {
if ($permohonan->tujuanPenilaian) {
$jaminanTypes[] = $permohonan->tujuanPenilaian->name;
}
}
$uniqueJaminan = array_unique($jaminanTypes);
return implode(' & ', $uniqueJaminan);
}
/**
* Download PDF memo penyelesaian
*
* @param int $id - ID permohonan
* @return \Illuminate\Http\Response
*/
public function downloadPdf($id)
{
Log::info('MemoController: Download PDF memo penyelesaian untuk permohonan ID: ' . $id);
try {
// Cari NOC berdasarkan permohonan_id
$noc = Noc::where('permohonan_id', $id)->first();
if (!$noc || !$noc->memo_penyelesaian) {
Log::warning('MemoController: PDF memo penyelesaian tidak ditemukan untuk permohonan ID: ' . $id);
return redirect()->back()->with('error', 'File PDF memo penyelesaian tidak ditemukan.');
}
// Cek apakah file ada di storage
if (!Storage::disk('public')->exists($noc->memo_penyelesaian)) {
Log::warning('MemoController: File PDF tidak ada di storage: ' . $noc->memo_penyelesaian);
return redirect()->back()->with('error', 'File PDF tidak ditemukan di server.');
}
// Download file
$fileName = 'memo-penyelesaian-' . $noc->memo_penyelesaian_number . '.pdf';
Log::info('MemoController: Berhasil download PDF memo penyelesaian: ' . $fileName);
return Storage::disk('public')->download($noc->memo_penyelesaian, $fileName);
} catch (Exception $e) {
Log::error('MemoController: Error saat download PDF memo penyelesaian - ' . $e->getMessage());
return redirect()->back()->with('error', 'Terjadi kesalahan saat mengunduh file PDF.');
}
}
}

View File

@@ -5,6 +5,8 @@
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Lpj\Exports\NilaiPlafondExport;
use Modules\Lpj\Http\Requests\NilaiPlafondRequest;
@@ -14,137 +16,239 @@
{
public $user;
/**
* Menampilkan halaman daftar Nilai Plafond.
* Log setiap akses dan sebelum return view.
*/
public function index()
{
return view('lpj::nilai_plafond.index');
Log::info('NilaiPlafondController@index: akses halaman index');
return \view('lpj::nilai_plafond.index');
}
/**
* Menyimpan data Nilai Plafond baru termasuk field biaya.
* Gunakan validasi dari NilaiPlafondRequest, log proses, dan bungkus dengan transaksi DB.
*/
public function store(NilaiPlafondRequest $request)
{
Log::info('NilaiPlafondController@store: mulai proses simpan');
$validate = $request->validated();
if ($validate) {
DB::beginTransaction();
try {
// Save to database
NilaiPlafond::create($validate);
return redirect()
$record = NilaiPlafond::create($validate);
DB::commit();
Log::info('NilaiPlafondController@store: simpan berhasil', ['id' => $record->id]);
return \redirect()
->route('basicdata.nilai-plafond.index')
->with('success', 'Jenis Aset created successfully');
->with('success', 'Nilai Plafond berhasil dibuat');
} catch (Exception $e) {
return redirect()
DB::rollBack();
Log::error('NilaiPlafondController@store: simpan gagal', ['error' => $e->getMessage()]);
return \redirect()
->route('basicdata.nilai-plafond.create')
->with('error', 'Failed to create nilai plafond');
->with('error', 'Gagal membuat Nilai Plafond');
}
}
}
/**
* Menampilkan form pembuatan Nilai Plafond.
* Log akses sebelum return view.
*/
public function create()
{
return view('lpj::nilai_plafond.create');
Log::info('NilaiPlafondController@create: akses halaman create');
return \view('lpj::nilai_plafond.create');
}
/**
* Menampilkan form edit Nilai Plafond berdasarkan ID.
* Gunakan transaksi untuk pembacaan data dan logging.
*/
public function edit($id)
{
$nilaiPlafond = NilaiPlafond::find($id);
return view('lpj::nilai_plafond.create', compact('nilaiPlafond'));
Log::info('NilaiPlafondController@edit: mulai proses edit', ['id' => $id]);
DB::beginTransaction();
try {
$nilaiPlafond = NilaiPlafond::find($id);
DB::commit();
Log::info('NilaiPlafondController@edit: data ditemukan', ['id' => $id]);
return \view('lpj::nilai_plafond.create', compact('nilaiPlafond'));
} catch (Exception $e) {
DB::rollBack();
Log::error('NilaiPlafondController@edit: gagal mengambil data', ['id' => $id, 'error' => $e->getMessage()]);
return \redirect()
->route('basicdata.nilai-plafond.index')
->with('error', 'Gagal mengambil data Nilai Plafond');
}
}
/**
* Memperbarui data Nilai Plafond termasuk field biaya.
* Validasi input, logging, dan gunakan transaksi DB.
*/
public function update(NilaiPlafondRequest $request, $id)
{
Log::info('NilaiPlafondController@update: mulai proses update', ['id' => $id]);
$validate = $request->validated();
if ($validate) {
DB::beginTransaction();
try {
// Update in database
$nilaiPlafond = NilaiPlafond::find($id);
if (!$nilaiPlafond) {
Log::warning('NilaiPlafondController@update: data tidak ditemukan', ['id' => $id]);
DB::rollBack();
return \redirect()
->route('basicdata.nilai-plafond.index')
->with('error', 'Data Nilai Plafond tidak ditemukan');
}
$nilaiPlafond->update($validate);
return redirect()
DB::commit();
Log::info('NilaiPlafondController@update: update berhasil', ['id' => $id]);
return \redirect()
->route('basicdata.nilai-plafond.index')
->with('success', 'Jenis Aset updated successfully');
->with('success', 'Nilai Plafond berhasil diperbarui');
} catch (Exception $e) {
return redirect()
DB::rollBack();
Log::error('NilaiPlafondController@update: update gagal', ['id' => $id, 'error' => $e->getMessage()]);
return \redirect()
->route('basicdata.nilai-plafond.edit', $id)
->with('error', 'Failed to update nilai plafond');
->with('error', 'Gagal memperbarui Nilai Plafond');
}
}
}
/**
* Menghapus data Nilai Plafond berdasarkan ID.
* Logging setiap langkah dan gunakan transaksi DB.
*/
public function destroy($id)
{
Log::info('NilaiPlafondController@destroy: mulai proses hapus', ['id' => $id]);
DB::beginTransaction();
try {
// Delete from database
$nilaiPlafond = NilaiPlafond::find($id);
$nilaiPlafond->delete();
if (!$nilaiPlafond) {
DB::rollBack();
Log::warning('NilaiPlafondController@destroy: data tidak ditemukan', ['id' => $id]);
return \response()->json(['success' => false, 'message' => 'Data Nilai Plafond tidak ditemukan']);
}
echo json_encode(['success' => true, 'message' => 'Jenis Aset deleted successfully']);
$nilaiPlafond->delete();
DB::commit();
Log::info('NilaiPlafondController@destroy: hapus berhasil', ['id' => $id]);
return \response()->json(['success' => true, 'message' => 'Nilai Plafond berhasil dihapus']);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Failed to delete nilai plafond']);
DB::rollBack();
Log::error('NilaiPlafondController@destroy: hapus gagal', ['id' => $id, 'error' => $e->getMessage()]);
return \response()->json(['success' => false, 'message' => 'Gagal menghapus Nilai Plafond']);
}
}
/**
* Menyediakan data untuk datatables dengan pencarian, sortir, dan paginasi.
* Logging proses dan gunakan transaksi DB untuk konsistensi pembacaan.
*/
public function dataForDatatables(Request $request)
{
Log::info('NilaiPlafondController@dataForDatatables: mulai proses');
if (is_null($this->user) || !$this->user->can('nilai_plafond.view')) {
//abort(403, 'Sorry! You are not allowed to view users.');
}
// Retrieve data from the database
$query = NilaiPlafond::query();
DB::beginTransaction();
try {
// Retrieve data from the database
$query = NilaiPlafond::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('code', 'LIKE', "%$search%");
$q->orWhere('name', 'LIKE', "%$search%");
});
// 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('code', 'LIKE', "%$search%");
$q->orWhere('name', 'LIKE', "%$search%");
});
}
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// 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
$data = $query->get();
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
// Calculate the current page number
$currentPage = 0 + 1;
DB::commit();
Log::info('NilaiPlafondController@dataForDatatables: proses selesai, mengembalikan data');
// 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' => $data,
]);
} catch (Exception $e) {
DB::rollBack();
Log::error('NilaiPlafondController@dataForDatatables: gagal memproses data', ['error' => $e->getMessage()]);
return \response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => 0,
'recordsFiltered' => 0,
'pageCount' => 0,
'page' => 1,
'totalCount' => 0,
'data' => [],
]);
}
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// 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
$data = $query->get();
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
// Calculate the current page number
$currentPage = 0 + 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' => $data,
]);
}
/**
* Mengekspor data Nilai Plafond ke Excel.
* Log akses sebelum proses download.
*/
public function export()
{
Log::info('NilaiPlafondController@export: mulai proses export');
return Excel::download(new NilaiPlafondExport, 'nilai_plafond.xlsx');
}
}

View File

@@ -6,10 +6,10 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Modules\Lpj\Http\Requests\NocRequest;
use Modules\Lpj\Models\Noc;
use Modules\Lpj\Models\PenawaranTender;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\Bucok;
use Modules\Lpj\Models\Noc;
use Modules\Lpj\Models\PersetujuanPenawaran;
use Modules\Lpj\Models\JenisPenilaian;
class NocController extends Controller
{
@@ -26,13 +26,15 @@
public function pembayaran()
{
$persetujuanPenawarans = PersetujuanPenawaran::all();
return view('lpj::noc.pembayaran', compact('persetujuanPenawarans'));
$jenisPenilaians = JenisPenilaian::get();
return view('lpj::noc.pembayaran', compact('persetujuanPenawarans', 'jenisPenilaians'));
}
public function penyelesaian()
{
$persetujuanPenawarans = PersetujuanPenawaran::all();
return view('lpj::noc.penyelesaian', compact('persetujuanPenawarans'));
$jenisPenilaians = JenisPenilaian::get();
return view('lpj::noc.penyelesaian', compact('persetujuanPenawarans', 'jenisPenilaians'));
}
/**
@@ -47,28 +49,42 @@
public function store(NocRequest $request)
{
$validated = $request->validated();
$validated['updated_by'] = Auth::id();
if (request()->get('status_bayar') == "sudah_bayar") {
$validated['status'] = '1';
$status = "spk";
} else {
$status = "persetujuan-penawaran";
}
$dataNoc = [
'nominal_bayar' => $validated['nominal_bayar'],
'total_pembukuan' => $validated['total_pembukuan'],
'tanggal_pembayaran' => $validated['tanggal_pembayaran'] ?? date('Y-m-d'),
'status_bayar' => $validated['nominal_bayar'] < $validated['total_harus_bayar'] ? false : true,
'catatan_noc' => $validated['catatan_noc'],
'catatan_noc' => $validated['catatan_noc'] ?? '',
'status_kurang_bayar' => $validated['status_kurang_bayar'] ?? '0',
'status_lebih_bayar' => $validated['status_lebih_bayar'] ?? '0',
'nominal_kurang_bayar' => $validated['nominal_kurang_bayar'] ?? '0',
'nominal_lebih_bayar' => $validated['nominal_lebih_bayar'] ?? '0',
'bukti_pengembalian' => $validated['bukti_pengembalian'] ?? '',
];
$noc = Noc::updateOrCreate(
if($validated['permohonan_id']){
$noc = Noc::updateOrCreate(
[
'permohonan_id' => $validated['permohonan_id'],
'persetujuan_penawaran_id' => $validated['persetujuan_penawaran_id'],
],
$dataNoc,
);
} else {
$noc = Noc::updateOrCreate(
[
'persetujuan_penawaran_id' => $validated['persetujuan_penawaran_id'],
],
$dataNoc,
);
}
$folderPath = 'noc/' . request()->get('persetujuan_penawaran_id') . '/bukti_ksl/';
@@ -80,26 +96,13 @@
}
$noc->save();
/* Update the status of the related permohonan to 'spk'
$permohonan = Permohonan::find(request()->get('permohonan_id'));
if ($permohonan) {
$permohonan->status_bayar = request()->get('status_pembayar');
if ($permohonan->jenis_penilaian_id == 2) {
$permohonan->status = $status;
}
$permohonan->save();
// andy add, update status penawaran.status='spk'
// $penawaran = PenawaranTender::where('nomor_registrasi',$permohonan->nomor_registrasi)->first();
if ($permohonan->jenis_penilaian_id == 2) {
PenawaranTender::where('nomor_registrasi', $permohonan->nomor_registrasi)->update([
'status' => $status,
'updated_by' => Auth::id(),
'updated_at' => now(),
]);
}
// andy add, update status penawaran.status='spk'
}*/
$bucok = Bucok::where('nomor_tiket', $noc->nomor_tiket)->orWhere('permohonan_id', $noc->permohonan_id)->first();
if($bucok){
$bucok->nominal_penyelesaian = $noc->total_pembukuan ?? '';
$bucok->tanggal_penyelesaian = $noc->tanggal_pembayaran ?? date('Y-m-d');
$bucok->penyelesaian = 'Selesai';
$bucok->save();
}
return redirect()
->route('noc.index')->with('success', 'NOC berhasil disimpan.');
@@ -112,6 +115,7 @@
{
$validated = $request->validated();
if($request->get('is_memo')){
$memo = Noc::find($request->get('is_memo'));
@@ -133,6 +137,7 @@
->route('laporan.index')->with('success', 'Memo Penyelesaian updated successfully');
}
$dataNoc = [
'total_pembukuan' => $validated['total_pembukuan'],
'nominal_penyelesaian' => $validated['nominal_penyelesaian'],
'tanggal_penyelesaian' => $validated['tanggal_penyelesaian'] ?? date('Y-m-d'),
'status_pelunasan' => ((int)$validated['nominal_bayar'] + (int)$validated['nominal_penyelesaian']) === (int)$validated['total_harus_bayar'] ? true : false,
@@ -215,15 +220,19 @@
$query = PersetujuanPenawaran::query();
// Filter for pembayaran (where memo_penyelesaian is null)
$query->whereDoesntHave('noc', function($q) {
/*$query->whereDoesntHave('noc', function($q) {
$q->whereNotNull('memo_penyelesaian');
});
});*/
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->orWhereRelation('penawaran', 'nomor_registrasi', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('penawaran', 'nomor_registrasi', 'LIKE', '%' . $search . '%')
->orWhereRelation('permohonan', 'nomor_registrasi', 'LIKE', '%' . $search . '%')
->orWhereRelation('permohonan.debiture','name', 'LIKE', '%' . $search . '%')
->orWhereRelation('permohonan.jenisPenilaian', 'name', 'LIKE', '%' . $search . '%')
->orWhere('nomor_tiket', 'LIKE', '%' . $search . '%');
});
}
@@ -255,9 +264,11 @@
$data = $data->map(function ($persetujuanPenawaran) {
return [
'id' => $persetujuanPenawaran->id,
'nomor_registrasi' => $persetujuanPenawaran->permohonan->nomor_registrasi ?? $persetujuanPenawaran->penawaran->nomor_registrasi,
'nama_debitur' => $persetujuanPenawaran->permohonan->debiture->name ?? $persetujuanPenawaran->penawaran->permohonan->debiture->name,
'cabang' => $persetujuanPenawaran->permohonan->branch->name ?? $persetujuanPenawaran->penawaran->permohonan->branch->name,
'nomor_registrasi' => $persetujuanPenawaran->permohonan?->nomor_registrasi ?? $persetujuanPenawaran->penawaran?->nomor_registrasi,
'nomor_tiket' => $persetujuanPenawaran->nomor_tiket ?? '',
'nama_debitur' => $persetujuanPenawaran?->permohonan?->debiture->name ?? $persetujuanPenawaran->penawaran?->permohonan?->debiture->name ?? $persetujuanPenawaran->noc?->debiture->name,
'kode_cabang' => $persetujuanPenawaran?->permohonan?->branch->code ?? $persetujuanPenawaran->penawaran?->permohonan?->branch->code ?? $persetujuanPenawaran->noc?->branch->code,
'cabang' => $persetujuanPenawaran?->permohonan?->branch->name ?? $persetujuanPenawaran->penawaran?->permohonan?->branch->name ?? $persetujuanPenawaran->noc?->branch->name,
'tanggal_pembayaran' => dateFormat(
$persetujuanPenawaran->noc->tanggal_pembayaran ?? $persetujuanPenawaran->noc?->created_at,
true,
@@ -267,11 +278,19 @@
'nominal_diterima' => currencyFormat(
$persetujuanPenawaran->noc->nominal_bayar ?? 0,
),
'jenis_penilaian' => $persetujuanPenawaran->permohonan?->jenisPenilaian?->name ?? "",
'bukti_ksl' => $persetujuanPenawaran->noc->bukti_ksl ?? $persetujuanPenawaran->bukti_ksl ?? null,
'bukti_bayar' => $persetujuanPenawaran->bukti_bayar ?? null,
'updated_at' => dateFormat($persetujuanPenawaran->updated_at, true),
];
})->sortBy('updated_at', 1);
})->sortBy('updated_at', 1)->values();
// Calculate total nominal diterima from all filtered data (not just current page)
$totalNominalDiterima = $data->sum(function ($item) {
// Extract numeric value from formatted currency string
$nominal = str_replace(['Rp', '.', ',00'], '', $item['nominal_diterima']);
return (float) $nominal;
});
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
@@ -287,6 +306,7 @@
'pageCount' => $pageCount,
'page' => $currentPage,
'totalCount' => $totalRecords,
'totalNominalDiterima' => $totalNominalDiterima,
'data' => $data,
]);
}
@@ -301,7 +321,7 @@
$query = PersetujuanPenawaran::query();
// Filter for penyelesaian (where memo_penyelesaian is not null)
$query->whereHas('noc', function($q) {
$query->whereDoesntHave('noc', function($q) {
$q->whereNotNull('memo_penyelesaian');
});
@@ -309,7 +329,15 @@
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->orWhereRelation('penawaran', 'nomor_registrasi', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('penawaran', 'nomor_registrasi', 'LIKE', '%' . $search . '%')
->orWhereRelation('permohonan', 'nomor_registrasi', 'LIKE', '%' . $search . '%')
->orWhereRelation('permohonan.debiture','name', 'LIKE', '%' . $search . '%')
->orWhereRelation('permohonan.jenisPenilaian', 'name', 'LIKE', '%' . $search . '%')
->orWhere('nomor_tiket', 'LIKE', '%' . $search . '%'); $q->orWhereRelation('penawaran', 'nomor_registrasi', 'LIKE', '%' . $search . '%')
->orWhereRelation('permohonan', 'nomor_registrasi', 'LIKE', '%' . $search . '%')
->orWhereRelation('permohonan.debiture','name', 'LIKE', '%' . $search . '%')
->orWhereRelation('permohonan.jenisPenilaian', 'name', 'LIKE', '%' . $search . '%')
->orWhere('nomor_tiket', 'LIKE', '%' . $search . '%');
});
}
@@ -341,9 +369,11 @@
$data = $data->map(function ($persetujuanPenawaran) {
return [
'id' => $persetujuanPenawaran->id,
'nomor_registrasi' => $persetujuanPenawaran->permohonan->nomor_registrasi ?? $persetujuanPenawaran->penawaran->nomor_registrasi,
'nama_debitur' => $persetujuanPenawaran->permohonan->debiture->name ?? $persetujuanPenawaran->penawaran->permohonan->debiture->name,
'cabang' => $persetujuanPenawaran->permohonan->branch->name ?? $persetujuanPenawaran->penawaran->permohonan->branch->name,
'nomor_registrasi' => $persetujuanPenawaran->permohonan?->nomor_registrasi ?? $persetujuanPenawaran->penawaran?->nomor_registrasi ?? '',
'nomor_tiket' => $persetujuanPenawaran->nomor_tiket ?? '',
'nama_debitur' => $persetujuanPenawaran->permohonan?->debiture?->name ?? $persetujuanPenawaran->penawaran?->permohonan?->debiture?->name ?? $persetujuanPenawaran->noc?->debiture?->name,
'kode_cabang' => $persetujuanPenawaran?->permohonan?->branch?->code ?? $persetujuanPenawaran->penawaran?->permohonan?->branch?->code ?? $persetujuanPenawaran->noc?->branch?->code ?? '',
'cabang' => $persetujuanPenawaran->permohonan?->branch?->name ?? $persetujuanPenawaran->penawaran?->permohonan?->branch?->name ?? $persetujuanPenawaran->noc?->branch?->name ?? '',
'tanggal_pembayaran' => dateFormat(
$persetujuanPenawaran->noc->tanggal_pembayaran ?? $persetujuanPenawaran->noc?->created_at,
true,
@@ -353,6 +383,7 @@
'nominal_diterima' => currencyFormat(
$persetujuanPenawaran->noc->nominal_bayar ?? 0,
),
'jenis_penilaian' => $persetujuanPenawaran->permohonan?->jenisPenilaian?->name ?? "",
'bukti_ksl' => $persetujuanPenawaran->noc->bukti_ksl ?? $persetujuanPenawaran->bukti_ksl ?? null,
'bukti_bayar' => $persetujuanPenawaran->bukti_bayar ?? null,
'memo_penyelesaian' => $persetujuanPenawaran->noc->memo_penyelesaian ?? $persetujuanPenawaran->memo_penyelesaian ?? null,
@@ -365,7 +396,15 @@
true) : '-',
'updated_at' => dateFormat($persetujuanPenawaran->updated_at, true),
];
})->sortBy('updated_at', 1);
})->sortBy('updated_at', 1)->values();
// Calculate total nominal diterima from all filtered data (not just current page)
$totalNominalDiterima = $data->sum(function ($item) {
// Extract numeric value from formatted currency string
$nominal = str_replace(['Rp', '.', ',00'], '', $item['nominal_diterima']);
return (float) $nominal;
});
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
@@ -381,6 +420,7 @@
'pageCount' => $pageCount,
'page' => $currentPage,
'totalCount' => $totalRecords,
'totalNominalDiterima' => $totalNominalDiterima,
'data' => $data,
]);
}

View File

@@ -3,26 +3,12 @@
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Barryvdh\DomPDF\Facade\Pdf;
use Exception;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Location\Models\City;
use Modules\Location\Models\District;
use Modules\Location\Models\Province;
use Modules\Location\Models\Village;
use Modules\Lpj\Exports\PermohonanExport;
use Modules\Lpj\Http\Requests\PermohonanRequest;
use Modules\Lpj\Models\Branch;
use Modules\Lpj\Models\Debiture;
use Modules\Lpj\Models\DokumenJaminan;
use Modules\Lpj\Models\JenisFasilitasKredit;
use Modules\Lpj\Models\NilaiPlafond;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\PermohonanPembatalan;
use Modules\Lpj\Models\StatusPermohonan;
use Modules\Lpj\Models\TujuanPenilaian;
use Modules\Lpj\Services\PermohonanHistoryService;
use Illuminate\Support\Facades\Auth;
class PembatalanController extends Controller
{
@@ -92,6 +78,10 @@
// Retrieve data from the database
$query = PermohonanPembatalan::query();
if (Auth::user()->hasAnyRole(['pemohon-ao','pemohon-eo'])) {
$query = $query->whereRelation('permohonan', 'branch_id', Auth::user()->branch_id);
}
$query = $query->orderBy('created_at', 'desc');
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {

View File

@@ -2,21 +2,17 @@
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\JsonResponse;
use Modules\Lpj\Models\Noc;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Lpj\Http\Requests\PersetujuanPenawaranRequest;
use Modules\Lpj\Models\PenawaranTender;
use Modules\Lpj\Models\Bucok;
use Illuminate\Http\JsonResponse;
use Modules\Lpj\Models\Permohonan;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Modules\Lpj\Models\PenawaranTender;
use Modules\Lpj\Models\PersetujuanPenawaran;
// use Modules\Lpj\Models\JenisPenilaian;
// use Modules\Lpj\Models\Regions;
use Modules\Lpj\Http\Requests\PersetujuanPenawaranRequest;
class PembayaranController extends Controller
{
@@ -27,6 +23,14 @@ class PembayaranController extends Controller
return view('lpj::pembayaran.index');
}
public function kurang(){
return view('lpj::pembayaran.kurang');
}
public function lebih(){
return view('lpj::pembayaran.lebih');
}
public function approval()
{
return view('lpj::pembayaran.approval');
@@ -35,13 +39,16 @@ class PembayaranController extends Controller
public function dataApprovalForDatatables(Request $request)
{
if (is_null($this->user) || !$this->user->can('noc.view')) {
//abort(403, 'Sorry! You are not allowed to view persetujuan penawaran.');
//abort(403, 'Sorry! You are not allowed to view persetujuan penawaran.');
}
// Retrieve data from the database
// Retrieve data from the database
$query = PersetujuanPenawaran::query();
if (Auth::user()->hasAnyRole(['pemohon-ao','pemohon-eo'])) {
$query = $query->whereRelation('permohonan', 'branch_id', Auth::user()->branch_id);
}
// Apply search filter if provided
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
@@ -49,29 +56,29 @@ class PembayaranController extends Controller
});
}
// Apply sorting if provided
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// Get the total count of records
// Get the total count of records
$totalRecords = $query->count();
// Apply pagination if provided
// 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
$offset = ($page - 1) * $size; // Calculate the offset
$query->skip($offset)->take($size);
}
// Get the filtered count of records
// Get the filtered count of records
$filteredRecords = $query->count();
// Get the data for the current page
// Get the data for the current page
$data = $query
->with(
[
@@ -89,13 +96,13 @@ class PembayaranController extends Controller
)->get();
// Calculate the page count
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
// Calculate the current page number
// Calculate the current page number
$currentPage = $request->get('page', 1);
// Return the response data as a JSON object
// Return the response data as a JSON object
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,
@@ -107,34 +114,130 @@ class PembayaranController extends Controller
]);
}
public function create(){
return view('lpj::pembayaran.create');
}
public function edit($id)
{
$permohonan = Permohonan::find($id);
$persetujuanPenawaran = PersetujuanPenawaran::where('permohonan_id', $permohonan->id)->first();
$req = request()->all();
if(isset($req['tiket'])){
$persetujuanPenawaran = PersetujuanPenawaran::find($id);
$permohonan = Permohonan::find($persetujuanPenawaran?->permohonan_id);
} else {
$permohonan = Permohonan::find($id);
$persetujuanPenawaran = PersetujuanPenawaran::where('permohonan_id', $permohonan->id)->first();
}
return view('lpj::pembayaran.form', compact('permohonan', 'persetujuanPenawaran'));
}
public function editKurang($id){
$noc = Noc::find($id);
$permohonan = Permohonan::find($noc->permohonan_id);
$persetujuanPenawaran = PersetujuanPenawaran::where('permohonan_id', $permohonan->id)->first();
return view('lpj::pembayaran.form-kurang', compact('noc','permohonan','persetujuanPenawaran'));
}
public function editLebih($id){
$noc = Noc::find($id);
$permohonan = Permohonan::find($noc->permohonan_id);
$persetujuanPenawaran = PersetujuanPenawaran::where('permohonan_id', $permohonan->id)->first();
return view('lpj::pembayaran.form-lebih', compact('noc','permohonan','persetujuanPenawaran'));
}
public function store(PersetujuanPenawaranRequest $request)
{
$req = request()->all();
if(isset($req['type'])){
if($req['type'] == 'create'){
$data = [
'nomor_tiket' => $req['nomor_tiket'] ?? '',
'nominal_bayar' => $req['nominal_bayar'] ?? '',
'catatan' => $req['catatan'] ?? ''
];
if(request()->hasFile('bukti_bayar')){
$folderPath = 'persetujuan_penawaran/bukti_bayar/' . $req['nomor_tiket'];
$data['bukti_bayar'] = $request->file('bukti_bayar')->store($folderPath, 'public');
}
$persetujuanPenawaran = PersetujuanPenawaran::create($data);
$noc = [
'persetujuan_penawaran_id' => $persetujuanPenawaran->id,
'nomor_tiket' => $req['nomor_tiket'] ?? '',
'debiture_id' => $req['debitur_id'] ?? '',
'branch_id' => Auth::user()->branch_id,
];
$noc = Noc::create($noc);
$bucok = [
'tanggal_penuh' => $persetujuanPenawaran->created_at ?? $noc->created_at,
'tanggal' => $persetujuanPenawaran->created_at?->format('d') ?? $noc->created_at?->format('d'),
'bulan' => $persetujuanPenawaran->created_at?->format('m') ?? $noc->created_at?->format('m'),
'tahun' => $persetujuanPenawaran->created_at?->format('Y') ?? $noc->created_at?->format('Y'),
'nomor_tiket' => $req['nomor_tiket'] ?? '',
'nominal' => $req['nominal_bayar'] ?? '',
'nominal_berjalan' => $req['nominal_bayar'] ?? '',
'penyelesaian' => 'Belum Selesai',
'nama_sub_direktorat' => $noc->branch?->name ?? '',
'nama_direktorat_cabang' => $noc->branch?->name ?? '',
];
Bucok::updateOrCreate([
'nomor_tiket' => $req['nomor_tiket'] ?? '',
], $bucok);
return redirect()
->route('pembayaran.index')->with('success', 'Pembayaran berhasil disimpan.');
}
if($req['type'] == 'kurang_bayar'){
$noc = Noc::find($req['noc_id']);
$noc->nominal_pelunasan = $req['nominal_pelunasan'];
if (request()->hasFile('bukti_ksl_kurang_bayar')) {
$folderPath = 'persetujuan_penawaran/bukti_ksl_kurang_bayar/' . $req['noc_id'];
$noc->bukti_ksl_kurang_bayar = $request->file('bukti_ksl_kurang_bayar')->store($folderPath, 'public');
}
$noc->save();
$persetujuanPenawaran = PersetujuanPenawaran::find($noc->persetujuan_penawaran_id);
$persetujuanPenawaran->bukti_ksl_kurang_bayar = $noc->bukti_ksl_kurang_bayar;
$persetujuanPenawaran->nominal_kurang_bayar = $req['nominal_pelunasan'];
$persetujuanPenawaran->save();
return redirect()
->route('pembayaran.kurang.index')->with('success', 'Pelunasan Kurang Bayar berhasil disimpan.');
}
if($req['type'] == 'lebih_bayar'){
$noc = Noc::find($req['noc_id']);
if (request()->hasFile('bukti_ksl_lebih_bayar')) {
$folderPath = 'persetujuan_penawaran/bukti_ksl_lebih_bayar/' . $req['noc_id'];
$noc->bukti_ksl_lebih_bayar = $request->file('bukti_ksl_lebih_bayar')->store($folderPath, 'public');
}
$noc->save();
return redirect()
->route('pembayaran.lebih.index')->with('success', 'Pengembalian Lebih Bayar berhasil disimpan.');
}
}
$validated = $request->validated();
$validated['nominal_bayar'] = $req['nominal_bayar'] ?? 0;
$validated['created_by'] = Auth::id();
$validated['created_at'] = now();
$validated['status'] = '0';
$persetujuanPenawaran = PersetujuanPenawaran::where('permohonan_id', $validated['permohonan_id'] ?? null)->first();
$permohonan = Permohonan::find(request()->get('permohonan_id'));
$permohonan = Permohonan::find(request()->get('permohonan_id'));
if ($persetujuanPenawaran) {
// if (isset($validated['penawaran_id'])) {
// $persetujuanPenawaran = PersetujuanPenawaran::create(
// ['penawaran_id' => $validated['penawaran_id']],
// $validated,
// );
$persetujuanPenawaran->fill($validated);
if ($request->hasFile('bukti_bayar')) {
$folderPath = 'persetujuan_penawaran/' . $validated['penawaran_id'];
$folderPath = 'persetujuan_penawaran/' . $validated['penawaran_id'];
$persetujuanPenawaran->bukti_bayar = $request->file('bukti_bayar')->store($folderPath, 'public');
}
@@ -142,13 +245,22 @@ class PembayaranController extends Controller
$permohonan->approve_bayar_by = null;
$permohonan->approve_bayar_at = null;
$permohonan->status = 'done';
$permohonan->status = 'proses-laporan';
$permohonan->save();
} else {
$persetujuanPenawaran = PersetujuanPenawaran::create(
$validated
);
if(isset($validated['nomor_tiket'])){
$noc = Noc::where('nomor_tiket',$validated['nomor_tiket'])->first();
if($noc){
$noc->persetujuan_penawaran_id = $persetujuanPenawaran->id;
$noc->permohonan_id = $validated['permohonan_id'];
$noc->save();
}
}
$folderPath = 'persetujuan_penawaran/' . $validated['penawaran_id'];
if ($request->hasFile('bukti_bayar')) {
@@ -157,20 +269,47 @@ class PembayaranController extends Controller
$persetujuanPenawaran->save();
}
// Update the status of the related permohonan to 'spk'
$bucok = [
'tanggal_penuh' => $persetujuanPenawaran->created_at ?? $validated['created_at'],
'tanggal' => $persetujuanPenawaran->created_at?->format('d') ?? $validated['created_at']?->format('d'),
'bulan' => $persetujuanPenawaran->created_at?->format('m') ?? $validated['created_at']?->format('m'),
'tahun' => $persetujuanPenawaran->created_at?->format('Y') ?? $validated['created_at']?->format('Y'),
'nomor_tiket' => $req['nomor_tiket'] ?? '',
'nominal' => $req['nominal_bayar'] ?? '',
'nominal_berjalan' => $req['nominal_bayar'] ?? '',
'penyelesaian' => 'Belum Selesai',
'nama_sub_direktorat' => $noc->branch?->name ?? '',
'nama_direktorat_cabang' => $noc->branch?->name ?? '',
'permohonan_id' => $permohonan->id,
'nomor_registrasi' => $permohonan->nomor_registrasi,
];
if(isset($req['nomor_tiket']) && $req['nomor_tiket'] !=''){
Bucok::updateOrCreate([
'nomor_registrasi' => $permohonan->nomor_registrasi,
'nomor_tiket' => $req['nomor_tiket'],
], $bucok);
} else {
Bucok::updateOrCreate([
'nomor_registrasi' => $permohonan->nomor_registrasi
], $bucok);
}
// Update the status of the related permohonan to 'spk'
if ($permohonan) {
$permohonan->status_bayar = request()->get('status_bayar');
$permohonan->save();
// andy add, update status penawaran.status='spk'
// $penawaran = PenawaranTender::where('nomor_registrasi',$permohonan->nomor_registrasi)->first();
// andy add, update status penawaran.status='spk'
// $penawaran = PenawaranTender::where('nomor_registrasi',$permohonan->nomor_registrasi)->first();
PenawaranTender::where('nomor_registrasi', $permohonan->nomor_registrasi)->update([
'status' => 'noc',
'updated_by' => Auth::id(),
'updated_at' => now(),
]);
// andy add, update status penawaran.status='spk'
// andy add, update status penawaran.status='spk'
}
@@ -180,7 +319,7 @@ class PembayaranController extends Controller
public function update(Request $request, $id): JsonResponse
{
// init
// init
$data = [];
$output = [];
$tindakan = null;
@@ -196,22 +335,22 @@ class PembayaranController extends Controller
}
$output['data'] = $data;
// Update the status of the related permohonan to 'spk'
// Update the status of the related permohonan to 'spk'
$permohonan = Permohonan::find($id);
if ($permohonan) {
if ($request->type === 'revisi') {
$data['status'] = 'revisi-pembayaran';
$data['status'] = 'revisi-pembayaran';
$data['status_bayar'] = 'belum_bayar';
} else {
$data['status_bayar'] = 'sudah_bayar';
$data['status'] = 'proses-laporan';
}
$data['status'] = 'proses-laporan';
if ($permohonan->jenis_penilaian_id == 2) {
$data['status_bayar'] = 'sudah_bayar';
$data['status'] = 'spk';
if ($permohonan->jenis_penilaian_id == 2) {
$data['status_bayar'] = 'sudah_bayar';
$data['status'] = 'spk';
}
}
if ($permohonan->jenis_penilaian_id == 1) {
@@ -251,47 +390,43 @@ class PembayaranController extends Controller
public function dataForDatatables(Request $request)
{
if (is_null($this->user) || !$this->user->can('debitur.view')) {
// abort(403, 'Sorry! You are not allowed to view users.');
// abort(403, 'Sorry! You are not allowed to view users.');
}
$query = Permohonan::query()->where(function ($query) {
$query->where(['status_bayar' => 'belum_bayar', 'jenis_penilaian_id' => 1])
->orWhere('status', 'revisi-pembayaran');
})
->where(function ($query) {
$query->whereNotIn('id', function ($subquery) {
$subquery->select('permohonan_id')
->from('persetujuan_penawaran')
->whereNotNull('permohonan_id');
});
});
$query = PersetujuanPenawaran::query();
if (Auth::user()->hasAnyRole(['pemohon-ao','pemohon-eo'])) {
$query = $query->whereRelation('permohonan', 'branch_id', Auth::user()->branch_id);
}
/*$query->where(function($q) {
$q->whereRelation('permohonan', function($query) {
$query->where('status_bayar', 'belum_bayar')
->where('jenis_penilaian_id', 1);
});
});*/
$query->orWhereRelation('permohonan','status_bayar','revisi-pembayaran');
$query->orWhere(function($q) {
$q->where('permohonan_id',null);
$q->where('nomor_tiket','!=',null);
});
// Pencarian berdasarkan parameter search
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('nomor_registrasi', 'LIKE', '%' . $search . '%');
$q->orWhere('tanggal_permohonan', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('user', 'name', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('debiture', 'name', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('jenisPenilaian', 'name', 'LIKE', '%' . $search . '%');
$q->orWhereRelation('branch', 'name', 'LIKE', '%' . $search . '%');
$q->orWhere('status', 'LIKE', '%' . $search . '%');
});
}
// Sorting berdasarkan sortField dan sortOrder
// Sorting berdasarkan sortField dan sortOrder
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// Hitung total records
// Hitung total records
$totalRecords = $query->count();
// Pagination (default page size 10)
// Pagination (default page size 10)
$size = $request->get('size', 10);
if ($size == 0) {
$size = 10;
@@ -304,20 +439,172 @@ class PembayaranController extends Controller
$query->skip($offset)->take($size);
}
// Filtered records
// Filtered records
$filteredRecords = $query->count();
// Ambil data dengan relasi
$data = $query->with(['user', 'debiture', 'branch', 'jenisPenilaian'])->get();
// Ambil data dengan relasi
$data = $query->get();
$data = $data->map(function ($item) {
return [
'id' => $item->permohonan?->id ?? $item->id,
'nomor_registrasi' => $item->permohonan?->nomor_registrasi,
'nomor_tiket' => $item->nomor_tiket ?? '',
'debiture' => $item->permohonan?->debiture ?? $item->noc?->debiture,
'user' => $item->permohonan?->user ?? $item->creator,
'status_bayar' => $item->permohonan?->status_bayar ?? ($item->nomor_tiket ? 'Sudah Bayar' : ''),
'tanggal_permohonan' => $item->permohonan?->tanggal_permohonan ?? '',
'branch' => $item->permohonan?->branch ?? $item->noc?->branch,
'is_permohonan' => $item->permohonan ?? ''
];
});
// Hitung jumlah halaman
// Hitung jumlah halaman
$pageCount = ceil($totalRecords / $size);
// Ambil current page
// Ambil current page
$currentPage = max(1, $request->get('page', 1));
// Return JSON response
// Return JSON response
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,
'recordsFiltered' => $filteredRecords,
'pageCount' => $pageCount,
'page' => $currentPage,
'totalCount' => $totalRecords,
'data' => $data,
]);
}
public function dataForDatatablesKurang(Request $request)
{
if (is_null($this->user) || !$this->user->can('debitur.view')) {
// abort(403, 'Sorry! You are not allowed to view users.');
}
$query = Noc::query()->where(function ($query) {
$query->where(['status_kurang_bayar' => '1'])
->where('bukti_ksl_kurang_bayar',null);
});
// Sorting berdasarkan sortField dan sortOrder
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// Hitung total records
$totalRecords = $query->count();
// Pagination (default page size 10)
$size = $request->get('size', 10);
if ($size == 0) {
$size = 10;
}
if ($request->has('page') && $request->has('size')) {
$page = $request->get('page', 1);
$offset = ($page - 1) * $size;
$query->skip($offset)->take($size);
}
// Filtered records
$filteredRecords = $query->count();
// Ambil data dengan relasi
$data = $query->get();
$data = $data->map(function ($item) {
return [
'id' => $item->id,
'permohonan' => $item->permohonan,
'pemohon' => $item->permohonan->user,
'branch' => $item->permohonan->branch,
'debiture' => $item->permohonan->debiture,
'nominal_kurang_bayar' => formatRupiah($item->nominal_kurang_bayar,2)
];
});
// Hitung jumlah halaman
$pageCount = ceil($totalRecords / $size);
// Ambil current page
$currentPage = max(1, $request->get('page', 1));
// Return JSON response
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,
'recordsFiltered' => $filteredRecords,
'pageCount' => $pageCount,
'page' => $currentPage,
'totalCount' => $totalRecords,
'data' => $data,
]);
}
public function dataForDatatablesLebih(Request $request)
{
if (is_null($this->user) || !$this->user->can('debitur.view')) {
// abort(403, 'Sorry! You are not allowed to view users.');
}
$query = Noc::query()->where(function ($query) {
$query->where(['status_lebih_bayar' => '1'])
->where('bukti_ksl_lebih_bayar',null);
});
// Sorting berdasarkan sortField dan sortOrder
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// Hitung total records
$totalRecords = $query->count();
// Pagination (default page size 10)
$size = $request->get('size', 10);
if ($size == 0) {
$size = 10;
}
if ($request->has('page') && $request->has('size')) {
$page = $request->get('page', 1);
$offset = ($page - 1) * $size;
$query->skip($offset)->take($size);
}
// Filtered records
$filteredRecords = $query->count();
// Ambil data dengan relasi
$data = $query->get();
$data = $data->map(function ($item) {
return [
'id' => $item->id,
'permohonan' => $item->permohonan,
'pemohon' => $item->permohonan->user,
'branch' => $item->permohonan->branch,
'debiture' => $item->permohonan->debiture,
'nominal_lebih_bayar' => formatRupiah($item->nominal_lebih_bayar,2)
];
});
// Hitung jumlah halaman
$pageCount = ceil($totalRecords / $size);
// Ambil current page
$currentPage = max(1, $request->get('page', 1));
// Return JSON response
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,

View File

@@ -26,18 +26,21 @@ use Modules\Lpj\Http\Requests\FormSurveyorRequest;
use Modules\Lpj\Models\Authorization;
use Modules\Lpj\Models\Debiture;
use Modules\Lpj\Services\SaveFormInspesksiService;
use Modules\Lpj\Services\PreviewLaporanService;
class PenilaiController extends Controller
{
public $user;
protected $surveyorController;
protected $inspeksiService;
protected $previewLaporanService;
public function __construct(SurveyorController $surveyorController, SaveFormInspesksiService $inspeksiService)
public function __construct(SurveyorController $surveyorController, SaveFormInspesksiService $inspeksiService, PreviewLaporanService $previewLaporanService)
{
$this->surveyorController = $surveyorController;
$this->inspeksiService = $inspeksiService;
$this->previewLaporanService = $previewLaporanService;
}
/**
@@ -154,7 +157,7 @@ class PenilaiController extends Controller
$permohonan = $this->surveyorController->getPermohonanJaminanId($id, $documentId, $jaminanId);
if ($permohonan->status == 'proses-laporan') {
return redirect()->back()->with('error', 'Masih dalam proses laporan');
//return redirect()->back()->with('error', 'Masih dalam proses laporan');
}
$basicData = $this->surveyorController->getCommonData();
@@ -548,13 +551,7 @@ class PenilaiController extends Controller
return view('lpj::penilai.components.call-report', compact('permohonan', 'basicData', 'nomorLaporan', 'forminspeksi', 'cities', 'districts', 'villages', 'cekAlamat', 'callReport'));
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
{
//
}
public function dataForDatatables(Request $request)
{
@@ -622,7 +619,8 @@ class PenilaiController extends Controller
'jenisfasilitasKredit',
'penilaian.userPenilai',
'penilai',
'nilaiPlafond'
'nilaiPlafond',
'penawaran'
])->get();
// Calculate the page count
@@ -700,15 +698,17 @@ class PenilaiController extends Controller
'lokasi_lengkap' => $data->lokasi_lengkap ?? '',
];
// Extract data pembanding
if (isset($dataPembanding['data_pembanding'])) {
foreach ($dataPembanding['data_pembanding'] as $index => $pembanding) {
if ($index == 0) {
$exportData['pembanding1'] = $pembanding;
} elseif ($index == 1) {
$exportData['pembanding2'] = $pembanding;
} elseif ($index == 2) {
$exportData['pembanding3'] = $pembanding;
if(isset($dataPembanding)){
// Extract data pembanding
if (isset($dataPembanding['data_pembanding'])) {
foreach ($dataPembanding['data_pembanding'] as $index => $pembanding) {
if ($index == 0) {
$exportData['pembanding1'] = $pembanding;
} elseif ($index == 1) {
$exportData['pembanding2'] = $pembanding;
} elseif ($index == 2) {
$exportData['pembanding3'] = $pembanding;
}
}
}
}
@@ -841,10 +841,10 @@ class PenilaiController extends Controller
$permohonan = Permohonan::findOrFail($id);
if ($permohonan->status === 'proses-laporan') {
return response()->json([
/*return response()->json([
'success' => false,
'message' => 'Masih proses laporan',
], 400);
], 400);*/
}
if ($permohonan->status === 'proses-paparan') {
@@ -1006,11 +1006,10 @@ class PenilaiController extends Controller
],
];
Inspeksi::create([
Inspeksi::updateOrCreate([
'permohonan_id' => $validatedData['permohonan_id'],
'dokument_id' => $validatedData['dokument_id'],
'dokument_id' => $validatedData['dokument_id']
],[
'data_form' => json_encode($newData),
'name' => $validatedData['type']
]);
@@ -1252,9 +1251,10 @@ class PenilaiController extends Controller
Inspeksi::create([
Inspeksi::updateOrCreate([
'permohonan_id' => $validated['permohonan_id'],
'dokument_id' => $validated['dokument_id'],
'dokument_id' => $validated['dokument_id']
],[
'data_form' => json_encode($newData),
'name' => $validated['type']
]);
@@ -1275,8 +1275,6 @@ class PenilaiController extends Controller
}
}
public function print_out(Request $request)
{
$documentId = $request->query('documentId');
@@ -1392,6 +1390,15 @@ class PenilaiController extends Controller
}
}
public function print_out_laporan($permohonan_id, $document_id, $jaminan_id)
{
// jika tidak ada id kembalikan ke halaman sebelumnya
if (!$permohonan_id || !$document_id || !$jaminan_id) {
return redirect()->back()->with('error', 'Laporan tidak valid');
}
return $this->previewLaporanService->printOutLaporan($permohonan_id, $document_id, $jaminan_id);
}
private function getViewLaporan($tipe)
{
$viewMap = [
@@ -1771,4 +1778,24 @@ class PenilaiController extends Controller
'message' => 'Berhasil Revisi Ke surveyor',
], 200);
}
public function showLaporanInspeksi(
$permohonan_id,
$dokumen_id,
$jaminan_id,
Request $request)
{
if ($request->type == 'penilai') {
$back = route('penilai.show', $permohonan_id);
}else{
$back = route('surveyor.show', $permohonan_id);
}
return $this->previewLaporanService->previewLaporan($permohonan_id, $dokumen_id, $jaminan_id, $back);
}
public function showInspectionReportReview($permohonan_id, $dokumen_id, $jaminan_id)
{
}
}

View File

@@ -517,12 +517,18 @@ class PenilaianController extends Controller
$role = Auth::user()->roles[0]->name;
$status = 'done';
$approvalField = null;
$lpj_ = optional(json_decode($permohonan->penilai->lpj));
$npw = $lpj_->total_nilai_pasar_wajar ?? 0;
$npw = str_replace('.', '', $npw);
if ($role === 'senior-officer') {
$approvalField = 'approval_so';
$status = in_array($permohonan->nilai_plafond_id, [3]) ? 'done' : 'proses-laporan';
$status = $npw <=1000000000 ? 'done' : 'proses-laporan';
} elseif ($role === 'EO Appraisal') {
$approvalField = 'approval_eo';
$status = in_array($permohonan->nilai_plafond_id, [2, 1]) ? 'done' : 'proses-laporan';
$status = $npw <=5000000000 ? 'done' : 'proses-laporan';
} elseif ($role === 'DD Appraisal') {
$approvalField = 'approval_dd';
$status = 'done';

View File

@@ -23,9 +23,13 @@
use Modules\Lpj\Models\Penilaian;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\PermohonanPembatalan;
use Modules\Lpj\Models\PersetujuanPenawaran;
use Modules\Lpj\Models\StatusPermohonan;
use Modules\Lpj\Models\TujuanPenilaian;
use Modules\Lpj\Services\PermohonanHistoryService;
use Modules\Lpj\Models\Noc;
class PermohonanController extends Controller
{
@@ -46,6 +50,9 @@
{
$validate = $request->validated();
if ($validate) {
if(auth()->user()->hasRole('admin')){
$validate['status'] = "preregister";
}
try {
// Process file upload
$filePath = null;
@@ -168,7 +175,7 @@
// Retrieve data from the database
$query = Permohonan::query();
if (!Auth::user()->hasAnyRole(['administrator'])) {
if (!Auth::user()->hasAnyRole(['administrator','admin'])) {
$query = $query->where('branch_id', Auth::user()->branch_id);
}
@@ -358,6 +365,55 @@
$permohonan->status = $request->status;
$permohonan->keterangan = $request->keterangan;
$permohonan->save();
if ($permohonan->status_bayar == 'belum_bayar') {
PersetujuanPenawaran::firstOrCreate(
['permohonan_id' => $id],
['created_by' => Auth::id()]
);
}
if ($permohonan->status == 'sudah_dibayar') {
$documents = $permohonan->dokumenjaminan->first(function ($doc) {
return $doc->detail && $doc->detail->contains('name', 'Bukti Bayar');
});
$buktiBayar = $documents->detail->filter(function ($detail) {
return $detail->name == 'Bukti Bayar';
})->first() ?? null;
if ($buktiBayar->isEmpty()) {
return redirect()->route('authorization.show', $id)->with('error', 'Bukti Bayar harus diunggah');
}
$dokumenJaminan = json_decode($buktiBayar->dokumen_jaminan);
$persetujuanPenawaran = PersetujuanPenawaran::firstOrCreate(
['permohonan_id' => $id],
[
'created_by' => Auth::id(),
'bukti_bayar' => $buktiBayar->first()->dokumen_jaminan[0],
]
);
try {
Noc::updateOrCreate([
'permohonan_id' => $persetujuanPenawaran->permohonan_id,
'persetujuan_penawaran_id' => $persetujuanPenawaran->id
],[
'bukti_bayar' => $persetujuanPenawaran->bukti_bayar,
]);
} catch (\Exception $e) {
\Log::error('Failed to create or update NOC: ' . $e->getMessage());
return redirect()
->route('persetujuan-penawaran.index')
->with('error', 'Persetujuan Penawaran berhasil disimpan tetapi gagal membuat NOC: ' . $e->getMessage());
}
}
} catch (Exception $e) {
return redirect()->route('authorization.show', $id)->with('error', 'Failed to update permohonan');
}

View File

@@ -40,6 +40,7 @@
$validated = $request->validated();
$validated['created_by'] = Auth::id();
$validated['status'] = '0';
$validated['nominal_bayar'] = $validated['biaya_final'];
$persetujuanPenawaran = PersetujuanPenawaran::updateOrCreate(
['penawaran_id' => $validated['penawaran_id']],
@@ -179,8 +180,8 @@
public function edit($id)
{
$permohonan = Permohonan::with(['debiture', 'penawaranTender.detail'])->find($id);
return view('lpj::persetujuan_penawaran.form', compact('permohonan'));
$persetujuanPenawaran = PersetujuanPenawaran::where('permohonan_id', $id)->first();
return view('lpj::persetujuan_penawaran.form', compact('permohonan', 'persetujuanPenawaran'));
}
/**
@@ -201,8 +202,12 @@
}
// Retrieve data from the database
$query = Permohonan::query()->where(['status' => 'persetujuan-penawaran']);
//$query = Permohonan::query()->where(['status' => 'persetujuan-penawaran']);
$query = Permohonan::query()
->where(['status' => 'persetujuan-penawaran'])
->whereHas('penawaranTender', function ($q) {
$q->where('status', 'persetujuan-penawaran');
});
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');

View File

@@ -49,7 +49,7 @@
$query =PenawaranTender::query()
->select('penawaran.*', 'tujuan_penilaian_kjpp.name as tujuan_penilaian_kjpp_name')
->leftJoin('tujuan_penilaian_kjpp', 'tujuan_penilaian_kjpp.id','=','penawaran.tujuan_penilaian_kjpp_id')
->where('penawaran.status','=','spk')
->where('penawaran.status','=','registrasi-final')
->withCount('penawarandetails');
// Apply search filter if provided

View File

@@ -0,0 +1,548 @@
<?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\Exports\SlikExport;
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);
}
}
/**
* Export data SLIK ke Excel
*
* Method ini menangani export data SLIK ke format Excel dengan:
* - Logging aktivitas export
* - Error handling yang proper
* - Format Excel yang sesuai dengan struktur SLIK
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function export()
{
try {
Log::info('SLIK Export: Memulai proses export data SLIK', [
'user_id' => Auth::id(),
'timestamp' => now(),
'memory_usage' => memory_get_usage(true) / 1024 / 1024 . ' MB'
]);
// Hitung total data yang akan di-export
$totalData = Slik::count();
Log::info('SLIK Export: Informasi data export', [
'total_records' => $totalData,
'user_id' => Auth::id(),
'timestamp' => now()
]);
// Generate nama file dengan timestamp
$filename = 'slik_export_' . date('Y-m-d_H-i-s') . '.xlsx';
// Proses export menggunakan SlikExport class
$export = Excel::download(new SlikExport(), $filename);
Log::info('SLIK Export: Berhasil generate file export', [
'filename' => $filename,
'total_records' => $totalData,
'user_id' => Auth::id(),
'timestamp' => now(),
'memory_peak' => memory_get_peak_usage(true) / 1024 / 1024 . ' MB'
]);
return $export;
} catch (\Exception $e) {
Log::error('SLIK Export: Gagal melakukan export data SLIK', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'user_id' => Auth::id(),
'timestamp' => now(),
'memory_usage' => memory_get_usage(true) / 1024 / 1024 . ' MB'
]);
return response()->json([
'success' => false,
'message' => 'Gagal melakukan export data SLIK: ' . $e->getMessage()
], 500);
}
}
/**
* Truncate all SLIK data
*
* @return \Illuminate\Http\JsonResponse
*/
public function truncate()
{
try {
DB::beginTransaction();
Log::info('SLIK Truncate: Memulai proses truncate data SLIK', [
'user_id' => Auth::id(),
'timestamp' => now()
]);
// Truncate tabel SLIK
Slik::truncate();
DB::commit();
Log::info('SLIK Truncate: Berhasil menghapus semua data SLIK', [
'user_id' => Auth::id(),
'timestamp' => now()
]);
return response()->json([
'success' => true,
'message' => 'Semua data SLIK berhasil dihapus'
]);
} catch (\Exception $e) {
DB::rollback();
Log::error('SLIK Truncate: Gagal menghapus data SLIK', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'user_id' => Auth::id(),
'timestamp' => now()
]);
return response()->json([
'success' => false,
'message' => 'Gagal menghapus data SLIK: ' . $e->getMessage()
], 500);
}
}
}

View File

@@ -345,6 +345,19 @@ use Illuminate\Support\Facades\Auth;
$content = $pdf->download()->getOriginalContent();
Storage::put('public/'.$newFileNameWithPath,$content);
$permohonanModel = Permohonan::where('nomor_registrasi', $penawaran->nomor_registrasi)->first();
if ($permohonanModel) {
$permohonanModel->status = 'registrasi-final';
$permohonanModel->save();
}
$persetujuanPenawaran = PenawaranTender::where('id', $penawaran->id)->first();
if ($persetujuanPenawaran) {
$persetujuanPenawaran->status = 'registrasi-final';
$persetujuanPenawaran->save();
}
$data1['status'] = 'success';
$data1['spkpenawaran_path'] = $spkpenawaran_path;
$data1['message']['message_success'] = array('Generate SPK PDF successfully');

View File

@@ -3,25 +3,19 @@
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Lpj\Exports\BasicDataSurveyorExport;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\Response;
use Modules\Lpj\Models\Debiture;
use Modules\Lpj\Models\LaporanExternal;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\Branch;
use Modules\Lpj\Models\Surveyor;
use Modules\Lpj\Models\BentukTanah;
use Modules\Lpj\Models\KonturTanah;
use Modules\Location\Models\Province;
@@ -41,17 +35,8 @@ use Modules\Lpj\Models\SpekBangunan;
use Modules\Lpj\Models\SpekKategoritBangunan;
use Modules\Lpj\Models\SaranaPelengkap;
use Modules\Lpj\Models\ArahMataAngin;
use Modules\Lpj\Models\Analisa;
use Modules\Lpj\Models\Penilaian;
use Modules\Lpj\Models\PerkerasanJalan;
use Modules\Lpj\Models\AnalisaFakta;
use Modules\Lpj\Models\AnalisaLingkungan;
use Modules\Lpj\Models\AnalisaTanahBagunan;
use Modules\Lpj\Models\SpekBangunanAnalisa;
use Modules\Lpj\Models\Denah;
use Modules\Lpj\Models\FotoJaminan;
use Modules\Lpj\Models\Lingkungan;
use Modules\Lpj\Models\LantaiUnit;
use Modules\Lpj\Models\Teams;
use Modules\Lpj\Models\Lantai;
use Modules\Lpj\Models\Inspeksi;
@@ -62,29 +47,24 @@ use Modules\Lpj\Models\PosisiUnit;
use Modules\Lpj\Models\TerletakArea;
use Modules\Lpj\Models\FasilitasObjek;
use Modules\Lpj\Models\MerupakanDaerah;
use Modules\Lpj\Models\ObjekJaminan;
use Modules\Lpj\Models\ModelAlatBerat;
use Modules\Lpj\Models\JenisPesawat;
use Modules\Lpj\Models\DokumenJaminan;
use Modules\Lpj\Models\DetailDokumenJaminan;
use Modules\Lpj\Models\JenisKapal;
use Modules\Lpj\Models\JenisKendaraan;
use Modules\Lpj\Models\RuteJaminan;
use Modules\Lpj\Models\HubunganPemilikJaminan;
use Modules\Lpj\Models\HubunganPenghuniJaminan;
use Modules\Lpj\Models\AnalisaUnit;
use Modules\Lpj\Models\GolonganMasySekitar;
use Modules\Lpj\Models\TingkatKeramaian;
use Modules\Lpj\Models\TujuanPenilaian;
use Modules\Lpj\Models\LaluLintasLokasi;
use Modules\Lpj\Models\SpekBagunanAnalisaDetail;
use Modules\Lpj\Http\Requests\SurveyorRequest;
use Modules\Lpj\Http\Requests\FormSurveyorRequest;
use Modules\Lpj\Jobs\SendJadwalKunjunganEmailJob;
use App\Helpers\Lpj;
use Modules\Lpj\Models\Authorization;
use Modules\Lpj\Services\SurveyorValidateService;
use Modules\Lpj\Services\SaveFormInspesksiService;
use Modules\Lpj\Models\PermohonanHistory;
class SurveyorController extends Controller
{
@@ -151,13 +131,17 @@ class SurveyorController extends Controller
});
}
$catatan_revisi_survey = PermohonanHistory::where('permohonan_id', $id)
->where('status', 'revisi-survey')->latest()->first();
return view('lpj::surveyor.detail', compact(
'permohonan',
'surveyor',
'branches',
'provinces',
'bentukTanah',
'inspeksiData'
'inspeksiData',
'catatan_revisi_survey',
));
}
@@ -794,12 +778,12 @@ class SurveyorController extends Controller
]);
if ($permohonan->jenisPenilaian->name == "External") {
if (in_array(strtolower($permohonan->jenisPenilaian->name), ['external', 'eksternal'])) {
LaporanExternal::updateOrCreate(
['permohonan_id' => $permohonan->id],
[
'nomor_laporan' => $permohonan->nomor_registrasi,
'tanggal_laporan' => now(),
'tgl_final_laporan' => now(),
'created_by' => Auth::id(),
]
);
@@ -1396,6 +1380,11 @@ class SurveyorController extends Controller
}
public function storeDataPembanding(Request $request)
{
$request->merge([
'kordinat_lat' => $request->get('kordinat_lat') ? str_replace(',', '', $request->get('kordinat_lat')) : null,
'kordinat_lng' => $request->get('kordinat_lng') ? str_replace(',', '', $request->get('kordinat_lng')) : null,
]);
try {
DB::beginTransaction();
@@ -1847,10 +1836,10 @@ class SurveyorController extends Controller
return redirect()
->route('basicdata.' . $type . '.index')
->with('success', 'created successfully');
} catch (Exeception $e) {
} catch (\Exception $e) {
return redirect()
->route('basicdata.' . $type . '.index')
->with('error', $th->getMessage());
->with('error', $e->getMessage());
}
}
}
@@ -2116,7 +2105,7 @@ class SurveyorController extends Controller
public function dataForDatatablesData(Request $request, $type)
{
if (is_null(auth()->user()) || !$this->user->can('jenis_aset.view')) {
if (is_null(Auth::user()) || !$this->user->can('jenis_aset.view')) {
//abort(403, 'Sorry! You are not allowed to view users.');
}
@@ -2156,7 +2145,7 @@ class SurveyorController extends Controller
if (array_key_exists($type, $models)) {
$query = $models[$type]::query();
} else {
throw new InvalidArgumentException("Invalid type: $type");
throw new \InvalidArgumentException("Invalid type: $type");
}
if ($request->has('search') && !empty($request->get('search'))) {
@@ -2229,9 +2218,9 @@ class SurveyorController extends Controller
$model = $modelClass::findOrFail($id);
$model->delete();
return response()->json(['success' => true, 'message' => 'deleted successfully']);
} catch (ModelNotFoundException $e) {
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
return response()->json(['success' => false, 'message' => 'not found.'], 404);
} catch (Exception $e) {
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Failed to delete.'], 500);
}
}
@@ -2387,14 +2376,14 @@ class SurveyorController extends Controller
$path = $file->storeAs("public/surveyor/{$request->type}", $fileName);
if ($path === false) {
throw new Exception("Failed to store file for {$fileKey}");
throw new \Exception("Failed to store file for {$fileKey}");
}
if (isset($data[$fileKey]) && $data[$fileKey]) {
$this->deleteFile($data[$fileKey]);
}
return str_replace('public/', '', $path);
} else {
throw new Exception("Invalid file upload for {$fileKey}");
throw new \Exception("Invalid file upload for {$fileKey}");
}
} elseif (isset($data[$fileKey]) && $data[$fileKey]) {
return $data[$fileKey];
@@ -2423,14 +2412,14 @@ class SurveyorController extends Controller
public function uploadFile($file, $type)
{
if (!$file->isValid()) {
throw new Exception("Invalid file upload for {$type}");
throw new \Exception("Invalid file upload for {$type}");
}
$fileName = time() . '_' . $file->getClientOriginalName();
$path = $file->storeAs("public/surveyor/{$type}", $fileName);
if ($path === false) {
throw new Exception("Failed to store file for {$type}");
throw new \Exception("Failed to store file for {$type}");
}
return str_replace('public/', '', $path);
@@ -2880,9 +2869,10 @@ class SurveyorController extends Controller
$inspeksi->data_form = json_encode($existingData);
$inspeksi->save();
} else {
Inspeksi::create([
Inspeksi::updateOrCreate([
'permohonan_id' => $request->input('permohonan_id'),
'dokument_id' => $request->input('document_id'),
'dokument_id' => $request->input('document_id')
],[
'data_form' => json_encode($existingData),
]);
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Modules\Lpj\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CategoryDaftarPustakaRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'name' => 'required|max:255',
];
if ($this->method() == 'PUT') {
$rules['code'] = 'required|max:50|unique:category_daftar_pustaka,code,' . $this->id;
} else {
$rules['code'] = 'required|max:50|unique:category_daftar_pustaka,code';
}
return $rules;
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Modules\Lpj\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DaftarPustakaRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'judul' => 'required|max:255',
'category_id' => 'required',
'deskripsi' => 'nullable',
];
if ($this->method() == 'PUT') {
$rules['attachment'] = 'nullable|mimes:pdf,jpg,jpeg,png,gif';
} else {
$rules['attachment'] = 'required|mimes:pdf,jpg,jpeg,png,gif';
}
return $rules;
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -14,6 +14,7 @@
{
$rules = [
'name' => 'required|max:255',
'biaya' => 'nullable|numeric|min:0',
];
if ($this->method() == 'PUT') {

View File

@@ -52,7 +52,7 @@
private function getBasicInfoRules()
{
return [
'permohonan_id' => 'required|exists:permohonan,id',
'permohonan_id' => 'nullable|exists:permohonan,id',
'persetujuan_penawaran_id' => 'required|exists:persetujuan_penawaran,id',
'status' => 'nullable|boolean',
'created_by' => 'nullable|exists:users,id',
@@ -72,10 +72,16 @@
return [
'total_harus_bayar' => 'nullable|numeric|min:0',
'nominal_bayar' => 'nullable|numeric|min:0',
'total_pembukuan' => 'nullable|numeric|min:0',
'bukti_ksl' => $fileRule,
'bukti_bayar' => $fileRule,
'status_bayar' => 'nullable|boolean',
'tanggal_pembayaran' => 'nullable|date',
'status_kurang_bayar' => 'nullable|boolean',
'status_lebih_bayar' => 'nullable|boolean',
'nominal_kurang_bayar' => 'nullable|numeric|min:0',
'nominal_lebih_bayar' => 'nullable|numeric|min:0',
'bukti_pengembalian' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:2048',
];
}
@@ -89,12 +95,16 @@
$fileRule = 'nullable|file|mimes:' . self::ALLOWED_FILE_TYPES . '|max:' . self::MAX_FILE_SIZE;
return [
'nominal_penyelesaian' => 'nullable|numeric|min:0',
'status_penyelesaian' => 'nullable|boolean',
'tanggal_penyelesaian' => 'nullable|date',
'bukti_penyelesaian' => $fileRule,
'memo_penyelesaian' => $fileRule,
'catatan_noc' => 'nullable|string',
'nominal_penyelesaian' => 'nullable|numeric|min:0',
'status_penyelesaian' => 'nullable|boolean',
'tanggal_penyelesaian' => 'nullable|date',
'bukti_penyelesaian' => $fileRule,
'memo_penyelesaian' => $fileRule,
'catatan_noc' => 'nullable|string',
'nomor_tiket' => 'nullable|string',
'nomor_rekening_lebih_bayar' => 'nullable|string',
'bukti_ksl_lebih_bayar' => 'nullable|string',
'bukti_ksl_kurang_bayar' => 'nullable|string'
];
}
@@ -120,7 +130,6 @@
public function messages()
{
return [
'permohonan_id.required' => 'ID Permohonan harus diisi',
'permohonan_id.exists' => 'ID Permohonan tidak valid',
'persetujuan_penawaran_id.required' => 'ID Persetujuan Penawaran harus diisi',
'persetujuan_penawaran_id.exists' => 'ID Persetujuan Penawaran tidak valid',
@@ -148,6 +157,11 @@
'authorized_status.boolean' => 'Status Otorisasi harus berupa boolean',
'authorized_at.date' => 'Format Tanggal Otorisasi tidak valid',
'authorized_by.exists' => 'User Otorisasi tidak valid',
'status_kurang_bayar.boolean' => 'Status Kurang Bayar harus berupa boolean',
'status_lebih_bayar.boolean' => 'Status Lebih Bayar harus berupa boolean',
'nominal_kurang_bayar.numeric' => 'Nominal Kurang Bayar harus berupa angka',
'nominal_kurang_bayar.min' => 'Nominal Kurang Bayar minimal 0',
'nominal_lebih_bayar.numeric' => 'Nominal Lebih Bayar harus berupa angka',
];
}
}

View File

@@ -48,7 +48,6 @@
),
'tanggal_permohonan' => date('Y-m-d'),
'user_id' => auth()->user()->id,
'branch_id' => auth()->user()->branch_id,
'status' => 'order'
]);
}

View File

@@ -14,49 +14,54 @@
public function rules()
{
return [
'permohonan_id' => 'nullable|exists:permohonan,id',
'penawaran_id' => 'nullable|exists:penawaran,id',
'nomor_proposal_penawaran' => 'nullable|string|max:255',
'tanggal_proposal_penawaran' => 'nullable|date',
'biaya_final' => 'nullable|numeric|min:0',
'sla_resume' => 'nullable|numeric|min:0',
'sla_final' => 'nullable|numeric|min:0',
'file_persetujuan_penawaran' => 'nullable|file|mimes:pdf,doc,docx|max:10240',
'surat_representasi' => 'nullable|file|mimes:pdf,doc,docx|max:10240',
'status' => 'nullable|boolean',
'authorized_status' => 'boolean',
'authorized_at' => 'nullable|date',
'authorized_by' => 'nullable|exists:users,id',
'catatan' => 'nullable|string',
];
'permohonan_id' => 'nullable|exists:permohonan,id',
'penawaran_id' => 'nullable|exists:penawaran,id',
'nomor_proposal_penawaran' => 'nullable|string|max:255',
'nomor_tiket' => 'nullable|string|max:100',
'nominal_kurang_bayar' => 'nullable|string|max:100',
'bukti_ksl_kurang_bayar' => 'nullable|file|mimes:pdf,doc,docx|max:10240',
'tanggal_proposal_penawaran' => 'nullable|date',
'biaya_final' => 'nullable|numeric|min:0',
'sla_resume' => 'nullable|numeric|min:0',
'sla_final' => 'nullable|numeric|min:0',
'file_persetujuan_penawaran' => 'nullable|file|mimes:pdf,doc,docx|max:10240',
'surat_representasi' => 'nullable|file|mimes:pdf,doc,docx|max:10240',
'status' => 'nullable|boolean',
'authorized_status' => 'boolean',
'authorized_at' => 'nullable|date',
'authorized_by' => 'nullable|exists:users,id',
'catatan' => 'nullable|string',
];
}
public function messages()
{
return [
'penawaran_id.required' => 'Penawaran ID wajib diisi.',
'penawaran_id.exists' => 'Penawaran ID tidak valid.',
'nomor_proposal_penawaran.required' => 'Nomor proposal penawaran wajib diisi.',
'tanggal_proposal_penawaran.required' => 'Tanggal proposal penawaran wajib diisi.',
'tanggal_proposal_penawaran.date' => 'Tanggal proposal penawaran harus berupa tanggal yang valid.',
'biaya_final.required' => 'Biaya final wajib diisi.',
'biaya_final.numeric' => 'Biaya final harus berupa angka.',
'biaya_final.min' => 'Biaya final tidak boleh kurang dari 0.',
'sla_resume.required' => 'SLA Resume wajib diisi.',
'sla_final.required' => 'SLA Final wajib diisi.',
'file_persetujuan_penawaran.file' => 'File Persetujuan Penawaran harus berupa file.',
'file_persetujuan_penawaran.mimes' => 'File Persetujuan Penawaran harus berupa file PDF, DOC, atau DOCX.',
'file_persetujuan_penawaran.max' => 'Ukuran File Persetujuan Penawaran tidak boleh lebih dari 10MB.',
'surat_representasi.file' => 'Surat Representasi harus berupa file.',
'surat_representasi.mimes' => 'Surat Representasi harus berupa file PDF, DOC, atau DOCX.',
'surat_representasi.max' => 'Ukuran Surat Representasi tidak boleh lebih dari 10MB.',
'region_id.required' => 'Region ID wajib diisi.',
'region_id.exists' => 'Region ID tidak valid.',
'status.required' => 'Status wajib diisi.',
'status.boolean' => 'Status harus berupa nilai boolean.',
'authorized_status.boolean' => 'Status otorisasi harus berupa nilai boolean.',
'authorized_at.date' => 'Tanggal otorisasi harus berupa tanggal yang valid.',
'authorized_by.exists' => 'ID pengguna yang mengotorisasi tidak valid.',
];
'penawaran_id.required' => 'Penawaran ID wajib diisi.',
'penawaran_id.exists' => 'Penawaran ID tidak valid.',
'nomor_proposal_penawaran.required' => 'Nomor proposal penawaran wajib diisi.',
'nomor_tiket.string' => 'Nomor tiket harus berupa teks.',
'nomor_tiket.max' => 'Nomor tiket tidak boleh lebih dari 100 karakter.',
'tanggal_proposal_penawaran.required' => 'Tanggal proposal penawaran wajib diisi.',
'tanggal_proposal_penawaran.date' => 'Tanggal proposal penawaran harus berupa tanggal yang valid.',
'biaya_final.required' => 'Biaya final wajib diisi.',
'biaya_final.numeric' => 'Biaya final harus berupa angka.',
'biaya_final.min' => 'Biaya final tidak boleh kurang dari 0.',
'sla_resume.required' => 'SLA Resume wajib diisi.',
'sla_final.required' => 'SLA Final wajib diisi.',
'file_persetujuan_penawaran.file' => 'File Persetujuan Penawaran harus berupa file.',
'file_persetujuan_penawaran.mimes' => 'File Persetujuan Penawaran harus berupa file PDF, DOC, atau DOCX.',
'file_persetujuan_penawaran.max' => 'Ukuran File Persetujuan Penawaran tidak boleh lebih dari 10MB.',
'surat_representasi.file' => 'Surat Representasi harus berupa file.',
'surat_representasi.mimes' => 'Surat Representasi harus berupa file PDF, DOC, atau DOCX.',
'surat_representasi.max' => 'Ukuran Surat Representasi tidak boleh lebih dari 10MB.',
'region_id.required' => 'Region ID wajib diisi.',
'region_id.exists' => 'Region ID tidak valid.',
'status.required' => 'Status wajib diisi.',
'status.boolean' => 'Status harus berupa nilai boolean.',
'authorized_status.boolean' => 'Status otorisasi harus berupa nilai boolean.',
'authorized_at.date' => 'Tanggal otorisasi harus berupa tanggal yang valid.',
'authorized_by.exists' => 'ID pengguna yang mengotorisasi tidak valid.',
];
}
}

341
app/Imports/BucokImport.php Normal file
View File

@@ -0,0 +1,341 @@
<?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;
}
}

415
app/Imports/SlikImport.php Normal file
View 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;
}
}
}

View 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);
}
}
}

View File

@@ -6,7 +6,7 @@
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Wildside\Userstamps\Userstamps;
use Mattiverse\Userstamps\Traits\Userstamps;
use Illuminate\Notifications\Notifiable;

115
app/Models/Bucok.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
namespace Modules\Lpj\Models;
use Illuminate\Foundation\Auth\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
/**
* Model Bucok untuk mengelola data bucok
*
* @property int $id
* @property int|null $no
* @property string|null $tanggal
* @property string|null $bulan
* @property int|null $tahun
* @property string|null $tanggal_penuh
* @property string|null $nomor_categ
* @property string|null $coa_summary
* @property string|null $nomor_coa
* @property string|null $nama_coa
* @property string $nomor_tiket
* @property string|null $deskripsi
* @property float|null $nominal
* @property string|null $penyelesaian
* @property int|null $umur_aging
* @property string|null $cost_center
* @property string|null $nama_sub_direktorat
* @property string|null $nama_direktorat_cabang
* @property string|null $tanggal_penyelesaian
* @property float|null $nominal_penyelesaian
* @property float|null $nominal_berjalan
* @property float|null $amortisasi_berjalan
* @property float|null $sistem_berjalan
* @property float|null $lainnya_berjalan
* @property float|null $nominal_gantung
* @property float|null $aset_gantung
* @property string|null $keterangan_gantung
* @property string|null $lainnya_satu
* @property string|null $lainnya_dua
*/
class Bucok extends Base
{
use HasFactory;
/**
* Nama tabel yang digunakan oleh model
*/
protected $table = 'bucoks';
/**
* Field yang dapat diisi secara mass assignment
*/
protected $fillable = [
'no',
'tanggal',
'bulan',
'tahun',
'tanggal_penuh',
'nomor_categ',
'coa_summary',
'nomor_coa',
'nama_coa',
'nomor_tiket',
'deskripsi',
'nominal',
'penyelesaian',
'umur_aging',
'cost_center',
'nama_sub_direktorat',
'nama_direktorat_cabang',
'tanggal_penyelesaian',
'nominal_penyelesaian',
'nominal_berjalan',
'amortisasi_berjalan',
'sistem_berjalan',
'lainnya_berjalan',
'nominal_gantung',
'aset_gantung',
'keterangan_gantung',
'lainnya_satu',
'lainnya_dua',
'nomor_registrasi',
'permohonan_id',
];
/**
* Casting tipe data untuk field tertentu
*/
protected $casts = [
'no' => 'integer',
'tanggal' => 'date',
'tahun' => 'integer',
'tanggal_penuh' => 'date',
'nominal' => 'decimal:2',
'umur_aging' => 'integer',
'tanggal_penyelesaian' => 'date',
'nominal_penyelesaian' => 'decimal:2',
'nominal_berjalan' => 'decimal:2',
'amortisasi_berjalan' => 'decimal:2',
'sistem_berjalan' => 'decimal:2',
'lainnya_berjalan' => 'decimal:2',
'nominal_gantung' => 'decimal:2',
'aset_gantung' => 'decimal:2',
];
/**
* Field yang akan di-hidden saat serialization
*/
protected $hidden = [
'created_by',
'updated_by',
'deleted_by',
];
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Modules\Lpj\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
// use Modules\Lpj\Database\Factories\CategoryDaftarPustakaFactory;
class CategoryDaftarPustaka extends Model
{
use HasFactory;
protected $table = 'category_daftar_pustaka';
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'id',
'name',
'code',
];
public function daftarPustaka(){
return $this->hasMany(DaftarPustaka::class);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Modules\Lpj\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
// use Modules\Lpj\Database\Factories\DaftarPustakaFactory;
class DaftarPustaka extends Model
{
use HasFactory;
protected $table = 'daftar_pustaka';
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'id',
'category_id',
'judul',
'attachment',
'deskripsi',
];
public function category(){
return $this->belongsTo(CategoryDaftarPustaka::class);
}
}

View File

@@ -14,7 +14,7 @@ class Inspeksi extends Base
/**
* The attributes that are mass assignable.
*/
protected $fillable = ['data_form', 'foto_form', 'denah_form','permohonan_id', 'name', 'status', 'authorized_status', 'authorized_at', 'authorized_by', 'created_by', 'updated_by', 'deleted_by','dokument_id','data_pembanding'];
protected $fillable = ['data_form', 'foto_form', 'denah_form','permohonan_id', 'name', 'status', 'authorized_status', 'authorized_at', 'authorized_by', 'created_by', 'updated_by', 'deleted_by','dokument_id','data_pembanding','mig_detail_data_jaminan'];
public function permohonan()
{

View File

@@ -27,7 +27,7 @@ class LampiranDokumen extends Base
{
$user = Auth::user();
if ($user && $user->hasAnyRole(['penilai', 'administrator', 'Penilai', 'admin','surveyor'])) {
if ($user && $user->hasAnyRole(['penilai', 'administrator', 'Penilai', 'admin','surveyor','pemohon-ao','pemohon-eo'])) {
$file = $fileData['file'];
$fileName = $fileData['nama_file'] ?? time() . '_' . $file->getClientOriginalName();
$filePath = $file->storeAs('lampiran_dokumen', $fileName, 'public');

View File

@@ -18,7 +18,9 @@ class LaporanAdminKredit extends Base
'tanggal_kunjungan',
'nilai_pasar_wajar',
'nilai_likuidasi',
'nama_penilai'
'nama_penilai',
'keterangan',
'kolektibilitas'
];
protected $casts = [

114
app/Models/LaporanSlik.php Normal file
View File

@@ -0,0 +1,114 @@
<?php
namespace Modules\Lpj\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Modules\Usermanagement\Models\User;
class LaporanSlik extends Model
{
use HasFactory;
protected $table = 'laporan_slik';
protected $fillable = [
'slik_id',
'sandi_bank',
'kode_kantor',
'kode_cabang',
'tahun',
'bulan',
'no_rekening',
'cif',
'kode_jenis',
'kode_jenis_ket',
'kode_sifat',
'kode_sifat_ket',
'kode_valuta',
'kode_valuta_ket',
'baki_debet',
'kolektibilitas',
'kolektibilitas_ket',
'tanggal_mulai',
'tanggal_jatuh_tempo',
'tanggal_selesai',
'tanggal_restrukturisasi',
'kode_sebab_macet',
'kode_sebab_macet_ket',
'tanggal_macet',
'kode_kondisi',
'kode_kondisi_ket',
'tanggal_kondisi',
'nilai_agunan',
'nilai_agunan_ket',
'jenis_agunan',
'kode_agunan',
'kode_agunan_ket',
'peringkat_agunan',
'peringkat_agunan_ket',
'nama_debitur',
'npwp',
'no_ktp',
'no_telp',
'kode_kab_kota',
'kode_kab_kota_ket',
'kode_negara_domisili',
'kode_negara_domisili_ket',
'kode_pos',
'alamat',
'fasilitas',
'status_agunan',
'tanggal_lapor',
'status',
'created_by',
'updated_by',
];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function slik()
{
return $this->belongsTo(Slik::class, 'slik_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
/**
* Scope untuk filter berdasarkan status
*/
public function scopeActive($query)
{
return $query->where('status', 'aktif');
}
/**
* Accessor untuk nilai agunan yang diformat
*/
public function getNilaiAgunanFormattedAttribute()
{
return number_format($this->nilai_agunan ?? 0, 0, ',', '.');
}
/**
* Accessor untuk status badge
*/
public function getStatusBadgeAttribute()
{
$status = $this->status ?? 'aktif';
$class = $status == 'aktif' ? 'success' : 'danger';
return '<span class="badge badge-light-' . $class . '">' . ucfirst($status) . '</span>';
}
}

View File

@@ -6,5 +6,5 @@
class NilaiPlafond extends Base
{
protected $table = 'nilai_plafond';
protected $fillable = ['code', 'name'];
protected $fillable = ['code', 'name', 'biaya'];
}

View File

@@ -2,41 +2,28 @@
namespace Modules\Lpj\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
// use Modules\Lpj\Database\Factories\NocFactory;
use Illuminate\Foundation\Auth\User;
class Noc extends Base
{
protected $table = 'noc';
protected $fillable = [
'permohonan_id',
'persetujuan_penawaran_id',
'bukti_bayar',
'nominal_bayar',
'status_bayar',
'tanggal_pembayaran',
'nominal_penyelesaian',
'status_penyelesaiaan',
'tanggal_penyelesaian',
'bukti_penyelesaian',
'bukti_ksl',
'memo_penyelesaian',
'catatan_noc',
'status',
'authorized_status',
'authorized_at',
'authorized_by',
];
protected $guarded = ['id'];
protected $casts = [
'nominal_bayar' => 'decimal:2',
'status_bayar' => 'boolean',
'status_kurang_bayar' => 'boolean',
'nominal_kurang_bayar' => 'decimal:2',
'status_lebih_bayar' => 'boolean',
'nominal_lebih_bayar' => 'decimal:2',
'tanggal_pembayaran' => 'date',
'nominal_penyelesaian' => 'decimal:2',
'status_penyelesaiaan' => 'boolean',
'tanggal_penyelesaian' => 'date',
'memo_penyelesaian_date' => 'date',
'memo_penyelesaian_payment_date' => 'date',
'memo_penyelesaian_created_at' => 'datetime',
'status' => 'boolean',
'authorized_status' => 'boolean',
'authorized_at' => 'datetime',
@@ -59,4 +46,12 @@ class Noc extends Base
{
return $this->belongsTo(User::class, 'authorized_by');
}
public function debiture(){
return $this->belongsTo(Debiture::class,'debiture_id');
}
public function branch(){
return $this->belongsTo(Branch::class,'branch_id');
}
}

190
app/Models/Slik.php Normal file
View 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
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Modules\Lpj\Services;
use Modules\Lpj\Models\DaftarPustaka;
class DaftarPustakaService
{
public function storeDaftarPustaka(array $data, $file)
{
if ($file) {
$data['attachment'] = $this->handleUpload($file);
}
return DaftarPustaka::create($data);
}
public function updateDaftarPustaka($data, $file, $id)
{
// Ambil data inputan yang diperlukan saja
$daftarPustaka = DaftarPustaka::findOrFail($id);
// Jika ada file baru yang diupload
if ($file) {
// (Opsional) Hapus file lama
if ($daftarPustaka->attachment && file_exists(public_path($daftarPustaka->attachment))) {
unlink(public_path($daftarPustaka->attachment));
}
// Upload file baru
$data['attachment'] = $this->handleUpload($file);
}
// Update data
$daftarPustaka->update($data);
return $daftarPustaka;
}
public function deleteDaftarPustaka($id)
{
return DaftarPustaka::where('id', $id)->delete();
}
public function getDaftarPustakaById($id)
{
return DaftarPustaka::where('id', $id)->first();
}
// get all with pagination
public function getAllDaftarPustaka($request)
{
$query = DaftarPustaka::query();
// Filter pencarian
if (!empty($request->get('search'))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->orWhere('judul', 'LIKE', "%$search%");
});
}
// Filter kategori
if (!empty($request->get('category'))) {
$category = explode(',', $request->input('category'));
$query->whereIn('category_id', $category);
}
// Default pagination
$page = (int) $request->get('page', 1);
$size = (int) $request->get('size', 10);
return $query->paginate($size, ['*'], 'page', $page);
}
private function handleUpload($file)
{
$today = now();
$folderPath = 'daftar_pustaka/' . $today->format('Y/m/d');
$fileName = $file->getClientOriginalName();
$filePath = $file->storeAs($folderPath, $fileName, 'public');
return $filePath;
}
}

View 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;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Modules\Lpj\Services;
use Illuminate\Http\Request;
use Modules\Lpj\Models\Permohonan;
class LaporanBiayaService
{
public function buildDataTableQuery(array $data){
$query = Permohonan::with([
'noc'
]);
if($data['search']){
}
return $query;
}
}

View File

@@ -5,124 +5,119 @@ namespace Modules\Lpj\Services;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\Penilaian;
use Illuminate\Http\Request;
use Carbon\Carbon;
class LaporanSLAPenilaiService
{
public function dataForDatatables(Request $request)
{
// Retrieve data from the database
$query = Permohonan::query();
$query = $query->where('status', 'done');
{
// Retrieve data from the database
$query = Permohonan::query();
$query = $query->where('status', 'done');
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = json_decode($request->get('search'));
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = json_decode($request->get('search'));
if (isset($search->start_date) || isset($search->end_date)) {
$query->whereBetween('tanggal_permohonan', [
$search->start_date ?? '1900-01-01',
$search->end_date ?? now()->toDateString()
]);
}
if (isset($search->penilai_id) && !empty($search->penilai_id)) {
$query->whereHas('penilaian._user_penilai.userPenilaiTeam', function($q) use ($search) {
$q->where('user_id', $search->penilai_id);
});
}
if (isset($search->search)) {
$query->where(function ($q) use ($search) {
$q->where('nomor_registrasi', 'LIKE', '%' . $search->search . '%');
$q->orWhere('tanggal_permohonan', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('user', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('tujuanPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('branch', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('jenisFasilitasKredit', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('jenisPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhere('status', 'LIKE', '%' . $search->search . '%');
});
}
if (isset($search->start_date) || isset($search->end_date)) {
$query->whereBetween('tanggal_permohonan', [
$search->start_date ?? '1900-01-01',
$search->end_date ?? now()->toDateString()
]);
}
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
if (isset($search->penilai_id) && !empty($search->penilai_id)) {
$query->whereHas('penilaian._user_penilai.userPenilaiTeam', function ($q) use ($search) {
$q->where('user_id', $search->penilai_id);
});
}
// Get the total count of records
$totalRecords = $query->count();
if (isset($search->search)) {
// 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);
$query->where(function ($q) use ($search) {
$q->where('nomor_registrasi', 'LIKE', '%' . $search->search . '%');
$q->orWhere('tanggal_permohonan', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('user', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('tujuanPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('branch', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('jenisFasilitasKredit', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhereRelation('jenisPenilaian', 'name', 'LIKE', '%' . $search->search . '%');
$q->orWhere('status', 'LIKE', '%' . $search->search . '%');
});
}
// Get the filtered count of records
$filteredRecords = $query->count();
// Get the data for the current page
$data = $query->with(['debiture.branch'])->get();
$data = $data->map(function ($permohonan) {
$luas_tanah = 0;
$luas_bangunan = 0;
$nilai_tanah = 0;
$nilai_bangunan = 0;
$npw = 0;
$nilai_liquidasi = 0;
if (isset($permohonan->penilai->lpj)) {
$lpj = json_decode($permohonan->penilai->lpj, true);
$npw = str_replace('.', '', $lpj['total_nilai_pasar_wajar'] ?? 0);
$luas_tanah = $lpj['luas_tanah'] ?? 0;
$luas_bangunan = $lpj['luas_bangunan'] ?? 0;
// Calculate nilai_tanah dynamically by looking for all keys that start with 'nilai_tanah_'
$nilai_tanah = str_replace('.', '', $lpj['nilai_tanah_2'] ?? 0);
$nilai_bangunan = str_replace('.', '', $lpj['nilai_bangunan_2'] ?? 0);
$nilai_liquidasi = str_replace('.', '', $lpj['likuidasi_nilai_2'] ?? 0);
}
return [
'id' => $permohonan->id,
'nomor_registrasi' => $permohonan->nomor_registrasi,
'tanggal_permohonan' => $permohonan->tanggal_permohonan,
'branch' => $permohonan->debiture?->branch?->name,
'name' => $permohonan->debiture?->name,
'pemohon' => $permohonan->creator?->name,
'tujuan_penilaian' => $permohonan->tujuanPenilaian?->name,
'jenis_agunan' => $permohonan->documents?->pluck('jenisJaminan.name')->unique()->implode(', '),
'tanggal_laporan' => $permohonan->approval_dd_at ?? $permohonan->approval_eo_at ?? '',
'tanggal_review' => $permohonan->penilaian?->tanggal_kunjungan ?? '',
'nama_penilai' => $permohonan->penilaian?->_user_penilai?->userPenilaiTeam?->name,
];
});
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
// 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' => $data,
]);
}
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
}
// 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
$data = $query->with(['documents','debiture.branch'])->get();
$data = $data->map(function ($permohonan) {
$tgl_kunjungan = $permohonan->penilaian?->tanggal_kunjungan;
$tgl_otorisator = $permohonan->approval_dd_at ?? $permohonan->approval_eo_at ?? $permohonan->approval_so_at;
$jangkaWaktu = $this->hitungTotalJangkaWaktuSla($tgl_kunjungan, $tgl_otorisator);
return [
'id' => $permohonan->id,
'nomor_registrasi' => $permohonan->nomor_registrasi,
'tanggal_permohonan' => $permohonan->tanggal_permohonan,
'branch' => $permohonan->debiture?->branch?->name,
'name' => $permohonan->debiture?->name,
'pemohon' => $permohonan->creator?->name,
'tujuan_penilaian' => $permohonan->tujuanPenilaian?->name,
'tanggal_laporan' => $permohonan->approval_dd_at ?? $permohonan->approval_eo_at ?? '',
'tanggal_approval' => $permohonan->approval_dd_at ?? $permohonan->approval_eo_at ?? '',
'tanggal_kunjungan' => $permohonan->penilaian?->tanggal_kunjungan ?? '',
'nama_penilai' => $permohonan->penilaian?->_user_penilai?->userPenilaiTeam?->name,
'jangka_waktu' => $jangkaWaktu,
'keterangan' => $permohonan->keterangan
];
});
// Calculate the page count
$pageCount = ceil($totalRecords / $request->get('size'));
// 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' => $data,
]);
}
private function hitungTotalJangkaWaktuSla($tgl_kunjungan,$tgl_otorisator){
$countHariKerja = hitungHariKerja($tgl_kunjungan, $tgl_otorisator);
return $countHariKerja;
}
}

View File

@@ -85,96 +85,25 @@ class LaporanUserService
$data = $query->with(['debiture.branch'])->get();
$data = $data->map(function ($permohonan) {
$luas_tanah = 0;
$luas_bangunan = 0;
$nilai_tanah = 0;
$nilai_bangunan = 0;
$npw = 0;
$nilai_liquidasi = 0;
if (isset($permohonan->penilai->lpj)) {
$lpj = json_decode($permohonan->penilai->lpj, true);
$npw = str_replace('.', '', $lpj['total_nilai_pasar_wajar'] ?? 0);
$luas_tanah = $lpj['luas_tanah'] ?? 0;
$luas_bangunan = $lpj['luas_bangunan'] ?? 0;
// Calculate nilai_tanah dynamically by looking for all keys that start with 'nilai_tanah_'
$nilai_tanah = str_replace('.', '', $lpj['nilai_tanah_2'] ?? 0);
$nilai_bangunan = str_replace('.', '', $lpj['nilai_bangunan_2'] ?? 0);
$nilai_liquidasi = str_replace('.', '', $lpj['likuidasi_nilai_2'] ?? 0);
}
return [
'id' => $permohonan->id,
'nomor_registrasi' => $permohonan->nomor_registrasi,
'jenis_penilaian' => $permohonan->jenisPenilaian?->name,
'tujuan_penilaian' => $permohonan->tujuanPenilaian?->name,
'jenis_fasilitas_kredit' => $permohonan->jenisFasilitasKredit?->name,
'branch' => $permohonan->debiture->branch?->name,
'pemohon' => $permohonan->creator?->name,
'cif' => $permohonan->debiture->cif,
'name' => $permohonan->debiture?->name,
'jenis_agunan' => $permohonan->documents?->pluck('jenisJaminan.name')
->unique()
->implode(', '),
'alamat_agunan' => $permohonan->documents?->map(function ($document) {
return formatAlamat($document);
})->unique()->implode(', '),
'bukti_kepemilikan' => (function () use ($permohonan) {
$legalitasItems = $permohonan->documents?->flatMap(function ($document) {
return $document->detail->map(function ($detail) {
// Jika tidak ada jenis legalitas jaminan, lewati
if (empty($detail->jenisLegalitasJaminan)) {
return null;
}
// Hanya tampilkan detail yang memiliki dokumen_jaminan
if (empty($detail->dokumen_jaminan)) {
return null;
}
// Tampilkan nama legalitas jaminan saja
return $detail->jenisLegalitasJaminan->name ?? '';
});
})->filter()->unique()->values()->toArray();
// Buat daftar bernomor
$result = '';
foreach ($legalitasItems as $index => $item) {
$result .= ($index + 1) . '. ' . $item . "\n";
}
return $result;
})(),
'nama_pemilik' => $permohonan->documents?->pluck('pemilik.name')
->unique()
->implode(', '),
'luas_tanah' => $luas_tanah . ' m²',
'nilai_tanah' => formatRupiah($nilai_tanah, 2),
'luas_bangunan' => $luas_bangunan . ' m²',
'nilai_bangunan' => formatRupiah($nilai_bangunan, 2),
'nilai_njop' => formatRupiah($permohonan->nilai_njop, 2),
'nilai_pasar_wajar' => formatRupiah($npw, 2),
'nilai_likuidasi' => formatRupiah($nilai_liquidasi, 2),
'tanggal_documen_diterima' => $permohonan->documents?->map(function ($document) {
return $document->created_at->format('d-m-Y');
}),
'tanggal_spk' => '',
'nomor_spk' => '',
'tanggal_rencana_kunjunagn' => '',
'tanggal_kunjungan' => '',
'taggal_delivered' => '',
'jangka_waktu_sla' => '',
'nama_penilai' => $permohonan->penilaian?->_user_penilai?->userPenilaiTeam?->name,
'nama_team_leader' => $permohonan->penilaian?->teams,
'saran' => '',
'catatan' => '',
'pemohon' => $permohonan->creator?->name,
'tanggal_permohonan' => $permohonan->tanggal_permohonan,
'nama_penilai' => $permohonan->penilaian?->_user_penilai?->userPenilaiTeam?->name,
'tanggal_laporan' => $permohonan->approval_dd_at ?? $permohonan->approval_eo_at ?? '',
'tanggal_review' => $permohonan->penilaian?->tanggal_kunjungan ?? '',
'nilai_pasar_wajar' => formatRupiah($npw, 2)
];
});

View File

@@ -0,0 +1,735 @@
<?php
namespace Modules\Lpj\Services;
use Carbon\Carbon;
use App\Helpers\Lpj;
use Modules\Lpj\Models\Denah;
use Modules\Lpj\Models\Teams;
use Modules\Lpj\Models\Branch;
use Modules\Lpj\Models\Lantai;
use Barryvdh\DomPDF\Facade\Pdf;
use Modules\Lpj\Models\Analisa;
use Modules\Lpj\Models\Penilai;
use Modules\Lpj\Models\Debiture;
use Modules\Lpj\Models\Inspeksi;
use Modules\Lpj\Models\Surveyor;
use Modules\Lpj\Models\ViewUnit;
use Modules\Location\Models\City;
use Modules\Lpj\Models\JenisUnit;
use Modules\Lpj\Models\Penilaian;
use Modules\Lpj\Models\Perizinan;
use Modules\Lpj\Models\BentukUnit;
use Modules\Lpj\Models\JenisKapal;
use Modules\Lpj\Models\LantaiUnit;
use Modules\Lpj\Models\Lingkungan;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\PosisiUnit;
use Illuminate\Support\Facades\Log;
use Modules\Lpj\Models\AnalisaUnit;
use Modules\Lpj\Models\BentukTanah;
use Modules\Lpj\Models\FotoJaminan;
use Modules\Lpj\Models\KonturTanah;
use Modules\Lpj\Models\RuteJaminan;
use Modules\Location\Models\Village;
use Modules\Lpj\Models\AnalisaFakta;
use Modules\Lpj\Models\JenisJaminan;
use Modules\Lpj\Models\JenisPesawat;
use Modules\Lpj\Models\ObjekJaminan;
use Modules\Lpj\Models\SpekBangunan;
use Modules\Lpj\Models\TerletakArea;
use Modules\Location\Models\District;
use Modules\Location\Models\Province;
use Modules\Lpj\Models\ArahMataAngin;
use Modules\Lpj\Models\JenisBangunan;
use Modules\Lpj\Models\PosisiKavling;
use Modules\Lpj\Models\SifatBangunan;
use Modules\Lpj\Models\DokumenJaminan;
use Modules\Lpj\Models\FasilitasObjek;
use Modules\Lpj\Models\JenisKendaraan;
use Modules\Lpj\Models\ModelAlatBerat;
use Modules\Lpj\Models\KetinggianTanah;
use Modules\Lpj\Models\KondisiBangunan;
use Modules\Lpj\Models\LaporanExternal;
use Modules\Lpj\Models\MerupakanDaerah;
use Modules\Lpj\Models\PerkerasanJalan;
use Modules\Lpj\Models\SaranaPelengkap;
use Modules\Lpj\Models\TujuanPenilaian;
use Modules\Lpj\Models\FotoObjekJaminan;
use Modules\Lpj\Models\LaluLintasLokasi;
use Modules\Lpj\Models\TingkatKeramaian;
use Modules\Lpj\Models\AnalisaLingkungan;
use Modules\Lpj\Models\KondisiFisikTanah;
use Modules\Lpj\Models\AnalisaTanahBagunan;
use Modules\Lpj\Models\GolonganMasySekitar;
use Modules\Lpj\Models\SpekBangunanAnalisa;
use Modules\Lpj\Models\DetailDokumenJaminan;
use Modules\Lpj\Models\SpekKategoritBangunan;
use Modules\Lpj\Http\Requests\SurveyorRequest;
use Modules\Lpj\Models\HubunganPemilikJaminan;
use Modules\Lpj\Models\HubunganPenghuniJaminan;
use Modules\Lpj\Models\SpekBagunanAnalisaDetail;
use Modules\Lpj\Jobs\SendJadwalKunjunganEmailJob;
use Modules\Lpj\Http\Requests\FormSurveyorRequest;
class PreviewLaporanService
{
/**
* Preview Laporan dan unduh foto terkait dengan logika fallback path.
*
* Menghasilkan PDF atau paket unduhan foto berdasarkan status laporan.
* Jika file foto asli tidak ditemukan dan status LPJ adalah 1, maka
* sistem akan mencoba menggunakan fallback path dengan pola
* `surveyor/001/{lastTwoParts}` untuk meminimalisir gambar hilang.
*
* @param int $permohonan_id ID Permohonan
* @param int $dokumen_id ID Dokumen
* @param int $jaminan_id ID Jaminan
* @param string $back URL atau rute kembali
* @return \Symfony\Component\HttpFoundation\Response
*/
public function previewLaporan($permohonan_id, $dokumen_id, $jaminan_id, $back)
{
$permohonan = $this->getPermohonanJaminanId(
$permohonan_id,
$dokumen_id,
$jaminan_id
);
// $tipeLaporanResponse = $this->checkPrintOutLaporan($permohonan_id, $document_id);
// $tipeLaporan = $tipeLaporanResponse->getData();
// if (!$tipeLaporan->status) {
// return redirect()->back()->with('error', 'Laporan tidak valid');
// }
$basicData = $this->getCommonData();
$inspeksi = Inspeksi::where('permohonan_id', $permohonan_id)->where('dokument_id', $dokumen_id)->first();
$lpj = Penilai::where('permohonan_id', $permohonan_id)->where('dokument_id', $dokumen_id)->first();
$mig_permohonan = json_decode($permohonan->mig_permohonan);
$nomorLaporan = getNomorLaporan($permohonan_id, $dokumen_id);
$tanggalLaporan = $permohonan->is_mig
? ($mig_permohonan->mig_mst_jaminan_tgl_laporan ?? $mig_permohonan->mig_mst_jaminan_tgl_create ?? null)
: ($lpj->created_at ?? null);
$forminspeksi = null;
$lpjData = null;
$formFoto = null;
// if ($inspeksi) {
$forminspeksi = json_decode($inspeksi?->data_form, true) ?? null;
$formFoto = json_decode($inspeksi?->foto_form, true) ?? null;
// $denahForm = json_decode($data->denah_form, true);
$dataPembanding = json_decode($inspeksi?->data_pembanding, true) ?? null;
// }
// if ($lpj) {
$lpjData = json_decode($lpj?->lpj, true) ?? null;
$memo = json_decode($lpj?->memo, true) ?? null;
$resumeData = json_decode($lpj?->resume, true) ?? null;
$rap = json_decode($lpj?->rap, true);
$report = json_decode($lpj?->call_report, true) ?? null;
// }
$inputAddress = $forminspeksi['asset']['alamat']['sesuai'] ?? $forminspeksi['asset']['alamat']['tidak sesuai'] ?? [];
$alamat = [
'address' => $inputAddress['address'] ?? null,
'village_code' => getWilayahName($inputAddress['village_code'] ?? null, 'village'),
'district_code' => getWilayahName($inputAddress['district_code'] ?? null, 'district'),
'city_code' => getWilayahName($inputAddress['city_code'] ?? null, 'city'),
'province_code' => getWilayahName($inputAddress['province_code'] ?? null, 'province')
];
$statusLpj = 0;
// $viewLaporan = $this->getViewLaporan($tipeLaporan);
return view('lpj::component.show-laporan-inspeksi', compact('permohonan', 'basicData', 'forminspeksi', 'alamat', 'lpjData', 'memo', 'resumeData', 'rap', 'report', 'lpj', 'formFoto', 'nomorLaporan', 'tanggalLaporan', 'dataPembanding', 'inspeksi', 'statusLpj', 'permohonan_id', 'back', ));
}
public function printOutLaporan($permohonan_id, $document_id, $jaminan_id)
{
$tipeLaporanResponse = $this->checkPrintOutLaporan($permohonan_id, $document_id);
$tipeLaporan = $tipeLaporanResponse->getData();
//dd($permohonan_id, $document_id, $tipeLaporan);
if (!$tipeLaporan->status) {
//return redirect()->back()->with('error', 'Laporan tidak valid');
}
$permohonan = $this->getPermohonanJaminanId(
$permohonan_id,
$document_id,
$jaminan_id
);
$basicData = $this->getCommonData();
$inspeksi = Inspeksi::where('permohonan_id', $permohonan_id)->where('dokument_id', $document_id)->first();
$lpj = Penilai::where('permohonan_id', $permohonan_id)->first(); //->where('dokument_id', $document_id)->first();
$mig_permohonan = json_decode($permohonan->mig_permohonan);
$nomorLaporan = getNomorLaporan($permohonan_id, $document_id);
//Carbon::createFromFormat('d/m/Y H:i:s', $mig_permohonan->mig_mst_jaminan_tgl_laporan)->format('Y-m-d H:i:s');
$tanggalLaporan = $permohonan->is_mig
? ($mig_permohonan->mig_mst_jaminan_tgl_laporan
? Carbon::createFromFormat('d/m/Y H:i:s', $mig_permohonan->mig_mst_jaminan_tgl_laporan)->format('Y-m-d H:i:s')
: ($mig_permohonan->mig_mst_jaminan_tgl_oto
? Carbon::createFromFormat('d/m/Y H:i:s', $mig_permohonan->mig_mst_jaminan_tgl_oto)->format('Y-m-d H:i:s')
: null))
: ($lpj->created_at ?? null);
$forminspeksi = null;
$lpjData = null;
$formFoto = null;
$dataPembanding ='';
if ($inspeksi) {
$forminspeksi = json_decode($inspeksi->data_form, true);
$formFoto = json_decode($inspeksi->foto_form, true);
// $denahForm = json_decode($data->denah_form, true);
$dataPembanding = json_decode($inspeksi->data_pembanding, true);
}
if ($lpj) {
$lpjData = json_decode($lpj->lpj, true);
$memo = json_decode($lpj->memo, true);
$resumeData = json_decode($lpj->resume, true);
$rap = json_decode($lpj->rap, true);
$report = json_decode($lpj->call_report, true);
}
$inputAddress = $forminspeksi['asset']['alamat']['sesuai'] ?? $forminspeksi['asset']['alamat']['tidak sesuai'] ?? [];
$inputAddress = $forminspeksi['asset']['alamat']['sesuai'] ?? $forminspeksi['asset']['alamat']['tidak sesuai'] ?? [];
$alamat = [
'address' => $inputAddress['address'] ?? null,
'village_code' => getWilayahName($inputAddress['village_code'] ?? null, 'village'),
'district_code' => getWilayahName($inputAddress['district_code'] ?? null, 'district'),
'city_code' => getWilayahName($inputAddress['city_code'] ?? null, 'city'),
'province_code' => getWilayahName($inputAddress['province_code'] ?? null, 'province')
];
$viewLaporan = $this->getViewLaporan($tipeLaporan->status);
$statusLpj = 1;
$mig_permohonan = json_decode($permohonan->mig_permohonan);
$nilaiPasar = $mig_permohonan->mig_mst_lpj_tot_nilai_pasar ?? null;
if(($tipeLaporan->status === 'memo' && $permohonan->mig_permohonan && $permohonan->is_mig) || ($nilaiPasar !== null && $nilaiPasar < 1 && $permohonan->is_mig)){
$paths = $formFoto['upload_foto'] ?? null;
if (!is_array($paths) || empty($paths)) {
return response()->json(['error' => 'No files to download'], 404);
}
$files = [];
foreach ($paths as $path) {
if (!$path['path']) {
Log::warning('PreviewLaporanService: Path kosong terdeteksi dalam daftar paths.');
continue;
}
// Logika fallback untuk path file
$originalPath = $path['path'];
$fallbackPath = null;
// Jika file asli tidak ditemukan, buat fallback path
if ($statusLpj == 1) {
$fullOriginalPath = storage_path('app/public/' . $originalPath);
if (!file_exists($fullOriginalPath)) {
// Ekstrak bagian akhir path (contoh: 251051/251051_2_2.png)
$pathParts = explode('/', $originalPath);
if (count($pathParts) >= 2) {
$lastTwoParts = array_slice($pathParts, -2);
$fallbackPath = 'surveyor/001/' . implode('/', $lastTwoParts);
Log::info('PreviewLaporanService: Menggunakan fallback path kandidat.', [
'original' => $originalPath,
'fallback' => $fallbackPath,
]);
}
}
}
// Tentukan path yang akan digunakan
$pathToUse = ($fallbackPath && $statusLpj == 1 && file_exists(storage_path('app/public/' . $fallbackPath)))
? $fallbackPath
: $originalPath;
$fullPath = storage_path('app/public/' . $pathToUse);
if (!file_exists($fullPath)) {
Log::warning('PreviewLaporanService: File tidak ditemukan untuk original maupun fallback.', [
'original' => $originalPath,
'fallback' => $fallbackPath,
'resolved' => $pathToUse,
]);
continue;
}
Log::info('PreviewLaporanService: Menambahkan file untuk diunduh.', [
'resolved' => $pathToUse,
'fullPath' => $fullPath,
]);
$files[] = $fullPath;
}
if (empty($files)) {
Log::warning('PreviewLaporanService: Tidak ada file valid ditemukan setelah resolusi path.');
return response()->json(['error' => 'No valid files found'], 404);
}
// For single file, download directly
if (count($files) === 1) {
Log::info('PreviewLaporanService: Mengunduh single file.', ['file' => $files[0]]);
return response()->download($files[0]);
}
// For multiple files, create zip and download
$zipName = 'photos_' . time() . '.zip';
$zipPath = storage_path('app/public/' . $zipName);
$zip = new \ZipArchive();
if ($zip->open($zipPath, \ZipArchive::CREATE) === true) {
foreach ($files as $file) {
$zip->addFile($file, basename($file));
}
$zip->close();
Log::info('PreviewLaporanService: Zip file berhasil dibuat.', ['zip' => $zipPath, 'count' => count($files)]);
return response()->download($zipPath)->deleteFileAfterSend(true);
}
Log::error('PreviewLaporanService: Gagal membuat zip file.');
return response()->json(['error' => 'Failed to create zip file'], 500);
}
try {
$pdf = $this->generatePDF($viewLaporan, compact(
'permohonan',
'forminspeksi',
'lpjData',
'formFoto',
'basicData',
'inspeksi',
'lpj',
'statusLpj',
'alamat',
'dataPembanding',
'nomorLaporan',
'memo',
'resumeData',
'tanggalLaporan',
'rap',
'report'
));
$cleanNomorLaporan = str_replace(['/', '\\'], '-', $nomorLaporan);
$filename = 'Laporan_' . $tipeLaporan->status . '_' . $permohonan->debiture->name . '_' . $cleanNomorLaporan;
return $pdf->download($filename . '_data.pdf');
} catch (\Exception $e) {
Log::error('PDF generation failed: ' . $e->getMessage());
return response()->json(['error' => 'Failed to generate PDF. Please check the log for details.'], 500);
}
}
private function generatePDF(string $viewLaporan, array $data)
{
//return view('lpj::' . $viewLaporan, $data);
$pdf = PDF::loadView('lpj::' . $viewLaporan, $data);
$pdf->setPaper('A4', 'portrait');
$pdf->set_option('isHtml5ParserEnabled', true);
$pdf->set_option('isPhpEnabled', true);
return $pdf;
}
private function getViewLaporan($tipe)
{
$viewMap = [
'sederhana' => 'penilai.components.print-out-sederhana',
'standar' => 'penilai.components.print-out-standar',
'resume' => 'penilai.components.print-resume',
'memo' => 'penilai.components.print-memo',
'rap' => 'penilai.components.print-out-rap',
'call-report' => 'penilai.components.print-out-call-report'
];
return $viewMap[$tipe] ?? 'penilai.components.print-resume';
}
public function checkPrintOutLaporan($permohonan_id, $dokumen_id)
{
// Ambil data berdasarkan ID
$statusLpj = Penilai::where('permohonan_id', $permohonan_id)
//->where('dokument_id', $dokumen_id)
->first();
$permohonan = Permohonan::where('id', $permohonan_id)->first();
// Jika data tidak ditemukan, kembalikan status null
if (!$statusLpj) {
return response()->json(['status' => null]);
}
// Tentukan tipe berdasarkan kondisi
$type = $statusLpj->type_penilai ?? null;
if ($type === 'memo' && $permohonan->is_mig!=1) {
return $this->checkDataMemo($type, $statusLpj);
}
if ($type === 'resume') {
return $this->checkDataResume($type, $statusLpj);
}
if ($type === 'standar' || $type === 'sederhana') {
return $this->checkDataLpj($type, $statusLpj);
}
if ($type === 'rap') {
return $this->checkDataRap($type, $statusLpj);
}
// Kembalikan respons dengan tipe yang sesuai
return response()->json(['status' => $type]);
}
public function checkDataMemo($type, $statusLpj)
{
// Ambil data JSON dari statusLpj
$data = json_decode($statusLpj->memo, true) ?? [];
$validationRules = [
'memo' => [
'kepada',
'dari',
'nomor_memo',
'tanggal',
'perihal',
'jenis_asset_tidak_sesuai',
'lokasi.lokasi',
'lokasi.province_code',
'lokasi.city_code',
'lokasi.district_code',
'lokasi.village_code',
'lokasi.penilai',
'terlampir',
'hasil_survey',
'kesimpulan_saran',
],
];
// Validasi data JSON
if (isset($validationRules[$type])) {
$missingFields = [];
foreach ($validationRules[$type] as $field) {
$keys = explode('.', $field);
$value = $data;
foreach ($keys as $key) {
if (!isset($value[$key])) {
$missingFields[] = $field;
break;
}
$value = $value[$key];
}
}
// Jika ada field yang kosong, kembalikan error
if (!empty($missingFields)) {
return response()->json([
'status' => null,
'message' => "Silahkan lengkapi data memo terlebih dahulu.",
'missing_fields' => $missingFields,
], 400);
}
}
// Jika data valid
return response()->json([
'status' => $type,
'message' => "Data memo valid.",
]);
}
public function checkDataResume($type, $statusLpj)
{
// Ambil data JSON dari statusLpj
$data = json_decode($statusLpj->resume, true) ?? [];
$validationRules = [
'resume' => [
'fisik'
],
];
// Validasi data JSON
if (isset($validationRules[$type])) {
$missingFields = [];
foreach ($validationRules[$type] as $field) {
$keys = explode('.', $field);
$value = $data;
foreach ($keys as $key) {
if (!isset($value[$key])) {
$missingFields[] = $field;
break;
}
$value = $value[$key];
}
// Validasi khusus untuk array fisik dan sesuai_imb
if ($field === 'fisik' || $field === 'sesuai_imb') {
if (empty($value) || !is_array($value)) {
$missingFields[] = $field;
continue;
}
// Validasi struktur data di dalam array
foreach ($value as $item) {
$requiredKeys = ['sertifikat', 'luas_tanah', 'nilai'];
foreach ($requiredKeys as $requiredKey) {
if (!isset($item[$requiredKey])) {
$missingFields[] = $field . '.' . $requiredKey;
}
}
}
}
}
// Jika ada field yang kosong, kembalikan error
if (!empty($missingFields)) {
return response()->json([
'status' => null,
'message' => "Silahkan lengkapi data resume terlebih dahulu.",
'missing_fields' => $missingFields,
], 400);
}
}
// Jika data valid
return response()->json([
'status' => $type,
'message' => "Data resume valid.",
]);
}
public function checkDataLpj($type, $statusLpj)
{
// Ambil data JSON dari statusLpj
$data = json_decode($statusLpj->lpj, true) ?? [];
$validationRules = [
'lpj' => [
'luas_tanah',
'nilai_tanah_1',
'nilai_tanah_2',
'luas_bangunan',
'nilai_bangunan_1',
'nilai_bangunan_2',
'total_nilai_pasar_wajar',
'likuidasi',
'likuidasi_nilai_1',
'likuidasi_nilai_2',
'asuransi_luas_bangunan',
'asuransi_nilai_1',
'asuransi_nilai_2',
'npw_tambahan'
],
];
// Validasi data JSON
if (isset($validationRules[$type])) {
$missingFields = [];
foreach ($validationRules[$type] as $field) {
// Penanganan khusus untuk field yang boleh null
if (in_array($field, ['sarana_pelengkap_penilai', 'nilai_sarana_pelengkap_1', 'nilai_sarana_pelengkap_2'])) {
continue;
}
if (!isset($data[$field])) {
$missingFields[] = $field;
continue;
}
// Validasi khusus untuk npw_tambahan
if ($field === 'npw_tambahan' && is_array($data[$field])) {
foreach ($data[$field] as $index => $item) {
$requiredKeys = ['name', 'luas', 'nilai_1', 'nilai_2'];
foreach ($requiredKeys as $key) {
if (!isset($item[$key])) {
$missingFields[] = "npw_tambahan[$index].$key";
}
}
}
}
}
// Jika ada field yang kosong, kembalikan error
if (!empty($missingFields)) {
return response()->json([
'status' => null,
'message' => "Silahkan lengkapi data LPJ terlebih dahulu.",
'missing_fields' => $missingFields,
], 400);
}
}
// Jika data valid
return response()->json([
'status' => $type,
'message' => "Data LPJ valid.",
]);
}
public function checkDataRap($type, $statusLpj)
{
// Ambil data JSON dari statusLpj
$data = json_decode($statusLpj->rap, true) ?? [];
$requiredFields = [
'dari',
'kepada',
'perihal',
'tanggal',
'nomor_rap'
];
// Cek apakah ada field yang kosong
$missingFields = [];
foreach ($requiredFields as $field) {
if (!isset($data[$field]) || empty($data[$field])) {
$missingFields[] = $field;
}
}
// Jika ada field yang kosong, kembalikan error
if (!empty($missingFields)) {
return response()->json([
'status' => null,
'message' => "Silahkan lengkapi data RAP terlebih dahulu.",
'missing_fields' => $missingFields
], 400);
}
// Jika semua data terisi
return response()->json([
'status' => $type,
'message' => "Data RAP valid."
]);
}
private function getPermohonanJaminanId($id, $dokumentId, $jaminanId)
{
return Permohonan::with([
'user',
'debiture.province',
'debiture.city',
'debiture.district',
'debiture.village',
'branch',
'tujuanPenilaian',
'penilaian',
'penawaran',
'debiture.documents' => function ($query) use ($dokumentId, $jaminanId) {
$query->where('id', $dokumentId)
->where('jenis_jaminan_id', $jaminanId);
}
])->findOrFail($id);
}
public function getCommonData()
{
return [
'branches' => Branch::all(),
'bentukTanah' => BentukTanah::all(),
'konturTanah' => KonturTanah::all(),
'posisiKavling' => PosisiKavling::all(),
'ketinggianTanah' => KetinggianTanah::all(),
'kondisiFisikTanah' => KondisiFisikTanah::all(),
'jenisBangunan' => JenisBangunan::all(),
'kondisiBangunan' => KondisiBangunan::all(),
'sifatBangunan' => SifatBangunan::all(),
'spekKategoriBangunan' => SpekKategoritBangunan::all(),
'spekBangunan' => SpekBangunan::all(),
'saranaPelengkap' => SaranaPelengkap::all(),
'arahMataAngin' => ArahMataAngin::all(),
'lantai' => Lantai::all(),
'viewUnit' => ViewUnit::all(),
'golMasySekitar' => GolonganMasySekitar::all(),
'tingkatKeramaian' => TingkatKeramaian::all(),
'laluLintasLokasi' => LaluLintasLokasi::all(),
'jenisPesawat' => JenisPesawat::all(),
'modelAlatBerat' => ModelAlatBerat::all(),
'jenisKapal' => JenisKapal::all(),
'jenisKendaraan' => JenisKendaraan::all(),
'terletakArea' => TerletakArea::all(),
'posisiUnit' => PosisiUnit::all(),
'bentukUnit' => BentukUnit::all(),
'fasilitasObjek' => FasilitasObjek::all(),
'merupakanDaerah' => MerupakanDaerah::all(),
'jenisUnit' => JenisUnit::all(),
'jenisJaminan' => JenisJaminan::all(),
'hubCadeb' => HubunganPemilikJaminan::all(),
'hubPenghuni' => HubunganPenghuniJaminan::all(),
'perkerasanJalan' => PerkerasanJalan::all(),
'terletakDiArea' => TerletakArea::all(),
'tujuanPenilaian' => TujuanPenilaian::all(),
'perizinan' => Perizinan::all(),
'foto' => FotoObjekJaminan::all()
];
}
private const HEADERS = [
'bentuk-tanah' => ['Bentuk Tanah', 'bentuk-tanah'],
'kontur-tanah' => ['Kontur Tanah', 'kontur-tanah'],
'posisi-kavling' => ['Posisi Kavling', 'posisi-kavling'],
'ketinggian-tanah' => ['Ketinggian Tanah', 'ketinggian-tanah'],
'kondisi-fisik-tanah' => ['Kondisi Fisik Tanah', 'kondisi-fisik-tanah'],
'jenis-bangunan' => ['Jenis Bangunan', 'jenis-bangunan'],
'kondisi-bangunan' => ['Kondisi Bangunan', 'kondisi-bangunan'],
'sifat-bangunan' => ['Sifat Bangunan', 'sifat-bangunan'],
'sarana-pelengkap' => ['Sarana Pelengkap', 'sarana-pelengkap'],
'lalu-lintas-lokasi' => ['Lalu Lintas Depan Lokasi', 'lalu-lintas-lokasi'],
'tingkat-keramaian' => ['Tingkat Keramaian', 'tingkat-keramaian'],
'gol-mas-sekitar' => ['Golongan Masyarakat Sekitar', 'gol-mas-sekitar'],
'spek-kategori-bangunan' => ['Spek Kategori Bangunan', 'spek-kategori-bangunan'],
'spek-bangunan' => ['Spek Bangunan', 'spek-bangunan'],
'lantai-unit' => ['Lantai Unit', 'lantai-unit'],
'view-unit' => ['View Unit', 'view-unit'],
'perkerasan-jalan' => ['Perkerasan jalan', 'perkerasan-jalan'],
'jenis-pesawat' => ['Jenis pesawat', 'jenis-pesawat'],
'model-alat-berat' => ['Model alat berat', 'model-alat-berat'],
'jenis-kapal' => ['Jenis kapal', 'jenis-kapal'],
'jenis-kendaraan' => ['Jenis kendaraan', 'jenis-kendaraan'],
'jenis-unit' => ['Jenis unit', 'jenis-unit'],
'terletak-area' => ['Terletak di Area', 'terletak-area'],
'merupakan-daerah' => ['Merupakan Daerah', 'merupakan-daerah'],
'posisi-unit' => ['Posisi unit', 'posisi-unit'],
'bentuk-unit' => ['Bentuk unit', 'bentuk-unit'],
'fasilitas-objek' => ['Fasilitas Umum Dekat Objek', 'fasilitas-objek'],
'foto-objek-jaminan' => ['Foto Objek Jaminan', 'foto-objek-jaminan'],
'perizinan' => ['Perizinan', 'perizinan'],
];
}

View File

@@ -450,8 +450,6 @@ class SaveFormInspesksiService
$data['perizinan'] = $perizinanData;
$partisiResult = [];
if (isset($data['partisi'])) {
foreach ($data['partisi'] as $name => $values) {

View File

@@ -0,0 +1,107 @@
<?php
namespace Modules\Lpj\Services;
class TypeLaporanService
{
public function handleRequest($type, $permohonanId, $documentId, $jaminanId, $surveyorController)
{
$permohonan = $this->getPermohonanData($surveyorController, $permohonanId, $documentId, $jaminanId);
if ($permohonan->status == 'proses-laporan') {
throw new \Exception('Masih dalam proses laporan');
}
$nomorLaporan = $this->generateNomorLaporan($surveyorController, $permohonan, $documentId, $type);
$inspeksi = $this->getInspeksi($permohonanId, $documentId);
$this->updatePenilai($permohonanId, $documentId, $type);
$debitur = Debiture::find($permohonan->debiture_id);
$forminspeksi = $inspeksi ? json_decode($inspeksi->data_form, true) : null;
$locationCodes = $this->getAlamatData($debitur, $forminspeksi);
$locationData = $this->getLocationData(
$locationCodes['provinceCode'],
$locationCodes['cityCode'],
$locationCodes['districtCode'],
$forminspeksi
);
return [
'permohonan' => $permohonan,
'nomorLaporan' => $nomorLaporan,
'basicData' => $surveyorController->getCommonData(),
'cities' => $locationData['cities'],
'districts' => $locationData['districts'],
'villages' => $locationData['villages'],
'forminspeksi' => $forminspeksi,
];
}
private function getPermohonanData($surveyorController, $permohonanId, $documentId, $jaminanId)
{
return $surveyorController->getPermohonanData($permohonanId, $documentId, $jaminanId);
}
private function generateNomorLaporan($surveyorController, $permohonan, $documentId, $type)
{
return $surveyorController->generateNomorLaporan($permohonan, $documentId, $type);
}
private function getInspeksi($permohonanId, $documentId)
{
return Inspeksi::where('permohonan_id', $permohonanId)
->where('document_id', $documentId)
->first();
}
private function updatePenilai($permohonanId, $documentId, $type)
{
// Logika untuk memperbarui data berdasarkan tipe laporan
}
private function getAlamatData($debitur, $forminspeksi)
{
return [
'provinceCode' => $debitur->province_id ?? $forminspeksi['province'],
'cityCode' => $debitur->city_id ?? $forminspeksi['city'],
'districtCode' => $debitur->district_id ?? $forminspeksi['district'],
];
}
private function getLocationData($provinceCode, $cityCode, $districtCode, $forminspeksi)
{
return [
'cities' => $this->fetchCityData($provinceCode),
'districts' => $this->fetchDistrictData($cityCode),
'villages' => $this->fetchVillageData($districtCode),
];
}
private function fetchCityData($provinceCode)
{
return City::where('province_code', $provinceCode)->get();
}
private function fetchDistrictData($cityCode)
{
return District::where('city_code', $cityCode)->get();
}
private function fetchVillageData($districtCode)
{
return Village::where('district_code', $districtCode)->get();
}
private function updateOrCreatePenilai($permohonan_id, $document_id, $type, $data)
{
return Penilai::updateOrCreate(
[
'permohonan_id' => $permohonan_id,
'dokument_id' => $document_id,
],
[
$type => json_encode($data),
]
);
}
}

View File

@@ -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),
],
],
];

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('debitures', function (Blueprint $table) {
// nullable
$table->string('cif', 16)
->nullable()
->default('0000000000')
->comment('asal data LPJ.PRM_DEBITUR.KODE_CIF. Pada KODE_CIF ada yang digitnya 16 => 3372040405810002')
->change();
$table->unsignedBigInteger('branch_id')->nullable()->change();
$table->string('nomor_id', 50)->nullable()->change();
$table->unsignedBigInteger('mig_kd_debitur_seq')->nullable()->comment('asal data LPJ.PRM_DEBITUR.KD_DEBITUR_SEQ. Berguna untuk update debitur_id menggunakan KD_DEBITUR_SEQ nya');
$table->timestamp('processed_at')->nullable();
$table->char('is_mig', 1)->nullable()->comment('untuk menandakan row ini dari LPJ OLD');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('debitures', function (Blueprint $table) {
// Kembalikan kolom branch_id agar tidak nullable (asumsi awal NOT NULL)
$table->unsignedBigInteger('branch_id')->nullable(false)->change();
// Kembalikan kolom yang diubah nullable menjadi NOT NULL
$table->string('cif', 10)->nullable(false)->change();
$table->string('nomor_id', 50)->nullable(false)->change();
// Hapus kolom tambahan yang dibuat di up()
$table->dropColumn([
'mig_kd_debitur_seq',
'mig_urut_seq_addr',
'mig_urut_seq_comm',
'processed_at',
'is_mig'
]);
});
}
};

View File

@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('permohonan', function (Blueprint $table) {
// nullable
$table->unsignedBigInteger('branch_id')->nullable()->change();
$table->unsignedBigInteger('user_id')->nullable()->change();
$table->unsignedBigInteger('mig_kd_debitur_seq')->nullable()->comment('asal data LPJ.PRM_DEBITUR.KD_DEBITUR_SEQ. Berguna untuk update debitur_id menggunakan KD_DEBITUR_SEQ nya');
$table->unsignedBigInteger('nomor_lpj')->nullable();
$table->string('mig_nama_ao')->nullable();
$table->char('is_mig', 1)->nullable()->comment('untuk menandakan row ini dari LPJ OLD');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('permohonan', function (Blueprint $table) {
// Kembalikan kolom branch_id agar tidak nullable (asumsi awal NOT NULL)
$table->unsignedBigInteger('branch_id')->nullable(false)->change();
$table->unsignedBigInteger('user_id')->nullable(false)->change();
// Hapus kolom tambahan yang dibuat di up()
$table->dropColumn([
'mig_kd_debitur_seq',
'nomor_lpj',
'mig_nama_ao',
'is_mig'
]);
});
}
};

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('dokumen_jaminan', function (Blueprint $table) {
// nullable
$table->unsignedBigInteger('mig_kd_debitur_seq')->nullable()->comment('asal data LPJ.PRM_DEBITUR.KD_DEBITUR_SEQ. Berguna untuk update debitur_id menggunakan KD_DEBITUR_SEQ nya');
$table->unsignedBigInteger('nomor_lpj')->nullable();
$table->timestamp('processed_at')->nullable();
$table->char('is_mig', 1)->nullable()->comment('untuk menandakan row ini dari LPJ OLD');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('dokumen_jaminan', function (Blueprint $table) {
// Hapus kolom tambahan yang dibuat di up()
$table->dropColumn([
'nomor_lpj',
'mig_kd_debitur_seq',
'processed_at',
'is_mig'
]);
});
}
};

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('pemilik_jaminan', function (Blueprint $table) {
// nullable
$table->unsignedBigInteger('mig_kd_debitur_seq')->nullable()->comment('asal data LPJ.PRM_DEBITUR.KD_DEBITUR_SEQ. Berguna untuk update debitur_id menggunakan KD_DEBITUR_SEQ nya');
$table->timestamp('processed_at')->nullable();
$table->char('is_mig', 1)->nullable()->comment('untuk menandakan row ini dari LPJ OLD');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('pemilik_jaminan', function (Blueprint $table) {
// Hapus kolom tambahan yang dibuat di up()
$table->dropColumn([
'mig_kd_debitur_seq',
'processed_at',
'is_mig'
]);
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('inspeksi', function (Blueprint $table) {
$table->unsignedBigInteger('nomor_lpj')->nullable();
$table->timestamp('processed_at')->nullable();
$table->char('is_mig', 1)->nullable()->comment('untuk menandakan row ini dari LPJ OLD');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('inspeksi', function (Blueprint $table) {
$table->dropColumn('nomor_lpj');
$table->dropColumn('processed_at');
$table->dropColumn('is_mig');
});
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('penilai', function (Blueprint $table) {
$table->unsignedBigInteger('nomor_lpj')->nullable();
$table->timestamp('processed_at')->nullable();
$table->char('is_mig', 1)->nullable()->comment('untuk menandakan row ini dari LPJ OLD');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('penilai', function (Blueprint $table) {
$table->dropColumn('nomor_lpj');
$table->dropColumn('processed_at');
$table->dropColumn('is_mig');
});
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('category_daftar_pustaka', function (Blueprint $table) {
$table->id();
$table->string('code')->unique()->index();
$table->string('name');
$table->timestamps();
$table->timestamp('authorized_at')->nullable();
$table->char('authorized_status', 1)->nullable();
$table->softDeletes();
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->unsignedBigInteger('deleted_by')->nullable();
$table->unsignedBigInteger('authorized_by')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('category_daftar_pustaka');
}
};

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('daftar_pustaka', function (Blueprint $table) {
$table->id();
$table->string('judul');
$table->string('attachment')->nullable();
$table->text('deskripsi');
$table->unsignedBigInteger('category_id');
$table->foreign('category_id')->references('id')->on('category_daftar_pustaka');
$table->timestamps();
$table->timestamp('authorized_at')->nullable();
$table->char('authorized_status', 1)->nullable();
$table->softDeletes();
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->unsignedBigInteger('deleted_by')->nullable();
$table->unsignedBigInteger('authorized_by')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('daftar_pustaka');
}
};

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('noc', function (Blueprint $table) {
// Field untuk informasi memo penyelesaian
$table->string('memo_penyelesaian_number')->nullable()->after('memo_penyelesaian');
$table->date('memo_penyelesaian_date')->nullable()->after('memo_penyelesaian_number');
$table->date('memo_penyelesaian_payment_date')->nullable()->after('memo_penyelesaian_date');
$table->timestamp('memo_penyelesaian_created_at')->nullable()->after('memo_penyelesaian_date');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('noc', function (Blueprint $table) {
$table->dropColumn([
'memo_penyelesaian_number',
'memo_penyelesaian_date',
'memo_penyelesaian_payment_date',
'memo_penyelesaian_created_at'
]);
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('noc', function (Blueprint $table) {
$table->decimal('total_pembukuan', 10, 2)->after('nominal_bayar');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('noc', function (Blueprint $table) {
$table->dropColumn('total_pembukuan');
});
}
};

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Menjalankan migration untuk menambahkan field status pembayaran
* Field yang ditambahkan: status_kurang_bayar, nominal_kurang_bayar,
* status_lebih_bayar, nominal_lebih_bayar, bukti_pengembalian
*/
public function up(): void
{
Schema::table('noc', function (Blueprint $table) {
// Field untuk status kurang bayar
$table->boolean('status_kurang_bayar')->default(false)->after('status_bayar');
$table->decimal('nominal_kurang_bayar', 15, 2)->nullable()->after('status_kurang_bayar');
// Field untuk status lebih bayar
$table->boolean('status_lebih_bayar')->default(false)->after('nominal_kurang_bayar');
$table->decimal('nominal_lebih_bayar', 15, 2)->nullable()->after('status_lebih_bayar');
// Field untuk bukti pengembalian jika lebih bayar
$table->string('bukti_pengembalian')->nullable()->after('nominal_lebih_bayar');
});
}
/**
* Rollback migration
*/
public function down(): void
{
Schema::table('noc', function (Blueprint $table) {
$table->dropColumn([
'status_kurang_bayar',
'nominal_kurang_bayar',
'status_lebih_bayar',
'nominal_lebih_bayar',
'bukti_pengembalian'
]);
});
}
};

View File

@@ -0,0 +1,67 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('bucoks', function (Blueprint $table) {
$table->id();
$table->string('no')->nullable(); // Nomor urut
$table->string('tanggal')->nullable(); // Tanggal
$table->string('bulan')->nullable(); // Bulan
$table->string('tahun')->nullable(); // Tahun
$table->string('tanggal_penuh')->nullable(); // Tanggal lengkap
$table->string('nomor_categ')->nullable(); // Nomor kategori
$table->string('coa_summary')->nullable(); // COA Summary
$table->string('nomor_coa')->nullable(); // Nomor COA
$table->string('nama_coa')->nullable(); // Nama COA
$table->string('nomor_tiket')->unique(); // Nomor tiket (unique)
$table->string('deskripsi')->nullable(); // Deskripsi
$table->string('nominal')->nullable(); // Nominal
$table->string('penyelesaian')->nullable(); // Penyelesaian
$table->string('umur_aging')->nullable(); // Umur aging
$table->string('cost_center')->nullable(); // Cost center
$table->string('nama_sub_direktorat')->nullable(); // Nama sub direktorat
$table->string('nama_direktorat_cabang')->nullable(); // Nama direktorat cabang
$table->string('tanggal_penyelesaian')->nullable(); // Tanggal penyelesaian
$table->string('nominal_penyelesaian')->nullable(); // Nominal penyelesaian
$table->string('nominal_berjalan')->nullable(); // Nominal berjalan
$table->string('amortisasi_berjalan')->nullable(); // Amortisasi berjalan
$table->string('sistem_berjalan')->nullable(); // Sistem berjalan
$table->string('lainnya_berjalan')->nullable(); // Lainnya berjalan
$table->string('nominal_gantung')->nullable(); // Nominal gantung
$table->string('aset_gantung')->nullable(); // Aset gantung
$table->string('keterangan_gantung')->nullable(); // Keterangan gantung
$table->string('lainnya_satu')->nullable(); // Lainnya satu
$table->string('lainnya_dua')->nullable(); // Lainnya dua
// 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
$table->index(['nomor_tiket']);
$table->index(['tanggal']);
$table->index(['tahun']);
$table->index(['nomor_coa']);
$table->index(['cost_center']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bucoks');
}
};

View File

@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
* Menambahkan kolom nomor_tiket ke tabel persetujuan_penawaran
*/
public function up(): void
{
Schema::table('persetujuan_penawaran', function (Blueprint $table) {
// Menambahkan kolom nomor_tiket setelah nomor_proposal_penawaran
$table->string('nomor_tiket', 100)->nullable()->after('nomor_proposal_penawaran')
->comment('Nomor tiket untuk tracking persetujuan penawaran');
$table->string('nominal_kurang_bayar')->nullable()->after('nomor_proposal_penawaran');
$table->string('bukti_ksl_kurang_bayar')->nullable()->after('nominal_kurang_bayar');
});
Schema::table('noc', function (Blueprint $table) {
// Menambahkan kolom nomor_tiket setelah nomor_proposal_penawaran
$table->string('nomor_tiket', 100)->nullable()->after('persetujuan_penawaran_id')
->comment('Nomor tiket untuk tracking persetujuan penawaran');
$table->string('nomor_rekening_lebih_bayar',20)->nullable()->after('nominal_lebih_bayar');
$table->string('bukti_ksl_lebih_bayar')->nullable()->after('nomor_rekening_lebih_bayar');
$table->string('bukti_ksl_kurang_bayar')->nullable()->after('bukti_ksl_lebih_bayar');
$table->string('nominal_pelunasan')->nullable()->after('bukti_ksl_kurang_bayar');
$table->string('debiture_id')->nullable()->after('persetujuan_penawaran_id');
});
}
/**
* Reverse the migrations.
* Menghapus kolom nomor_tiket dari tabel persetujuan_penawaran
*/
public function down(): void
{
Schema::table('persetujuan_penawaran', function (Blueprint $table) {
// Menghapus kolom nomor_tiket
$table->dropColumn('nomor_tiket');
$table->dropColumn('nominal_kurang_bayar');
$table->dropColumn('bukti_ksl_kurang_bayar');
});
Schema::table('noc', function (Blueprint $table) {
// Menghapus kolom nomor_tiket
$table->dropColumn('nomor_tiket');
$table->dropColumn('nomor_rekening_lebih_bayar');
$table->dropColumn('bukti_ksl_lebih_bayar');
$table->dropColumn('bukti_ksl_kurang_bayar');
$table->dropColumn('nominal_pelunasan');
$table->dropColumn('debiture_id');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('noc', function (Blueprint $table) {
$table->bigInteger('branch_id')->nullable()->after('debiture_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('noc', function (Blueprint $table) {
$table->dropColumn('branch_id');
});
}
};

View 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');
}
};

View File

@@ -0,0 +1,84 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('laporan_slik', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('slik_id')->nullable();
$table->string('sandi_bank', 10)->nullable();
$table->string('kode_kantor', 10)->nullable();
$table->string('kode_cabang', 10)->nullable();
$table->string('tahun', 4)->nullable();
$table->string('bulan', 2)->nullable();
$table->string('no_rekening', 50)->nullable();
$table->string('cif', 50)->nullable();
$table->string('kode_jenis', 10)->nullable();
$table->string('kode_jenis_ket', 100)->nullable();
$table->string('kode_sifat', 10)->nullable();
$table->string('kode_sifat_ket', 100)->nullable();
$table->string('kode_valuta', 5)->nullable();
$table->string('kode_valuta_ket', 50)->nullable();
$table->string('baki_debet', 20)->nullable();
$table->string('kolektibilitas', 5)->nullable();
$table->string('kolektibilitas_ket', 50)->nullable();
$table->string('tanggal_mulai', 10)->nullable();
$table->string('tanggal_jatuh_tempo', 10)->nullable();
$table->string('tanggal_selesai', 10)->nullable();
$table->string('tanggal_restrukturisasi', 10)->nullable();
$table->string('kode_sebab_macet', 10)->nullable();
$table->string('kode_sebab_macet_ket', 100)->nullable();
$table->string('tanggal_macet', 10)->nullable();
$table->string('kode_kondisi', 10)->nullable();
$table->string('kode_kondisi_ket', 100)->nullable();
$table->string('tanggal_kondisi', 10)->nullable();
$table->string('nilai_agunan', 20)->nullable();
$table->string('nilai_agunan_ket', 100)->nullable();
$table->string('jenis_agunan', 50)->nullable();
$table->string('kode_agunan', 10)->nullable();
$table->string('kode_agunan_ket', 100)->nullable();
$table->string('peringkat_agunan', 10)->nullable();
$table->string('peringkat_agunan_ket', 100)->nullable();
$table->string('nama_debitur', 100)->nullable();
$table->string('npwp', 50)->nullable();
$table->string('no_ktp', 50)->nullable();
$table->string('no_telp', 50)->nullable();
$table->string('kode_kab_kota', 10)->nullable();
$table->string('kode_kab_kota_ket', 100)->nullable();
$table->string('kode_negara_domisili', 10)->nullable();
$table->string('kode_negara_domisili_ket', 100)->nullable();
$table->string('kode_pos', 10)->nullable();
$table->string('alamat', 200)->nullable();
$table->string('fasilitas', 100)->nullable();
$table->string('status_agunan', 20)->nullable();
$table->string('tanggal_lapor', 10)->nullable();
$table->string('status', 20)->default('aktif');
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->timestamps();
$table->index('slik_id');
$table->index('no_rekening');
$table->index('cif');
$table->index('nama_debitur');
$table->index(['tahun', 'bulan']);
$table->index('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('laporan_slik');
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('laporan_admin_kredit', function (Blueprint $table) {
$table->text('keterangan')->nullable()->comment('Keterangan tambahan untuk laporan admin kredit');
$table->string('kolektibilitas', 10)->nullable()->comment('Status kolektibilitas kredit');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('laporan_admin_kredit', function (Blueprint $table) {
$table->dropColumn(['keterangan', 'kolektibilitas']);
});
}
};

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Menambahkan kolom nomor_registrasi dan permohonan_id pada table bucoks
* untuk menghubungkan data bucok dengan permohonan penilaian
*/
public function up(): void
{
Schema::table('bucoks', function (Blueprint $table) {
// Menambahkan kolom nomor_registrasi untuk tracking nomor registrasi
$table->string('nomor_registrasi')->nullable()->after('nomor_tiket')->comment('Nomor registrasi terkait');
// Menambahkan kolom permohonan_id sebagai foreign key ke table permohonan
$table->unsignedBigInteger('permohonan_id')->nullable()->after('nomor_registrasi')->comment('ID permohonan terkait');
// Menambahkan foreign key constraint ke table permohonan
$table->foreign('permohonan_id')->references('id')->on('permohonan')->onDelete('set null');
// Menambahkan index untuk performa query
$table->index(['nomor_registrasi']);
$table->index(['permohonan_id']);
});
}
/**
* Reverse the migrations.
*
* Menghapus kolom nomor_registrasi dan permohonan_id dari table bucoks
*/
public function down(): void
{
Schema::table('bucoks', function (Blueprint $table) {
// Menghapus foreign key constraint terlebih dahulu
$table->dropForeign(['permohonan_id']);
// Menghapus index
$table->dropIndex(['nomor_registrasi']);
$table->dropIndex(['permohonan_id']);
// Menghapus kolom
$table->dropColumn(['nomor_registrasi', 'permohonan_id']);
});
}
};

View File

@@ -0,0 +1,25 @@
<?php
return new class extends \Illuminate\Database\Migrations\Migration
{
/**
* Tambah kolom biaya pada tabel nilai_plafond.
*/
public function up(): void
{
\Illuminate\Support\Facades\Schema::table('nilai_plafond', function (\Illuminate\Database\Schema\Blueprint $table) {
// PostgreSQL akan memetakan decimal ke numeric
$table->decimal('biaya', 15, 2)->default(0);
});
}
/**
* Menghapus kolom biaya jika dilakukan rollback.
*/
public function down(): void
{
\Illuminate\Support\Facades\Schema::table('nilai_plafond', function (\Illuminate\Database\Schema\Blueprint $table) {
$table->dropColumn('biaya');
});
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Tambahkan kolom biaya pada tabel nilai_plafond
*
* Catatan:
* - Menggunakan tipe decimal(15,2) agar kompatibel dengan PostgreSQL (numeric)
*/
public function up(): void
{
Schema::table('nilai_plafond', function (Blueprint $table) {
// Tambahkan kolom biaya sebagai decimal dengan 2 pecahan
$table->decimal('biaya', 15, 2)->nullable();
});
}
/**
* Rollback perubahan: hapus kolom biaya dari tabel nilai_plafond
*/
public function down(): void
{
if (Schema::hasColumn('nilai_plafond', 'biaya')) {
Schema::table('nilai_plafond', function (Blueprint $table) {
$table->dropColumn('biaya');
});
}
}
};

View File

@@ -13,24 +13,25 @@ class HubunganPemilikJaminanSeeder extends Seeder
public function run(): void
{
$hubungan_pemilik_jaminan = [
[
'name' => 'Milik Pribadi'
],
[
'name' => 'Suami/Istri'
],
[
'name' => 'Anak'
],
[
'name' => 'Saudara Kandung'
],
[
'name' => 'Ayah'
],
[
'name' => 'Ibu'
]
['name' => 'Milik Pribadi'],
['name' => 'Suami'],
['name' => 'Anak'],
['name' => 'Saudara'],
['name' => 'Ayah'],
['name' => 'Ibu'],
['name' => 'Nenek'],
['name' => 'Penjual/Developer'],
[ 'name' => 'Kakak/adik kandung'],
[ 'name' => 'Orang tua'],
[ 'name' => 'Mitra Usaha'],
[ 'name' => 'Pihak lain'],
[ 'name' => 'Negara'],
[ 'name' => 'Nenek/kakek'],
[ 'name' => 'Milik Keluarga'],
[ 'name' => 'Kakak/adik Orangtua'],
[ 'name' => 'Istri'],
[ 'name' => 'Pengurus'],
[ 'name' => 'Lain - lain'],
];
foreach ($hubungan_pemilik_jaminan as $hpj) {

View File

@@ -69,6 +69,49 @@ class JenisFasilitasKreditSeeder extends Seeder
'created_at' => now(),
'updated_at' => now()
],
[
'code' => 'JFK009',
'name' => 'UMKM',
'status' => 1,
'created_at' => now(),
'updated_at' => now()
],
[
'code' => 'JFK010',
'name' => 'KORPORASI',
'status' => 1,
'created_at' => now(),
'updated_at' => now()
],
[
'code' => 'JFK011',
'name' => 'KPR 2',
'status' => 1,
'created_at' => now(),
'updated_at' => now()
],
[
'code' => 'JFK012',
'name' => 'KONSUMER',
'status' => 1,
'created_at' => now(),
'updated_at' => now()
],
[
'code' => 'JFK013',
'name' => 'KOMERSIL',
'status' => 1,
'created_at' => now(),
'updated_at' => now()
],
[
'code' => 'JFK014',
'name' => 'KPR REGULER',
'status' => 1,
'created_at' => now(),
'updated_at' => now()
],
]);
}
}

View File

@@ -12,36 +12,50 @@ class LpjDatabaseSeeder extends Seeder
public function run(): void
{
$this->call([
BranchSeeder::class,
CurrencySeeder::class,
HolidayCalendarSeeder::class,
JenisFasilitasKreditSeeder::class,
JenisLegalitasJaminanSeeder::class,
JenisJaminanSeeder::class,
JenisDokumenSeeder::class,
TujuanPenilaianSeeder::class,
NilaiPlatformSeeder::class,
HubunganPemilikJaminanSeeder::class,
HubunganPenghuniJaminanSeeder::class,
ArahMataAnginSeeder::class,
StatusPermohonanSeeder::class,
RegionSeeder::class,
TeamsSeeder::class,
TeamUsersSeeder::class,
JenisPenilaianSeeder::class,
IjinUsahaSeeder::class,
TujuanPenilaianKJPPSeeder::class,
KJPPSeeder::class,
JenisLaporanSeeder::class,
DebitureSeeder::class,
PemilikJaminanSeeder::class,
DokumenJaminanSeeder::class,
DetailDokumenJaminanSeeder::class,
PermohonanSeeder::class,
FotoObjekJaminanSeeder::class,
// BranchSeeder::class,
// CurrencySeeder::class,
// HolidayCalendarSeeder::class,
// JenisFasilitasKreditSeeder::class,
// JenisLegalitasJaminanSeeder::class,
// JenisJaminanSeeder::class,
// JenisDokumenSeeder::class,
// TujuanPenilaianSeeder::class,
// NilaiPlatformSeeder::class,
// HubunganPemilikJaminanSeeder::class,
// HubunganPenghuniJaminanSeeder::class,
// ArahMataAnginSeeder::class,
// StatusPermohonanSeeder::class,
// RegionSeeder::class,
// TeamsSeeder::class,
// TeamUsersSeeder::class,
// MasterDataSurveyorSeeder::class
// JenisPenilaianSeeder::class,
// IjinUsahaSeeder::class,
// TujuanPenilaianKJPPSeeder::class,
// KJPPSeeder::class,
// JenisLaporanSeeder::class,
// DebitureSeeder::class,
// PemilikJaminanSeeder::class,
// DokumenJaminanSeeder::class,
// DetailDokumenJaminanSeeder::class,
// PermohonanSeeder::class,
// FotoObjekJaminanSeeder::class,
// PenawaranSeeder::class,
// DetailPenawaranSeeder::class,
// PenilaianSeeder::class,
// MigrationDebitureSeeder::class,
// MigrationPermohonanSeeder::class,
// MigrationDokumentJaminanSeeder::class,
// MigrationDetailDokumenJaminanSeeder::class,
// MigPenilaianAndPenilainTeamSeeder::class,
// MigrationInpseksiSeeder::class,
MigrationGambarInspeksiSeeder::class,
// MigrationPembandingSeeder::class,
// MigrationPenilaiSeeder::class
// ini untuk penilaian external
// MigExternalPenawaranSeeder::class,
]);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
class MasterDataSurveyorSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$nameTable = [
'fasilitas_objek',
'gol_mas_sekitar',
'jenis_bangunan',
'jenis_kapal',
'jenis_kendaraan',
'jenis_pesawat',
'kondisi_bangunan',
'kondisi_fisik_tanah',
'kontur_tanah',
'lalu_lintas_lokasi',
'lantai',
'merupakan_daerah',
'perkerasan_jalan',
'sifat_bangunan',
'model_alat_berat',
'posisi_kavling',
'posisi_unit',
'tingkat_keramaian',
'sarana_pelengkap',
'spek_kategori_bangunan',
'spek_bangunan',
'terletak_diarea',
'view_unit'
];
foreach ($nameTable as $table) {
DB::unprepared(file_get_contents(__DIR__ . '/sql/' . $table . '.sql'));
}
}
}

View File

@@ -0,0 +1,306 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\PenawaranTender;
use Modules\Usermanagement\Models\User;
use Modules\Lpj\Models\Penilaian;
use Modules\Lpj\Models\PenilaianTeam;
class MigExternalPenawaranSeeder extends Seeder
{
protected $errorLogFile = __DIR__ . '/csv/penawaran/mig_penawaran_external_error.csv';
/**
* Run the database seeds.
*/
public function run()
{
// Bersihkan/Inisialisasi file error log
$this->initializeErrorLog();
// Path ke file csv
$filePath = realpath(__DIR__ . '/csv/penawaran/mig_penawaran_external.csv');
if (!$filePath) {
Log::error('File csv tidak ditemukan: ' . __DIR__ . '/csv/permohonan/mig_penawaran_external.csv');
$this->command->error('File csv tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, '~');
$rows = [];
$batchSize = 500; // Ukuran batch
$userDataChace = [];
$nomorRegisCahce = [];
$errorCount = 0; // Inisialisasi variabel errorCount
$errorDebitureIds = []; // Inisialisasi array errorDebitureIds
$totalData = 0;
// Menghitung total data di file CSV
while (($data = fgetcsv($handle, 0, '~')) !== false) {
$totalData++;
}
rewind($handle); // Reset pointer ke awal file
fgetcsv($handle, 0, '~'); // Skip header
$batchCount = 0;
$currentRow = 0;
while (($data = fgetcsv($handle, 0, '~')) !== false) {
if (count($data) != count($header)) {
Log::warning('Baris CSV memiliki jumlah kolom yang tidak sesuai: ' . json_encode($data));
continue;
}
$rows[] = array_combine($header, $data);
$currentRow++;
// Jika sudah mencapai batch size, proses batch
if (count($rows) >= $batchSize) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $nomorRegisCahce, $userDataChace, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
$rows = [];
}
}
// Proses sisa data jika ada
// print_r($rows[0]);
if (!empty($rows)) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $nomorRegisCahce, $userDataChace, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
}
fclose($handle);
$this->command->info("Data debiture berhasil dimigrasikan. Total data: {$totalData}, Total batch: {$batchCount}, Total error: {$errorCount}");
}
private function processBatch(
array $rows,
array &$userDataChace,
array &$nomorRegisCahce,
int &$errorCount,
array &$errorDebitureIds,
int $totalData,
int $batchCount,
int $currentRow
) {
$userData = [];
foreach ($rows as $index => $row) {
try {
// Jalankan setiap baris dengan transaksi sendiri
DB::beginTransaction();
// Cek apakah sudah diproses
$existingRecord = PenawaranTender::where('nomor_registrasi', $row['mig_nomor_jaminan'])->first();
if ($existingRecord && $existingRecord->created_at) {
$this->command->info('Data sudah diproses sebelumnya: ' . $row['mig_nomor_jaminan']);
continue;
}
// Ambil nomor registrasi
$nomor_registrasi = $this->getNomorRegistrasiPermohonan($row['mig_nomor_jaminan'], $nomorRegisCahce);
if (!$nomor_registrasi) {
throw new \Exception("Nomor registrasi tidak valid");
}
$userIdUpdate = $this->getUserIdData($row['mig_created_by'] ?? null, $userData)['id'];
$userIdOtorisasi = $this->getUserIdData($row['mig_authorized_by'] ?? null, $userData)['id'];
if (!$userIdUpdate || !$userIdOtorisasi) {
// $this->logError($userIdUpdate, 'Salah satu user tidak ditemukan');
continue;
}
// Mapping field user
$mapUser = [
'created_by' => $userIdUpdate,
'updated_by' => $userIdUpdate,
'authorized_by' => $userIdOtorisasi,
];
// create random code
$tgl = $this->parseTimestamp($row['mig_tgl_penyerahan']);
// ambil nilai tahun terakhir contoh 2023 maka ambil 23
$year = date('y', strtotime($tgl));
// random code berdasarkan tgl contoh NP2300001
$code = 'NP' . $year . str_pad(rand(1, 99999), 5, '0', STR_PAD_LEFT); // outputnya NP2300001:
// Cek apakah kode sudah ada
$existingCode = PenawaranTender::where('code', $code)->first();
if ($existingCode) {
$code = 'NP' . $year . str_pad(rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
// Buat penilaian
$penilaian = PenawaranTender::create([
'code' => $code,
'nama_kjpp_sebelumnya' => $row['mig_nama_kjpp'],
'biaya_kjpp_sebelumnya' => $row['mig_tot_jasa'],
'tanggal_penilaian_sebelumnya' => $this->parseTimestamp($row['mig_tgl_penyerahan']),
'nomor_registrasi' => $nomor_registrasi,
'tujuan_penilaian_kjpp_id' => $this->checkTujuanPenilaian($row['mig_mst_jaminan_kd_tujuan_seq']),
'jenis_laporan_id' => 1,
'start_date' => $this->parseTimestamp($row['mig_tgl_penyerahan']),
'end_date' => $this->parseTimestamp($row['mig_terima_laporan']),
'catatan' => $row['mig_catatan'],
'status' => 'spk',
'created_at' => $this->parseTimestamp($row['mig_created_at']),
'updated_at' => $this->parseTimestamp($row['mig_updated_at']),
'authorized_by' => $mapUser['authorized_by'],
'created_by' => $mapUser['created_by'],
'updated_by' => $mapUser['updated_by'],
]);
// Commit transaksi
DB::commit();
$this->command->info('Proses data penilaian ' . $row['mig_nomor_jaminan'] . ' (' . ($index + 1) . '/' . count($rows) . ')');
} catch (\Exception $e) {
// Rollback jika ada error
DB::rollBack();
Log::error('Error pada baris: ' . json_encode($row) . '. Pesan: ' . $e->getMessage());
$errorCount++;
$errorDebitureIds[] = $row['mig_team_id'] ?? '-';
$this->logError($row['mig_team_id'] ?? '-', $e->getMessage());
continue;
}
}
$this->command->info("Batch {$batchCount} selesai. Total error: " . count($errorDebitureIds));
}
private function getNomorRegistrasiPermohonan($nomor_registrasi_id, $nomorRegisCahce)
{
if (isset($nomorRegisCahce[$nomor_registrasi_id])) {
return $nomorRegisCahce[$nomor_registrasi_id];
}
$nomorRegis = Permohonan::where('nomor_registrasi', $nomor_registrasi_id)->first();
if (!$nomorRegis) {
return null;
}
$nomorRegisCahce[$nomor_registrasi_id] = $nomorRegis->nomor_registrasi;
return $nomorRegis->nomor_registrasi;
}
private function getUserIdData(?string $code, array &$cache): array
{
if (!$code) {
return ['id' => null, 'branch_id' => null];
}
if (isset($cache[$code])) {
return $cache[$code];
}
$user = User::where('nik', $code)->first();
if ($user) {
$cache[$code] = ['id' => $user->id, 'branch_id' => $user->branch_id];
return $cache[$code];
}
return ['id' => null, 'branch_id' => null];
}
private function getUserId($mig_user_id, $userDataChace)
{
if (isset($userDataChace[$mig_user_id])) {
return $userDataChace[$mig_user_id];
}
$userId = User::where('nik', $mig_user_id)->first();
if (!$userId) {
return null;
}
$userDataChace[$mig_user_id] = $userId->id;
return $userId->id;
}
private function parseTimestamp(?string $timestamp): ?string
{
try {
if ($timestamp) {
// Cek jika format hanya tanggal (Y-m-d)
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d', $timestamp)
->startOfDay()
->toDateTimeString();
}
// Format lengkap (Y-m-d H:i:s)
return \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $timestamp)->toDateTimeString();
}
return null;
} catch (\Exception $e) {
Log::error('Gagal memparsing timestamp: ' . $timestamp . '. Pesan: ' . $e->getMessage());
return null;
}
}
private function initializeErrorLog()
{
// Jika file lama ada, hapus
if (file_exists($this->errorLogFile)) {
unlink($this->errorLogFile);
}
// Buat file baru dengan header
$handle = fopen($this->errorLogFile, 'w');
fputcsv($handle, ['mig_kd_debitur_seq', 'Error']);
fclose($handle);
}
private function logError(string $kode, string $message)
{
// Catat ke log
Log::error("Error migrasi debiture [$kode]: $message");
// Tulis ke file error
$handle = fopen($this->errorLogFile, 'a');
fputcsv($handle, [$kode, $message]);
fclose($handle);
}
private function checkTujuanPenilaian($code): int
{
$mapping = [
1 => 7,
2 => 5,
3 => 8,
6 => 6,
];
return $mapping[$code] ?? 7;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\Basicdata\Models\Branch;
use Modules\Lpj\Models\Penilaian;
use Modules\Usermanagement\Models\User;
use Modules\Lpj\Models\PenilaianTeam;
use Modules\Lpj\Models\Permohonan;
class MigPenilaianAndPenilainTeamSeeder extends Seeder
{
protected $errorLogFile = __DIR__ . '/csv/penilaian/penilaian.team.fix.new_20251012_error.csv';
/**
* Run the database seeds.
*/
public function run()
{
// Bersihkan/Inisialisasi file error log
$this->initializeErrorLog();
// Path ke file csv
$filePath = realpath(__DIR__ . '/csv/penilaian/penilaian.team.fix.new_20251012.csv');
if (!$filePath) {
Log::error('File csv tidak ditemukan: ' . __DIR__ . '/csv/penilaian/penilaian.team.fix.new_20251012.csv');
$this->command->error('File csv tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, ',');
$rows = [];
$batchSize = 500; // Ukuran batch
$userDataChace = [];
$nomorRegisCahce = [];
$errorCount = 0; // Inisialisasi variabel errorCount
$errorDebitureIds = []; // Inisialisasi array errorDebitureIds
$totalData = 0;
// Menghitung total data di file CSV
while (($data = fgetcsv($handle, 0, ',')) !== false) {
$totalData++;
}
rewind($handle); // Reset pointer ke awal file
fgetcsv($handle, 0, ','); // Skip header
$batchCount = 0;
$currentRow = 0;
while (($data = fgetcsv($handle, 0, ',')) !== false) {
if (count($data) != count($header)) {
Log::warning('Baris CSV memiliki jumlah kolom yang tidak sesuai: ' . json_encode($data));
continue;
}
$rows[] = array_combine($header, $data);
$currentRow++;
// Jika sudah mencapai batch size, proses batch
if (count($rows) >= $batchSize) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $nomorRegisCahce, $userDataChace, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
$rows = [];
}
}
// Proses sisa data jika ada
// print_r($rows[0]);
if (!empty($rows)) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $nomorRegisCahce, $userDataChace, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
}
fclose($handle);
$this->command->info("Data debiture berhasil dimigrasikan. Total data: {$totalData}, Total batch: {$batchCount}, Total error: {$errorCount}");
}
private function processBatch(
array $rows,
array &$userDataChace,
array &$nomorRegisCahce,
int &$errorCount,
array &$errorDebitureIds,
int $totalData,
int $batchCount,
int $currentRow
) {
$userData = [];
foreach ($rows as $index => $row) {
try {
// Jalankan setiap baris dengan transaksi sendiri
//DB::beginTransaction();
// Cek apakah sudah diproses
$existingRecord = Penilaian::where('nomor_registrasi', $row['mig_nomor_registrasi'])->first();
if ($existingRecord && $existingRecord->created_at) {
$this->command->info('Data sudah diproses sebelumnya: ' . $row['mig_nomor_registrasi']);
//continue;
}
// Ambil nomor registrasi
$nomor_registrasi = $this->getNomorRegistrasiPermohonan($row['mig_nomor_registrasi'], $nomorRegisCahce);
if (!$nomor_registrasi) {
throw new \Exception($row['mig_nomor_registrasi']."Nomor registrasi tidak valid");
}
// Ambil user ID
$userId = $this->getUserId($row['mig_user_id'], $userDataChace);
if (!$userId) {
// $this->logError($row['mig_user_id'], 'Salah satu user tidak ditemukan');
throw new \Exception($row['mig_user_id'] . " User tidak ditemukan");
continue;
}
$userIdUpdate = $this->getUserIdData($row['mig_created_by'] ?? null, $userData)['id'];
$userIdOtorisasi = $this->getUserIdData($row['mig_authorized_by'] ?? null, $userData)['id'];
if (!$userIdUpdate || !$userIdOtorisasi) {
// $this->logError($userIdUpdate, 'Salah satu user tidak ditemukan');
//continue;
}
// Mapping field user
$mapUser = [
'created_by' => $userIdUpdate,
'updated_by' => $userIdUpdate,
'authorized_by' => $userIdOtorisasi,
];
// Ambil team ID
$teamId = $this->checkTeams($row['mig_team_id']);
if (!$teamId) {
throw new \Exception("Team tidak ditemukan");
}
$idPenilaian = Penilaian::orderBy('id', 'desc')->first()->id;
$data = [
'id' => $idPenilaian+1,
'nomor_registrasi' => $nomor_registrasi,
'jenis_penilaian_id' => 1,
'tanggal_kunjungan' => $this->parseTimestamp($row['mig_tanggal_kunjungan']) ?? now(),
'waktu_penilaian' => $this->parseTimestamp($row['mig_waktu_penilaian']),
'status' => 'done',
'keterangan' => null,
'created_at' => $this->parseTimestamp($row['mig_created_at']),
'updated_at' => $this->parseTimestamp($row['mig_updated_at']),
'authorized_by' => $mapUser['authorized_by'],
'created_by' => $mapUser['created_by'],
'updated_by' => $mapUser['updated_by']
];
if($nomor_registrasi=='251722'){
Log::info("Data penilaian 251722 : ",$data);
}
// Buat penilaian
$penilaian = Penilaian::updateOrCreate([
'nomor_registrasi' => $nomor_registrasi
],[
'id' => $idPenilaian+1,
'nomor_registrasi' => $nomor_registrasi,
'jenis_penilaian_id' => 1,
'tanggal_kunjungan' => $this->parseTimestamp($row['mig_tanggal_kunjungan']) ?? now(),
'waktu_penilaian' => $this->parseTimestamp($row['mig_waktu_penilaian']),
'status' => 'done',
'keterangan' => null,
'created_at' => $this->parseTimestamp($row['mig_created_at']),
'updated_at' => $this->parseTimestamp($row['mig_updated_at']),
'authorized_by' => $mapUser['authorized_by'],
'created_by' => $mapUser['created_by'],
'updated_by' => $mapUser['updated_by'],
]);
//DB::commit();
// Buat tim penilaian
$roles = ['surveyor', 'penilai'];
foreach ($roles as $role) {
$idPenilaianTeam = PenilaianTeam::orderBy('id', 'desc')->first()->id;
PenilaianTeam::updateOrCreate([
'penilaian_id' => $penilaian->id
],[
'id' => $idPenilaianTeam+1,
'penilaian_id' => $penilaian->id,
'user_id' => $userId,
'role' => $role,
'team_id' => $teamId,
'created_at' => $this->parseTimestamp($row['mig_created_at']),
'updated_at' => $this->parseTimestamp($row['mig_updated_at']),
]);
}
// Commit transaksi
//DB::commit();
$this->command->info('Proses data penilaian ' . $row['mig_nomor_lpj'] . ' (' . ($index + 1) . '/' . count($rows) . ')');
} catch (\Exception $e) {
// Rollback jika ada error
//DB::rollBack();
Log::error('Error pada baris: ' . json_encode($row) . '. Pesan: ' . $e->getMessage());
$errorCount++;
$errorDebitureIds[] = $row['mig_team_id'] ?? '-';
$this->logError($row['mig_team_id'] ?? '-', $e->getMessage());
continue;
}
}
$this->command->info("Batch {$batchCount} selesai. Total error: " . count($errorDebitureIds));
}
private function getNomorRegistrasiPermohonan($nomor_registrasi_id, $nomorRegisCahce)
{
if (isset($nomorRegisCahce[$nomor_registrasi_id])) {
return $nomorRegisCahce[$nomor_registrasi_id];
}
$nomorRegis = Permohonan::where('nomor_registrasi', $nomor_registrasi_id)->first();
if (!$nomorRegis) {
return null;
}
$nomorRegisCahce[$nomor_registrasi_id] = $nomorRegis->nomor_registrasi;
return $nomorRegis->nomor_registrasi;
}
private function getUserIdData(?string $code, array &$cache): array
{
if (!$code) {
return ['id' => null, 'branch_id' => null];
}
if (isset($cache[$code])) {
return $cache[$code];
}
$user = User::where('nik', $code)->first();
if ($user) {
$cache[$code] = ['id' => $user->id, 'branch_id' => $user->branch_id];
return $cache[$code];
}
return ['id' => null, 'branch_id' => null];
}
private function getUserId($mig_user_id, $userDataChace)
{
if (isset($userDataChace[$mig_user_id])) {
return $userDataChace[$mig_user_id];
}
$userId = User::where('nik', $mig_user_id)->first();
if (!$userId) {
return null;
}
$userDataChace[$mig_user_id] = $userId->id;
return $userId->id;
}
private function checkTeams($code): int
{
$mapping = [
'01' => 1,
'02' => 2,
'04' => 4,
'07' => 5,
'06' => 3,
];
return $mapping[(string)$code] ?? 1;
}
private function parseTimestamp(?string $timestamp): ?string
{
try {
if ($timestamp) {
// Cek jika format hanya tanggal (Y-m-d)
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d', $timestamp)
->startOfDay()
->toDateTimeString();
}
// Format lengkap (Y-m-d H:i:s)
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $timestamp)->toDateTimeString();
}
// Format d/m/Y H:i:s (contoh: 28/4/2017 14:43:43)
if (preg_match('/^\d{1,2}\/\d{1,2}\/\d{4} \d{2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('d/m/Y H:i:s', $timestamp)->toDateTimeString();
}
}
return null;
} catch (\Exception $e) {
Log::error('Gagal memparsing timestamp: ' . $timestamp . '. Pesan: ' . $e->getMessage());
return null;
}
}
private function initializeErrorLog()
{
// Jika file lama ada, hapus
if (file_exists($this->errorLogFile)) {
unlink($this->errorLogFile);
}
// Buat file baru dengan header
$handle = fopen($this->errorLogFile, 'w');
fputcsv($handle, ['mig_kd_debitur_seq', 'Error']);
fclose($handle);
}
private function logError(string $kode, string $message)
{
// Catat ke log
Log::error("Error migrasi debiture [$kode]: $message");
// Tulis ke file error
$handle = fopen($this->errorLogFile, 'a');
fputcsv($handle, [$kode, $message]);
fclose($handle);
}
}

View File

@@ -0,0 +1,255 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\Basicdata\Models\Branch;
use Modules\Lpj\Models\Debiture;
use Modules\Usermanagement\Models\User;
class MigrationDebitureSeeder extends Seeder
{
protected $errorLogFile = __DIR__ . '/csv/debitures/error_migration.csv';
/**
* Run the database seeds.
*/
public function run()
{
// Bersihkan/Inisialisasi file error log
$this->initializeErrorLog();
// Path ke file csv
$filePath = realpath(__DIR__ . '/csv/debitures/debitur.latest.csv');
if (!$filePath) {
Log::error('File csv tidak ditemukan: ' . __DIR__ . '/csv/debitures/debitur.latest.csv');
$this->command->error('File csv tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, '|');
$rows = [];
$batchSize = 500;
$userData = [];
$branchCache = [];
$totalData = 0;
// Hitung total baris
while (($data = fgetcsv($handle, 0, '|')) !== false) {
$totalData++;
}
rewind($handle);
fgetcsv($handle, 0, '|'); // Lewati header
$currentRow = 0;
$batchCount = 0;
while (($data = fgetcsv($handle, 0, '|')) !== false) {
if (count($data) != count($header)) {
$this->logError($data[0] ?? '-', 'Jumlah kolom tidak sesuai');
continue;
}
$rows[] = array_combine($header, $data);
$currentRow++;
if (count($rows) >= $batchSize) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $userData);
$rows = [];
}
}
// print_r($rows);
if (!empty($rows)) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $userData);
}
fclose($handle);
$this->command->info("Data debiture berhasil dimigrasikan. Total data: {$totalData}, Total batch: {$batchCount}");
}
private function processBatch(array $rows, array &$branchCache, array &$userData)
{
foreach ($rows as $index => $row) {
try {
$kode = $row['mig_kd_debitur_seq'] ?? '-';
// Cek apakah sudah diproses sebelumnya
$existingRecord = Debiture::where('mig_kd_debitur_seq', $kode)->first();
if ($existingRecord && $existingRecord->processed_at) {
$this->command->info("Data sudah diproses sebelumnya: $kode");
//continue;
}
// Ambil branch_id
$branchId = $this->getBranchId($row['mig_kd_cabang'] ?? null, $branchCache);
if (!$branchId) {
$this->logError($row['mig_kd_cabang'], 'Cabang tidak ditemukan');
//continue;
}
// Ambil user IDs berdasarkan NIK
$userIdUpdate = $this->getUserId($row['mig_user_update'] ?? null, $userData)['id'];
$userIdOtorisasi = $this->getUserId($row['mig_user_oto'] ?? null, $userData)['id'];
// if (!$userIdUpdate || !$userIdOtorisasi) {
// $this->logError($kode, 'Salah satu user tidak ditemukan');
// continue;
// }
// Mapping field user
$mapUser = [
'created_by' => $userIdUpdate,
'updated_by' => $userIdUpdate,
'authorized_by' => $userIdOtorisasi,
];
// Parsing nomor HP
$nomorHp = !empty($row['mig_phone']) ? preg_replace('/[^0-9]/', '', $row['mig_phone']) : null;
$email = !empty($row['email']) ? $row['email'] : null;
$nomorId = !empty($row['nomor_id']) ? $row['nomor_id'] : null;
// Parsing waktu
$authorizedAt = $this->parseTimestamp($row['mig_tgl_oto'] ?? null);
$createdAt = $this->parseTimestamp($row['created_at'] ?? null);
$updatedAt = $this->parseTimestamp($row['updated_at'] ?? null);
if (!$createdAt || !$updatedAt) {
$this->logError($kode, 'Gagal parsing created_at / updated_at');
continue;
}
// Simpan data
Debiture::updateOrCreate([
'mig_kd_debitur_seq' => (int) strtok($row['mig_kd_debitur_seq'], '.'),
], [
'branch_id' => $branchId,
'name' => $row['name'] ?? '',
'cif' => (int) strtok($row['cif'],'.') ?: '0000000000',
'phone' => $nomorHp,
'nomor_id' => $nomorId,
'email' => $email,
'npwp' => null,
'address' => $row['address'] ?? null,
'authorized_at' => $authorizedAt,
'authorized_by' => $mapUser['authorized_by'],
'created_by' => $mapUser['created_by'] ?? null,
'updated_by' => $mapUser['updated_by'] ?? null,
'created_at' => $createdAt,
'updated_at' => $updatedAt,
'mig_kd_debitur_seq' => (int) strtok($row['mig_kd_debitur_seq'], '.'),
'processed_at' => now(),
'mig_debitur' => json_encode($row),
'is_mig' => 1
]);
$this->command->info("Proses data debiture $kode (" . ($index + 1) . '/' . count($rows) . ')');
} catch (\Exception $e) {
$this->logError($row['mig_kd_debitur_seq'] ?? '-', "Error eksepsi: " . $e->getMessage());
}
}
}
private function getBranchId(?string $code, array &$cache): ?int
{
if (!$code) return null;
if (isset($cache[$code])) {
return $cache[$code];
}
$branch = Branch::where('code', $code)->first();
if ($branch) {
$cache[$code] = $branch->id;
return $branch->id;
}
return null;
}
private function getUserId(?string $code, array &$cache): array
{
if (!$code) return ['id' => null, 'branch_id' => null];
if (isset($cache[$code])) {
return $cache[$code];
}
$user = User::where('nik', $code)->first();
if ($user) {
$cache[$code] = ['id' => $user->id, 'branch_id' => $user->branch_id];
return $cache[$code];
}
return ['id' => null, 'branch_id' => null];
}
private function parseTimestamp(?string $timestamp): ?string
{
try {
if ($timestamp) {
// Cek jika format hanya tanggal (Y-m-d)
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d', $timestamp)
->startOfDay()
->toDateTimeString();
}
// Format lengkap (Y-m-d H:i:s)
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $timestamp)->toDateTimeString();
}
// Format d/m/Y H:i:s (contoh: 28/4/2017 14:43:43)
if (preg_match('/^\d{1,2}\/\d{1,2}\/\d{4} \d{2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('d/m/Y H:i:s', $timestamp)->toDateTimeString();
}
}
return null;
} catch (\Exception $e) {
Log::error('Gagal memparsing timestamp: ' . $timestamp . '. Pesan: ' . $e->getMessage());
return null;
}
}
private function initializeErrorLog()
{
// Jika file lama ada, hapus
if (file_exists($this->errorLogFile)) {
unlink($this->errorLogFile);
}
// Buat file baru dengan header
$handle = fopen($this->errorLogFile, 'w');
fputcsv($handle, ['mig_kd_debitur_seq', 'Error']);
fclose($handle);
}
private function logError(string $kode, string $message)
{
// Catat ke log
Log::error("Error migrasi debiture [$kode]: $message");
// Tulis ke file error
$handle = fopen($this->errorLogFile, 'a');
fputcsv($handle, [$kode, $message]);
fclose($handle);
}
}

View File

@@ -0,0 +1,356 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Modules\Lpj\Models\Debiture;
use Modules\Location\Models\City;
use Modules\Location\Models\District;
use Modules\Location\Models\Province;
use Modules\Location\Models\Village;
use Modules\Lpj\Models\DokumenJaminan;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\JenisJaminan;
use Modules\Lpj\Models\JenisLegalitasJaminan;
use Modules\Lpj\Models\PemilikJaminan;
use Modules\Lpj\Models\DetailDokumenJaminan;
use Illuminate\Support\Facades\Log;
class MigrationDetailDokumenJaminanSeeder extends Seeder
{
protected $errorLogFile = __DIR__ . '/csv/detail.dokumen.jaminan_20251016_error.csv';
/**
* Run the database seeds.
*/
public function run()
{
$this->initializeErrorLog();
// Path ke file csv
$filePath = realpath(__DIR__ . '/csv/detail.dokumen.jaminan_20251016.csv');
if (!$filePath) {
Log::error('File csv tidak ditemukan: ' . __DIR__ . '/csv/detail.dokumen.jaminan_20251016.csv');
$this->command->error('File csv tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, ',');
$rows = [];
$batchSize = 1000; // Ukuran batch
$permohonanCache = [];
$jenisJaminanCache = [];
$dokumenJaminanCache = [];
$provinceCache = [];
$cityCache = [];
$districtCache = [];
$subdistrictCache = [];
$totalData = 0;
$errorCount = 0;
$errorDebitureIds = [];
$hubunganPemilikCache = [];
// Menghitung total data di file CSV
while (($data = fgetcsv($handle, 0, ',')) !== false) {
$totalData++;
}
rewind($handle); // Reset pointer ke awal file
fgetcsv($handle, 0, ','); // Skip header
$batchCount = 0;
$currentRow = 0;
// Membaca setiap baris dalam CSV
while (($data = fgetcsv($handle, 0, ',')) !== false) {
if (count($data) != count($header)) {
Log::warning('Baris CSV memiliki jumlah kolom yang tidak sesuai: ' . json_encode($data));
$errorCount++;
$errorDebitureIds[] = $data[0] ?? 'ID tidak valid'; // Menyimpan ID yang error
continue;
}
$rows[] = array_combine($header, $data);
$currentRow++;
// print_r($rows);
if (count($rows) >= $batchSize) {
$errorDebitureIds[] = $data[0] ?? 'ID tidak valid'; // Menyimpan ID yang error
$this->processBatch($rows, $permohonanCache, $jenisJaminanCache, $dokumenJaminanCache, $batchCount, $currentRow, $totalData, $errorCount, $errorDebitureIds);
$rows = [];
}
}
// print_r($rows[0]);
if (!empty($rows)) {
$this->processBatch($rows, $permohonanCache, $jenisJaminanCache, $dokumenJaminanCache, $batchCount, $currentRow, $totalData, $errorCount, $errorDebitureIds);
}
fclose($handle);
$this->command->info('Data debiture berhasil dimigrasikan.');
}
private function processBatch(
array $rows,
array &$permohonanCache,
array &$jenisJaminanCache,
array &$dokumenJaminanCache,
int $batchCount,
int $currentRow,
int $totalData,
int &$errorCount,
array &$errorDebitureIds
) {
$groupedRows = $this->groupRowsByNomorLaporanAndKeterangan($rows);
// print_r($groupedRows);
foreach ($groupedRows as $nomorLaporan => $dataPerGrup) {
// print_r($dataPerGrup);
try {
// Ambil salah satu baris untuk referensi (misal baris pertama dari grup)
$firstRow = reset($dataPerGrup)['details'] ? reset($dataPerGrup) : reset($dataPerGrup);
$debiturId = null;
$nomorJaminan = null;
// Cari debitur ID dari salah satu field
foreach ($rows as $row) {
if ($row['mig_nomor_laporan'] == $nomorLaporan) {
$debiturId = $row['mig_kd_debitur_seq'] ?? null;
$nomorJaminan = $row['mig_nomor_jaminan'] ?? null;
break;
}
}
if (!$debiturId) {
throw new \Exception('Debitur ID tidak ditemukan');
}
// Ambil ID dokumen jaminan
$dokumenJaminanId = $this->getDokumenJaminanId($debiturId,$nomorJaminan, $dokumenJaminanCache);
if (!$dokumenJaminanId) {
throw new \Exception('Dokumen jaminan tidak ditemukan');
}
//dd($debiturId,$nomorJaminan);
foreach ($dataPerGrup as $groupKey => $rowData) {
// Ambil legalitas jaminan ID berdasarkan grup keterangan
$legalitasJaminanId = $this->getLegalitasJaminanId($groupKey, $jenisJaminanCache);
if (!$legalitasJaminanId) {
throw new \Exception("Legalitas jaminan tidak ditemukan untuk grup: {$groupKey}");
}
// Simpan ke database
$detail =DetailDokumenJaminan::updateOrCreate(
[
'name' => $groupKey,
'dokumen_jaminan_id' => $dokumenJaminanId,
],
[
'details' => json_encode($rowData['details']),
'jenis_legalitas_jaminan_id' => $legalitasJaminanId,
'dokumen_jaminan' => !empty($rowData['documents']) ? json_encode($rowData['documents']) : null,
'dokumen_nomor' => $nomorLaporan,
'keterangan' => $groupKey,
]
);
if($nomorJaminan=='253339'){
//dd($debiturId,$nomorJaminan,$legalitasJaminanId, $detail, $dokumenJaminanId);
}
}
// Info progress
$this->command->info("Proses data detail dokumen (Nomor Laporan: {$nomorLaporan}, batch ke: {$batchCount}, total dari: {$currentRow}/{$totalData})");
} catch (\Exception $e) {
Log::error("Error pada nomor laporan {$nomorLaporan}: " . $e->getMessage());
$this->logError($debiturId ?? '-', $e->getMessage());
$errorDebitureIds[] = $debiturId ?? '-';
$errorCount++;
}
}
}
private function groupRowsByNomorLaporann(array $rows): array
{
$grouped = [];
foreach ($rows as $row) {
$nomorJaminan = $row['mig_nomor_laporan'] ?? null;
if (!empty($nomorJaminan)) {
$grouped[$nomorJaminan][] = $row;
}
}
return $grouped;
}
// private function groupRowsByNomorLaporanAndKeterangan(array $rows): array
// {
// $groupedByNomorLaporan = $this->groupRowsByNomorLaporann($rows);
// $finalGrouped = [];
// foreach ($groupedByNomorLaporan as $nomorLaporan => $rowList) {
// $groupedByGrpKeterangan = [];
// // Urutkan berdasarkan mig_urutan_sub (ascending)
// usort($rowList, function ($a, $b) {
// return $a['mig_urutan_sub'] <=> $b['mig_urutan_sub'];
// });
// foreach ($rowList as $row) {
// $grpKeterangan = $row['mig_grp_keterangan'] ?? 'UNKNOWN_GROUP';
// if (!isset($groupedByGrpKeterangan[$grpKeterangan])) {
// $groupedByGrpKeterangan[$grpKeterangan] = [
// 'details' => [],
// 'documents' => []
// ];
// }
// $keyKeterangan = $row['mig_key_keterangan'] ?? 'UNKNOWN_KEY';
// $value = $row['mig_nilai'] ?? null;
// // Jika belum ada detail, inisialisasi sebagai array asosiatif
// if (empty($groupedByGrpKeterangan[$grpKeterangan]['details'])) {
// $groupedByGrpKeterangan[$grpKeterangan]['details'][] = [];
// }
// // Masukkan ke dalam object tunggal (bukan item baru)
// if ($value !== null) {
// $lastIndex = count($groupedByGrpKeterangan[$grpKeterangan]['details']) - 1;
// $groupedByGrpKeterangan[$grpKeterangan]['details'][$lastIndex][$keyKeterangan] = $value;
// }
// // Tambahkan dokumen jika ada
// $urlFile = $row['mig_url_file'] ?? null;
// if (!empty($urlFile)) {
// $groupedByGrpKeterangan[$grpKeterangan]['documents'][] = $urlFile;
// }
// }
// $finalGrouped[$nomorLaporan] = $groupedByGrpKeterangan;
// }
// // print_r($finalGrouped);
// return $finalGrouped;
// }
private function groupRowsByNomorLaporanAndKeterangan(array $rows): array
{
$groupedByNomorLaporan = $this->groupRowsByNomorLaporann($rows);
$finalGrouped = [];
foreach ($groupedByNomorLaporan as $nomorLaporan => $rowList) {
$groupedByGrpKeterangan = [];
// Urutkan berdasarkan mig_urutan_sub
usort($rowList, function ($a, $b) {
return $a['mig_urutan_sub'] <=> $b['mig_urutan_sub'];
});
foreach ($rowList as $row) {
$grpKeterangan = $row['mig_grp_keterangan'] ?? 'UNKNOWN_GROUP';
$keyKeterangan = $row['mig_key_keterangan'] ?? 'UNKNOWN_KEY';
$value = $row['mig_nilai'] ?? null;
if (!isset($groupedByGrpKeterangan[$grpKeterangan])) {
$groupedByGrpKeterangan[$grpKeterangan] = [
'details' => [],
'documents' => []
];
}
// Masukkan sebagai object baru ke dalam details
if ($value !== null) {
$groupedByGrpKeterangan[$grpKeterangan]['details'][] = [
$keyKeterangan => $value
];
}
// Dokumen tetap masuk sebagai array string
$urlFile = $row['mig_url_file'] ?? null;
if (!empty($urlFile)) {
$groupedByGrpKeterangan[$grpKeterangan]['documents'][] = $urlFile;
}
}
$finalGrouped[$nomorLaporan] = $groupedByGrpKeterangan;
}
return $finalGrouped;
}
private function getDokumenJaminanId(string $code,string $nomorLaporan, array &$dokumenJaminanCache)
{
$dokumen = DokumenJaminan::where('mig_kd_debitur_seq', $code)
->where('nomor_lpj', $nomorLaporan)
->first();
if ($dokumen) {
$dokumenJaminanCache[$code] = $dokumen->id;
return $dokumen->id;
}
}
private function getLegalitasJaminanId(string $code, array &$legalitasJaminanCache)
{
if (isset($legalitasJaminanCache[$code])) {
return $legalitasJaminanCache[$code];
}
$legalitas = JenisLegalitasJaminan::whereRaw('LOWER(name) = ?', [strtolower($code)])->first();
if ($legalitas) {
$legalitasJaminanCache[$code] = $legalitas->id;
return $legalitas->id;
}
return null;
}
private function initializeErrorLog()
{
$file = $this->errorLogFile;
if (file_exists($file)) {
unlink($file); // Hapus file lama
}
$handle = fopen($file, 'w');
fputcsv($handle, ['mig_kd_debitur_seq', 'Error']);
fclose($handle);
}
private function logError(string $kode, string $message)
{
Log::error("Error migrasi dokumen jaminan [$kode]: $message");
$handle = fopen($this->errorLogFile, 'a');
fputcsv($handle, [$kode, $message]);
fclose($handle);
}
}

View File

@@ -0,0 +1,508 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Modules\Lpj\Models\Debiture;
use Modules\Location\Models\City;
use Modules\Location\Models\District;
use Modules\Location\Models\Province;
use Modules\Location\Models\Village;
use Modules\Lpj\Models\DokumenJaminan;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\JenisJaminan;
use Modules\Lpj\Models\PemilikJaminan;
use Modules\Lpj\Models\HubunganPemilikJaminan;
use Illuminate\Support\Facades\Log;
class MigrationDokumentJaminanSeeder extends Seeder
{
protected $errorLogFile = __DIR__ . '/csv/dokumen_20251022_error_log.csv';
/**
* Run the database seeds.
*/
public function run()
{
$this->initializeErrorLog();
// Path ke file csv
$filePath = realpath(__DIR__ . '/csv/dokumen_20251022.csv');
if (!$filePath) {
Log::error('File csv tidak ditemukan: ' . __DIR__ . '/csv/dokumen_20251022.csv');
$this->command->error('File csv tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, ',');
$rows = [];
$batchSize = 1000; // Ukuran batch
$permohonanCache = [];
$jenisJaminanCache = [];
$pemilikJaminanCache = [];
$provinceCache = [];
$cityCache = [];
$districtCache = [];
$subdistrictCache = [];
$totalData = 0;
$errorCount = 0;
$errorDebitureIds = [];
$hubunganPemilikCache = [];
// Menghitung total data di file CSV
while (($data = fgetcsv($handle, 0, ',')) !== false) {
$totalData++;
}
rewind($handle); // Reset pointer ke awal file
fgetcsv($handle, 0, ','); // Skip header
$batchCount = 0;
$currentRow = 0;
// Membaca setiap baris dalam CSV
while (($data = fgetcsv($handle, 0, ',')) !== false) {
if (count($data) != count($header)) {
Log::warning('Baris CSV memiliki jumlah kolom yang tidak sesuai: ' . json_encode($data));
$errorCount++;
$errorDebitureIds[] = $data[0] ?? 'ID tidak valid'; // Menyimpan ID yang error
continue;
}
$rows[] = array_combine($header, $data);
$currentRow++;
// print_r($rows);
if (count($rows) >= $batchSize) {
$errorDebitureIds[] = $data[0] ?? 'ID tidak valid'; // Menyimpan ID yang error
$this->processBatch($rows, $permohonanCache, $jenisJaminanCache, $pemilikJaminanCache, $provinceCache, $cityCache, $districtCache, $subdistrictCache, $batchCount, $currentRow, $totalData, $errorCount, $errorDebitureIds, $hubunganPemilikCache);
$rows = [];
}
}
// print_r($rows[0]);
if (!empty($rows)) {
$this->processBatch($rows, $permohonanCache, $jenisJaminanCache, $pemilikJaminanCache, $provinceCache, $cityCache, $districtCache, $subdistrictCache, $batchCount, $currentRow, $totalData, $errorCount, $errorDebitureIds, $hubunganPemilikCache);
}
fclose($handle);
$this->command->info('Data debiture berhasil dimigrasikan.');
}
/**
* Proses batch data.
*/
private function processBatch(
array $rows,
array &$permohonanCache,
array &$jenisJaminanCache,
array &$pemilikJaminanCache,
array &$provinceCache,
array &$cityCache,
array &$districtCache,
array &$subdistrictCache,
int $batchCount,
int $currentRow,
int $totalData,
int &$errorCount,
array &$errorDebitureIds,
array &$hubunganPemilikCache
) {
foreach ($rows as $index => $row) {
try {
// Jalankan transaksi per-baris
// DB::beginTransaction();
// Cari permohonan
$permohonan = Permohonan::where('nomor_registrasi', $row['mig_nomor_jaminan'])->first();
if (empty($permohonan)) {
throw new \Exception('Missing debiture_id' . $row['mig_nomor_jaminan']);
continue;
}
// Pastikan permohonan_id belum digunakan di dokumen_jaminan
$existingDokumen = DokumenJaminan::where('permohonan_id', $permohonan->id)->first();
if ($existingDokumen) {
//throw new \Exception("permohonan_id {$permohonan->id} sudah digunakan di dokumen_jaminan");
//continue;
}
// Ambil lokasi
// jika external silahkan matikan ini
$proviceCode = $this->getProvinceCode($row['mig_province_name'], $provinceCache);
$cityCode = $this->getCityCode($row['mig_city_name'], $cityCache);
$districtCode = $this->getDistrictCode($row['mig_district_name'], $districtCache);
$subdistrict = $this->getSubdistrictCode($row['mig_village_name'], $subdistrictCache, $districtCache);
// $hubunganPemilik = $this->getHubunganPemilikJaminanId($row['mig_hubungan_pemilik_jaminan'], $hubunganPemilikCache);
$pemilik_jaminan_name = $this->getDebitureId($row['mig_kd_debitur_seq'], $pemilikJaminanCache);
// Buat Pemilik Jaminan
$pemilik_jaminan = PemilikJaminan::updateOrCreate([
'debiture_id' => $permohonan->debiture_id,
'hubungan_pemilik_jaminan_id' => 1,
// 'name' => $row['name'],
'name' => $pemilik_jaminan_name,
'detail_sertifikat' => null,
'npwp' => null,
'nomor_id' => null,
'email' => null,
'phone' => null,
// jika external silahkan matikan ini
'province_code' => $proviceCode,
'city_code' => $cityCode,
'district_code' => $districtCode,
'village_code' => $subdistrict['code'],
'postal_code' => $subdistrict['postal_code'],
'address' => $row['address'],
'created_at' => $this->parseTimestamp($row['created_at']),
'updated_at' => $this->parseTimestamp($row['updated_at']),
'mig_kd_debitur_seq' => $row['mig_kd_debitur_seq'],
'processed_at' => now(),
'is_mig' => 1
]);
// Buat Dokumen Jaminan
$dokumenJaminan = DokumenJaminan::updateOrCreate([
'permohonan_id' => $permohonan->id,
'mig_kd_debitur_seq' => $row['mig_kd_debitur_seq'],
'nomor_lpj' => $row['mig_nomor_jaminan']
],[
'debiture_id' => $permohonan->debiture_id,
'permohonan_id' => $permohonan->id,
'jenis_jaminan_id' => $this->getJaminanId($row['mig_jenis_seq']) ?? '',
'pemilik_jaminan_id' => $pemilik_jaminan->id,
// jika external silahkan matikan ini
'province_code' => $proviceCode ?? '',
'city_code' => $cityCode ?? '',
'district_code' => $districtCode ?? '',
'village_code' => $subdistrict['code'] ?? '',
'postal_code' => $subdistrict['postal_code'] ?? '',
'address' => $row['address'],
'created_at' => $this->parseTimestamp($row['created_at']),
'updated_at' => $this->parseTimestamp($row['updated_at']),
'mig_kd_debitur_seq' => $row['mig_kd_debitur_seq'],
'processed_at' => now(),
'nomor_lpj' => $row['mig_nomor_jaminan'],
'is_mig' => 1
]);
// Commit jika semua sukses
// DB::commit();
$this->command->info("Proses dokumen jaminan: " . $row['mig_kd_debitur_seq'] . "Batch: {$batchCount} Baris: {$currentRow} Total: {$totalData} Error: {$errorCount}");
} catch (\Exception $e) {
// Rollback hanya untuk baris ini
Log::error("Error pada baris: " . json_encode($row) . ". Pesan: " . $e->getMessage());
$this->logError($row['mig_kd_debitur_seq'] ?? '-', $e->getMessage());
$errorDebitureIds[] = $row['mig_kd_debitur_seq'] ?? '-';
// DB::rollBack();
continue;
}
}
$this->command->info("Batch {$batchCount} selesai. Total error: " . count($errorDebitureIds));
}
// private function getPermohonanId($code,$cache)
// {
// if (isset($cache[$code])) {
// return $cache[$code];
// }
// $permohonan = Permohonan::where('mig_kd_debitur_seq', $code)->where('nomor_registrasi', $mig_nomor_jaminan)->first();
// if ($permohonan) {
// $cache[$code] = $permohonan;
// return $permohonan;
// }
// return null;
// }
private function getJaminanId($code): ?int
{
/*$mapping = [
7 => 17,
8 => 13,
6 => 32,
1 => 17,
2 => 26,
3 => 27,
4 => 50,
5 => 21,
138051314724 => 23,
138027243057 => 34,
138027664224 => 35,
138027738489 => 10,
138051485796 => 48,
138051492883 => 47,
138051515419 => 40,
138051753311 => 41,
138051754843 => 46,
138051759078 => 42,
138051480538 => 45,
123382184742 => 18,
138051483711 => 44,
991 => 52
];*/
$mapping = [
1 => 1,
2 => 17,
3 => 19,
4 => 15,
5 => 18,
6 => 26,
7 => 26,
8 => 26,
9 => 26,
10 => 24,
11 => 28,
12 => 29,
13 => 32,
991001 => 17,
121965631354 => 17,
122267387302 => 13,
122267391891 => 27,
123242566528 => 1,
123391628912 => 18,
123779076991 => 26,
123779092232 => 26,
123837866231 => 19,
124159228236 => 14,
124280447242 => 32,
124385048902 => 30,
124539856281 => 22,
124635294016 => 13,
124963468687 => 18,
125178371127 => 31,
125228814911 => 17,
125749523699 => 27,
126156105725 => 15,
127407367039 => 15,
132065123922 => 32,
138027244724 => 33,
138027246193 => 34,
138027693348 => 35,
138027764236 => 10,
138050882693 => 15,
138050910670 => 20,
138051316169 => 23,
138051517359 => 36,
138051519318 => 37,
138051522331 => 38,
138051601738 => 39,
138051602831 => 40,
138051773783 => 41,
138051776693 => 46,
138051780489 => 42,
164921358499 => 32,
165216289979 => 49,
165216294371 => 49,
173035895092 => 24
];
return $mapping[$code] ?? 17;
}
private function getPemilikJaminanId(string $code, array &$cache): ?int
{
if (isset($cache[$code])) {
return $cache[$code];
}
$jaminan = PemilikJaminan::where('mig_kd_debitur_seq', $code)->first();
if ($jaminan) {
$cache[$code] = $jaminan->id;
return $jaminan->id;
}
return 1;
}
private function getDebitureId(string $code, array &$cache): ?string
{
if (isset($cache[$code])) {
return $cache[$code];
}
$debiture = Debiture::where('mig_kd_debitur_seq', $code)->first();
if ($debiture) {
$cache[$code] = $debiture->name;
return $debiture->name;
}
return null;
}
private function getProvinceCode(string $name, array &$cache): ?string
{
$normalizedName = strtolower($name);
if (isset($cache[$normalizedName])) {
return $cache[$normalizedName];
}
$province = Province::whereRaw('LOWER(name) = ?', [strtolower($name)])->first();
if ($province) {
$cache[$normalizedName] = $province->code;
return $province->code;
}
return null;
}
private function getCityCode(string $name, array &$cache): ?string
{
$normalizedName = strtolower($name);
if (isset($cache[$normalizedName])) {
return $cache[$normalizedName];
}
$city = City::whereRaw('LOWER(name) = ?', [strtolower($name)])->first();
if ($city) {
$cache[$normalizedName] = $city->code;
return $city->code;
}
return null;
}
private function getDistrictCode(string $name, array &$cache): ?string
{
$normalizedName = strtolower($name);
if (isset($cache[$normalizedName])) {
return $cache[$normalizedName];
}
$district = District::whereRaw('LOWER(name) = ?', [strtolower($name)])->first();
if ($district) {
$cache[$normalizedName] = $district->code;
return $district->code;
}
return null;
}
private function getSubdistrictCode(string $name, array &$cache, array &$districtCache): ?array
{
$normalizedName = strtolower($name);
// Pastikan cache menyimpan array, bukan hanya kode
if (isset($cache[$normalizedName])) {
return $cache[$normalizedName];
}
// Ambil subdistrict dari database
$subdistrict = Village::whereRaw('LOWER(name) = ?', [$normalizedName])->first();
// Jika ditemukan, simpan ke dalam cache sebagai array lengkap
if ($subdistrict) {
$cache[$normalizedName] = [
'code' => $subdistrict->code,
'postal_code' => $subdistrict->postal_code
];
return $cache[$normalizedName];
}
// Jika tidak ditemukan, kembalikan null
return [
'code' => null,
'postal_code' => null
];
}
private function getHubunganPemilikJaminanId(string $code, array &$cache): ?int
{
if (isset($cache[$code])) {
return $cache[$code];
}
$jaminan = HubunganPemilikJaminan::whereRaw('LOWER(name) = ?', [strtolower($code)])->first();
if ($jaminan) {
$cache[$code] = $jaminan->id;
return $jaminan->id;
}
return null;
}
/**
* Mengonversi nilai TIMESTAMP menjadi format datetime yang valid.
*/
private function parseTimestamp(?string $timestamp): ?string
{
try {
if ($timestamp) {
// Cek jika format hanya tanggal (Y-m-d)
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d', $timestamp)
->startOfDay()
->toDateTimeString();
}
// Format lengkap (Y-m-d H:i:s)
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $timestamp)->toDateTimeString();
}
// Format d/m/Y H:i:s (contoh: 28/4/2017 14:43:43)
if (preg_match('/^\d{1,2}\/\d{1,2}\/\d{4} \d{2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('d/m/Y H:i:s', $timestamp)->toDateTimeString();
}
}
return null;
} catch (\Exception $e) {
Log::error('Gagal memparsing timestamp: ' . $timestamp . '. Pesan: ' . $e->getMessage());
return null;
}
}
private function initializeErrorLog()
{
$file = $this->errorLogFile;
if (file_exists($file)) {
unlink($file); // Hapus file lama
}
$handle = fopen($file, 'w');
fputcsv($handle, ['mig_kd_debitur_seq', 'Error']);
fclose($handle);
}
private function logError(string $kode, string $message)
{
Log::error("Error migrasi dokumen jaminan [$kode]: $message");
$handle = fopen($this->errorLogFile, 'a');
fputcsv($handle, [$kode, $message]);
fclose($handle);
}
}

View File

@@ -0,0 +1,297 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\Lpj\Models\Inspeksi;
use Modules\Basicdata\Models\Branch;
use Modules\Lpj\Models\DokumenJaminan;
use Modules\Lpj\Models\Permohonan;
use DateTime;
class MigrationGambarInspeksiSeeder extends Seeder
{
/**
* Run the database seeds.
*/
protected $errorLogFile = __DIR__ . '/csv/inspeksi/foto_20251014-error.csv';
/**
* Migrasi data gambar inspeksi dari file csv ke tabel inspeksi_gambar
*
* @return void
*/
public function run()
{
$this->initializeErrorLog();
// Path ke file csv
$filePath = realpath(__DIR__ . '/csv/inspeksi/foto_20251014.csv');
if (!$filePath) {
Log::error('File csv tidak ditemukan: ' . __DIR__ . '/csv/inspeksi/foto_20251014.csv');
$this->command->error('File csv tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, ',');
$rows = [];
$batchSize = 500;
$userData = [];
$branchCache = []; // <-- Gunakan sebagai cache
$totalData = 0;
while (($data = fgetcsv($handle, 0, ',')) !== false) {
$totalData++;
}
rewind($handle);
fgetcsv($handle, 0, ','); // Skip header
$batchCount = 0;
$currentRow = 0;
$errorCount = 0;
$errorDebitureIds = [];
while (($data = fgetcsv($handle, 0, ',')) !== false) {
if (count($data) != count($header)) {
Log::warning('Baris CSV memiliki jumlah kolom yang tidak sesuai: ' . json_encode($data));
continue;
}
$rows[] = array_combine($header, $data);
$currentRow++;
if (count($rows) >= $batchSize) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $userData, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
$rows = [];
}
}
// info_harga_laporan_202505021522.csv
// print_r($rows[0]);
if (!empty($rows)) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $userData, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
}
fclose($handle);
$this->command->info("Data debiture berhasil dimigrasikan. Total data: {$totalData}, Total batch: {$batchCount}");
}
private function processBatch(array $rows, array &$branchCache, array &$userData, int &$errorCount, array &$errorDebitureIds, int $totalData, int $batchCount, int $currentRow)
{
// Kelompokkan berdasarkan mig_nomor_jaminan
$groupedRows = $this->groupRowsByJaminan($rows);
foreach ($groupedRows as $nomorJaminan => $groupRows) {
try {
// Ambil informasi permohonan dan dokument_id
$nomorRegis = $this->getNomorRegistrasiPermohonan($nomorJaminan, $branchCache);
if (!$nomorRegis) {
Log::warning("Nomor registrasi tidak ditemukan untuk nomor jaminan: {$nomorJaminan}");
$errorCount++;
$errorDebitureIds[] = $nomorJaminan;
continue;
}
// Bangun JSON foto_form
$fotoJson = $this->checkFoto($groupRows);
Inspeksi::updateOrCreate(
[
'permohonan_id' => $nomorRegis['id'],
'dokument_id' => $nomorRegis['dokument_id']
],
[
'foto_form' => $fotoJson,
'updated_at' => now()
]
);
$this->command->info("Berhasil update foto_form untuk nomor jaminan: {$nomorJaminan}");
} catch (\Exception $e) {
Log::error("Error pada nomor jaminan {$nomorJaminan}: " . $e->getMessage());
$errorCount++;
$errorDebitureIds[] = $nomorJaminan;
continue;
}
}
}
private function groupRowsByJaminan(array $rows): array
{
$grouped = [];
foreach ($rows as $row) {
$nomorJaminan = $row['mig_nomor_jaminan'] ?? null;
if (!empty($nomorJaminan)) {
$grouped[$nomorJaminan][] = $row;
}
}
return $grouped;
}
private function checkFoto(array $rows)
{
// Inisialisasi kelompok untuk tiap kategori
$kategoriPrioritas = [
'PETA LOKASI' => [],
'GAMBAR SITUASI / SURAT UKUR' => [],
'FOTO JAMINAN' => [],
'MAK' => [],
'lainnya' => []
];
foreach ($rows as $row) {
// Ambil kolom penting
$namaFoto = trim($row['mig_nama_gambar'] ?? '');
$pathFoto = trim($row['mig_url_gambar'] ?? '');
$kategori = trim($row['mig_kategori'] ?? 'lainnya');
$urutan = (int)($row['mig_urutan_gambar'] ?? 999);
$tgl = trim($row['mig_tgl'] ?? '');
$nomorLpj = trim($row['mig_nomor_laporan'] ?? '');
if (empty($pathFoto) || empty($tgl)) {
continue; // Lewati jika tidak lengkap
}
try {
$tanggal = \Carbon\Carbon::createFromFormat('d/m/Y H:i:s', $tgl)->startOfDay();
if (!$tanggal) {
throw new \Exception("Tanggal tidak valid");
}
} catch (\Exception $e) {
continue; // Lewati jika tanggal tidak valid
}
$tahun = $tanggal->format('Y');
$bulanAngka = $tanggal->format('n');
$bulanNama = [
1 => 'JANUARI', 2 => 'FEBRUARI', 3 => 'MARET',
4 => 'APRIL', 5 => 'MEI', 6 => 'JUNI',
7 => 'JULI', 8 => 'AGUSTUS', 9 => 'SEPTEMBER',
10 => 'OKTOBER', 11 => 'NOVEMBER', 12 => 'DESEMBER'
][(int)$bulanAngka] ?? 'UNKNOWN';
$tanggalFormat = $tanggal->format('dmY');
if (empty($namaFoto)) {
$namaFoto = basename($pathFoto) ?: 'image.jpg';
}
// Gunakan '/' sebagai separator path
$finalPath = "surveyor/{$tahun}/{$bulanNama}/{$tanggalFormat}/{$nomorLpj}/{$pathFoto}";
$fotoItem = [
'urutan' => $urutan,
'name' => $namaFoto,
'path' => $finalPath,
'category' => $kategori,
'sub' => null,
'description' => '',
'created_by' => null,
'created_at' => null
];
// Masukkan ke dalam kelompok kategori
if (isset($kategoriPrioritas[$kategori])) {
$kategoriPrioritas[$kategori][] = $fotoItem;
} else {
$kategoriPrioritas['lainnya'][] = $fotoItem;
}
}
// Urutkan masing-masing kategori berdasarkan urutan
foreach ($kategoriPrioritas as &$kelompok) {
usort($kelompok, function ($a, $b) {
return $a['urutan'] <=> $b['urutan'];
});
}
// Gabungkan dengan urutan prioritas: PETA LOKASI -> GAMBAR SITUASI -> FOTO JAMINAN -> lainnya
$finalFotos = array_merge(
$kategoriPrioritas['PETA LOKASI'],
$kategoriPrioritas['GAMBAR SITUASI / SURAT UKUR'],
$kategoriPrioritas['FOTO JAMINAN'],
$kategoriPrioritas['lainnya']
);
// Hapus indeks 'urutan'
$finalFotos = array_map(function ($foto) {
unset($foto['urutan']);
return $foto;
}, $finalFotos);
return json_encode([
'upload_foto' => $finalFotos
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
private function getNomorRegistrasiPermohonan($nomor_jaminan_id, array &$cache)
{
// Cek apakah sudah ada di cache
if (isset($cache[$nomor_jaminan_id])) {
return $cache[$nomor_jaminan_id];
}
// Cari di tabel Permohonan berdasarkan nomor registrasi
$permohonan = Permohonan::where('nomor_registrasi', $nomor_jaminan_id)->first();
if (!$permohonan) {
// Tidak ditemukan
$cache[$nomor_jaminan_id] = null;
return null;
}
// Cari dokument jaminan terkait
$dokumenJaminan = DokumenJaminan::where('permohonan_id', $permohonan->id)->first();
// Simpan hasil ke cache
$result = [
'id' => $permohonan->id,
'dokument_id' => $dokumenJaminan ? $dokumenJaminan->id : null
];
$cache[$nomor_jaminan_id] = $result;
return $result;
}
private function initializeErrorLog()
{
$file = $this->errorLogFile;
if (file_exists($file)) {
unlink($file); // Hapus file lama
}
$handle = fopen($file, 'w');
fputcsv($handle, ['mig_kd_debitur_seq', 'Error']);
fclose($handle);
}
private function logError(string $kode, string $message)
{
Log::error("Error migrasi dokumen jaminan [$kode]: $message");
$handle = fopen($this->errorLogFile, 'a');
fputcsv($handle, [$kode, $message]);
fclose($handle);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
class MigrationLaporanSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// $this->call([]);
}
}

View File

@@ -0,0 +1,225 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\Lpj\Models\Inspeksi;
use Modules\Basicdata\Models\Branch;
use Modules\Lpj\Models\DokumenJaminan;
use Modules\Lpj\Models\Permohonan;
class MigrationPembandingSeeder extends Seeder
{
protected $errorLogFile = __DIR__ . '/csv/inspeksi/pembanding_20251016_error.csv';
/**
* Run the database seeds.
*/
public function run()
{
// Path ke file csv
$filePath = realpath(__DIR__ . '/csv/inspeksi/pembanding_20251016.csv');
if (!$filePath) {
Log::error('File csv tidak ditemukan: ' . __DIR__ . '/csv/inspeksi/pembanding_20251016.csv');
$this->command->error('File csv tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, ',');
$rows = [];
$batchSize = 300;
$userData = [];
$branchCache = []; // <-- Gunakan sebagai cache
$totalData = 0;
while (($data = fgetcsv($handle, 0, ',')) !== false) {
$totalData++;
}
rewind($handle);
fgetcsv($handle, 0, ','); // Skip header
$batchCount = 0;
$currentRow = 0;
$errorCount = 0;
$errorDebitureIds = [];
while (($data = fgetcsv($handle, 0, ',')) !== false) {
if (count($data) != count($header)) {
Log::warning('Baris CSV memiliki jumlah kolom yang tidak sesuai: ' . json_encode($data));
continue;
}
$rows[] = array_combine($header, $data);
$currentRow++;
if (count($rows) >= $batchSize) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $userData, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
$rows = [];
}
}
// info_harga_laporan_202505021522.csv
// print_r($rows[0]);
if (!empty($rows)) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $userData, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
}
fclose($handle);
$this->command->info("Data debiture berhasil dimigrasikan. Total data: {$totalData}, Total batch: {$batchCount}");
}
private function processBatch(array $rows, array &$branchCache, array &$userData, int &$errorCount, array &$errorDebitureIds, int $totalData, int $batchCount, int $currentRow)
{
// Kelompokkan berdasarkan mig_nomor_lpj
$groupedRows = $this->groupRowsByJaminan($rows);
foreach ($groupedRows as $nomorLpj => $groupRows) {
try {
// Dapatkan info permohonan untuk update inspeksi
$nomorRegis = $this->getNomorRegistrasiPermohonan($nomorLpj, $branchCache);
if (!$nomorRegis) {
Log::warning("Nomor registrasi tidak ditemukan untuk nomor LPJ: {$nomorLpj}");
$errorCount++;
$errorDebitureIds[] = $nomorLpj;
continue;
}
// Bangun JSON data pembanding
$pembandingJson = $this->checkPembanding($groupRows);
// Update ke tabel inspeksi
// print_r($pembandingJson);
$inspeksi = Inspeksi::where('permohonan_id', $nomorRegis['id'])
->where('dokument_id', $nomorRegis['dokument_id'])
->update([
'data_pembanding' => $pembandingJson,
'updated_at' => now()
]);
$this->command->info("Berhasil update data_pembanding untuk nomor LPJ: {$nomorLpj}");
} catch (\Exception $e) {
Log::error("Error pada nomor LPJ {$nomorLpj}: " . $e->getMessage());
$errorCount++;
$errorDebitureIds[] = $nomorLpj;
continue;
}
}
}
private function groupRowsByJaminan(array $rows): array
{
$grouped = [];
foreach ($rows as $row) {
$nomorJaminan = $row['mig_nomor_lpj'] ?? null;
if (!empty($nomorJaminan)) {
$grouped[$nomorJaminan][] = $row;
}
}
if($row['mig_nomor_lpj']=='251936'){
dd($grouped);
}
return $grouped;
}
private function checkPembanding(array $rows)
{
$pembandingList = [];
foreach ($rows as $row) {
// Pastikan kolom penting tersedia
$urutan = (int)($row['mig_urutan'] ?? 999);
$pembanding = trim($row['mig_pembanding'] ?? '');
// CUKUP VALIDASI PEMBANDING SAJA
if (empty($pembanding)) {
continue;
}
$pembandingList[] = [
'urutan' => $urutan,
'keterangan' => $pembanding,
];
}
// Urutkan berdasarkan urutan
usort($pembandingList, function ($a, $b) {
return $a['urutan'] <=> $b['urutan'];
});
// Hapus indeks 'urutan'
$finalPembanding = array_map(function ($item) {
unset($item['urutan']);
return $item;
}, $pembandingList);
return json_encode([
'data_pembanding' => $finalPembanding
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
private function getNomorRegistrasiPermohonan($nomor_jaminan_id, array &$cache)
{
if (isset($cache[$nomor_jaminan_id])) {
return $cache[$nomor_jaminan_id];
}
$permohonan = Permohonan::where('nomor_lpj', $nomor_jaminan_id)->first();
if (!$permohonan) {
$cache[$nomor_jaminan_id] = null;
return null;
}
$dokumenJaminan = DokumenJaminan::where('permohonan_id', $permohonan->id)->first();
$result = [
'id' => $permohonan->id,
'dokument_id' => $dokumenJaminan ? $dokumenJaminan->id : null
];
$cache[$nomor_jaminan_id] = $result;
return $result;
}
private function initializeErrorLog()
{
$file = $this->errorLogFile;
// Hapus file lama jika ada
if (file_exists($file)) {
unlink($file);
}
// Buat file baru dengan header
$handle = fopen($file, 'w');
fputcsv($handle, ['mig_mst_jaminan_nomor_jaminan', 'Error']);
fclose($handle);
}
private function logError($nomorJaminan, string $message)
{
Log::error("Error migrasi permohonan [$nomorJaminan]: $message");
// Tulis ke file error
$handle = fopen($this->errorLogFile, 'a');
fputcsv($handle, [$nomorJaminan, $message]);
}
}

View File

@@ -0,0 +1,282 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Modules\Lpj\Models\HubunganPemilikJaminan;
use Modules\Lpj\Models\Debiture;
use Modules\Lpj\Models\PemilikJaminan;
use Modules\Basicdata\Models\Branch;
use Modules\Usermanagement\Models\User;
use Illuminate\Support\Facades\Log;
use Modules\Location\Models\Province;
use Modules\Location\Models\City;
use Modules\Location\Models\District;
use Modules\Location\Models\Village;
class MigrationPemilikJaminanSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
$filePath = realpath(__DIR__ . '/csv/pemilik_20251002.csv');
if (!$filePath) {
Log::error('File CSV tidak ditemukan: ' . __DIR__ . '/csv/pemilik_20251002.csv');
$this->command->error('File CSV tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, ',');
$rows = [];
$batchSize = 100; // Ukuran batch
$branchCache = [];
$debitureCache = [];
$hubunganPemilikCache = [];
$provinceCache = [];
$cityCache = [];
$districtCache = [];
$subdistrictCache = [];
while (($data = fgetcsv($handle, 0, ',')) !== false) {
if (count($data) != count($header)) {
Log::warning('Baris CSV memiliki jumlah kolom yang tidak sesuai: ' . json_encode($data));
continue;
}
$rows[] = array_combine($header, $data);
if (count($rows) >= $batchSize) {
$this->processBatch($rows, $branchCache, $debitureCache, $hubunganPemilikCache, $provinceCache, $cityCache, $districtCache, $subdistrictCache);
$rows = [];
}
}
// print_r($rows);
if (!empty($rows)) {
$this->processBatch($rows, $branchCache, $debitureCache, $hubunganPemilikCache, $provinceCache, $cityCache, $districtCache, $subdistrictCache);
}
fclose($handle);
$this->command->info('Data permohonan berhasil dimigrasikan.');
}
private function processBatch(array $rows, array &$branchCache, array &$debitureCache, array &$hubunganPemilikCache, array &$provinceCache, array &$cityCache, array &$districtCache, array &$subdistrictCache)
{
foreach ($rows as $index => $row) {
$debitureId = $this->getDebiturId($row['mig_kd_debitur_seq'], $debitureCache);
if (!$debitureId) {
Log::warning('Debitur tidak ditemukan untuk kode: ' . $row['mig_kd_debitur_seq']);
continue;
}
$hubunganPemilik = $this->getHubunganPemilikId($row['mig_hubungan_pemilik_jaminan'], $hubunganPemilikCache);
$proviceCode = $this->getProvinceCode($row['mig_province_name'], $provinceCache);
$cityCode = $this->getCityCode($row['mig_city_name'], $cityCache);
$districtCode = $this->getDistrictCode($row['mig_district_name'], $districtCache);
$subdistrictCode = $this->getSubdistrictCode($row['mig_village_name'], $subdistrictCache, $districtCache);
PemilikJaminan::updateOrCreate([
'mig_kd_debitur_seq' => $row['mig_kd_debitur_seq'],
],[
'debiture_id' => $debitureId,
'hubungan_pemilik_jaminan_id' => $hubunganPemilik,
'name' => $row['name'],
'detail_sertifikat' => null,
'npwp' => null,
'nomor_id' => null,
'email' => null,
'phone' => null,
'province_code' => $proviceCode,
'city_code' => $cityCode,
'district_code' => $districtCode,
'village_code' => $subdistrictCode['code'],
'postal_code' => $subdistrictCode['postal_code'],
'address' => $row['address'],
'created_at' => $this->parseTimestamp($row['created_at']),
'updated_at' => $this->parseTimestamp($row['updated_at']),
'mig_kd_debitur_seq' => $row['mig_kd_debitur_seq'],
'processed_at' => now(),
'is_mig' => 1
]);
$this->command->info('Proses data Pemilik Jaminan ' . $row['name'] . ' (' . ($index + 1) . '/' . count($rows) . ')');
}
}
private function getDebiturId(string $code, array &$cache): ?int
{
if (isset($cache[$code])) {
return $cache[$code];
}
$debitur = Debiture::where('mig_kd_debitur_seq', $code)->first();
if ($debitur) {
$cache[$code] = $debitur->id;
return $debitur->id;
}
return null;
}
private function getHubunganPemilikId(string $name, array &$cache): ?int
{
// Normalisasi nama
$normalizedName = strtolower(trim($name));
// Cek cache untuk menghindari query berulang
if (isset($cache[$normalizedName])) {
return $cache[$normalizedName];
}
// Query database dengan pengamanan tambahan
$hubunganPemilik = HubunganPemilikJaminan::whereRaw('LOWER(name) = ?', [$normalizedName])
->whereNull('deleted_at') // Tambahkan ini jika Anda menggunakan soft deletes
->first();
// Jika data ditemukan, simpan dalam cache
if ($hubunganPemilik) {
$cache[$normalizedName] = $hubunganPemilik->id;
return $hubunganPemilik->id;
}
// Default jika data tidak ditemukan
$cache[$normalizedName] = 1; // Cache nilai default untuk menghindari query berulang
return 1;
}
private function getProvinceCode(string $name, array &$cache): ?string
{
$normalizedName = strtolower($name);
if (isset($cache[$normalizedName])) {
return $cache[$normalizedName];
}
$province = Province::whereRaw('LOWER(name) = ?', [strtolower($name)])->first();
if ($province) {
$cache[$normalizedName] = $province->code;
return $province->code;
}
return null;
}
private function getCityCode(string $name, array &$cache): ?string
{
$normalizedName = strtolower($name);
if (isset($cache[$normalizedName])) {
return $cache[$normalizedName];
}
$city = City::whereRaw('LOWER(name) = ?', [strtolower($name)])->first();
if ($city) {
$cache[$normalizedName] = $city->code;
return $city->code;
}
return null;
}
private function getDistrictCode(string $name, array &$cache): ?string
{
$normalizedName = strtolower($name);
if (isset($cache[$normalizedName])) {
return $cache[$normalizedName];
}
$district = District::whereRaw('LOWER(name) = ?', [strtolower($name)])->first();
if ($district) {
$cache[$normalizedName] = $district->code;
return $district->code;
}
return null;
}
private function getSubdistrictCode(string $name, array &$cache, array &$districtCache): ?array
{
$normalizedName = strtolower($name);
// Pastikan cache menyimpan array, bukan hanya kode
if (isset($cache[$normalizedName])) {
return $cache[$normalizedName];
}
// Ambil subdistrict dari database
$subdistrict = Village::whereRaw('LOWER(name) = ?', [$normalizedName])->first();
// Jika ditemukan, simpan ke dalam cache sebagai array lengkap
if ($subdistrict) {
$cache[$normalizedName] = [
'code' => $subdistrict->code,
'postal_code' => $subdistrict->postal_code
];
return $cache[$normalizedName];
}
// Jika tidak ditemukan, kembalikan null
return [
'code' => null,
'postal_code' => null
];
}
private function parseDate(?string $date): ?string
{
try {
return $date ? \Carbon\Carbon::createFromFormat('Y-m-d', $date)->toDateString() : null;
} catch (\Exception $e) {
Log::error('Gagal memparsing tanggal: ' . $date);
return null;
}
}
private function parseTimestamp(?string $timestamp): ?string
{
try {
if ($timestamp) {
// Cek jika format hanya tanggal (Y-m-d)
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d', $timestamp)
->startOfDay()
->toDateTimeString();
}
// Format lengkap (Y-m-d H:i:s)
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $timestamp)->toDateTimeString();
}
// Format d/m/Y H:i:s (contoh: 28/4/2017 14:43:43)
if (preg_match('/^\d{1,2}\/\d{1,2}\/\d{4} \d{2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('d/m/Y H:i:s', $timestamp)->toDateTimeString();
}
}
return null;
} catch (\Exception $e) {
Log::error('Gagal memparsing timestamp: ' . $timestamp . '. Pesan: ' . $e->getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,505 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Modules\Lpj\Models\DokumenJaminan;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\Penilai;
use Modules\Lpj\Models\Laporan;
use Illuminate\Support\Facades\Log;
class MigrationPenilaiSeeder extends Seeder
{
/**
* Run the database seeds.
*/
protected $errorLogFile = __DIR__ . '/csv/penilaian/penilai.fix.latest_error.csv';
public function run()
{
$this->initializeErrorLog();
// Path ke file csv
$filePath = realpath(__DIR__ . '/csv/penilaian/penilai.fix.latest_20251011.csv');
if (!$filePath) {
Log::error('File csv tidak ditemukan: ' . __DIR__ . '/csv/penilaian/penilai.fix.latest_20251011.csv');
$this->command->error('File csv tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, ',');
$rows = [];
$batchSize = 500;
$userData = [];
$branchCache = []; // <-- Gunakan sebagai cache
$totalData = 0;
while (($data = fgetcsv($handle, 0, ',')) !== false) {
$totalData++;
}
rewind($handle);
fgetcsv($handle, 0, ','); // Skip header
$batchCount = 0;
$currentRow = 0;
$errorCount = 0;
$errorDebitureIds = [];
while (($data = fgetcsv($handle, 0, ',')) !== false) {
if (count($data) != count($header)) {
Log::warning('Baris CSV memiliki jumlah kolom yang tidak sesuai: ' . json_encode($data));
continue;
}
$rows[] = array_combine($header, $data);
$currentRow++;
if (count($rows) >= $batchSize) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $userData, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
$rows = [];
}
}
// info_harga_laporan_202505021522.csv
// print_r($rows);
if (!empty($rows)) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $userData, $errorCount, $errorDebitureIds, $totalData, $batchCount, $currentRow);
}
fclose($handle);
$this->command->info("Data debiture berhasil dimigrasikan. Total data: {$totalData}, Total batch: {$batchCount}");
}
private function processBatch(array $rows, array &$branchCache, array &$userData, int &$errorCount, array &$errorDebitureIds, int $totalData, int $batchCount, int $currentRow)
{
// Kelompokkan berdasarkan mig_nomor_lpj
$groupedRows = $this->groupRowsByLpj($rows);
foreach ($groupedRows as $nomorLpj => $groupRows) {
try {
// Ambil informasi permohonan dan dokument_id
$nomorRegis = $this->getNomorRegistrasiPermohonan($nomorLpj, $branchCache);
if (!$nomorRegis) {
Log::warning("Nomor registrasi tidak ditemukan untuk nomor LPJ: {$nomorLpj}");
$errorCount++;
$errorDebitureIds[] = $nomorLpj;
//continue;
}
// Dapatkan type_penilai (misal: standar, sederhana)
$firstRow = reset($groupRows);
$typePenilai = $this->checkTypePenilai($firstRow['mig_kode_jenis_laporan'] ?? '');
if (!$typePenilai) {
Log::warning("Tidak ada jenis laporan valid untuk nomor LPJ: {$nomorLpj}");
$errorCount++;
$errorDebitureIds[] = $nomorLpj;
//continue;
}
// Bangun JSON type_penilai
$penilaiJson = $this->cekJenisPenilai($groupRows);
// Simpan ke tabel Penilai
// print_r(json_decode($penilaiJson, true));
// Mapping field JSON berdasarkan tipe penilaian
$fillableFieldMap = [
'memo' => 'memo',
'standar' => 'lpj',
'sederhana' => 'lpj',
'call_report' => 'call_report',
'rap' => 'rap',
'resume' => 'resume'
];
$fieldToUpdate = $fillableFieldMap[$typePenilai] ?? null;
if (!$fieldToUpdate) {
Log::warning("Field tidak dikenali untuk tipe: {$typePenilai}");
$errorCount++;
$errorDebitureIds[] = $nomorLpj;
//continue;
}
// NO: 001/241917/LPJ/PJ-2251/VII/24
// 001 => kode cabang
// 241917 => nomor lpj
// LPJ => jenis laporan
// PJ-2251 => nomor registrasi ambil nilai akhirnnya 242251
// VII => bulan
// 24 => tahun
// Generate nomor_laporan
$mig_permohonan = json_decode($nomorRegis['mig_permohonan'],true) ?? '';
$tanggal = $mig_permohonan['mig_mst_lpj_tgl_laporan'] ?? $mig_permohonan['mig_mst_lpj_tgl_oto'] ?? $firstRow['mig_created_at'];
//$tanggal = $firstRow['mig_created_at'];
$nomorD = $nomorRegis['nomor_registrasi'];
$nomorRegistrasi = "PJ-{$nomorD}";
$nomorLaporan = $this->generateNomorLaporan($typePenilai, $nomorLpj, $nomorRegistrasi, $tanggal, $nomorRegis['id']);
// Simpan atau update ke tabel Penilai
$penilaiLP = Penilai::updateOrCreate(
[
'permohonan_id' => $nomorRegis['id'],
'dokument_id' => $nomorRegis['dokument_id']
],
[
'type' => $typePenilai,
$fieldToUpdate => $penilaiJson,
'type_penilai' => $typePenilai,
'created_at' => $this->parseTimestamp($tanggal),
'updated_at' => $this->parseTimestamp($tanggal),
]
);
// Simpan ke tabel Laporan
Laporan::updateOrCreate(
[
'permohonan_id' => $penilaiLP->permohonan_id,
'dokumen_jaminan_id' => $penilaiLP->dokument_id
],
[
'nomor_laporan' => $nomorLaporan,
'created_at' => $this->parseTimestamp($tanggal),
'updated_at' => $this->parseTimestamp($tanggal)
]
);
$this->command->info("Berhasil simpan data penilai untuk nomor LPJ: {$nomorLpj}");
} catch (\Exception $e) {
Log::error("Error pada nomor LPJ {$nomorLpj}: " . $e->getMessage());
$errorCount++;
$errorDebitureIds[] = $nomorLpj;
continue;
}
}
}
private function getNomorRegistrasiPermohonan($nomorLpj, array &$cache)
{
if (isset($cache[$nomorLpj])) {
return $cache[$nomorLpj];
}
$permohonan = Permohonan::where('nomor_lpj', $nomorLpj)->first();
if (!$permohonan) {
$cache[$nomorLpj] = null;
return null;
}
$dokumenJaminan = DokumenJaminan::where('permohonan_id', $permohonan->id)->first();
$result = [
'id' => $permohonan->id,
'dokument_id' => $dokumenJaminan ? $dokumenJaminan->id : null,
'nomor_registrasi' => $permohonan->nomor_registrasi,
'mig_permohonan' => $permohonan->mig_permohonan
];
$cache[$nomorLpj] = $result;
return $result;
}
private function groupRowsByLpj(array $rows): array
{
$grouped = [];
foreach ($rows as $row) {
$nomorLpj = $row['mig_nomor_lpj'] ?? null;
if (!empty($nomorLpj)) {
$grouped[$nomorLpj][] = $row;
}
}
return $grouped;
}
private function checkTypePenilai($type)
{
$data = [
'MAK' => 'memo',
'STD' => 'standar',
'SPL' => 'sederhana',
'RHP' => 'call-report',
'RAP' => 'rap',
'PRG' => 'resume',
];
return $data[$type];
}
private function cekJenisPenilai(array $groupRows)
{
// Urutkan grup berdasarkan mig_urutan_seq
usort($groupRows, function ($a, $b) {
return ($a['mig_urutan_seq'] ?? 999) <=> ($b['mig_urutan_seq'] ?? 999);
});
// Inisialisasi struktur JSON
$penilaiJson = [
'luas_bangunan' => null,
'luas_tanah' => null,
'nilai_tanah_1' => null,
'nilai_tanah_2' => null,
'sarana_pelengkap_penilai' => null,
'nilai_sarana_pelengkap_1' => null,
'nilai_sarana_pelengkap_2' => null,
'total_nilai_pasar_wajar' => null,
'likuidasi' => null,
'likuidasi_nilai_1' => null,
'likuidasi_nilai_2' => null,
'asuransi_luas_bangunan' => null,
'asuransi_nilai_1' => null,
'asuransi_nilai_2' => "0",
'npw_tambahan' => []
];
// Ambil mainRow (urutan pertama)
$mainRow = null;
foreach ($groupRows as $row) {
if (($row['mig_urutan_seq'] ?? '') == 1) {
$mainRow = $row;
break;
}
}
// Jika tidak ada mainRow, ambil baris pertama sebagai fallback
if (!$mainRow && !empty($groupRows[0])) {
$mainRow = $groupRows[0];
}
if ($mainRow) {
$penilaiJson['total_nilai_pasar_wajar'] = $mainRow['mig_nilai_total_nilai_pasar'] ?? null;
$penilaiJson['likuidasi'] = $mainRow['mig_nilai_likudasi'] ?? null;
// Hitung likuidasi nilai 2 jika ada total dan persen likuidasi
$totalPasarWajar = (int)str_replace('.', '', $mainRow['mig_nilai_total_nilai_pasar'] ?? 0);
$persenLikuidasi = (int)($mainRow['mig_nilai_likudasi'] ?? 0);
$penilaiJson['likuidasi_nilai_1'] = $mainRow['mig_nilai_total_nilai_pasar'] ?? null;
$penilaiJson['likuidasi_nilai_2'] = number_format(
$totalPasarWajar * ($persenLikuidasi / 100),
0,
'',
''
);
// Isi data utama hanya untuk urutan 1
$penilaiJson['luas_tanah'] = $mainRow['mig_nilai_satuan'] ?? null;
$penilaiJson['nilai_tanah_1'] = $mainRow['mig_harga_satuan'] ?? null;
$penilaiJson['nilai_tanah_2'] = number_format(
(int)str_replace('.', '', $mainRow['mig_nilai_satuan'] ?? 0) *
(int)str_replace('.', '', $mainRow['mig_harga_satuan'] ?? 0),
0,
'',
''
);
}
// Proses tambahan (urutan > 1)
foreach ($groupRows as $row) {
// Hanya proses jika mig_urutan_seq ada
$urutan = $row['mig_urutan_seq'] ?? '';
if (empty($urutan)) {
continue;
}
// Tambahkan ke npw_tambahan
$penilaiJson['npw_tambahan'][] = [
'name' => $row['mig_keterangan'] ?? 'Luas Bangunan Tambahan',
'luas' => $row['mig_nilai_satuan'] ?? null,
'nilai_1' => $row['mig_harga_satuan'] ?? null,
'nilai_2' => number_format(
(int)str_replace('.', '', $row['mig_nilai_satuan'] ?? 0) *
(int)str_replace('.', '', $row['mig_harga_satuan'] ?? 0),
0,
'',
''
)
];
}
// Kosongkan npw_tambahan jika kosong
if (empty($penilaiJson['npw_tambahan'])) {
$penilaiJson['npw_tambahan'] = [];
}
return json_encode($penilaiJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
private function convertToRoman($month)
{
$roman = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'];
return $month >= 1 && $month <= 12 ? $roman[$month - 1] : '';
}
private function generateNomorLaporan($typePenilai, $nomorLpj, $nomorRegistrasi, $tanggal)
{
// Mapping type_penilai ke singkatan laporan
$laporanMap = [
'memo' => 'MEMO',
'standar' => 'LPJ',
'sederhana' => 'LPJ',
'call_report' => 'CALL',
'rap' => 'RAP',
'resume' => 'RESUME'
];
// Dapatkan tahun dan bulan dari tanggal
$tanggal_ = $this->parseTimestamp($tanggal);
$date = \Carbon\Carbon::parse($tanggal_);
$kodeCabang = '001'; // bisa diambil dari user atau parameter lain jika dinamis
$tahun = substr($date->year, -2); // 2024 → 24
$bulan = $this->convertToRoman($date->month); // 7 → VII
// Format akhir nomor registrasi (PJ-XXX)
$nomorDebiturAkhir = substr($nomorRegistrasi, -4); // PJ-2251 → 2251
return sprintf(
"%s/%s/%s/%s/%s/%s",
$kodeCabang,
$nomorLpj,
$laporanMap[$typePenilai] ?? strtoupper($typePenilai),
"PJ-" . $nomorDebiturAkhir,
$bulan,
$tahun
);
}
private function parseTimestamp(?string $timestamp): ?string
{
if (!$timestamp) {
return null;
}
// Trim whitespace dan normalize
$timestamp = trim($timestamp);
// Log untuk debugging
Log::info('Mencoba parsing timestamp: "' . $timestamp . '"');
// Parsing dengan DateTime native PHP untuk lebih robust
try {
// Pattern untuk format d/m/Y H:i:s
if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})$/', $timestamp, $matches)) {
$day = (int) $matches[1];
$month = (int) $matches[2];
$year = (int) $matches[3];
$hour = (int) $matches[4];
$minute = (int) $matches[5];
$second = (int) $matches[6];
// Validasi nilai
if ($day >= 1 && $day <= 31 && $month >= 1 && $month <= 12 && $year >= 1900 && $year <= 2100 &&
$hour >= 0 && $hour <= 23 && $minute >= 0 && $minute <= 59 && $second >= 0 && $second <= 59) {
// Buat DateTime object langsung
$dateTime = new \DateTime();
$dateTime->setDate($year, $month, $day);
$dateTime->setTime($hour, $minute, $second);
$result = $dateTime->format('Y-m-d H:i:s');
Log::info('Berhasil parsing dengan DateTime: ' . $timestamp . ' -> ' . $result);
return $result;
}
}
// Pattern untuk format d/m/Y tanpa waktu
if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/', $timestamp, $matches)) {
$day = (int) $matches[1];
$month = (int) $matches[2];
$year = (int) $matches[3];
// Validasi nilai
if ($day >= 1 && $day <= 31 && $month >= 1 && $month <= 12 && $year >= 1900 && $year <= 2100) {
// Buat DateTime object langsung
$dateTime = new \DateTime();
$dateTime->setDate($year, $month, $day);
$dateTime->setTime(0, 0, 0);
$result = $dateTime->format('Y-m-d H:i:s');
Log::info('Berhasil parsing tanpa waktu dengan DateTime: ' . $timestamp . ' -> ' . $result);
return $result;
}
}
} catch (\Exception $e) {
Log::error('Gagal parsing dengan DateTime: ' . $timestamp . '. Error: ' . $e->getMessage());
}
// Fallback ke format Carbon standar untuk format lainnya
$formats = [
'Y-m-d H:i:s',
'Y-m-d',
'd-m-Y H:i:s',
'd-m-Y',
'j-n-Y H:i:s',
'j-n-Y',
];
foreach ($formats as $format) {
try {
$carbon = \Carbon\Carbon::createFromFormat($format, $timestamp);
if ($carbon && $carbon->format($format) === $timestamp) {
// Jika format tidak mengandung waktu, set ke awal hari
if (!str_contains($format, 'H:i:s')) {
$carbon = $carbon->startOfDay();
}
Log::info('Berhasil parsing dengan format ' . $format . ': ' . $timestamp . ' -> ' . $carbon->toDateTimeString());
return $carbon->toDateTimeString();
}
} catch (\Exception $e) {
// Lanjut ke format berikutnya
continue;
}
}
Log::error('Tidak dapat memparsing timestamp dengan format apapun: "' . $timestamp . '"');
return null;
}
private function initializeErrorLog()
{
$file = $this->errorLogFile;
if (file_exists($file)) {
unlink($file); // Hapus file lama
}
$handle = fopen($file, 'w');
fputcsv($handle, ['mig_kd_debitur_seq', 'Error']);
fclose($handle);
}
private function logError(string $kode, string $message)
{
Log::error("Error migrasi dokumen jaminan [$kode]: $message");
$handle = fopen($this->errorLogFile, 'a');
fputcsv($handle, [$kode, $message]);
fclose($handle);
}
}

View File

@@ -0,0 +1,414 @@
<?php
namespace Modules\Lpj\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\Lpj\Models\Permohonan;
use Modules\Lpj\Models\Debiture;
use Modules\Basicdata\Models\Branch;
use Modules\Usermanagement\Models\User;
class MigrationPermohonanSeeder extends Seeder
{
protected $errorLogFile = __DIR__ . '/csv/permohonan/permohonan.2024.2025_error.csv';
public function run()
{
// Bersihkan file error sebelum mulai
$this->initializeErrorLog();
$filePath = realpath(__DIR__ . '/csv/permohonan/permohonan.2024.2025.csv');
if (!$filePath) {
Log::error('File CSV tidak ditemukan: ' . __DIR__ . '/csv/permohonan/permohonan.2024.2025.csv');
$this->command->error('File CSV tidak ditemukan.');
return;
}
if (($handle = fopen($filePath, 'r')) === false) {
Log::error('Gagal membuka file CSV: ' . $filePath);
$this->command->error('Gagal membuka file CSV.');
return;
}
$header = fgetcsv($handle, 0, ',');
$rows = [];
$batchSize = 1000;
$branchCache = [];
$debitureCache = [];
$totalData = 0;
$currentRow = 0;
$batchCount = 0;
// Hitung total data
while (fgetcsv($handle, 0, ',') !== false) {
$totalData++;
}
rewind($handle);
fgetcsv($handle, 0, ','); // Lewati header
while (($data = fgetcsv($handle, 0, ',')) !== false) {
if (count($data) != count($header)) {
$nomorJaminan = $data[array_search('mig_mst_jaminan_nomor_jaminan', $header)] ?? '-';
$this->logError($nomorJaminan, count($data).' Jumlah kolom tidak sesuai dengan header. Header: '.count($header));
continue;
}
$rows[] = array_combine($header, $data);
$currentRow++;
// print_r($rows);
if (count($rows) >= $batchSize) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $debitureCache, $totalData, $batchCount);
$rows = [];
}
}
// print_r($rows);
if (!empty($rows)) {
$batchCount++;
$this->command->info("Memproses batch ke-{$batchCount} ({$currentRow}/{$totalData})");
$this->processBatch($rows, $branchCache, $debitureCache, $totalData, $batchCount);
}
fclose($handle);
$this->command->info("Migrasi selesai. Total data diproses: $totalData.");
}
private function processBatch(array $rows, array &$branchCache, array &$debitureCache, int $totalData, int $batchCount)
{
$userData = [];
foreach ($rows as $index => $row) {
try {
$nomorJaminan = $row['mig_mst_jaminan_nomor_jaminan'] ?? '-';
// Cek apakah sudah diproses
$existingRecord = Permohonan::where('nomor_registrasi', $row['mig_mst_jaminan_nomor_jaminan'])->first();
if ($existingRecord && $existingRecord->processed_at) {
$this->command->info("Data sudah diproses: $nomorJaminan");
//continue;
}
// Ambil branch_id
$branchId = $this->getBranchId($row['mig_mst_jaminan_kd_cabang'] ?? null, $branchCache);
// if (!$branchId) {
// $this->logError($branchId, 'Cabang tidak ditemukan');
// continue;
// }
// Ambil Debitur ID
$debitureId = $this->getDebiturId($row['mig_mst_jaminan_kd_debitur_seq'], $debitureCache);
if (!$debitureId) {
$this->logError($nomorJaminan, 'Debitur tidak ditemukan');
continue;
}
// Ambil User IDs
$userId = $this->getUserId($row['mig_mst_jaminan_nama_ao'], $branchCache, true);
// jika external matikan
$approved1Id = $this->getUserId($row['mig_mst_lpj_user_approved_1'], $branchCache, false);
$approved2Id = $this->getUserId($row['mig_mst_lpj_user_approved_2'], $branchCache, false);
$periksaId = $this->getUserId($row['mig_mst_lpj_user_periksa'], $branchCache, false);
// // Ambil user IDs berdasarkan NIK
$userIdUpdate = $this->getUserIdData($row['mig_mst_jaminan_user_create'] ?? null, $userData)['id'];
$userIdOtorisasi = $this->getUserIdData($row['mig_mst_jaminan_user_oto'] ?? null, $userData)['id'];
// jika external matikan
// if (!$userIdUpdate || !$userIdOtorisasi) {
// $this->logError($userIdUpdate, 'Salah satu user tidak ditemukan');
// continue;
// }
// Mapping field user
$mapUser = [
'created_by' => $userIdUpdate,
'updated_by' => $userIdUpdate,
'authorized_by' => $userIdOtorisasi,
];
// jika external matikan
// if (!$userId || !$approved1Id || !$approved2Id) {
// $this->logError($userId, 'Salah satu user tidak ditemukan');
// continue;
// }
// Mapping data
$jenisFasilitas = $this->checkJenisFasilitas($row['mig_mst_jaminan_kd_jenis_fas_seq']);
$tujuanPenilaian = $this->checkTujuanPenilaian($row['mig_mst_jaminan_kd_tujuan_seq']);
$regionId = $this->checkRegion($row['mig_mst_kode_kelompok_region']);
$nomor_lpj = isset($row['mig_mst_lpj_nomor_lpj']) ? $row['mig_mst_lpj_nomor_lpj'] : '';
$nomor_lpj = is_numeric($nomor_lpj) ? (int)$nomor_lpj : 0;
$jenisPenilaian = $row['mig_internal_or_external'] == 1 ? 2 : 1;
// Simpan data
$permohonan = Permohonan::updateOrCreate([
'nomor_registrasi' => $nomorJaminan,
],[
'nomor_registrasi' => $nomorJaminan,
'tanggal_permohonan' => $this->parseDate($row['tanggal_permohonan']),
'user_id' => $userId['id'],
'branch_id' => $branchId,
'tujuan_penilaian_id' => $tujuanPenilaian,
'debiture_id' => $debitureId,
'jenis_fasilitas_kredit_id' => $jenisFasilitas,
'nilai_plafond_id' => 2,
'status' => 'done',
// jika external matikan
'approval_eo' => $approved1Id['id'] ?? 0,
'approval_eo_at' => $this->parseTimestamp($row['mig_mst_lpj_tgl_approved_1']),
'approval_dd' => $approved2Id['id'] ?? 0,
'approval_dd_at' => $this->parseTimestamp($row['mig_mst_lpj_tgl_approved_2']),
'approval_so' => $periksaId['id'] ?? 0,
'approval_so_at' => $this->parseTimestamp($row['mig_mst_lpj_tgl_periksa']),
// end external matikan
'keterangan' => $row['mig_mst_jaminan_catatan'] ?? null,
'status_bayar' => 'sudah_bayar',
'created_at' => $this->parseTimestamp($row['mig_mst_jaminan_tgl_create']),
'updated_at' => $this->parseTimestamp($row['mig_mst_jaminan_tgl_update']),
'mig_kd_debitur_seq' => $row['mig_mst_jaminan_kd_debitur_seq'],
'nomor_lpj' => $nomor_lpj,
'region_id' => $regionId,
'jenis_penilaian_id' => $jenisPenilaian,
'authorized_by' => $mapUser['authorized_by'],
'created_by' => $mapUser['created_by'],
'updated_by' => $mapUser['updated_by'],
'mig_nama_ao' => $row['mig_mst_jaminan_nama_ao'],
'mig_permohonan' => json_encode($row),
'is_mig' => 1
]);
if($permohonan && $nomorJaminan=='253122'){
Log::info($permohonan);
}
$this->command->info("Proses data permohonan $nomorJaminan (" . ($index + 1) . '/' . count($rows) . " pada batch ke-$batchCount)");
} catch (\Exception $e) {
$nomorJaminan = $row['mig_mst_jaminan_nomor_jaminan'] ?? '-';
$this->logError($nomorJaminan, "Error eksepsi: " . $e->getMessage());
continue;
}
}
}
private function getUserIdData(?string $code, array &$cache): array
{
if (!$code) {
return ['id' => null, 'branch_id' => null];
}
if (isset($cache[$code])) {
return $cache[$code];
}
$user = User::where('nik', $code)->first();
if ($user) {
$cache[$code] = ['id' => $user->id, 'branch_id' => $user->branch_id];
return $cache[$code];
}
return ['id' => null, 'branch_id' => null];
}
private function getUserId(string $value, array &$cache, bool $includeBranch = false): ?array
{
if (isset($cache[$value])) {
return $cache[$value];
}
$user = null;
if ($includeBranch) {
$user = User::whereRaw('LOWER(name) = ?', [strtolower($value)])->first();
} else {
$user = User::where('nik', $value)->first();
}
if ($user) {
$result = ['id' => $user->id];
if ($includeBranch) {
$result['branch_id'] = $user->branch_id;
}
$cache[$value] = $result;
return $result;
}
return ['id' => null, 'branch_id' => null];
}
private function getDebiturId(string $code, array &$cache): ?int
{
if (isset($cache[$code])) {
return $cache[$code];
}
$debitur = Debiture::where('mig_kd_debitur_seq', $code)->first();
if ($debitur) {
$cache[$code] = $debitur->id;
return $debitur->id;
}
return null;
}
private function getBranchId(?string $code, array &$cache): ?int
{
if (!$code) {
return null;
}
if (isset($cache[$code])) {
return $cache[$code];
}
$branch = Branch::where('code', $code)->first();
if ($branch) {
$cache[$code] = $branch->id;
return $branch->id;
}
return null;
}
private function checkJenisFasilitas($code): int
{
// Extract the integer part before the decimal
$intCode = (int) $code;
$mapping = [
161337594516 => 1,
161337598118 => 14,
155739382483 => 7,
2 => 9,
153568936592 => 10,
155737674431 => 11,
161337561199 => 12,
1 => 13,
];
return $mapping[$intCode] ?? 0;
}
private function checkTujuanPenilaian($code): int
{
$code = (int) $code;
$mapping = [
1 => 1,
2 => 2,
3 => 9,
4 => 10,
5 => 8,
6 => 3,
];
return $mapping[$code] ?? 1;
}
private function checkRegion($code): int
{
$mapping = [
'01' => 1,
'02' => 2,
'04' => 3,
'07' => 4,
'06' => 5,
];
return $mapping[$code] ?? 1;
}
private function parseDate(?string $date): ?string
{
if (!$date) {
return null;
}
try {
// Coba format d/m/Y terlebih dahulu (contoh: 30/1/2025)
return \Carbon\Carbon::createFromFormat('d/m/Y', $date)->toDateString();
} catch (\Exception $e) {
try {
// Fallback ke format Y-m-d jika gagal
return \Carbon\Carbon::createFromFormat('Y-m-d', $date)->toDateString();
} catch (\Exception $e2) {
// If both formats fail, try to parse with Carbon::parse as last resort
try {
return \Carbon\Carbon::parse($date)->toDateString();
} catch (\Exception $e3) {
return null;
}
}
}
}
private function parseTimestamp(?string $timestamp): ?string
{
try {
if (!$timestamp) {
return null;
}
// Cek format d/m/Y H:i:s (contoh: 29/9/2025 17:20:36)
if (preg_match('/^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('d/m/Y H:i:s', $timestamp)->toDateTimeString();
}
// Cek jika format hanya tanggal (Y-m-d)
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d', $timestamp)
->startOfDay()
->toDateTimeString();
}
// Format lengkap (Y-m-d H:i:s)
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $timestamp)) {
return \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $timestamp)->toDateTimeString();
}
// Fallback: coba parsing dengan createFromFormat tambahan atau gunakan parse
return \Carbon\Carbon::parse($timestamp)->toDateTimeString();
} catch (\Exception $e) {
Log::error('Gagal memparsing timestamp: ' . $timestamp . '. Pesan: ' . $e->getMessage());
return null;
}
}
private function initializeErrorLog()
{
$file = $this->errorLogFile;
// Hapus file lama jika ada
if (file_exists($file)) {
unlink($file);
}
// Buat file baru dengan header
$handle = fopen($file, 'w');
fputcsv($handle, ['mig_mst_jaminan_nomor_jaminan', 'Error']);
fclose($handle);
}
private function logError($nomorJaminan, string $message)
{
Log::error("Error migrasi permohonan [$nomorJaminan]: $message");
// Tulis ke file error
$handle = fopen($this->errorLogFile, 'a');
fputcsv($handle, [$nomorJaminan, $message]);
fclose($handle);
}
}

Some files were not shown because too many files have changed in this diff Show More