From cb36682d9e7f93ba108ed1860b02fd79a190c66c Mon Sep 17 00:00:00 2001 From: SyaharaniSalsabilla Date: Fri, 6 Nov 2026 22:38:45 +0700 Subject: [PATCH] Update Koperasi --- AISTANDAR.md | 2 +- app/Enums/Koperasi/AlasanPinjamanKoperasi.php | 27 + .../Koperasi/BagianOperasionalKoperasi.php | 19 + .../Koperasi/BagianRantaiNilaiKoperasi.php | 19 + .../Koperasi/BentukPembayaranKoperasi.php | 19 + app/Enums/Koperasi/BentukProdukKoperasi.php | 31 + .../Koperasi/EfektivitasSanksiKoperasi.php | 23 + .../Koperasi/FaktorManajemenKoperasi.php | 23 + app/Enums/Koperasi/FasilitasKoperasi.php | 25 + app/Enums/Koperasi/FokusKomoditasKoperasi.php | 9 + .../Koperasi/FormatPembukuanKoperasi.php | 21 + .../Koperasi/FrekuensiPertemuanKoperasi.php | 19 + .../Koperasi/HubunganPemasokKoperasi.php | 40 + app/Enums/Koperasi/JawabanYaTidakKoperasi.php | 17 + app/Enums/Koperasi/JenisKoperasi.php | 12 + app/Enums/Koperasi/KetersediaanKoperasi.php | 17 + .../Koperasi/KomoditasAgregasiKoperasi.php | 25 + app/Enums/Koperasi/LayananKoperasi.php | 21 + .../Koperasi/MekanismePembayaranKoperasi.php | 19 + .../Koperasi/ModelFermentasiKoperasi.php | 21 + app/Enums/Koperasi/PenetapanHargaKoperasi.php | 21 + .../Koperasi/PeranRantaiNilaiKoperasi.php | 27 + .../Koperasi/PerjanjianPetaniKoperasi.php | 19 + app/Enums/Koperasi/PosisiKoperasi.php | 4 +- .../Koperasi/PraktikFermentasiKoperasi.php | 21 + .../Koperasi/PraktikPengeringanKoperasi.php | 19 + app/Enums/Koperasi/PraktikSortasiKoperasi.php | 19 + app/Enums/Koperasi/SkalaOperasiKoperasi.php | 15 + app/Enums/Koperasi/StatusAdArtKoperasi.php | 21 + .../Koperasi/StatusPembukuanKoperasi.php | 17 + app/Enums/Koperasi/SumberPasokanKoperasi.php | 23 + app/Enums/Koperasi/TabKoperasi.php | 42 + .../Koperasi/TingkatKreditMacetKoperasi.php | 23 + .../Koperasi/TujuanPenjualanKoperasi.php | 25 + app/Enums/Koperasi/UrutanKoperasi.php | 23 + .../KelembagaanKoperasiController.php | 31 + .../Koperasi/PengurusKoperasiController.php | 174 +-- .../RantaiNilaiKoperasiController.php | 31 + .../SosialEkonomiKoperasiController.php | 25 + .../Web/Koperasi/SurveyKoperasiController.php | 30 + .../Koperasi/IndexKoperasiTabRequest.php | 11 + .../Koperasi/IndexPengurusKoperasiRequest.php | 35 + .../Requests/Koperasi/KoperasiRequest.php | 44 + .../Requests/Koperasi/KoperasiTabRequest.php | 37 + .../StoreKelembagaanKoperasiRequest.php | 190 +++ .../Koperasi/StorePengurusKoperasiRequest.php | 84 ++ .../StoreRantaiNilaiKoperasiRequest.php | 154 ++ .../StoreSosialEkonomiKoperasiRequest.php | 60 + .../UpdatePengurusKoperasiRequest.php | 5 + .../Koperasi/ValidatesWilayahKoperasi.php | 58 + app/Models/Koperasi/PengurusKoperasi.php | 120 +- .../Koperasi/KoperasiSurveyService.php | 86 ++ .../Koperasi/OperasionalKoperasiService.php | 123 ++ .../Koperasi/PengurusKoperasiService.php | 198 +++ .../Koperasi/RantaiNilaiKoperasiService.php | 71 + .../Koperasi/SosialEkonomiKoperasiService.php | 46 + .../Koperasi/WilayahKoperasiService.php | 96 ++ ..._enumerator_to_pengurus_koperasi_table.php | 43 + public/assets/css/demo.css | 94 +- public/assets/js/koperasi-list.js | 33 + public/assets/js/koperasi-wilayah.js | 49 + public/assets/js/koperasi.js | 119 ++ .../koperasi-form-actions.blade.php | 8 + .../koperasi-notifications.blade.php | 12 + .../koperasi-profile-summary.blade.php | 99 ++ .../components/koperasi-rupiah.blade.php | 6 + .../koperasi-survey-field.blade.php | 61 + .../kelembagaan-aktor/index.blade.php | 369 +---- .../views/koperasi/profil/_form.blade.php | 276 ++-- resources/views/koperasi/profil/add.blade.php | 35 - .../views/koperasi/profil/create.blade.php | 19 + .../views/koperasi/profil/edit.blade.php | 25 +- .../views/koperasi/profil/header.blade.php | 196 +-- .../views/koperasi/profil/index.blade.php | 265 ++-- .../rantai-nilai-akses-pasar/index.blade.php | 368 ++--- .../koperasi/sosial-ekonomi/index.blade.php | 164 +- resources/views/layout/sidebar.blade.php | 2 +- routes/web.php | 16 +- tests/Feature/KoperasiProfileTest.php | 1321 +++++++++++++++++ 79 files changed, 4639 insertions(+), 1378 deletions(-) create mode 100644 app/Enums/Koperasi/AlasanPinjamanKoperasi.php create mode 100644 app/Enums/Koperasi/BagianOperasionalKoperasi.php create mode 100644 app/Enums/Koperasi/BagianRantaiNilaiKoperasi.php create mode 100644 app/Enums/Koperasi/BentukPembayaranKoperasi.php create mode 100644 app/Enums/Koperasi/BentukProdukKoperasi.php create mode 100644 app/Enums/Koperasi/EfektivitasSanksiKoperasi.php create mode 100644 app/Enums/Koperasi/FaktorManajemenKoperasi.php create mode 100644 app/Enums/Koperasi/FasilitasKoperasi.php create mode 100644 app/Enums/Koperasi/FokusKomoditasKoperasi.php create mode 100644 app/Enums/Koperasi/FormatPembukuanKoperasi.php create mode 100644 app/Enums/Koperasi/FrekuensiPertemuanKoperasi.php create mode 100644 app/Enums/Koperasi/HubunganPemasokKoperasi.php create mode 100644 app/Enums/Koperasi/JawabanYaTidakKoperasi.php create mode 100644 app/Enums/Koperasi/JenisKoperasi.php create mode 100644 app/Enums/Koperasi/KetersediaanKoperasi.php create mode 100644 app/Enums/Koperasi/KomoditasAgregasiKoperasi.php create mode 100644 app/Enums/Koperasi/LayananKoperasi.php create mode 100644 app/Enums/Koperasi/MekanismePembayaranKoperasi.php create mode 100644 app/Enums/Koperasi/ModelFermentasiKoperasi.php create mode 100644 app/Enums/Koperasi/PenetapanHargaKoperasi.php create mode 100644 app/Enums/Koperasi/PeranRantaiNilaiKoperasi.php create mode 100644 app/Enums/Koperasi/PerjanjianPetaniKoperasi.php create mode 100644 app/Enums/Koperasi/PraktikFermentasiKoperasi.php create mode 100644 app/Enums/Koperasi/PraktikPengeringanKoperasi.php create mode 100644 app/Enums/Koperasi/PraktikSortasiKoperasi.php create mode 100644 app/Enums/Koperasi/SkalaOperasiKoperasi.php create mode 100644 app/Enums/Koperasi/StatusAdArtKoperasi.php create mode 100644 app/Enums/Koperasi/StatusPembukuanKoperasi.php create mode 100644 app/Enums/Koperasi/SumberPasokanKoperasi.php create mode 100644 app/Enums/Koperasi/TabKoperasi.php create mode 100644 app/Enums/Koperasi/TingkatKreditMacetKoperasi.php create mode 100644 app/Enums/Koperasi/TujuanPenjualanKoperasi.php create mode 100644 app/Enums/Koperasi/UrutanKoperasi.php create mode 100644 app/Http/Controllers/Web/Koperasi/KelembagaanKoperasiController.php create mode 100644 app/Http/Controllers/Web/Koperasi/RantaiNilaiKoperasiController.php create mode 100644 app/Http/Controllers/Web/Koperasi/SosialEkonomiKoperasiController.php create mode 100644 app/Http/Controllers/Web/Koperasi/SurveyKoperasiController.php create mode 100644 app/Http/Requests/Koperasi/IndexKoperasiTabRequest.php create mode 100644 app/Http/Requests/Koperasi/IndexPengurusKoperasiRequest.php create mode 100644 app/Http/Requests/Koperasi/KoperasiRequest.php create mode 100644 app/Http/Requests/Koperasi/KoperasiTabRequest.php create mode 100644 app/Http/Requests/Koperasi/StoreKelembagaanKoperasiRequest.php create mode 100644 app/Http/Requests/Koperasi/StorePengurusKoperasiRequest.php create mode 100644 app/Http/Requests/Koperasi/StoreRantaiNilaiKoperasiRequest.php create mode 100644 app/Http/Requests/Koperasi/StoreSosialEkonomiKoperasiRequest.php create mode 100644 app/Http/Requests/Koperasi/UpdatePengurusKoperasiRequest.php create mode 100644 app/Http/Requests/Koperasi/ValidatesWilayahKoperasi.php create mode 100644 app/Services/Koperasi/KoperasiSurveyService.php create mode 100644 app/Services/Koperasi/OperasionalKoperasiService.php create mode 100644 app/Services/Koperasi/PengurusKoperasiService.php create mode 100644 app/Services/Koperasi/RantaiNilaiKoperasiService.php create mode 100644 app/Services/Koperasi/SosialEkonomiKoperasiService.php create mode 100644 app/Services/Koperasi/WilayahKoperasiService.php create mode 100644 database/migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php create mode 100644 public/assets/js/koperasi-list.js create mode 100644 public/assets/js/koperasi-wilayah.js create mode 100644 public/assets/js/koperasi.js create mode 100644 resources/views/components/koperasi-form-actions.blade.php create mode 100644 resources/views/components/koperasi-notifications.blade.php create mode 100644 resources/views/components/koperasi-profile-summary.blade.php create mode 100644 resources/views/components/koperasi-rupiah.blade.php create mode 100644 resources/views/components/koperasi-survey-field.blade.php delete mode 100644 resources/views/koperasi/profil/add.blade.php create mode 100644 resources/views/koperasi/profil/create.blade.php create mode 100644 tests/Feature/KoperasiProfileTest.php diff --git a/AISTANDAR.md b/AISTANDAR.md index a66e1cd..1d3ae60 100644 --- a/AISTANDAR.md +++ b/AISTANDAR.md @@ -690,7 +690,7 @@ Status: `Belum` / `Proses` / `Selesai` / `Dikecualikan` | Program | Workshop (+ Sesi, Peserta) | B | Belum | Sesi/Peserta sedang dikerjakan | | Program | Kelompok Belajar Tani | B | Belum | | | Bumdes | Profil, Kelembagaan, Survei | B | Belum | | -| Koperasi | Profil, Kelembagaan, Survei | B | Belum | | +| Koperasi | Profil, Operasional, Rantai Nilai, Sosial Ekonomi | C | Proses | Daftar Data Koperasi berfokus pada identitas koperasi, jenis, layanan, dokumen kelembagaan, responden dan enumerator (admin/super admin), dengan ringkasan total/jenis koperasi dan dokumen lengkap, card filter wilayah bertingkat berbasis relasi desa serta jenis/layanan/dokumen/urutan seperti profil lain, Form Request pencarian/pagination dan validasi hierarki wilayah, service query, serta JS terpisah; migration desa_kelurahan_id/created_by diterapkan 6 November 2026 (batch 16); penginput lama dipulihkan hanya dari log created, profil tanpa desa tetap tampil pada daftar kabupaten agar wilayah lama dapat dilengkapi di Profil; Card ringkasan pada seluruh tab memakai komponen Blade dan service, mengikuti tampilan BUMDes/Kelompok Tani, menampilkan identitas, jenis koperasi dan layanan utama, anggota dan pengurus di bawahnya, serta kontak; Profil dan tiap tab memakai Form Request, controller resource, model binding, dan JS terpisah; tab survei memakai enum/casts dan percabangan bersama di tampilan/server, rincian wajib bersyarat, dan pembersihan jawaban tidak aktif; Operasional memiliki box pengelolaan dan box Rantai Nilai dan Pascapanen (termasuk K3), masing-masing menyimpan data sendiri, simpan pengelolaan menuju box berikutnya; D6 dapat diisi semua koperasi setelah profil tersimpan, dengan penyimpanan terpisah per box, simpan saluran menuju box harga, dan tabel harga lengkap empat bentuk produk × tiga musim panen; Sosial Ekonomi membatasi keperluan pinjaman serta ikatan keuangan berdasarkan jenis simpan pinjam pada profil, bentuk pembayaran mengikuti ketersediaan mekanisme, harga pasar mengikuti pembayaran hasil bumi, dan alasan wajib untuk harga di luar pasar; simpan tetap di tab aktif, rupiah memakai pemisah ribuan; URL survei lama tetap didukung; soft delete belum direfaktor | | Konfigurasi | User | B | Belum | Route `users` & `users.verify` dobel | | Konfigurasi | Profil akun | — | Belum | Hanya `edit`/`update` | | Ekosistem | Semua | — | Dikecualikan | Lihat 11.3 | diff --git a/app/Enums/Koperasi/AlasanPinjamanKoperasi.php b/app/Enums/Koperasi/AlasanPinjamanKoperasi.php new file mode 100644 index 0000000..1275be9 --- /dev/null +++ b/app/Enums/Koperasi/AlasanPinjamanKoperasi.php @@ -0,0 +1,27 @@ + 'Keperluan produksi', + self::PENDIDIKAN => 'Pendidikan', + self::KESEHATAN => 'Kesehatan', + self::ACARA_KELUARGA => 'Acara keluarga/adat/hari besar', + self::RENOVASI => 'Renovasi rumah', + self::SEKUNDER => 'Kebutuhan sekunder', + self::LAINNYA => 'Lainnya', + }; + } +} diff --git a/app/Enums/Koperasi/BagianOperasionalKoperasi.php b/app/Enums/Koperasi/BagianOperasionalKoperasi.php new file mode 100644 index 0000000..5a81713 --- /dev/null +++ b/app/Enums/Koperasi/BagianOperasionalKoperasi.php @@ -0,0 +1,19 @@ +value; + } + + public function afterSaveFragment(): string + { + return self::RANTAI_NILAI->fragment(); + } +} diff --git a/app/Enums/Koperasi/BagianRantaiNilaiKoperasi.php b/app/Enums/Koperasi/BagianRantaiNilaiKoperasi.php new file mode 100644 index 0000000..292e824 --- /dev/null +++ b/app/Enums/Koperasi/BagianRantaiNilaiKoperasi.php @@ -0,0 +1,19 @@ +value; + } + + public function afterSaveFragment(): string + { + return self::HARGA->fragment(); + } +} diff --git a/app/Enums/Koperasi/BentukPembayaranKoperasi.php b/app/Enums/Koperasi/BentukPembayaranKoperasi.php new file mode 100644 index 0000000..0226862 --- /dev/null +++ b/app/Enums/Koperasi/BentukPembayaranKoperasi.php @@ -0,0 +1,19 @@ + 'Hasil bumi (hasil panen)', + self::GANTI_JASA => 'Ganti jasa/tenaga harian', + self::LAINNYA => 'Lainnya', + }; + } +} diff --git a/app/Enums/Koperasi/BentukProdukKoperasi.php b/app/Enums/Koperasi/BentukProdukKoperasi.php new file mode 100644 index 0000000..e879b3b --- /dev/null +++ b/app/Enums/Koperasi/BentukProdukKoperasi.php @@ -0,0 +1,31 @@ + 'Buah segar hasil petik', + self::BASAH => 'Biji basah', + self::KERING => 'Biji kering/biji kupas', + self::FERMENTASI => 'Produk terfermentasi', + }; + } + + public function pricePrefix(): string + { + return match ($this) { + self::SEGAR => 'segar', + self::BASAH => 'basah', + self::KERING => 'kering', + self::FERMENTASI => 'fermentasi', + }; + } +} diff --git a/app/Enums/Koperasi/EfektivitasSanksiKoperasi.php b/app/Enums/Koperasi/EfektivitasSanksiKoperasi.php new file mode 100644 index 0000000..1d97aa0 --- /dev/null +++ b/app/Enums/Koperasi/EfektivitasSanksiKoperasi.php @@ -0,0 +1,23 @@ + 'Sangat efektif', + self::EFEKTIF => 'Efektif', + self::BIASA => 'Biasa saja', + self::KURANG => 'Kurang efektif', + self::TIDAK => 'Tidak efektif', + }; + } +} diff --git a/app/Enums/Koperasi/FaktorManajemenKoperasi.php b/app/Enums/Koperasi/FaktorManajemenKoperasi.php new file mode 100644 index 0000000..a888e6a --- /dev/null +++ b/app/Enums/Koperasi/FaktorManajemenKoperasi.php @@ -0,0 +1,23 @@ + 'Rasa saling percaya', + self::KEPEMIMPINAN => 'Kepemimpinan koperasi', + self::ANGGOTA => 'Jumlah anggota', + self::PEMBUKUAN => 'Catatan/pembukuan rapi', + self::LAINNYA => 'Lainnya', + }; + } +} diff --git a/app/Enums/Koperasi/FasilitasKoperasi.php b/app/Enums/Koperasi/FasilitasKoperasi.php new file mode 100644 index 0000000..755eaf2 --- /dev/null +++ b/app/Enums/Koperasi/FasilitasKoperasi.php @@ -0,0 +1,25 @@ + 'Gudang penyimpanan', + self::PENGERING => 'Alat pengering', + self::PEMECAH => 'Mesin pemecah', + self::PENGGILING => 'Mesin penggiling', + self::SORTASI => 'Mesin sortasi', + self::LAINNYA => 'Lainnya', + }; + } +} diff --git a/app/Enums/Koperasi/FokusKomoditasKoperasi.php b/app/Enums/Koperasi/FokusKomoditasKoperasi.php new file mode 100644 index 0000000..5cd6e06 --- /dev/null +++ b/app/Enums/Koperasi/FokusKomoditasKoperasi.php @@ -0,0 +1,9 @@ + 'Manual/buku kas', + self::DIGITAL => 'Digital/file Excel', + self::APLIKASI => 'Aplikasi keuangan khusus', + self::TIDAK_ADA => 'Tidak ada', + }; + } +} diff --git a/app/Enums/Koperasi/FrekuensiPertemuanKoperasi.php b/app/Enums/Koperasi/FrekuensiPertemuanKoperasi.php new file mode 100644 index 0000000..39ab21e --- /dev/null +++ b/app/Enums/Koperasi/FrekuensiPertemuanKoperasi.php @@ -0,0 +1,19 @@ + 'Satu bulan sekali', + self::TAHUNAN => 'Satu tahun sekali', + self::LAINNYA => 'Lainnya', + }; + } +} diff --git a/app/Enums/Koperasi/HubunganPemasokKoperasi.php b/app/Enums/Koperasi/HubunganPemasokKoperasi.php new file mode 100644 index 0000000..45fd32d --- /dev/null +++ b/app/Enums/Koperasi/HubunganPemasokKoperasi.php @@ -0,0 +1,40 @@ + 'Bebas/tidak terikat', + self::LANGGANAN => 'Langganan tetap', + self::HUTANG => 'Berbasis hutang/pinjaman', + self::KONSINYASI => 'Titip jual (konsinyasi)', + self::VOLUME => 'Komitmen volume', + self::SEMI_KONTRAK => 'Semi-kontrak', + self::LAINNYA => 'Lainnya', + }; + } + + public function description(): string + { + return match ($this) { + self::BEBAS => 'Petani bebas menjual kepada koperasi atau pihak lain tanpa ikatan.', + self::LANGGANAN => 'Petani rutin menjual kepada koperasi tanpa perjanjian formal.', + self::HUTANG => 'Petani menjual kepada koperasi karena memiliki pinjaman.', + self::KONSINYASI => 'Petani dibayar setelah hasil panen terjual.', + self::VOLUME => 'Ada kesepakatan volume yang dipenuhi secara berkala.', + self::SEMI_KONTRAK => 'Ada kesepakatan harga, volume, dan waktu penyerahan.', + self::LAINNYA => '', + }; + } +} diff --git a/app/Enums/Koperasi/JawabanYaTidakKoperasi.php b/app/Enums/Koperasi/JawabanYaTidakKoperasi.php new file mode 100644 index 0000000..7c899aa --- /dev/null +++ b/app/Enums/Koperasi/JawabanYaTidakKoperasi.php @@ -0,0 +1,17 @@ + 'Ya', + self::TIDAK => 'Tidak', + }; + } +} diff --git a/app/Enums/Koperasi/JenisKoperasi.php b/app/Enums/Koperasi/JenisKoperasi.php new file mode 100644 index 0000000..149b5f0 --- /dev/null +++ b/app/Enums/Koperasi/JenisKoperasi.php @@ -0,0 +1,12 @@ + 'Ada', + self::TIDAK_ADA => 'Tidak ada', + }; + } +} diff --git a/app/Enums/Koperasi/KomoditasAgregasiKoperasi.php b/app/Enums/Koperasi/KomoditasAgregasiKoperasi.php new file mode 100644 index 0000000..afa9bad --- /dev/null +++ b/app/Enums/Koperasi/KomoditasAgregasiKoperasi.php @@ -0,0 +1,25 @@ + 'Kopi', + self::KAKAO => 'Kakao', + self::VANILI => 'Vanili', + self::KEMIRI => 'Kemiri', + self::LAINNYA => 'Lainnya', + self::TIDAK_TERLIBAT => 'Tidak melakukan agregasi', + }; + } +} diff --git a/app/Enums/Koperasi/LayananKoperasi.php b/app/Enums/Koperasi/LayananKoperasi.php new file mode 100644 index 0000000..3e15e76 --- /dev/null +++ b/app/Enums/Koperasi/LayananKoperasi.php @@ -0,0 +1,21 @@ + 'Simpan pinjam', + self::INPUT_PRODUKSI => 'Pengadaan input produksi', + self::PEMASARAN => 'Pemasaran hasil', + self::LAINNYA => 'Lainnya', + }; + } +} diff --git a/app/Enums/Koperasi/MekanismePembayaranKoperasi.php b/app/Enums/Koperasi/MekanismePembayaranKoperasi.php new file mode 100644 index 0000000..fbf0b41 --- /dev/null +++ b/app/Enums/Koperasi/MekanismePembayaranKoperasi.php @@ -0,0 +1,19 @@ + 'Tunai langsung', + self::TEMPO => 'Tempo/dibayar kemudian', + self::CAMPURAN => 'Campuran, tergantung situasi', + }; + } +} diff --git a/app/Enums/Koperasi/ModelFermentasiKoperasi.php b/app/Enums/Koperasi/ModelFermentasiKoperasi.php new file mode 100644 index 0000000..a71bb05 --- /dev/null +++ b/app/Enums/Koperasi/ModelFermentasiKoperasi.php @@ -0,0 +1,21 @@ + 'Individual', + self::KOLEKTIF => 'Kolektif kelompok', + self::UNIT_DESA => 'Unit pengolah desa', + self::CAMPURAN => 'Campuran', + }; + } +} diff --git a/app/Enums/Koperasi/PenetapanHargaKoperasi.php b/app/Enums/Koperasi/PenetapanHargaKoperasi.php new file mode 100644 index 0000000..087c858 --- /dev/null +++ b/app/Enums/Koperasi/PenetapanHargaKoperasi.php @@ -0,0 +1,21 @@ + 'Mengikuti harga pasar/tengkulak setempat', + self::BIAYA => 'Kalkulasi biaya koperasi', + self::OFFTAKER => 'Mengikuti harga offtaker/mitra', + self::LAINNYA => 'Lainnya', + }; + } +} diff --git a/app/Enums/Koperasi/PeranRantaiNilaiKoperasi.php b/app/Enums/Koperasi/PeranRantaiNilaiKoperasi.php new file mode 100644 index 0000000..9e40b6c --- /dev/null +++ b/app/Enums/Koperasi/PeranRantaiNilaiKoperasi.php @@ -0,0 +1,27 @@ + 'Penyediaan input pertanian (pupuk, bibit, dan lainnya)', + self::AGREGASI => 'Agregasi produk pertanian dan perkebunan', + self::PEMASARAN_MENTAH => 'Pemasaran produk mentah', + self::PENGOLAHAN => 'Pengolahan produk pangan setengah jadi/jadi', + self::PEMASARAN_OLAHAN => 'Pemasaran produk olahan', + self::PENJUALAN_LANGSUNG => 'Penjualan langsung ke konsumen', + self::TIDAK_TERLIBAT => 'Tidak terlibat', + }; + } +} diff --git a/app/Enums/Koperasi/PerjanjianPetaniKoperasi.php b/app/Enums/Koperasi/PerjanjianPetaniKoperasi.php new file mode 100644 index 0000000..bcd1187 --- /dev/null +++ b/app/Enums/Koperasi/PerjanjianPetaniKoperasi.php @@ -0,0 +1,19 @@ + 'Ada, tertulis', + self::LISAN => 'Ada, kesepakatan lisan', + self::TIDAK_ADA => 'Tidak ada perjanjian', + }; + } +} diff --git a/app/Enums/Koperasi/PosisiKoperasi.php b/app/Enums/Koperasi/PosisiKoperasi.php index a1d7dec..342e637 100644 --- a/app/Enums/Koperasi/PosisiKoperasi.php +++ b/app/Enums/Koperasi/PosisiKoperasi.php @@ -2,9 +2,9 @@ namespace App\Enums\Koperasi; -enum PosisiKoperasi: string +enum PosisiKoperasi: string { case PENGURUS = 'Pengurus koperasi'; case ANGGOTA = 'Anggota koperasi'; case PENDAMPING = 'Pendamping koperasi'; -} \ No newline at end of file +} diff --git a/app/Enums/Koperasi/PraktikFermentasiKoperasi.php b/app/Enums/Koperasi/PraktikFermentasiKoperasi.php new file mode 100644 index 0000000..4985890 --- /dev/null +++ b/app/Enums/Koperasi/PraktikFermentasiKoperasi.php @@ -0,0 +1,21 @@ + 'Ya, rutin', + self::PERNAH => 'Pernah mencoba', + self::DIRENCANAKAN => 'Belum pernah, direncanakan', + self::TIDAK_DIRENCANAKAN => 'Belum pernah, tidak direncanakan', + }; + } +} diff --git a/app/Enums/Koperasi/PraktikPengeringanKoperasi.php b/app/Enums/Koperasi/PraktikPengeringanKoperasi.php new file mode 100644 index 0000000..bd3d198 --- /dev/null +++ b/app/Enums/Koperasi/PraktikPengeringanKoperasi.php @@ -0,0 +1,19 @@ + 'Ya, rutin', + self::KADANG => 'Kadang-kadang', + self::TIDAK => 'Tidak pernah', + }; + } +} diff --git a/app/Enums/Koperasi/PraktikSortasiKoperasi.php b/app/Enums/Koperasi/PraktikSortasiKoperasi.php new file mode 100644 index 0000000..4680ae7 --- /dev/null +++ b/app/Enums/Koperasi/PraktikSortasiKoperasi.php @@ -0,0 +1,19 @@ + 'Ya, rutin berstandar', + self::TIDAK_KONSISTEN => 'Ya, tidak konsisten', + self::BELUM => 'Belum pernah', + }; + } +} diff --git a/app/Enums/Koperasi/SkalaOperasiKoperasi.php b/app/Enums/Koperasi/SkalaOperasiKoperasi.php new file mode 100644 index 0000000..3c834e3 --- /dev/null +++ b/app/Enums/Koperasi/SkalaOperasiKoperasi.php @@ -0,0 +1,15 @@ + 'Ada dan lengkap', + self::TIDAK_LENGKAP => 'Ada, belum lengkap', + self::TIDAK_ADA => 'Tidak ada', + self::TIDAK_TAHU => 'Tidak tahu', + }; + } +} diff --git a/app/Enums/Koperasi/StatusPembukuanKoperasi.php b/app/Enums/Koperasi/StatusPembukuanKoperasi.php new file mode 100644 index 0000000..1377f4d --- /dev/null +++ b/app/Enums/Koperasi/StatusPembukuanKoperasi.php @@ -0,0 +1,17 @@ + 'Sudah', + self::BELUM => 'Belum ada', + }; + } +} diff --git a/app/Enums/Koperasi/SumberPasokanKoperasi.php b/app/Enums/Koperasi/SumberPasokanKoperasi.php new file mode 100644 index 0000000..6e27a35 --- /dev/null +++ b/app/Enums/Koperasi/SumberPasokanKoperasi.php @@ -0,0 +1,23 @@ + 'Anggota kelompok tani langsung', + self::NON_ANGGOTA => 'Petani non-anggota', + self::PENGUMPUL => 'Pengumpul/sub-agen desa', + self::CAMPURAN => 'Campuran', + self::LAINNYA => 'Lainnya', + }; + } +} diff --git a/app/Enums/Koperasi/TabKoperasi.php b/app/Enums/Koperasi/TabKoperasi.php new file mode 100644 index 0000000..8d8f54f --- /dev/null +++ b/app/Enums/Koperasi/TabKoperasi.php @@ -0,0 +1,42 @@ + 'kelembagaan-aktor', + self::RANTAI_NILAI => 'rantai-nilai-akses-pasar', + self::SOSIAL_EKONOMI => 'sosial-ekonomi', + }; + } + + public function label(): string + { + return match ($this) { + self::KELEMBAGAAN => 'Operasional', + self::RANTAI_NILAI => 'Rantai Nilai & Akses Pasar', + self::SOSIAL_EKONOMI => 'Sosial Ekonomi', + }; + } + + public function icon(): string + { + return match ($this) { + self::KELEMBAGAAN => 'clipboard-list', + self::RANTAI_NILAI => 'arrows-exchange', + self::SOSIAL_EKONOMI => 'heart-handshake', + }; + } + + public function routePrefix(): string + { + return 'koperasi.pengurus.'.$this->slug(); + } +} diff --git a/app/Enums/Koperasi/TingkatKreditMacetKoperasi.php b/app/Enums/Koperasi/TingkatKreditMacetKoperasi.php new file mode 100644 index 0000000..9fae12b --- /dev/null +++ b/app/Enums/Koperasi/TingkatKreditMacetKoperasi.php @@ -0,0 +1,23 @@ +75%'; + + public function label(): string + { + return match ($this) { + self::DI_BAWAH_SEPULUH => 'Di bawah 10%', + self::SEPULUH_DUA_PULUH_LIMA => '10–25%', + self::DUA_PULUH_LIMA_LIMA_PULUH => '25–50%', + self::LIMA_PULUH_TUJUH_PULUH_LIMA => '50–75%', + self::DI_ATAS_TUJUH_PULUH_LIMA => 'Di atas 75%', + }; + } +} diff --git a/app/Enums/Koperasi/TujuanPenjualanKoperasi.php b/app/Enums/Koperasi/TujuanPenjualanKoperasi.php new file mode 100644 index 0000000..dfc60e0 --- /dev/null +++ b/app/Enums/Koperasi/TujuanPenjualanKoperasi.php @@ -0,0 +1,25 @@ + 'Offtaker/eksportir langsung', + self::PEDAGANG => 'Pedagang besar/pengepul kabupaten', + self::KOPERASI => 'Koperasi atau BUMDes lain', + self::ECERAN => 'Eceran/pasar lokal', + self::BELUM_MENJUAL => 'Belum pernah menjual (disimpan/diproses)', + self::LAINNYA => 'Lainnya', + }; + } +} diff --git a/app/Enums/Koperasi/UrutanKoperasi.php b/app/Enums/Koperasi/UrutanKoperasi.php new file mode 100644 index 0000000..b8c9aa2 --- /dev/null +++ b/app/Enums/Koperasi/UrutanKoperasi.php @@ -0,0 +1,23 @@ + 'Urut Terbaru', + self::NAMA_ASC => 'Nama A-Z', + self::NAMA_DESC => 'Nama Z-A', + self::ANGGOTA_DESC => 'Anggota Terbanyak', + self::ANGGOTA_ASC => 'Anggota Tersedikit', + }; + } +} diff --git a/app/Http/Controllers/Web/Koperasi/KelembagaanKoperasiController.php b/app/Http/Controllers/Web/Koperasi/KelembagaanKoperasiController.php new file mode 100644 index 0000000..b2fb3a9 --- /dev/null +++ b/app/Http/Controllers/Web/Koperasi/KelembagaanKoperasiController.php @@ -0,0 +1,31 @@ +profileViewData($pengurusKoperasi)); + } + + public function store(StoreKelembagaanKoperasiRequest $request, PengurusKoperasi $pengurusKoperasi, PengurusKoperasiService $service) + { + $data = $request->validated(); + unset($data['section']); + $pengurusKoperasi->update($service->tabData($data)); + + $response = redirect()->route('koperasi.pengurus.kelembagaan-aktor.index', $pengurusKoperasi); + if ($section = $request->section()) { + $response->withFragment($section->afterSaveFragment()); + } + + return $response->with('success', 'Data operasional koperasi berhasil diperbarui.'); + } +} diff --git a/app/Http/Controllers/Web/Koperasi/PengurusKoperasiController.php b/app/Http/Controllers/Web/Koperasi/PengurusKoperasiController.php index 75560a7..c903600 100644 --- a/app/Http/Controllers/Web/Koperasi/PengurusKoperasiController.php +++ b/app/Http/Controllers/Web/Koperasi/PengurusKoperasiController.php @@ -3,175 +3,51 @@ namespace App\Http\Controllers\Web\Koperasi; use App\Http\Controllers\Controller; -use Illuminate\Http\Request; +use App\Http\Requests\Koperasi\IndexPengurusKoperasiRequest; +use App\Http\Requests\Koperasi\StorePengurusKoperasiRequest; +use App\Http\Requests\Koperasi\UpdatePengurusKoperasiRequest; use App\Models\Koperasi\PengurusKoperasi; +use App\Services\Koperasi\PengurusKoperasiService; class PengurusKoperasiController extends Controller { - public function index(Request $request) + public function index(IndexPengurusKoperasiRequest $request, PengurusKoperasiService $service) { - $query = PengurusKoperasi::query(); - - if ($request->filled('search')) { - $search = $request->search; - $query->where('nama_lengkap', 'like', "%{$search}%") - ->orWhere('nama_koperasi', 'like', "%{$search}%"); - } - - $perPage = $request->get('per_page', 10); - $pengurus = $query->latest()->paginate($perPage); - - $totalPengurus = PengurusKoperasi::count(); - $totalKoperasi = PengurusKoperasi::distinct('nama_koperasi')->count('nama_koperasi'); - $totalJabatan = PengurusKoperasi::distinct('posisi_dalam_koperasi')->count('posisi_dalam_koperasi'); - - return view('koperasi.profil.index', compact( - 'pengurus', - 'totalPengurus', - 'totalKoperasi', - 'totalJabatan', - )); - } - - public function create() - { - return view('koperasi.profil.add'); - } - - public function edit($id) - { - $pengurus = PengurusKoperasi::findOrFail($id); - return view('koperasi.profil.edit', compact('pengurus')); + return view('koperasi.profil.index', $service->indexData($request->validated())); } - public function update(Request $request, $id) + public function create(PengurusKoperasiService $service) { - $validatedData = $request->validate([ - 'nama_lengkap' => 'required|string|max:255', - 'posisi_dalam_koperasi' => 'required|string|max:255', - 'tahun_bergabung' => 'nullable|integer', - 'no_hp' => 'nullable|string|max:20', - 'alamat_tinggal' => 'nullable|string', - 'nama_koperasi' => 'required|string|max:255', - 'jenis_koperasi' => 'required|string|max:255', - 'jenis_koperasi_lainnya'=> 'nullable|string|max:255', - 'alamat_koperasi' => 'nullable|string', - 'skala_operasi' => 'required|string|max:255', - 'tahun_berdiri' => 'nullable|integer', - 'jumlah_anggota_awal' => 'nullable|integer', - 'jumlah_anggota_dewasa' => 'nullable|integer', - 'jumlah_anggota_anak' => 'nullable|integer', - 'jumlah_pengurus' => 'nullable|integer', - 'shu_2025' => 'nullable|numeric', - 'shu_2024' => 'nullable|numeric', - 'shu_2023' => 'nullable|numeric', - ]); - - if ($validatedData['jenis_koperasi'] === 'Lainnya' && !empty($validatedData['jenis_koperasi_lainnya'])) { - $validatedData['jenis_koperasi'] = $validatedData['jenis_koperasi_lainnya']; - } - unset($validatedData['jenis_koperasi_lainnya']); - - $pengurus = PengurusKoperasi::findOrFail($id); - $pengurus->update($validatedData); - - return redirect()->route('koperasi.pengurus.index') - ->with('success', 'Profil Koperasi berhasil diperbarui!'); - } - - public function survey(Request $request, $id) - { - $pengurus = PengurusKoperasi::findOrFail($id); - $step = $request->query('step', 1); - - if ($step == 1) { - return view('koperasi.kelembagaan-aktor.index', compact('pengurus')); - } elseif ($step == 2) { - return view('koperasi.rantai-nilai-akses-pasar.index', compact('pengurus')); - } elseif ($step == 3) { - return view('koperasi.sosial-ekonomi.index', compact('pengurus')); - } - - abort(404); + return view('koperasi.profil.create', $service->profileViewData()); } - public function surveyStore(Request $request, $id) + public function edit(PengurusKoperasi $pengurusKoperasi, PengurusKoperasiService $service) { - $pengurus = PengurusKoperasi::findOrFail($id); - $step = $request->query('step', 1); - $data = $request->except(['_token', 'pengurus_koperasi_id']); - - foreach ($data as $key => $value) { - if (is_array($value)) { - $data[$key] = json_encode($value); - } - } - - $pengurus->update($data); - - if ($step == 1) { - if ($request->has('agregasi_komoditas') && str_contains($data['agregasi_komoditas'], 'Tidak terlibat')) { - return redirect()->route('koperasi.pengurus.survey', ['id' => $id, 'step' => 3]) - ->with('success', 'Data Kelembagaan tersimpan! (Melewati Rantai Nilai)'); - } - - return redirect()->route('koperasi.pengurus.survey', ['id' => $id, 'step' => 2]) - ->with('success', 'Data Kelembagaan berhasil disimpan!'); - } - - if ($step == 2) { - return redirect()->route('koperasi.pengurus.survey', ['id' => $id, 'step' => 3]) - ->with('success', 'Data Rantai Nilai berhasil disimpan!'); - } - - if ($step == 3) { - return redirect()->route('koperasi.pengurus.index') - ->with('success', 'Seluruh data survei berhasil diselesaikan dan disimpan!'); - } - - return redirect()->back(); + return view('koperasi.profil.edit', $service->profileViewData($pengurusKoperasi)); } - public function store(Request $request) + public function update(UpdatePengurusKoperasiRequest $request, PengurusKoperasi $pengurusKoperasi, PengurusKoperasiService $service) { - $validatedData = $request->validate([ - 'nama_lengkap' => 'required|string|max:255', - 'posisi_dalam_koperasi' => 'required|string|max:255', - 'tahun_bergabung' => 'nullable|integer', - 'no_hp' => 'nullable|string|max:20', - 'alamat_tinggal' => 'nullable|string', - 'nama_koperasi' => 'required|string|max:255', - 'jenis_koperasi' => 'required|string|max:255', - 'jenis_koperasi_lainnya'=> 'nullable|string|max:255', - 'alamat_koperasi' => 'nullable|string', - 'skala_operasi' => 'required|string|max:255', - 'tahun_berdiri' => 'nullable|integer', - 'jumlah_anggota_awal' => 'nullable|integer', - 'jumlah_anggota_dewasa' => 'nullable|integer', - 'jumlah_anggota_anak' => 'nullable|integer', - 'jumlah_pengurus' => 'nullable|integer', - 'shu_2025' => 'nullable|numeric', - 'shu_2024' => 'nullable|numeric', - 'shu_2023' => 'nullable|numeric', - ]); + $pengurusKoperasi->update($service->profileData($request->validated())); - if ($validatedData['jenis_koperasi'] === 'Lainnya' && !empty($validatedData['jenis_koperasi_lainnya'])) { - $validatedData['jenis_koperasi'] = $validatedData['jenis_koperasi_lainnya']; - } - unset($validatedData['jenis_koperasi_lainnya']); - - $pengurus = PengurusKoperasi::create($validatedData); - - return redirect()->route('koperasi.pengurus.index') - ->with('success', 'Profil Koperasi berhasil ditambahkan! Silakan klik tombol hijau untuk mulai survei.'); + return redirect()->route('koperasi.pengurus.edit', $pengurusKoperasi) + ->with('success', 'Profil koperasi berhasil diperbarui.'); } - + + public function store(StorePengurusKoperasiRequest $request, PengurusKoperasiService $service) + { + $pengurus = $service->createProfile($request->validated(), $request->user()->id); + + return redirect()->route('koperasi.pengurus.edit', $pengurus) + ->with('success', 'Profil koperasi berhasil ditambahkan.'); + } + public function destroy($id) { $pengurus = PengurusKoperasi::findOrFail($id); $pengurus->delete(); return redirect()->route('koperasi.pengurus.index') - ->with('success', 'Data pengurus koperasi berhasil dihapus!'); + ->with('success', 'Data koperasi berhasil dihapus.'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Web/Koperasi/RantaiNilaiKoperasiController.php b/app/Http/Controllers/Web/Koperasi/RantaiNilaiKoperasiController.php new file mode 100644 index 0000000..e80e4be --- /dev/null +++ b/app/Http/Controllers/Web/Koperasi/RantaiNilaiKoperasiController.php @@ -0,0 +1,31 @@ +profileViewData($pengurusKoperasi)); + } + + public function store(StoreRantaiNilaiKoperasiRequest $request, PengurusKoperasi $pengurusKoperasi, PengurusKoperasiService $service) + { + $data = $request->validated(); + unset($data['section']); + $pengurusKoperasi->update($service->tabData($data)); + + $response = redirect()->route('koperasi.pengurus.rantai-nilai-akses-pasar.index', $pengurusKoperasi); + if ($section = $request->section()) { + $response->withFragment($section->afterSaveFragment()); + } + + return $response->with('success', 'Data rantai nilai dan akses pasar koperasi berhasil diperbarui.'); + } +} diff --git a/app/Http/Controllers/Web/Koperasi/SosialEkonomiKoperasiController.php b/app/Http/Controllers/Web/Koperasi/SosialEkonomiKoperasiController.php new file mode 100644 index 0000000..9012913 --- /dev/null +++ b/app/Http/Controllers/Web/Koperasi/SosialEkonomiKoperasiController.php @@ -0,0 +1,25 @@ +profileViewData($pengurusKoperasi)); + } + + public function store(StoreSosialEkonomiKoperasiRequest $request, PengurusKoperasi $pengurusKoperasi, PengurusKoperasiService $service) + { + $pengurusKoperasi->update($service->tabData($request->validated())); + + return redirect()->route('koperasi.pengurus.sosial-ekonomi.index', $pengurusKoperasi) + ->with('success', 'Data sosial ekonomi koperasi berhasil diperbarui.'); + } +} diff --git a/app/Http/Controllers/Web/Koperasi/SurveyKoperasiController.php b/app/Http/Controllers/Web/Koperasi/SurveyKoperasiController.php new file mode 100644 index 0000000..7949f77 --- /dev/null +++ b/app/Http/Controllers/Web/Koperasi/SurveyKoperasiController.php @@ -0,0 +1,30 @@ +input('step', 1)); + + return redirect()->route($tab->routePrefix().'.index', $pengurusKoperasi); + } + + public function store(IndexKoperasiTabRequest $request, PengurusKoperasi $pengurusKoperasi) + { + $controller = match (TabKoperasi::from((int) $request->input('step', 1))) { + TabKoperasi::KELEMBAGAAN => KelembagaanKoperasiController::class, + TabKoperasi::RANTAI_NILAI => RantaiNilaiKoperasiController::class, + TabKoperasi::SOSIAL_EKONOMI => SosialEkonomiKoperasiController::class, + }; + + return app()->call([app($controller), 'store'], ['pengurusKoperasi' => $pengurusKoperasi]); + } +} diff --git a/app/Http/Requests/Koperasi/IndexKoperasiTabRequest.php b/app/Http/Requests/Koperasi/IndexKoperasiTabRequest.php new file mode 100644 index 0000000..ba90406 --- /dev/null +++ b/app/Http/Requests/Koperasi/IndexKoperasiTabRequest.php @@ -0,0 +1,11 @@ + ['nullable', 'integer', 'in:1,2,3']]; + } +} diff --git a/app/Http/Requests/Koperasi/IndexPengurusKoperasiRequest.php b/app/Http/Requests/Koperasi/IndexPengurusKoperasiRequest.php new file mode 100644 index 0000000..16e1b25 --- /dev/null +++ b/app/Http/Requests/Koperasi/IndexPengurusKoperasiRequest.php @@ -0,0 +1,35 @@ + ['nullable', 'string', 'max:255'], + ...$this->wilayahRules(), + 'jenis_koperasi' => ['nullable', 'string', 'max:255'], + 'layanan' => ['nullable', Rule::enum(LayananKoperasi::class)], + 'ad_art' => ['nullable', Rule::enum(StatusAdArtKoperasi::class)], + 'sort_by' => ['nullable', Rule::enum(UrutanKoperasi::class)], + 'per_page' => ['nullable', 'integer', 'in:10,25'], + ]; + } + + public function attributes(): array + { + return [ + ...$this->wilayahAttributes(), + 'search' => 'Pencarian', 'jenis_koperasi' => 'Jenis Koperasi', 'layanan' => 'Layanan Utama', + 'ad_art' => 'Dokumen Kelembagaan', 'sort_by' => 'Urutan Data', 'per_page' => 'Jumlah data per halaman', + ]; + } +} diff --git a/app/Http/Requests/Koperasi/KoperasiRequest.php b/app/Http/Requests/Koperasi/KoperasiRequest.php new file mode 100644 index 0000000..2703034 --- /dev/null +++ b/app/Http/Requests/Koperasi/KoperasiRequest.php @@ -0,0 +1,44 @@ +exists($field)) { + $values[$field] = PengurusKoperasiService::normalizeRupiah($this->input($field)); + } + } + $this->merge($values); + } + + public function messages(): array + { + return [ + 'required' => ':attribute wajib diisi.', + 'exists' => 'Pilihan :attribute tidak ditemukan.', + 'prohibited' => ':attribute belum tersedia.', + 'string' => ':attribute harus berupa teks.', + 'max.string' => ':attribute maksimal :max karakter.', + 'integer' => ':attribute harus berupa bilangan bulat.', + 'numeric' => ':attribute harus berupa angka.', + 'array' => 'Pilihan :attribute tidak valid.', + 'in' => 'Pilihan :attribute tidak valid.', + 'min.numeric' => ':attribute minimal :min.', + 'max.numeric' => ':attribute maksimal :max.', + EnumRule::class => 'Pilihan :attribute tidak valid.', + ]; + } +} diff --git a/app/Http/Requests/Koperasi/KoperasiTabRequest.php b/app/Http/Requests/Koperasi/KoperasiTabRequest.php new file mode 100644 index 0000000..69d0794 --- /dev/null +++ b/app/Http/Requests/Koperasi/KoperasiTabRequest.php @@ -0,0 +1,37 @@ +route('pengurusKoperasi'); + + return $pengurus instanceof PengurusKoperasi && $pengurus->profilLengkap(); + } + + public function attributes(): array + { + $attributes = []; + foreach (array_keys($this->rules()) as $field) { + $attributes[$field] = ucfirst(str_replace(['_', '.*'], [' ', ''], $field)); + } + + return $attributes; + } + + protected function prepareForValidation(): void + { + parent::prepareForValidation(); + + // Checkbox yang dikosongkan harus menghapus pilihan yang sebelumnya tersimpan. + foreach ($this->rules() as $field => $rules) { + if (in_array('array', $rules, true) && ! $this->exists($field)) { + $this->merge([$field => []]); + } + } + } +} diff --git a/app/Http/Requests/Koperasi/StoreKelembagaanKoperasiRequest.php b/app/Http/Requests/Koperasi/StoreKelembagaanKoperasiRequest.php new file mode 100644 index 0000000..2654d90 --- /dev/null +++ b/app/Http/Requests/Koperasi/StoreKelembagaanKoperasiRequest.php @@ -0,0 +1,190 @@ +input('section'); + + return is_string($section) ? BagianOperasionalKoperasi::tryFrom($section) : null; + } + + protected function getRedirectUrl(): string + { + if ($section = $this->section()) { + return route('koperasi.pengurus.kelembagaan-aktor.index', $this->route('pengurusKoperasi')).'#'.$section->fragment(); + } + + return parent::getRedirectUrl(); + } + + protected function prepareForValidation(): void + { + parent::prepareForValidation(); + $policy = app(OperasionalKoperasiService::class); + $data = $policy->normalize($this->all(), $policy->context($this->route('pengurusKoperasi'))); + if ($section = $this->section()) { + $data = array_intersect_key($data, array_flip([...$policy->sectionFields($section), 'section', '_token'])); + } + $this->replace($data); + } + + public function rules(): array + { + $rules = [ + 'section' => ['nullable', Rule::enum(BagianOperasionalKoperasi::class)], + 'ad_art' => ['nullable', Rule::enum(StatusAdArtKoperasi::class)], + 'pelatihan_produksi' => ['nullable', Rule::enum(JawabanYaTidakKoperasi::class)], + 'pelatihan_pascapanen' => ['nullable', Rule::enum(JawabanYaTidakKoperasi::class)], + 'pelatihan_pengolahan' => ['nullable', Rule::enum(JawabanYaTidakKoperasi::class)], + 'pelatihan_keuangan' => ['nullable', Rule::enum(JawabanYaTidakKoperasi::class)], + 'pelatihan_pemasaran' => ['nullable', Rule::enum(JawabanYaTidakKoperasi::class)], + 'status_pembukuan' => ['nullable', Rule::enum(StatusPembukuanKoperasi::class)], + 'format_pembukuan' => ['nullable', Rule::enum(FormatPembukuanKoperasi::class)], + 'pertemuan_rutin' => ['nullable', Rule::enum(FrekuensiPertemuanKoperasi::class)], + 'kasus_kredit_macet' => ['nullable', Rule::enum(TingkatKreditMacetKoperasi::class)], + 'sanksi_kredit_macet' => ['nullable', Rule::enum(KetersediaanKoperasi::class)], + 'efektivitas_sanksi' => ['nullable', Rule::enum(EfektivitasSanksiKoperasi::class)], + 'pengolahan_lanjutan' => ['nullable', Rule::enum(JawabanYaTidakKoperasi::class)], + 'kontrak_offtaker' => ['nullable', Rule::enum(KetersediaanKoperasi::class)], + 'layanan_koperasi' => ['nullable', 'array'], + 'layanan_koperasi.*' => [Rule::enum(LayananKoperasi::class)], + 'faktor_manajemen' => ['nullable', 'array'], + 'faktor_manajemen.*' => [Rule::enum(FaktorManajemenKoperasi::class)], + 'keterlibatan_rantai_nilai' => ['nullable', 'array'], + 'keterlibatan_rantai_nilai.*' => [Rule::enum(PeranRantaiNilaiKoperasi::class)], + 'agregasi_komoditas' => ['nullable', 'array'], + 'agregasi_komoditas.*' => [Rule::enum(KomoditasAgregasiKoperasi::class)], + 'fasilitas_koperasi' => ['nullable', 'array'], + 'fasilitas_koperasi.*' => [Rule::enum(FasilitasKoperasi::class)], + 'layanan_lainnya' => ['nullable', 'string', 'max:255'], + 'pelatihan_lainnya' => ['nullable', 'string', 'max:255'], + 'pertemuan_lainnya' => ['nullable', 'string', 'max:255'], + 'faktor_manajemen_lainnya' => ['nullable', 'string', 'max:255'], + 'agregasi_komoditas_lainnya' => ['nullable', 'string', 'max:255'], + 'fasilitas_lainnya' => ['nullable', 'string', 'max:255'], + 'detail_pengolahan_lanjutan' => ['nullable', 'string', 'max:255'], + 'kontrak_komoditas' => ['nullable', 'string', 'max:255'], + 'cara_penentuan_harga' => ['nullable', 'string'], + 'tahun_ad_art' => ['nullable', 'integer', 'min:1900', 'max:'.now()->year], + 'praktik_sortasi' => ['nullable', Rule::enum(PraktikSortasiKoperasi::class)], + 'standar_sortasi' => ['nullable', 'string', 'max:255'], + 'praktik_pengeringan' => ['nullable', Rule::enum(PraktikPengeringanKoperasi::class)], + 'cara_pengeringan' => ['nullable', 'string', 'max:255'], + 'praktik_fermentasi' => ['nullable', Rule::enum(PraktikFermentasiKoperasi::class)], + 'model_fermentasi' => ['nullable', 'array'], + 'model_fermentasi.*' => [Rule::enum(ModelFermentasiKoperasi::class)], + 'proses_fermentasi' => ['nullable', 'string'], + 'harga_premium_rupiah' => ['nullable', 'integer', 'min:0'], + 'harga_premium_persen' => ['nullable', 'integer', 'min:0', 'max:100'], + ]; + $policy = app(OperasionalKoperasiService::class); + foreach ($policy->requiredFields() as $field) { + $rules[$field][] = Rule::requiredIf(fn () => $policy->active($field, $this->all(), $policy->context($this->route('pengurusKoperasi')))); + } + if ($section = $this->section()) { + $fields = $policy->sectionFields($section); + $rules = array_filter($rules, fn ($key) => $key === 'section' || in_array(explode('.', $key)[0], $fields, true), ARRAY_FILTER_USE_KEY); + } + + return $rules; + } + + public function after(): array + { + return [function (Validator $validator) { + $policy = app(OperasionalKoperasiService::class); + foreach ([ + 'keterlibatan_rantai_nilai' => PeranRantaiNilaiKoperasi::TIDAK_TERLIBAT->value, + 'agregasi_komoditas' => KomoditasAgregasiKoperasi::TIDAK_TERLIBAT->value, + ] as $field => $exclusive) { + $selected = $policy->values($this->input($field)); + if (in_array($exclusive, $selected, true) && count(array_unique($selected)) > 1) { + $validator->errors()->add($field, 'Pilihan tidak terlibat tidak boleh digabungkan dengan pilihan lainnya.'); + } + } + if ($this->input('status_pembukuan') === StatusPembukuanKoperasi::SUDAH->value + && $this->input('format_pembukuan') === FormatPembukuanKoperasi::TIDAK_ADA->value) { + $validator->errors()->add('format_pembukuan', 'Pilih format pembukuan yang digunakan.'); + } + if (filled($this->input('harga_premium_rupiah')) && filled($this->input('harga_premium_persen'))) { + $validator->errors()->add('harga_premium_persen', 'Isi premium harga dalam rupiah atau persen, bukan keduanya.'); + } + }]; + } + + public function attributes(): array + { + return [ + 'section' => 'Bagian form', + 'layanan_lainnya' => 'Layanan Lainnya', + 'pelatihan_lainnya' => 'Pelatihan Lainnya', + 'pertemuan_lainnya' => 'Frekuensi Pertemuan Lainnya', + 'faktor_manajemen_lainnya' => 'Faktor Pengelolaan Lainnya', + 'agregasi_komoditas_lainnya' => 'Komoditas Agregasi Lainnya', + 'fasilitas_lainnya' => 'Fasilitas Lainnya', + 'detail_pengolahan_lanjutan' => 'Jenis Pengolahan Lanjutan', + 'kontrak_komoditas' => 'Komoditas dalam Kontrak', + 'cara_penentuan_harga' => 'Penetapan Harga Beli', + 'tahun_ad_art' => 'Tahun Pembaruan AD/ART', + 'ad_art' => 'Status AD/ART', + 'pelatihan_produksi' => 'Pelatihan Produksi Pertanian', + 'pelatihan_pascapanen' => 'Pelatihan Pascapanen', + 'pelatihan_pengolahan' => 'Pelatihan Pengolahan Produk', + 'pelatihan_keuangan' => 'Pelatihan Keuangan', + 'pelatihan_pemasaran' => 'Pelatihan Pengemasan dan Pemasaran', + 'status_pembukuan' => 'Pembukuan Keuangan', + 'format_pembukuan' => 'Format Pembukuan', + 'pertemuan_rutin' => 'Frekuensi Pertemuan Anggota', + 'kasus_kredit_macet' => 'Tingkat Kredit Macet', + 'sanksi_kredit_macet' => 'Sanksi Kredit Macet', + 'efektivitas_sanksi' => 'Efektivitas Sanksi', + 'pengolahan_lanjutan' => 'Pengolahan Lanjutan', + 'kontrak_offtaker' => 'Kontrak dengan Mitra Pembeli', + 'layanan_koperasi' => 'Layanan Koperasi', + 'faktor_manajemen' => 'Faktor Pengelolaan', + 'keterlibatan_rantai_nilai' => 'Peran dalam Rantai Nilai', + 'agregasi_komoditas' => 'Komoditas Agregasi', + 'fasilitas_koperasi' => 'Fasilitas Pascapanen', + 'layanan_koperasi.*' => 'Layanan Koperasi', + 'faktor_manajemen.*' => 'Faktor Pengelolaan', + 'keterlibatan_rantai_nilai.*' => 'Peran dalam Rantai Nilai', + 'agregasi_komoditas.*' => 'Komoditas Agregasi', + 'fasilitas_koperasi.*' => 'Fasilitas Pascapanen', + 'praktik_sortasi' => 'Praktik Sortasi', + 'standar_sortasi' => 'Standar/Kriteria Sortasi', + 'praktik_pengeringan' => 'Praktik Pengeringan', + 'cara_pengeringan' => 'Cara dan Tempat Pengeringan', + 'praktik_fermentasi' => 'Praktik Fermentasi/Pengepresan', + 'model_fermentasi' => 'Model Fermentasi', + 'model_fermentasi.*' => 'Model Fermentasi', + 'proses_fermentasi' => 'Proses Fermentasi', + 'harga_premium_rupiah' => 'Premium Harga (Rupiah)', + 'harga_premium_persen' => 'Premium Harga (Persen)', + ]; + } +} diff --git a/app/Http/Requests/Koperasi/StorePengurusKoperasiRequest.php b/app/Http/Requests/Koperasi/StorePengurusKoperasiRequest.php new file mode 100644 index 0000000..5c795b0 --- /dev/null +++ b/app/Http/Requests/Koperasi/StorePengurusKoperasiRequest.php @@ -0,0 +1,84 @@ +available() && $this->exists('provinsi_id') && ! $this->exists('desa_kelurahan_id')) { + $this->merge(['desa_kelurahan_id' => null]); + } + } + + public function authorize(): bool + { + return true; + } + + public function rules(): array + { + return [ + ...$this->wilayahRules(true), + 'nama_lengkap' => ['required', 'string', 'max:255'], + 'posisi_dalam_koperasi' => ['required', Rule::enum(PosisiKoperasi::class)], + 'tahun_bergabung' => ['nullable', 'integer'], + 'no_hp' => ['nullable', 'string', 'max:20'], + 'alamat_tinggal' => ['nullable', 'string'], + 'nama_koperasi' => ['required', 'string', 'max:255'], + 'jenis_koperasi' => ['required', Rule::enum(JenisKoperasi::class)], + 'jenis_koperasi_lainnya' => ['nullable', 'string', 'max:255'], + 'alamat_koperasi' => ['nullable', 'string'], + 'sejarah_lembaga' => ['nullable', 'string'], + 'fokus_komoditas' => ['nullable', Rule::enum(FokusKomoditasKoperasi::class)], + 'detail_fokus_komoditas' => ['nullable', 'string', 'max:255'], + 'skala_operasi' => ['required', Rule::enum(SkalaOperasiKoperasi::class)], + 'tahun_berdiri' => ['nullable', 'integer'], + 'jumlah_anggota_awal' => ['nullable', 'integer'], + 'jumlah_anggota_dewasa' => ['nullable', 'integer'], + 'jumlah_anggota_anak' => ['nullable', 'integer'], + 'jumlah_pengurus' => ['nullable', 'integer'], + 'shu_2025' => ['nullable', 'numeric'], + 'shu_2024' => ['nullable', 'numeric'], + 'shu_2023' => ['nullable', 'numeric'], + ]; + } + + public function attributes(): array + { + return [ + ...$this->wilayahAttributes(), + 'nama_lengkap' => 'Nama responden', + 'posisi_dalam_koperasi' => 'Posisi dalam koperasi', + 'tahun_bergabung' => 'Tahun bergabung koperasi', + 'no_hp' => 'Nomor HP', + 'alamat_tinggal' => 'Alamat tinggal', + 'nama_koperasi' => 'Nama koperasi', + 'jenis_koperasi' => 'Jenis koperasi', + 'jenis_koperasi_lainnya' => 'Jenis koperasi lainnya', + 'alamat_koperasi' => 'Alamat koperasi', + 'sejarah_lembaga' => 'Sejarah dan tujuan pendirian', + 'fokus_komoditas' => 'Fokus komoditas pertanian', + 'detail_fokus_komoditas' => 'Komoditas yang dikembangkan', + 'skala_operasi' => 'Skala operasi koperasi', + 'tahun_berdiri' => 'Tahun berdiri', + 'jumlah_anggota_awal' => 'Jumlah anggota awal berdiri', + 'jumlah_anggota_dewasa' => 'Jumlah anggota dewasa', + 'jumlah_anggota_anak' => 'Jumlah anggota anak', + 'jumlah_pengurus' => 'Jumlah pengurus', + 'shu_2025' => 'SHU 2025', + 'shu_2024' => 'SHU 2024', + 'shu_2023' => 'SHU 2023', + ]; + } +} diff --git a/app/Http/Requests/Koperasi/StoreRantaiNilaiKoperasiRequest.php b/app/Http/Requests/Koperasi/StoreRantaiNilaiKoperasiRequest.php new file mode 100644 index 0000000..6d4d694 --- /dev/null +++ b/app/Http/Requests/Koperasi/StoreRantaiNilaiKoperasiRequest.php @@ -0,0 +1,154 @@ +input('section'); + + return is_string($section) ? BagianRantaiNilaiKoperasi::tryFrom($section) : null; + } + + protected function getRedirectUrl(): string + { + if ($section = $this->section()) { + return route('koperasi.pengurus.rantai-nilai-akses-pasar.index', $this->route('pengurusKoperasi')).'#'.$section->fragment(); + } + + return parent::getRedirectUrl(); + } + + protected function prepareForValidation(): void + { + parent::prepareForValidation(); + $policy = app(RantaiNilaiKoperasiService::class); + $data = $policy->normalize($this->all(), []); + if ($section = $this->section()) { + $data = array_intersect_key($data, array_flip([...$policy->sectionFields($section), 'section', '_token'])); + } + $this->replace($data); + } + + public function rules(): array + { + $rules = [ + 'section' => ['nullable', Rule::enum(BagianRantaiNilaiKoperasi::class)], + 'mekanisme_pembayaran' => ['nullable', Rule::enum(MekanismePembayaranKoperasi::class)], + 'status_perjanjian_tertulis' => ['nullable', Rule::enum(PerjanjianPetaniKoperasi::class)], + 'sumber_pasokan' => ['nullable', 'array'], + 'sumber_pasokan.*' => [Rule::enum(SumberPasokanKoperasi::class)], + 'bentuk_pembelian' => ['nullable', 'array'], + 'bentuk_pembelian.*' => [Rule::enum(BentukProdukKoperasi::class)], + 'tujuan_penjualan' => ['nullable', 'array'], + 'tujuan_penjualan.*' => [Rule::enum(TujuanPenjualanKoperasi::class)], + 'bentuk_penjualan' => ['nullable', 'array'], + 'bentuk_penjualan.*' => [Rule::enum(BentukProdukKoperasi::class)], + 'sifat_hubungan_bisnis' => ['nullable', 'array'], + 'sifat_hubungan_bisnis.*' => [Rule::enum(HubunganPemasokKoperasi::class)], + 'cara_menetapkan_harga_pasar' => ['nullable', 'array'], + 'cara_menetapkan_harga_pasar.*' => [Rule::enum(PenetapanHargaKoperasi::class)], + 'sumber_pasokan_lainnya' => ['nullable', 'string', 'max:255'], + 'tujuan_penjualan_lainnya' => ['nullable', 'string', 'max:255'], + 'sifat_hubungan_lainnya' => ['nullable', 'string', 'max:255'], + 'cara_menetapkan_harga_lainnya' => ['nullable', 'string', 'max:255'], + 'volume_beli_sekarang' => ['nullable', 'integer', 'min:0'], + 'volume_jual_sekarang' => ['nullable', 'integer', 'min:0'], + 'harga_segar_sekarang' => ['nullable', 'integer', 'min:0'], + 'harga_basah_sekarang' => ['nullable', 'integer', 'min:0'], + 'harga_kering_sekarang' => ['nullable', 'integer', 'min:0'], + 'harga_fermentasi_sekarang' => ['nullable', 'integer', 'min:0'], + 'volume_beli_1_tahun' => ['nullable', 'integer', 'min:0'], + 'volume_jual_1_tahun' => ['nullable', 'integer', 'min:0'], + 'harga_segar_1_tahun' => ['nullable', 'integer', 'min:0'], + 'harga_basah_1_tahun' => ['nullable', 'integer', 'min:0'], + 'harga_kering_1_tahun' => ['nullable', 'integer', 'min:0'], + 'harga_fermentasi_1_tahun' => ['nullable', 'integer', 'min:0'], + 'volume_beli_2_tahun' => ['nullable', 'integer', 'min:0'], + 'volume_jual_2_tahun' => ['nullable', 'integer', 'min:0'], + 'harga_segar_2_tahun' => ['nullable', 'integer', 'min:0'], + 'harga_basah_2_tahun' => ['nullable', 'integer', 'min:0'], + 'harga_kering_2_tahun' => ['nullable', 'integer', 'min:0'], + 'harga_fermentasi_2_tahun' => ['nullable', 'integer', 'min:0'], + ]; + $policy = app(RantaiNilaiKoperasiService::class); + foreach ($policy->requiredFields() as $field) { + $rules[$field][] = Rule::requiredIf(fn () => $policy->active($field, $this->all(), [])); + } + if ($section = $this->section()) { + $fields = $policy->sectionFields($section); + $rules = array_filter($rules, fn ($key) => $key === 'section' || in_array(explode('.', $key)[0], $fields, true), ARRAY_FILTER_USE_KEY); + } + + return $rules; + } + + public function after(): array + { + return [function (Validator $validator) { + if ($this->section() === BagianRantaiNilaiKoperasi::HARGA) { + return; + } + $selected = app(RantaiNilaiKoperasiService::class)->values($this->input('tujuan_penjualan')); + if (in_array(TujuanPenjualanKoperasi::BELUM_MENJUAL->value, $selected, true) && count(array_unique($selected)) > 1) { + $validator->errors()->add('tujuan_penjualan', 'Pilihan belum pernah menjual tidak boleh digabungkan dengan tujuan penjualan lainnya.'); + } + }]; + } + + public function attributes(): array + { + return [ + 'section' => 'Bagian form', + 'sumber_pasokan' => 'Sumber Pasokan', + 'sumber_pasokan_lainnya' => 'Sumber Pasokan Lainnya', + 'bentuk_pembelian' => 'Bentuk Produk yang Dibeli', + 'tujuan_penjualan' => 'Tujuan Penjualan', + 'tujuan_penjualan_lainnya' => 'Tujuan Penjualan Lainnya', + 'bentuk_penjualan' => 'Bentuk Produk yang Dijual', + 'sifat_hubungan_bisnis' => 'Hubungan dengan Petani Pemasok', + 'sifat_hubungan_lainnya' => 'Hubungan Pemasok Lainnya', + 'mekanisme_pembayaran' => 'Mekanisme Pembayaran kepada Petani', + 'status_perjanjian_tertulis' => 'Perjanjian Sebelum Pembelian', + 'cara_menetapkan_harga_pasar' => 'Penetapan Harga Beli', + 'cara_menetapkan_harga_lainnya' => 'Penetapan Harga Lainnya', + 'sumber_pasokan.*' => 'Sumber Pasokan', + 'bentuk_pembelian.*' => 'Bentuk Produk yang Dibeli', + 'tujuan_penjualan.*' => 'Tujuan Penjualan', + 'bentuk_penjualan.*' => 'Bentuk Produk yang Dijual', + 'sifat_hubungan_bisnis.*' => 'Hubungan dengan Petani Pemasok', + 'cara_menetapkan_harga_pasar.*' => 'Penetapan Harga Beli', + 'volume_beli_sekarang' => 'Volume Pembelian Panen Terakhir', + 'volume_jual_sekarang' => 'Volume Penjualan Panen Terakhir', + 'harga_segar_sekarang' => 'Harga Buah Segar Panen Terakhir', + 'harga_basah_sekarang' => 'Harga Biji Basah Panen Terakhir', + 'harga_kering_sekarang' => 'Harga Biji Kering/Kupas Panen Terakhir', + 'harga_fermentasi_sekarang' => 'Harga Produk Terfermentasi Panen Terakhir', + 'volume_beli_1_tahun' => 'Volume Pembelian Tahun Lalu', + 'volume_jual_1_tahun' => 'Volume Penjualan Tahun Lalu', + 'harga_segar_1_tahun' => 'Harga Buah Segar Tahun Lalu', + 'harga_basah_1_tahun' => 'Harga Biji Basah Tahun Lalu', + 'harga_kering_1_tahun' => 'Harga Biji Kering/Kupas Tahun Lalu', + 'harga_fermentasi_1_tahun' => 'Harga Produk Terfermentasi Tahun Lalu', + 'volume_beli_2_tahun' => 'Volume Pembelian Dua Tahun Lalu', + 'volume_jual_2_tahun' => 'Volume Penjualan Dua Tahun Lalu', + 'harga_segar_2_tahun' => 'Harga Buah Segar Dua Tahun Lalu', + 'harga_basah_2_tahun' => 'Harga Biji Basah Dua Tahun Lalu', + 'harga_kering_2_tahun' => 'Harga Biji Kering/Kupas Dua Tahun Lalu', + 'harga_fermentasi_2_tahun' => 'Harga Produk Terfermentasi Dua Tahun Lalu', + ]; + } +} diff --git a/app/Http/Requests/Koperasi/StoreSosialEkonomiKoperasiRequest.php b/app/Http/Requests/Koperasi/StoreSosialEkonomiKoperasiRequest.php new file mode 100644 index 0000000..fa11a9e --- /dev/null +++ b/app/Http/Requests/Koperasi/StoreSosialEkonomiKoperasiRequest.php @@ -0,0 +1,60 @@ +merge($policy->normalize($this->all(), $policy->context($this->route('pengurusKoperasi')))); + } + + public function rules(): array + { + $rules = [ + 'momen_simpanan_terbesar' => ['nullable', 'string', 'max:255'], + 'alasan_pinjaman_lainnya' => ['nullable', 'string', 'max:255'], + 'pembayaran_selain_uang' => ['nullable', Rule::enum(KetersediaanKoperasi::class)], + 'bentuk_pembayaran_lainnya' => ['nullable', 'string', 'max:255'], + 'perhitungan_harga_pasar' => ['nullable', Rule::enum(JawabanYaTidakKoperasi::class)], + 'alasan_bukan_harga_pasar' => ['nullable', 'string'], + 'alasan_pinjaman' => ['nullable', 'array'], + 'alasan_pinjaman.*' => [Rule::enum(AlasanPinjamanKoperasi::class)], + 'bentuk_pembayaran_barang' => ['nullable', 'array'], + 'bentuk_pembayaran_barang.*' => [Rule::enum(BentukPembayaranKoperasi::class)], + 'simpanan_terbesar' => ['nullable', 'integer', 'min:0'], + ]; + $policy = app(SosialEkonomiKoperasiService::class); + foreach ($policy->requiredFields() as $field) { + $rules[$field][] = Rule::requiredIf(fn () => $policy->active($field, $this->all(), $policy->context($this->route('pengurusKoperasi')))); + } + + return $rules; + } + + public function attributes(): array + { + return [ + 'simpanan_terbesar' => 'Simpanan Wajib Terbesar per Anggota', + 'momen_simpanan_terbesar' => 'Bulan dan Momen Setoran Lebih Tinggi', + 'alasan_pinjaman' => 'Keperluan Utama Pinjaman', + 'alasan_pinjaman.*' => 'Keperluan Utama Pinjaman', + 'alasan_pinjaman_lainnya' => 'Keperluan Pinjaman Lainnya', + 'pembayaran_selain_uang' => 'Pembayaran Pinjaman Selain Uang', + 'bentuk_pembayaran_barang' => 'Bentuk Pembayaran Selain Uang', + 'bentuk_pembayaran_barang.*' => 'Bentuk Pembayaran Selain Uang', + 'bentuk_pembayaran_lainnya' => 'Bentuk Pembayaran Lainnya', + 'perhitungan_harga_pasar' => 'Penilaian Hasil Bumi Sesuai Harga Pasar', + 'alasan_bukan_harga_pasar' => 'Alasan Tidak Menggunakan Harga Pasar', + ]; + } +} diff --git a/app/Http/Requests/Koperasi/UpdatePengurusKoperasiRequest.php b/app/Http/Requests/Koperasi/UpdatePengurusKoperasiRequest.php new file mode 100644 index 0000000..ad79a6a --- /dev/null +++ b/app/Http/Requests/Koperasi/UpdatePengurusKoperasiRequest.php @@ -0,0 +1,5 @@ + 'master_kabupaten_kota', 'kecamatan_id' => 'master_kecamatan', 'desa_kelurahan_id' => 'master_desa_kelurahan']; + if ($profile) { + $tables = ['provinsi_id' => 'master_provinsi', ...$tables]; + } + $available = app(WilayahKoperasiService::class)->available(); + $rules = []; + foreach ($tables as $field => $table) { + $rules[$field] = ['bail', Rule::prohibitedIf(! $available), 'nullable', 'integer', Rule::exists($table, 'id')->whereNull('deleted_at')]; + if ($profile) { + $others = array_diff(array_keys($tables), [$field]); + $rules[$field][] = Rule::requiredIf(fn () => collect($others)->contains(fn ($other) => $this->filled($other))); + } + } + + return $rules; + } + + protected function wilayahAttributes(): array + { + return ['provinsi_id' => 'Provinsi', 'kabupaten_kota_id' => 'Kabupaten/Kota', 'kecamatan_id' => 'Kecamatan', 'desa_kelurahan_id' => 'Desa/Kelurahan']; + } + + public function after(): array + { + return [function (Validator $validator) { + $labels = $this->wilayahAttributes(); + if ($validator->errors()->hasAny(array_keys($labels))) { + return; + } + foreach ([ + ['kabupaten_kota_id', 'provinsi_id', 'master_kabupaten_kota'], + ['kecamatan_id', 'kabupaten_kota_id', 'master_kecamatan'], + ['desa_kelurahan_id', 'kecamatan_id', 'master_desa_kelurahan'], + ] as [$child, $parent, $table]) { + if ($parent === 'provinsi_id' && ! $this instanceof StorePengurusKoperasiRequest) { + continue; + } + if ($this->filled($child) && $this->filled($parent) + && ! DB::table($table)->where('id', $this->input($child))->where($parent, $this->input($parent))->exists()) { + $validator->errors()->add($child, 'Pilihan '.$labels[$child].' tidak sesuai dengan '.$labels[$parent].'.'); + } + } + }]; + } +} diff --git a/app/Models/Koperasi/PengurusKoperasi.php b/app/Models/Koperasi/PengurusKoperasi.php index 7b201da..c89ceba 100644 --- a/app/Models/Koperasi/PengurusKoperasi.php +++ b/app/Models/Koperasi/PengurusKoperasi.php @@ -2,27 +2,60 @@ namespace App\Models\Koperasi; +use App\Enums\Koperasi\AlasanPinjamanKoperasi; +use App\Enums\Koperasi\BentukPembayaranKoperasi; +use App\Enums\Koperasi\BentukProdukKoperasi; +use App\Enums\Koperasi\EfektivitasSanksiKoperasi; +use App\Enums\Koperasi\FaktorManajemenKoperasi; +use App\Enums\Koperasi\FasilitasKoperasi; +use App\Enums\Koperasi\FokusKomoditasKoperasi; +use App\Enums\Koperasi\FormatPembukuanKoperasi; +use App\Enums\Koperasi\FrekuensiPertemuanKoperasi; +use App\Enums\Koperasi\HubunganPemasokKoperasi; +use App\Enums\Koperasi\JawabanYaTidakKoperasi; +use App\Enums\Koperasi\KetersediaanKoperasi; +use App\Enums\Koperasi\KomoditasAgregasiKoperasi; +use App\Enums\Koperasi\LayananKoperasi; +use App\Enums\Koperasi\MekanismePembayaranKoperasi; +use App\Enums\Koperasi\ModelFermentasiKoperasi; +use App\Enums\Koperasi\PenetapanHargaKoperasi; +use App\Enums\Koperasi\PeranRantaiNilaiKoperasi; +use App\Enums\Koperasi\PerjanjianPetaniKoperasi; +use App\Enums\Koperasi\PosisiKoperasi; +use App\Enums\Koperasi\PraktikFermentasiKoperasi; +use App\Enums\Koperasi\PraktikPengeringanKoperasi; +use App\Enums\Koperasi\PraktikSortasiKoperasi; +use App\Enums\Koperasi\SkalaOperasiKoperasi; +use App\Enums\Koperasi\StatusAdArtKoperasi; +use App\Enums\Koperasi\StatusPembukuanKoperasi; +use App\Enums\Koperasi\SumberPasokanKoperasi; +use App\Enums\Koperasi\TingkatKreditMacetKoperasi; +use App\Enums\Koperasi\TujuanPenjualanKoperasi; use App\Models\Concerns\CatatAktivitas; +use App\Models\User; +use App\Models\Wilayah\DesaKelurahan; +use Illuminate\Database\Eloquent\Casts\AsEnumCollection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class PengurusKoperasi extends Model { use CatatAktivitas; protected $table = 'pengurus_koperasi'; - + protected $fillable = [ - // Profil Dassar - 'nama_lengkap', 'posisi_dalam_koperasi', 'tahun_bergabung', - 'no_hp', 'alamat_tinggal', - + // Profil responden + 'nama_lengkap', 'posisi_dalam_koperasi', 'tahun_bergabung', + 'no_hp', 'alamat_tinggal', + // Profil Koperasi - 'nama_koperasi', 'jenis_koperasi', 'alamat_koperasi', + 'nama_koperasi', 'jenis_koperasi', 'alamat_koperasi', 'desa_kelurahan_id', 'created_by', 'skala_operasi', 'tahun_berdiri', - + // Anggota & Pengurus 'jumlah_anggota_awal', 'jumlah_anggota_dewasa', 'jumlah_anggota_anak', 'jumlah_pengurus', - + // SHU 'shu_2025', 'shu_2024', 'shu_2023', @@ -44,14 +77,14 @@ class PengurusKoperasi extends Model 'tujuan_penjualan', 'tujuan_penjualan_lainnya', 'bentuk_penjualan', 'volume_jual_sekarang', 'volume_jual_1_tahun', 'volume_jual_2_tahun', 'sifat_hubungan_bisnis', 'sifat_hubungan_lainnya', 'mekanisme_pembayaran', 'status_perjanjian_tertulis', - + // Harga Komoditas 'harga_segar_sekarang', 'harga_segar_1_tahun', 'harga_segar_2_tahun', 'harga_basah_sekarang', 'harga_basah_1_tahun', 'harga_basah_2_tahun', 'harga_kering_sekarang', 'harga_kering_1_tahun', 'harga_kering_2_tahun', 'harga_fermentasi_sekarang', 'harga_fermentasi_1_tahun', 'harga_fermentasi_2_tahun', - 'cara_menetapkan_harga_pasar', 'cara_menetapkan_harga_lainnya', - + 'cara_menetapkan_harga_pasar', 'cara_menetapkan_harga_lainnya', + // Praktik Kualitas & Pascapanen 'praktik_sortasi', 'standar_sortasi', 'praktik_pengeringan', 'cara_pengeringan', 'praktik_fermentasi', 'model_fermentasi', 'proses_fermentasi', @@ -63,6 +96,67 @@ class PengurusKoperasi extends Model 'bentuk_pembayaran_barang', 'bentuk_pembayaran_lainnya', 'perhitungan_harga_pasar', 'alasan_bukan_harga_pasar', // PENILAIAN ENUMERATOR - 'enum_potensi', 'enum_alasan_potensi', 'enum_kebutuhan', 'enum_kebutuhan_lain', 'enum_keterbukaan' + 'enum_potensi', 'enum_alasan_potensi', 'enum_kebutuhan', 'enum_kebutuhan_lain', 'enum_keterbukaan', ]; -} \ No newline at end of file + + protected function casts(): array + { + return [ + 'alasan_pinjaman' => AsEnumCollection::of(AlasanPinjamanKoperasi::class), + 'bentuk_pembayaran_barang' => AsEnumCollection::of(BentukPembayaranKoperasi::class), + 'pembayaran_selain_uang' => KetersediaanKoperasi::class, + 'perhitungan_harga_pasar' => JawabanYaTidakKoperasi::class, + 'posisi_dalam_koperasi' => PosisiKoperasi::class, + 'skala_operasi' => SkalaOperasiKoperasi::class, + 'fokus_komoditas' => FokusKomoditasKoperasi::class, + 'mekanisme_pembayaran' => MekanismePembayaranKoperasi::class, + 'status_perjanjian_tertulis' => PerjanjianPetaniKoperasi::class, + 'praktik_sortasi' => PraktikSortasiKoperasi::class, + 'praktik_pengeringan' => PraktikPengeringanKoperasi::class, + 'praktik_fermentasi' => PraktikFermentasiKoperasi::class, + 'sumber_pasokan' => AsEnumCollection::of(SumberPasokanKoperasi::class), + 'bentuk_pembelian' => AsEnumCollection::of(BentukProdukKoperasi::class), + 'tujuan_penjualan' => AsEnumCollection::of(TujuanPenjualanKoperasi::class), + 'bentuk_penjualan' => AsEnumCollection::of(BentukProdukKoperasi::class), + 'sifat_hubungan_bisnis' => AsEnumCollection::of(HubunganPemasokKoperasi::class), + 'cara_menetapkan_harga_pasar' => AsEnumCollection::of(PenetapanHargaKoperasi::class), + 'model_fermentasi' => AsEnumCollection::of(ModelFermentasiKoperasi::class), + 'ad_art' => StatusAdArtKoperasi::class, + 'pelatihan_produksi' => JawabanYaTidakKoperasi::class, + 'pelatihan_pascapanen' => JawabanYaTidakKoperasi::class, + 'pelatihan_pengolahan' => JawabanYaTidakKoperasi::class, + 'pelatihan_keuangan' => JawabanYaTidakKoperasi::class, + 'pelatihan_pemasaran' => JawabanYaTidakKoperasi::class, + 'status_pembukuan' => StatusPembukuanKoperasi::class, + 'format_pembukuan' => FormatPembukuanKoperasi::class, + 'pertemuan_rutin' => FrekuensiPertemuanKoperasi::class, + 'kasus_kredit_macet' => TingkatKreditMacetKoperasi::class, + 'sanksi_kredit_macet' => KetersediaanKoperasi::class, + 'efektivitas_sanksi' => EfektivitasSanksiKoperasi::class, + 'pengolahan_lanjutan' => JawabanYaTidakKoperasi::class, + 'kontrak_offtaker' => KetersediaanKoperasi::class, + 'layanan_koperasi' => AsEnumCollection::of(LayananKoperasi::class), + 'faktor_manajemen' => AsEnumCollection::of(FaktorManajemenKoperasi::class), + 'keterlibatan_rantai_nilai' => AsEnumCollection::of(PeranRantaiNilaiKoperasi::class), + 'agregasi_komoditas' => AsEnumCollection::of(KomoditasAgregasiKoperasi::class), + 'fasilitas_koperasi' => AsEnumCollection::of(FasilitasKoperasi::class), + ]; + } + + public function profilLengkap(): bool + { + return $this->exists && collect([ + 'nama_lengkap', 'posisi_dalam_koperasi', 'nama_koperasi', 'jenis_koperasi', 'skala_operasi', + ])->every(fn ($field) => filled($this->getAttributes()[$field] ?? null)); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function desaKelurahan(): BelongsTo + { + return $this->belongsTo(DesaKelurahan::class, 'desa_kelurahan_id'); + } +} diff --git a/app/Services/Koperasi/KoperasiSurveyService.php b/app/Services/Koperasi/KoperasiSurveyService.php new file mode 100644 index 0000000..cfe9411 --- /dev/null +++ b/app/Services/Koperasi/KoperasiSurveyService.php @@ -0,0 +1,86 @@ +all(); + } elseif (is_string($value)) { + $decoded = json_decode($value, true); + $value = is_array($decoded) ? $decoded : [$value]; + } + $values = array_map(fn ($item) => $item instanceof BackedEnum ? $item->value : $item, is_array($value) ? $value : [$value]); + + return array_values(array_filter($values, fn ($item) => is_string($item) && $item !== '')); + } + + protected function matches(array $condition, array $data, array $context): bool + { + if (isset($condition['any'])) { + foreach ($condition['any'] as $option) { + if ($this->matches($option, $data, $context)) { + return true; + } + } + + return false; + } + $parent = $condition['field']; + if ($condition['source'] === 'profile') { + $selected = $this->values($context[$parent] ?? null); + } else { + if (! $this->active($parent, $data, $context)) { + return false; + } + $selected = $this->values($data[$parent] ?? null); + } + + return (bool) array_intersect($selected, $condition['values']); + } + + public function active(string $field, array $data, array $context): bool + { + foreach ($this->dependencies()[$field] ?? [] as $condition) { + if (! $this->matches($condition, $data, $context)) { + return false; + } + } + + return true; + } + + public function normalize(array $data, array $context): array + { + foreach (array_keys($this->dependencies()) as $field) { + if (! $this->active($field, $data, $context)) { + $data[$field] = in_array($field, $this->arrayFields(), true) ? [] : null; + } + } + + return $data; + } +} diff --git a/app/Services/Koperasi/OperasionalKoperasiService.php b/app/Services/Koperasi/OperasionalKoperasiService.php new file mode 100644 index 0000000..3f71a0b --- /dev/null +++ b/app/Services/Koperasi/OperasionalKoperasiService.php @@ -0,0 +1,123 @@ + $model?->jenis_koperasi]; + } + + public function dependencies(): array + { + $when = fn (string $field, array $values, string $source = 'form') => [ + 'field' => $field, 'values' => $values, 'source' => $source, + ]; + $simpanPinjam = $when('jenis_koperasi', [JenisKoperasi::SIMPAN_PINJAM->value], 'profile'); + $komoditas = array_map(fn ($case) => $case->value, array_filter( + KomoditasAgregasiKoperasi::cases(), fn ($case) => $case !== KomoditasAgregasiKoperasi::TIDAK_TERLIBAT, + )); + $agregasi = $when('agregasi_komoditas', $komoditas); + + return [ + 'tahun_ad_art' => [$when('ad_art', [StatusAdArtKoperasi::LENGKAP->value])], + 'layanan_lainnya' => [$when('layanan_koperasi', [LayananKoperasi::LAINNYA->value])], + 'format_pembukuan' => [$when('status_pembukuan', [StatusPembukuanKoperasi::SUDAH->value])], + 'pertemuan_lainnya' => [$when('pertemuan_rutin', [FrekuensiPertemuanKoperasi::LAINNYA->value])], + 'kasus_kredit_macet' => [$simpanPinjam], + 'sanksi_kredit_macet' => [$simpanPinjam], + 'efektivitas_sanksi' => [$simpanPinjam, $when('sanksi_kredit_macet', [KetersediaanKoperasi::ADA->value])], + 'faktor_manajemen_lainnya' => [$when('faktor_manajemen', [FaktorManajemenKoperasi::LAINNYA->value])], + 'agregasi_komoditas' => [$when('keterlibatan_rantai_nilai', [PeranRantaiNilaiKoperasi::AGREGASI->value])], + 'agregasi_komoditas_lainnya' => [$when('agregasi_komoditas', [KomoditasAgregasiKoperasi::LAINNYA->value])], + 'fasilitas_koperasi' => [$agregasi], + 'fasilitas_lainnya' => [$when('fasilitas_koperasi', [FasilitasKoperasi::LAINNYA->value])], + 'pengolahan_lanjutan' => [$agregasi], + 'detail_pengolahan_lanjutan' => [$when('pengolahan_lanjutan', [JawabanYaTidakKoperasi::YA->value])], + 'cara_penentuan_harga' => [$agregasi], + 'kontrak_offtaker' => [$agregasi], + 'kontrak_komoditas' => [$when('kontrak_offtaker', [KetersediaanKoperasi::ADA->value])], + 'standar_sortasi' => [$when('praktik_sortasi', [PraktikSortasiKoperasi::RUTIN->value, PraktikSortasiKoperasi::TIDAK_KONSISTEN->value])], + 'cara_pengeringan' => [$when('praktik_pengeringan', [PraktikPengeringanKoperasi::RUTIN->value, PraktikPengeringanKoperasi::KADANG->value])], + 'model_fermentasi' => [$when('praktik_fermentasi', [PraktikFermentasiKoperasi::RUTIN->value, PraktikFermentasiKoperasi::PERNAH->value])], + 'proses_fermentasi' => [$when('praktik_fermentasi', [PraktikFermentasiKoperasi::RUTIN->value, PraktikFermentasiKoperasi::PERNAH->value])], + 'harga_premium_rupiah' => [$this->postHarvestCondition()], + 'harga_premium_persen' => [$this->postHarvestCondition()], + ]; + } + + public function requiredFields(): array + { + return [ + 'tahun_ad_art', 'layanan_lainnya', 'format_pembukuan', 'pertemuan_lainnya', + 'efektivitas_sanksi', 'faktor_manajemen_lainnya', 'agregasi_komoditas', + 'agregasi_komoditas_lainnya', 'fasilitas_lainnya', 'detail_pengolahan_lanjutan', 'kontrak_komoditas', + 'standar_sortasi', 'cara_pengeringan', 'model_fermentasi', 'proses_fermentasi', + ]; + } + + protected function arrayFields(): array + { + return ['agregasi_komoditas', 'fasilitas_koperasi', 'model_fermentasi']; + } + + public function sectionFields(BagianOperasionalKoperasi $section): array + { + return match ($section) { + BagianOperasionalKoperasi::PENGELOLAAN => [ + 'ad_art', 'tahun_ad_art', 'layanan_koperasi', 'layanan_lainnya', + 'pelatihan_produksi', 'pelatihan_pascapanen', 'pelatihan_pengolahan', + 'pelatihan_keuangan', 'pelatihan_pemasaran', 'pelatihan_lainnya', + 'status_pembukuan', 'format_pembukuan', 'pertemuan_rutin', 'pertemuan_lainnya', + 'kasus_kredit_macet', 'sanksi_kredit_macet', 'efektivitas_sanksi', + 'faktor_manajemen', 'faktor_manajemen_lainnya', + ], + BagianOperasionalKoperasi::RANTAI_NILAI => [ + 'keterlibatan_rantai_nilai', 'agregasi_komoditas', 'agregasi_komoditas_lainnya', + 'fasilitas_koperasi', 'fasilitas_lainnya', 'pengolahan_lanjutan', 'detail_pengolahan_lanjutan', + 'cara_penentuan_harga', 'kontrak_offtaker', 'kontrak_komoditas', + 'praktik_sortasi', 'standar_sortasi', 'praktik_pengeringan', 'cara_pengeringan', + 'praktik_fermentasi', 'model_fermentasi', 'proses_fermentasi', 'harga_premium_rupiah', 'harga_premium_persen', + ], + }; + } + + private function postHarvestCondition(): array + { + return ['any' => [ + ['field' => 'pengolahan_lanjutan', 'source' => 'form', 'values' => [JawabanYaTidakKoperasi::YA->value]], + ['field' => 'praktik_sortasi', 'source' => 'form', 'values' => [PraktikSortasiKoperasi::RUTIN->value, PraktikSortasiKoperasi::TIDAK_KONSISTEN->value]], + ['field' => 'praktik_pengeringan', 'source' => 'form', 'values' => [PraktikPengeringanKoperasi::RUTIN->value, PraktikPengeringanKoperasi::KADANG->value]], + ['field' => 'praktik_fermentasi', 'source' => 'form', 'values' => [PraktikFermentasiKoperasi::RUTIN->value, PraktikFermentasiKoperasi::PERNAH->value]], + ]]; + } + + public function normalize(array $data, array $context): array + { + $data = parent::normalize($data, $context); + if (($data['status_pembukuan'] ?? null) === StatusPembukuanKoperasi::BELUM->value) { + $data['format_pembukuan'] = FormatPembukuanKoperasi::TIDAK_ADA->value; + } + + return $data; + } +} diff --git a/app/Services/Koperasi/PengurusKoperasiService.php b/app/Services/Koperasi/PengurusKoperasiService.php new file mode 100644 index 0000000..7ff19ee --- /dev/null +++ b/app/Services/Koperasi/PengurusKoperasiService.php @@ -0,0 +1,198 @@ + $validated['search'] ?? '', + 'kabupaten_kota_id' => $validated['kabupaten_kota_id'] ?? ($this->wilayah->available() ? $this->wilayah->defaultKabupaten()?->id : ''), + 'kecamatan_id' => $validated['kecamatan_id'] ?? '', 'desa_kelurahan_id' => $validated['desa_kelurahan_id'] ?? '', + 'jenis_koperasi' => $validated['jenis_koperasi'] ?? '', + 'layanan' => $validated['layanan'] ?? '', 'ad_art' => $validated['ad_art'] ?? '', + 'sort_by' => $validated['sort_by'] ?? UrutanKoperasi::TERBARU->value, + ]; + $query = PengurusKoperasi::query()->with('creator:id,name') + ->when(filled($filters['jenis_koperasi']), function ($query) use ($filters) { + if ($filters['jenis_koperasi'] === JenisKoperasi::LAINNYA->value) { + $standard = array_map(fn ($jenis) => $jenis->value, array_filter(JenisKoperasi::cases(), fn ($jenis) => $jenis !== JenisKoperasi::LAINNYA)); + + return $query->whereNotIn('jenis_koperasi', $standard); + } + + return $query->where('jenis_koperasi', $filters['jenis_koperasi']); + }) + ->when(filled($filters['layanan']), fn ($query) => $query->whereJsonContains('layanan_koperasi', $filters['layanan'])) + ->when(filled($filters['ad_art']), fn ($query) => $query->where('ad_art', $filters['ad_art'])) + ->when(filled($filters['search']), function ($query) use ($filters) { + $query->where(function ($query) use ($filters) { + foreach (['nama_koperasi', 'alamat_koperasi', 'jenis_koperasi', 'nama_lengkap'] as $field) { + $query->orWhereLike($field, '%'.$filters['search'].'%'); + } + }); + }); + $this->wilayah->applyFilters($query, $filters); + $order = UrutanKoperasi::from($filters['sort_by']); + match ($order) { + UrutanKoperasi::NAMA_ASC, UrutanKoperasi::NAMA_DESC => $query->orderByRaw('LOWER(nama_koperasi) '.($order === UrutanKoperasi::NAMA_ASC ? 'asc' : 'desc')), + UrutanKoperasi::ANGGOTA_ASC, UrutanKoperasi::ANGGOTA_DESC => $query + ->orderByRaw('CASE WHEN jumlah_anggota_dewasa IS NULL OR jumlah_anggota_anak IS NULL THEN 1 ELSE 0 END') + ->orderByRaw('(jumlah_anggota_dewasa + jumlah_anggota_anak) '.($order === UrutanKoperasi::ANGGOTA_ASC ? 'asc' : 'desc')), + UrutanKoperasi::TERBARU => $query->latest(), + }; + $koperasi = $query->orderByDesc('id')->paginate((int) ($validated['per_page'] ?? 10))->withQueryString(); + $ringkasanKoperasi = $koperasi->getCollection()->mapWithKeys(fn (PengurusKoperasi $item) => [$item->id => $this->profileSummary($item)]); + $jenisTerdata = PengurusKoperasi::whereNotNull('jenis_koperasi')->where('jenis_koperasi', '!=', '') + ->distinct()->orderBy('jenis_koperasi')->pluck('jenis_koperasi'); + $jenisKoperasiOptions = collect(JenisKoperasi::cases())->map(fn ($jenis) => $jenis->value) + ->merge($jenisTerdata)->unique()->values(); + + return [ + 'koperasi' => $koperasi, + 'ringkasanKoperasi' => $ringkasanKoperasi, + 'filters' => $filters, + 'jenisKoperasiOptions' => $jenisKoperasiOptions, + ...$this->wilayah->filterOptions($filters), + 'showEnumerator' => in_array(auth()->user()?->role, [Role::SUPER_ADMIN, Role::ADMIN], true), + 'totalKoperasi' => PengurusKoperasi::count(), + 'totalJenisKoperasi' => $jenisTerdata->count(), + 'totalDokumenLengkap' => PengurusKoperasi::where('ad_art', StatusAdArtKoperasi::LENGKAP->value)->count(), + ]; + } + + public function profileViewData(?PengurusKoperasi $pengurus = null): array + { + return [ + 'pengurus' => $pengurus ?? new PengurusKoperasi, + 'ringkasanProfil' => $this->profileSummary($pengurus), + ...$this->wilayah->profileOptions($pengurus, session()->getOldInput()), + ]; + } + + public function profileSummary(?PengurusKoperasi $pengurus = null): array + { + $empty = 'Belum terisi'; + $dewasa = $pengurus?->jumlah_anggota_dewasa; + $anak = $pengurus?->jumlah_anggota_anak; + $count = fn ($value) => filled($value) ? number_format((int) $value, 0, ',', '.') : $empty; + $layanan = collect($pengurus?->layanan_koperasi ?? [])->map(function (LayananKoperasi $item) use ($pengurus) { + return $item === LayananKoperasi::LAINNYA && filled($pengurus->layanan_lainnya) + ? $item->label().': '.$pengurus->layanan_lainnya : $item->label(); + })->unique()->values()->all(); + + return [ + 'judul' => filled($pengurus?->nama_koperasi) ? $pengurus->nama_koperasi : 'Nama Koperasi', + 'jenis' => filled($pengurus?->jenis_koperasi) ? $pengurus->jenis_koperasi : $empty, + 'alamat' => filled($pengurus?->alamat_koperasi) ? $pengurus->alamat_koperasi : $empty, + 'tahun_berdiri' => $pengurus?->tahun_berdiri ?? $empty, + 'anggota' => filled($dewasa) && filled($anak) ? $count((int) $dewasa + (int) $anak) : $empty, + 'anggota_rincian' => filled($dewasa) || filled($anak) ? 'Dewasa: '.$count($dewasa).' · Anak: '.$count($anak) : null, + 'pengurus' => $count($pengurus?->jumlah_pengurus), + 'layanan' => $layanan, + 'responden' => filled($pengurus?->nama_lengkap) ? $pengurus->nama_lengkap : $empty, + 'jabatan' => $pengurus?->posisi_dalam_koperasi?->value ?? $empty, + 'kontak' => filled($pengurus?->no_hp) ? $pengurus->no_hp : $empty, + 'diperbarui' => $pengurus?->updated_at ?? $pengurus?->created_at, + 'tersimpan' => $pengurus?->exists ?? false, + ]; + } + + public static function normalizeRupiah(mixed $value): mixed + { + if (is_string($value) && preg_match('/^-?\d{1,3}(?:\.\d{3})+$/D', $value)) { + return str_replace('.', '', $value); + } + + return $value; + } + + public static function formatRupiah(mixed $value): string + { + $value = (string) self::normalizeRupiah($value); + + return preg_match('/^-?\d+$/D', $value) + ? preg_replace('/\B(?=(\d{3})+(?!\d))/', '.', $value) + : $value; + } + + public static function hasSelection(mixed $value, string $selection): bool + { + if ($value instanceof Collection) { + $value = $value->all(); + } + if ($value instanceof \BackedEnum) { + $value = $value->value; + } + if (is_array($value)) { + return in_array($selection, array_map(fn ($item) => $item instanceof \BackedEnum ? $item->value : $item, $value), true); + } + + $decoded = json_decode((string) $value, true); + + return is_array($decoded) + ? in_array($selection, $decoded, true) + : str_contains((string) $value, $selection); + } + + public function profileData(array $data): array + { + unset($data['provinsi_id'], $data['kabupaten_kota_id'], $data['kecamatan_id'], $data['created_by']); + if (! $this->wilayah->available()) { + unset($data['desa_kelurahan_id']); + } + if ($data['jenis_koperasi'] === JenisKoperasi::LAINNYA->value && ! empty($data['jenis_koperasi_lainnya'])) { + $data['jenis_koperasi'] = $data['jenis_koperasi_lainnya']; + } + + unset($data['jenis_koperasi_lainnya']); + + if (array_key_exists('fokus_komoditas', $data) && $data['fokus_komoditas'] !== FokusKomoditasKoperasi::YA->value) { + $data['detail_fokus_komoditas'] = null; + } + + return $data; + } + + public function createProfile(array $data, int $creatorId): PengurusKoperasi + { + $data = $this->profileData($data); + if ($this->wilayah->available()) { + $data['created_by'] = $creatorId; + } + + return PengurusKoperasi::create($data); + } + + public function tabData(array $data): array + { + $model = new PengurusKoperasi; + foreach ($data as $field => $value) { + if (is_array($value) && ! $model->hasCast($field)) { + $data[$field] = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); + } + } + + return $data; + } +} diff --git a/app/Services/Koperasi/RantaiNilaiKoperasiService.php b/app/Services/Koperasi/RantaiNilaiKoperasiService.php new file mode 100644 index 0000000..93c617a --- /dev/null +++ b/app/Services/Koperasi/RantaiNilaiKoperasiService.php @@ -0,0 +1,71 @@ + 'Panen Terakhir', '1_tahun' => 'Tahun Lalu', '2_tahun' => 'Dua Tahun Lalu']; + + public function dependencies(): array + { + $when = fn (string $field, array $values) => ['field' => $field, 'values' => $values, 'source' => 'form']; + $buyers = array_values(array_map(fn ($case) => $case->value, array_filter( + TujuanPenjualanKoperasi::cases(), fn ($case) => $case !== TujuanPenjualanKoperasi::BELUM_MENJUAL, + ))); + $rules = [ + 'sumber_pasokan_lainnya' => [$when('sumber_pasokan', [SumberPasokanKoperasi::LAINNYA->value])], + 'tujuan_penjualan_lainnya' => [$when('tujuan_penjualan', [TujuanPenjualanKoperasi::LAINNYA->value])], + 'sifat_hubungan_lainnya' => [$when('sifat_hubungan_bisnis', [HubunganPemasokKoperasi::LAINNYA->value])], + 'cara_menetapkan_harga_lainnya' => [$when('cara_menetapkan_harga_pasar', [PenetapanHargaKoperasi::LAINNYA->value])], + 'bentuk_penjualan' => [$when('tujuan_penjualan', $buyers)], + ]; + foreach (array_keys(self::PERIODS) as $period) { + $rules['volume_jual_'.$period] = [$when('tujuan_penjualan', $buyers)]; + } + + return $rules; + } + + public function requiredFields(): array + { + return ['sumber_pasokan_lainnya', 'tujuan_penjualan_lainnya', 'sifat_hubungan_lainnya', 'cara_menetapkan_harga_lainnya']; + } + + protected function arrayFields(): array + { + return ['bentuk_penjualan']; + } + + public function sectionFields(BagianRantaiNilaiKoperasi $section): array + { + if ($section === BagianRantaiNilaiKoperasi::HARGA) { + $fields = ['cara_menetapkan_harga_pasar', 'cara_menetapkan_harga_lainnya']; + foreach (BentukProdukKoperasi::cases() as $product) { + foreach (array_keys(self::PERIODS) as $period) { + $fields[] = 'harga_'.$product->pricePrefix().'_'.$period; + } + } + + return $fields; + } + + $fields = [ + 'sumber_pasokan', 'sumber_pasokan_lainnya', 'bentuk_pembelian', + 'tujuan_penjualan', 'tujuan_penjualan_lainnya', 'bentuk_penjualan', + 'sifat_hubungan_bisnis', 'sifat_hubungan_lainnya', 'mekanisme_pembayaran', 'status_perjanjian_tertulis', + ]; + foreach (array_keys(self::PERIODS) as $period) { + $fields[] = 'volume_beli_'.$period; + $fields[] = 'volume_jual_'.$period; + } + + return $fields; + } +} diff --git a/app/Services/Koperasi/SosialEkonomiKoperasiService.php b/app/Services/Koperasi/SosialEkonomiKoperasiService.php new file mode 100644 index 0000000..6567706 --- /dev/null +++ b/app/Services/Koperasi/SosialEkonomiKoperasiService.php @@ -0,0 +1,46 @@ + $model?->jenis_koperasi]; + } + + public function dependencies(): array + { + $when = fn (string $field, array $values, string $source = 'form') => [ + 'field' => $field, 'values' => $values, 'source' => $source, + ]; + $simpanPinjam = $when('jenis_koperasi', [JenisKoperasi::SIMPAN_PINJAM->value], 'profile'); + + return [ + 'alasan_pinjaman' => [$simpanPinjam], + 'alasan_pinjaman_lainnya' => [$when('alasan_pinjaman', [AlasanPinjamanKoperasi::LAINNYA->value])], + 'pembayaran_selain_uang' => [$simpanPinjam], + 'bentuk_pembayaran_barang' => [$when('pembayaran_selain_uang', [KetersediaanKoperasi::ADA->value])], + 'bentuk_pembayaran_lainnya' => [$when('bentuk_pembayaran_barang', [BentukPembayaranKoperasi::LAINNYA->value])], + 'perhitungan_harga_pasar' => [$when('bentuk_pembayaran_barang', [BentukPembayaranKoperasi::HASIL_BUMI->value])], + 'alasan_bukan_harga_pasar' => [$when('perhitungan_harga_pasar', [JawabanYaTidakKoperasi::TIDAK->value])], + ]; + } + + public function requiredFields(): array + { + return ['alasan_pinjaman_lainnya', 'bentuk_pembayaran_barang', 'bentuk_pembayaran_lainnya', 'perhitungan_harga_pasar', 'alasan_bukan_harga_pasar']; + } + + protected function arrayFields(): array + { + return ['alasan_pinjaman', 'bentuk_pembayaran_barang']; + } +} diff --git a/app/Services/Koperasi/WilayahKoperasiService.php b/app/Services/Koperasi/WilayahKoperasiService.php new file mode 100644 index 0000000..c03996e --- /dev/null +++ b/app/Services/Koperasi/WilayahKoperasiService.php @@ -0,0 +1,96 @@ +available ??= Schema::hasColumn('pengurus_koperasi', 'desa_kelurahan_id') + && Schema::hasColumn('pengurus_koperasi', 'created_by'); + } + + public function applyFilters(Builder $query, array $filters): void + { + if (! $this->available()) { + if (filled($filters['kabupaten_kota_id']) || filled($filters['kecamatan_id']) || filled($filters['desa_kelurahan_id'])) { + $query->whereRaw('1 = 0'); + } + + return; + } + + // Seperti BUMDes, lokasi koperasi memakai seluruh master wilayah, termasuk di luar program aktif. + // Profil yang belum memiliki desa tetap dapat ditemukan dan dilengkapi pada daftar kabupaten. + $query->when($filters['kabupaten_kota_id'], fn ($query, $id) => $query->where(fn ($wilayah) => $wilayah + ->whereHas('desaKelurahan', fn ($desa) => $desa + ->withoutGlobalScope('program')->whereHas('kecamatan', fn ($kecamatan) => $kecamatan + ->withoutGlobalScope('program')->where('kabupaten_kota_id', $id))) + ->orWhereNull('desa_kelurahan_id'))) + ->when($filters['kecamatan_id'], fn ($query, $id) => $query->whereHas('desaKelurahan', fn ($desa) => $desa + ->withoutGlobalScope('program')->where('kecamatan_id', $id))) + ->when($filters['desa_kelurahan_id'], fn ($query, $id) => $query->where('desa_kelurahan_id', $id)); + } + + public function filterOptions(array $filters): array + { + $ready = $this->available(); + + return [ + 'wilayahTersedia' => $ready, + 'selectedKabupaten' => filled($filters['kabupaten_kota_id']) + ? KabupatenKota::withoutGlobalScope('program')->find($filters['kabupaten_kota_id'], ['id', 'nama']) : $this->defaultKabupaten(), + 'kecamatanOptions' => $ready && filled($filters['kabupaten_kota_id']) + ? Kecamatan::withoutGlobalScope('program')->where('kabupaten_kota_id', $filters['kabupaten_kota_id'])->orderBy('nama')->get(['id', 'nama']) : collect(), + 'desaOptions' => $ready && filled($filters['kecamatan_id']) + ? DesaKelurahan::withoutGlobalScope('program')->where('kecamatan_id', $filters['kecamatan_id'])->orderBy('nama')->get(['id', 'nama']) : collect(), + ]; + } + + public function defaultKabupaten(): ?KabupatenKota + { + return KabupatenKota::withoutGlobalScope('program')->whereLike('nama', '%ngada%')->orderBy('nama')->first(['id', 'nama']); + } + + public function profileOptions(?PengurusKoperasi $model, array $old): array + { + if (! $this->available()) { + return ['wilayahTersedia' => false]; + } + $model?->loadMissing([ + 'desaKelurahan' => fn ($query) => $query->withoutGlobalScope('program'), + 'desaKelurahan.kecamatan' => fn ($query) => $query->withoutGlobalScope('program'), + 'desaKelurahan.kecamatan.kabupatenKota' => fn ($query) => $query->withoutGlobalScope('program'), + ]); + $desa = $model?->desaKelurahan; + $value = function ($key, $fallback) use ($old) { + $selected = array_key_exists($key, $old) ? $old[$key] : $fallback; + + return is_int($selected) || (is_string($selected) && ctype_digit($selected)) ? $selected : null; + }; + $selected = [ + 'provinsi_id' => $value('provinsi_id', $desa?->kecamatan?->kabupatenKota?->provinsi_id), + 'kabupaten_kota_id' => $value('kabupaten_kota_id', $desa?->kecamatan?->kabupaten_kota_id), + 'kecamatan_id' => $value('kecamatan_id', $desa?->kecamatan_id), + 'desa_kelurahan_id' => $value('desa_kelurahan_id', $model?->desa_kelurahan_id), + ]; + + return [ + ...$this->filterOptions($selected), + 'selectedWilayah' => $selected, + 'provinsiOptions' => Provinsi::withoutGlobalScope('program')->orderBy('nama')->get(['id', 'nama']), + 'kabupatenOptions' => filled($selected['provinsi_id']) + ? KabupatenKota::withoutGlobalScope('program')->where('provinsi_id', $selected['provinsi_id'])->orderBy('nama')->get(['id', 'nama']) : collect(), + ]; + } +} diff --git a/database/migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php b/database/migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php new file mode 100644 index 0000000..97670cd --- /dev/null +++ b/database/migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php @@ -0,0 +1,43 @@ +foreignId('desa_kelurahan_id')->nullable()->constrained('master_desa_kelurahan')->nullOnDelete(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + }); + + if (! Schema::hasTable('activity_log')) { + return; + } + + // Penginput lama hanya dipulihkan dari log pembuatan, bukan dari pengguna yang terakhir mengubah. + DB::table('pengurus_koperasi')->select('id')->whereNull('created_by')->chunkById(100, function ($rows) { + foreach ($rows as $row) { + $creator = DB::table('activity_log')->where('subject_type', (new PengurusKoperasi)->getMorphClass()) + ->where('subject_id', $row->id)->where('event', 'created') + ->where('causer_type', (new User)->getMorphClass())->orderBy('id')->value('causer_id'); + if ($creator && DB::table('users')->where('id', $creator)->exists()) { + DB::table('pengurus_koperasi')->where('id', $row->id)->update(['created_by' => $creator]); + } + } + }); + } + + public function down(): void + { + Schema::table('pengurus_koperasi', function (Blueprint $table) { + $table->dropConstrainedForeignId('created_by'); + $table->dropConstrainedForeignId('desa_kelurahan_id'); + }); + } +}; diff --git a/public/assets/css/demo.css b/public/assets/css/demo.css index 111756b..c3541cc 100644 --- a/public/assets/css/demo.css +++ b/public/assets/css/demo.css @@ -191,9 +191,10 @@ body > .select2-container--open { inline-size: auto !important; } -/* Tipografi profil Kelompok Tani dan BUMDes */ +/* Tipografi profil Kelompok Tani, BUMDes, dan Koperasi */ .kelompok-tani-page, -.bumdes-page { +.bumdes-page, +.koperasi-profile-page { font-family: var(--bs-body-font-family); font-size: var(--bs-body-font-size); font-weight: var(--bs-body-font-weight); @@ -201,7 +202,8 @@ body > .select2-container--open { } .kelompok-tani-page :is(.form-control, .form-select, .form-check-label, .select2-container), -.bumdes-page :is(.form-control, .form-select, .form-check-label, .select2-container) { +.bumdes-page :is(.form-control, .form-select, .form-check-label, .select2-container), +.koperasi-profile-page :is(.form-control, .form-select, .form-check-label, .select2-container) { font-family: var(--bs-body-font-family); } @@ -212,10 +214,15 @@ body > .select2-container--open { } #bumdes-notifications, +.koperasi-profile-page [data-koperasi-section], .bumdes-page #section-kepengurusan { scroll-margin-top: 7rem; } +.koperasi-profile-page [data-koperasi-section]:focus { + outline: none; +} + .bumdes-page .pengurus-status-switch-row { display: flex; align-items: center; @@ -294,22 +301,22 @@ body > .select2-container--open { .bumdes-row { cursor: pointer; } .bumdes-filter-row { padding-block: 1.5rem; } - .bumdes-filter-line { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; } - .bumdes-filter-label { display: flex; align-items: center; font-weight: 500; margin-right: .5rem; white-space: nowrap; } - .bumdes-filter-line .form-select, - .bumdes-filter-line .btn { min-height: 31px; } - .bumdes-filter-line .form-select { + :is(.bumdes-filter-line, .koperasi-filter-line) { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; } + :is(.bumdes-filter-label, .koperasi-filter-label) { display: flex; align-items: center; font-weight: 500; margin-right: .5rem; white-space: nowrap; } + :is(.bumdes-filter-line, .koperasi-filter-line) .form-select, + :is(.bumdes-filter-line, .koperasi-filter-line) .btn { min-height: 31px; } + :is(.bumdes-filter-line, .koperasi-filter-line) .form-select { background-color: var(--bs-paper-bg); border-color: var(--bs-border-color); border-radius: var(--bs-border-radius); color: var(--bs-body-color); } - .bumdes-filter-line .form-select:focus { + :is(.bumdes-filter-line, .koperasi-filter-line) .form-select:focus { border-color: rgba(40, 167, 69, .55); box-shadow: 0 0 0 .2rem rgba(40, 167, 69, .12); } - .bumdes-filter-line .btn { white-space: nowrap; } - .bumdes-filter-static { + :is(.bumdes-filter-line, .koperasi-filter-line) .btn { white-space: nowrap; } + :is(.bumdes-filter-static, .koperasi-filter-static) { display: flex; align-items: center; min-height: 31px; padding: .25rem .75rem; font-size: .8125rem; background-color: var(--bs-paper-bg); @@ -317,19 +324,19 @@ body > .select2-container--open { border-radius: var(--bs-border-radius); color: var(--bs-body-color); } - .bumdes-filter-line .input-group-sm { width: 240px; } - .bumdes-data-filters > div:not(.bumdes-filter-label) { + :is(.bumdes-filter-line, .koperasi-filter-line) .input-group-sm { width: 240px; } + .bumdes-data-filters > div:not(:is(.bumdes-filter-label, .koperasi-filter-label)) { flex: 1 1 11rem; min-inline-size: 0; } .bumdes-data-filters .form-select { text-overflow: ellipsis; } .bumdes-data-filters .input-group-sm { width: 100%; } @media (max-width: 767.98px) { - .bumdes-filter-line { display: grid; grid-template-columns: minmax(0, 1fr); width: 100%; } - .bumdes-filter-label { margin-right: 0; } - .bumdes-filter-line .btn { justify-content: center; width: 100%; } - .bumdes-filter-line .input-group-sm { width: 100%; } - .bumdes-filter-line .input-group .btn { width: auto; } + :is(.bumdes-filter-line, .koperasi-filter-line) { display: grid; grid-template-columns: minmax(0, 1fr); width: 100%; } + :is(.bumdes-filter-label, .koperasi-filter-label) { margin-right: 0; } + :is(.bumdes-filter-line, .koperasi-filter-line) .btn { justify-content: center; width: 100%; } + :is(.bumdes-filter-line, .koperasi-filter-line) .input-group-sm { width: 100%; } + :is(.bumdes-filter-line, .koperasi-filter-line) .input-group .btn { width: auto; } } @@ -367,7 +374,34 @@ body > .select2-container--open { } } -/* Tabel dan ringkasan profil BUMDes. */ +/* Tabel dan ringkasan profil. */ +.koperasi-filter-row { + padding-block: 1.5rem; +} + +.koperasi-filter-row .form-select { + min-inline-size: 0; + text-overflow: ellipsis; +} + +.koperasi-list-table .koperasi-list-identity { + min-inline-size: 14rem; + max-inline-size: 20rem; + overflow-wrap: anywhere; +} + +.koperasi-list-table .koperasi-list-type, +.koperasi-list-table .koperasi-list-services, +.koperasi-list-table .koperasi-list-documents { + min-inline-size: 10rem; + max-inline-size: 16rem; + overflow-wrap: anywhere; +} + +.koperasi-list-table [data-koperasi-row] { + cursor: pointer; +} + .bumdes-list-table .bumdes-list-identity { min-inline-size: 12rem; max-inline-size: 16rem; @@ -384,19 +418,22 @@ body > .select2-container--open { } .bumdes-profile-summary .bumdes-summary-content, -.kelompok-profile-summary .kelompok-summary-content { +.kelompok-profile-summary .kelompok-summary-content, +.koperasi-profile-summary .koperasi-summary-content { min-inline-size: 0; } .bumdes-profile-summary .bumdes-summary-label, -.kelompok-profile-summary .kelompok-summary-label { +.kelompok-profile-summary .kelompok-summary-label, +.koperasi-profile-summary .koperasi-summary-label { color: var(--bs-secondary-color); font-size: 0.8125rem; font-weight: 500; } .bumdes-profile-summary .badge, -.kelompok-profile-summary .badge { +.kelompok-profile-summary .badge, +.koperasi-profile-summary .badge { max-inline-size: 100%; line-height: 1.45; white-space: normal; @@ -405,18 +442,19 @@ body > .select2-container--open { } .bumdes-profile-summary .bumdes-summary-more, -.kelompok-profile-summary .kelompok-summary-more { +.kelompok-profile-summary .kelompok-summary-more, +.koperasi-profile-summary .koperasi-summary-more { min-block-size: 1.5rem; font-size: 0.8125rem; } /* Daftar kelompok tani. */ @media (min-width: 768px) { - .kelompok-tani-page .nav-pills-custom-spacing { display: flex; width: 100%; gap: .5rem; flex-wrap: nowrap !important; overflow-x: auto; } - .kelompok-tani-page .nav-pills-custom-spacing::-webkit-scrollbar { display: none; } - .kelompok-tani-page .nav-pills-custom-spacing .nav-item { margin-right: 0 !important; flex: 1; } - .kelompok-tani-page .nav-pills-custom-spacing .nav-link { display: flex; justify-content: center; align-items: center; padding-left: .5rem !important; padding-right: .5rem !important; width: 100%; white-space: nowrap; font-size: var(--bs-body-font-size) !important; } - .kelompok-tani-page .nav-pills-custom-spacing .nav-link i { margin-right: .4rem !important; } + :is(.kelompok-tani-page, .koperasi-profile-page) .nav-pills-custom-spacing { display: flex; width: 100%; gap: .5rem; flex-wrap: nowrap !important; overflow-x: auto; } + :is(.kelompok-tani-page, .koperasi-profile-page) .nav-pills-custom-spacing::-webkit-scrollbar { display: none; } + :is(.kelompok-tani-page, .koperasi-profile-page) .nav-pills-custom-spacing .nav-item { margin-right: 0 !important; flex: 1; } + :is(.kelompok-tani-page, .koperasi-profile-page) .nav-pills-custom-spacing .nav-link { display: flex; justify-content: center; align-items: center; padding-left: .5rem !important; padding-right: .5rem !important; width: 100%; white-space: nowrap; font-size: var(--bs-body-font-size) !important; } + :is(.kelompok-tani-page, .koperasi-profile-page) .nav-pills-custom-spacing .nav-link i { margin-right: .4rem !important; } } .kelompok-tani-row { cursor: pointer; } .kelompok-filter-row { padding-block: 1.5rem; } diff --git a/public/assets/js/koperasi-list.js b/public/assets/js/koperasi-list.js new file mode 100644 index 0000000..378d09e --- /dev/null +++ b/public/assets/js/koperasi-list.js @@ -0,0 +1,33 @@ +'use strict'; + +document.addEventListener('DOMContentLoaded', () => { + const page = document.querySelector('.koperasi-list-page'); + if (!page) return; + + page.querySelector('#koperasi-per-page').addEventListener('change', event => event.target.form.requestSubmit()); + + page.querySelectorAll('[data-koperasi-row]').forEach(row => { + row.addEventListener('click', event => { + if (event.target.closest('a, button, input, select, textarea, label, form')) return; + window.location.href = row.querySelector('.edit-record').href; + }); + }); + + page.querySelectorAll('[data-koperasi-delete]').forEach(form => { + form.addEventListener('submit', event => { + event.preventDefault(); + Swal.fire({ + title: 'Hapus koperasi?', + text: `Data koperasi "${form.dataset.nama}" akan dihapus.`, + icon: 'warning', + showCancelButton: true, + confirmButtonText: 'Ya, hapus', + cancelButtonText: 'Batal', + customClass: { confirmButton: 'btn btn-danger me-3', cancelButton: 'btn btn-label-secondary' }, + buttonsStyling: false + }).then(result => { + if (result.isConfirmed) form.submit(); + }); + }); + }); +}); diff --git a/public/assets/js/koperasi-wilayah.js b/public/assets/js/koperasi-wilayah.js new file mode 100644 index 0000000..7dd9551 --- /dev/null +++ b/public/assets/js/koperasi-wilayah.js @@ -0,0 +1,49 @@ +'use strict'; + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('[data-koperasi-wilayah], #koperasi-filter-form').forEach(container => { + const levels = ['provinsi', 'kabupaten', 'kecamatan', 'desa'] + .map(name => ({ name, input: container.querySelector(`[data-wilayah-${name}]`) })) + .filter(level => level.input); + const controllers = new Map(); + const updateRequired = () => { + if (!container.matches('[data-koperasi-wilayah]')) return; + const hasSelection = levels.some(level => level.input.value); + levels.forEach(level => { level.input.required = hasSelection; }); + }; + const reset = level => { + controllers.get(level.name)?.abort(); + level.input.replaceChildren(new Option(level.input.options[0].textContent, '')); + level.input.disabled = true; + }; + levels.forEach((level, index) => { + if (index === levels.length - 1) return; + level.input.addEventListener('change', async () => { + levels.slice(index + 1).forEach(reset); + updateRequired(); + if (!level.input.value) return; + const child = levels[index + 1]; + const controller = new AbortController(); + controllers.set(child.name, controller); + const url = new URL(container.dataset[`${child.name}Url`].replace(':id', encodeURIComponent(level.input.value)), window.location.href); + url.searchParams.set('all_wilayah', '1'); + try { + const response = await fetch(url, { + signal: controller.signal, + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '' } + }); + if (!response.ok) throw new Error('Wilayah gagal dimuat'); + const options = await response.json(); + if (controller.signal.aborted) return; + options.forEach(option => child.input.add(new Option(option.nama, option.id))); + child.input.disabled = false; + updateRequired(); + } catch (error) { + if (error.name === 'AbortError') return; + child.input.add(new Option('Gagal memuat wilayah, pilih ulang wilayah induk', '', false, true)); + } + }); + }); + updateRequired(); + }); +}); diff --git a/public/assets/js/koperasi.js b/public/assets/js/koperasi.js new file mode 100644 index 0000000..018a94b --- /dev/null +++ b/public/assets/js/koperasi.js @@ -0,0 +1,119 @@ +'use strict'; + +document.addEventListener('DOMContentLoaded', () => { + const page = document.querySelector('.koperasi-profile-page'); + if (!page) return; + + page.querySelectorAll('[data-koperasi-locked-tab]').forEach(tab => { + tab.addEventListener('click', event => event.preventDefault()); + }); + + page.querySelectorAll('[data-koperasi-survey]').forEach(operasional => { + const fields = Array.from(operasional.querySelectorAll('[data-koperasi-survey-field]')); + const conditions = new Map(fields.map(wrapper => [wrapper.dataset.koperasiSurveyField, JSON.parse(wrapper.dataset.koperasiConditions)])); + const context = JSON.parse(operasional.dataset.koperasiContext); + const requiredFields = JSON.parse(operasional.dataset.koperasiRequired); + const selectedValues = name => Array.from(operasional.elements) + .filter(input => input.name === name || input.name === name + '[]') + .filter(input => !['checkbox', 'radio'].includes(input.type) || input.checked) + .map(input => input.value); + const matches = condition => { + if (condition.any) return condition.any.some(matches); + if (condition.source === 'profile') return condition.values.includes(context[condition.field]); + return active(condition.field) && condition.values.some(value => selectedValues(condition.field).includes(value)); + }; + const active = field => (conditions.get(field) || []).every(matches); + const updateDependencies = () => { + fields.forEach(wrapper => { + const field = wrapper.dataset.koperasiSurveyField; + const visible = active(field); + const required = visible && requiredFields.includes(field); + wrapper.classList.toggle('d-none', !visible); + wrapper.querySelectorAll('input, select, textarea').forEach(input => { + input.disabled = !visible; + input.required = required && input.type !== 'checkbox'; + input.setCustomValidity(''); + }); + const checkboxes = wrapper.querySelectorAll('input[type="checkbox"]'); + if (required && checkboxes.length && !Array.from(checkboxes).some(input => input.checked)) { + checkboxes[0].setCustomValidity('Pilih sedikitnya satu pilihan.'); + } + const indicator = wrapper.querySelector('[data-koperasi-required-indicator]'); + if (indicator) indicator.classList.toggle('d-none', !required); + }); + }; + operasional.addEventListener('change', event => { + const input = event.target; + const group = input.closest('[data-koperasi-exclusive]'); + if (group && input.type === 'checkbox' && input.checked) { + group.querySelectorAll('input[type="checkbox"]').forEach(option => { + if (option !== input && (input.value === group.dataset.koperasiExclusive || option.value === group.dataset.koperasiExclusive)) { + option.checked = false; + } + }); + } + updateDependencies(); + }); + updateDependencies(); + }); + + const activeSection = document.getElementById(window.location.hash.slice(1)); + if (activeSection && page.contains(activeSection) && activeSection.matches('[data-koperasi-section]')) { + const showSection = () => { + activeSection.scrollIntoView({ behavior: 'instant', block: 'start' }); + activeSection.focus({ preventScroll: true }); + }; + if (document.readyState === 'complete') { + requestAnimationFrame(showSection); + } else { + window.addEventListener('load', () => requestAnimationFrame(showSection), { once: true }); + } + } + + const form = page.querySelector('[data-koperasi-profile]'); + if (form) { + const jenis = form.querySelector('[name="jenis_koperasi"]'); + const wrapper = form.querySelector('#koperasi-jenis-lainnya-wrapper'); + const lainnya = form.querySelector('[name="jenis_koperasi_lainnya"]'); + const toggleJenisLainnya = () => { + const isLainnya = jenis.value === form.dataset.jenisLainnya; + wrapper.classList.toggle('d-none', !isLainnya); + lainnya.disabled = !isLainnya; + }; + jenis.addEventListener('change', toggleJenisLainnya); + toggleJenisLainnya(); + + const fokus = form.querySelector('[name="fokus_komoditas"]'); + const detail = form.querySelector('[name="detail_fokus_komoditas"]'); + const fokusWrapper = form.querySelector('#koperasi-detail-fokus-wrapper'); + const toggleFokus = () => { + const aktif = fokus.value === fokus.dataset.fokusYa; + fokusWrapper.classList.toggle('d-none', !aktif); + detail.disabled = !aktif; + }; + fokus.addEventListener('change', toggleFokus); + toggleFokus(); + } + + const formatRupiah = value => { + const negative = value.trimStart().startsWith('-'); + const digits = value.replace(/\D/g, '').replace(/^0+(?=\d)/, ''); + return (negative ? '-' : '') + digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); + }; + + page.querySelectorAll('[data-koperasi-rupiah]').forEach(input => { + if (/^-?[\d.]+$/.test(input.value)) input.value = formatRupiah(input.value); + input.addEventListener('input', () => { + const digitsBefore = input.value.slice(0, input.selectionStart).replace(/\D/g, '').length; + input.value = formatRupiah(input.value); + let cursor = input.value.startsWith('-') ? 1 : 0; + let count = 0; + while (cursor < input.value.length && count < digitsBefore) { + if (/\d/.test(input.value[cursor])) count++; + cursor++; + } + input.setSelectionRange(cursor, cursor); + }); + }); + +}); diff --git a/resources/views/components/koperasi-form-actions.blade.php b/resources/views/components/koperasi-form-actions.blade.php new file mode 100644 index 0000000..0fe32c0 --- /dev/null +++ b/resources/views/components/koperasi-form-actions.blade.php @@ -0,0 +1,8 @@ +
+
+ Kembali + +
+
diff --git a/resources/views/components/koperasi-notifications.blade.php b/resources/views/components/koperasi-notifications.blade.php new file mode 100644 index 0000000..018ccec --- /dev/null +++ b/resources/views/components/koperasi-notifications.blade.php @@ -0,0 +1,12 @@ +@if(session('success')) +
{{ session('success') }}
+@endif +@if($errors->any()) + +@endif diff --git a/resources/views/components/koperasi-profile-summary.blade.php b/resources/views/components/koperasi-profile-summary.blade.php new file mode 100644 index 0000000..04e088a --- /dev/null +++ b/resources/views/components/koperasi-profile-summary.blade.php @@ -0,0 +1,99 @@ +@props(['summary']) + +
+
+
+
+
+ +
+
{{ $summary['judul'] }}
+

+ {{ $summary['alamat'] }} +

+

Tahun Berdiri: {{ $summary['tahun_berdiri'] }}

+
+
+
+
+
+
+

Jenis Koperasi

+ {{ $summary['jenis'] }} +
+
+

Layanan Utama

+ @if(count($summary['layanan'])) +
+ @foreach(array_slice($summary['layanan'], 0, 2) as $item) + {{ $item }} + @endforeach + @if(count($summary['layanan']) > 2) + + @endif +
+ @if(count($summary['layanan']) > 2) +
+
+ @foreach(array_slice($summary['layanan'], 2) as $item) + {{ $item }} + @endforeach +
+
+ @endif + @else +

Belum terisi

+ @endif +
+
+

Total Anggota (2025)

+ {{ $summary['anggota'] }} + @if($summary['anggota_rincian'])

{{ $summary['anggota_rincian'] }}

@endif +
+
+

Total Pengurus

+ {{ $summary['pengurus'] }} +
+
+
+
+
+ +
diff --git a/resources/views/components/koperasi-rupiah.blade.php b/resources/views/components/koperasi-rupiah.blade.php new file mode 100644 index 0000000..01c0d97 --- /dev/null +++ b/resources/views/components/koperasi-rupiah.blade.php @@ -0,0 +1,6 @@ +@props(['name', 'value' => null, 'showError' => true]) + +class(['form-control', 'is-invalid' => $errors->has($name)]) }}> +@if($showError)@error($name)
{{ $message }}
@enderror @endif diff --git a/resources/views/components/koperasi-survey-field.blade.php b/resources/views/components/koperasi-survey-field.blade.php new file mode 100644 index 0000000..ab687b9 --- /dev/null +++ b/resources/views/components/koperasi-survey-field.blade.php @@ -0,0 +1,61 @@ +@props([ + 'model', 'name', 'label', 'options' => [], 'multiple' => false, + 'type' => 'text', 'placeholder' => null, 'exclusiveValue' => null, 'help' => null, + 'policy' => null, 'min' => 0, 'max' => null, 'unit' => null, +]) + +@php + $value = old($name, $model->{$name}); + $id = 'koperasi-' . str_replace('_', '-', $name); + $policy ??= app(\App\Services\Koperasi\OperasionalKoperasiService::class); + $data = array_replace($model->getAttributes(), session()->getOldInput()); + $visible = $policy->active($name, $data, $policy->context($model)); + $required = $visible && in_array($name, $policy->requiredFields(), true); + $error = $errors->first($name) ?: $errors->first($name . '.*'); +@endphp + +
class(['mb-4', 'form-control-validation', 'd-none' => !$visible]) }} + data-koperasi-survey-field="{{ $name }}" data-koperasi-conditions="{{ json_encode($policy->dependencies()[$name] ?? []) }}" + @if($exclusiveValue) data-koperasi-exclusive="{{ $exclusiveValue }}" @endif> + @if($multiple) +
+ {{ $label }} * +
+ @foreach($options as $option) +
+
+ value)) @disabled(!$visible)> + + @if(method_exists($option, 'description') && $option->description())
{{ $option->description() }}
@endif +
+
+ @endforeach +
+
+ @else + + @if($options) + + @elseif($type === 'textarea') + + @else + @if($unit || $type === 'rupiah')
@endif + @if($type === 'rupiah') + Rp + + @else + + @endif + @if($unit){{ $unit }}@endif + @if($unit || $type === 'rupiah')
@endif + @endif + @endif + @if($help)
{{ $help }}
@endif + @if($error)
{{ $error }}
@endif +
diff --git a/resources/views/koperasi/kelembagaan-aktor/index.blade.php b/resources/views/koperasi/kelembagaan-aktor/index.blade.php index 8d4fa6e..f2f37c8 100644 --- a/resources/views/koperasi/kelembagaan-aktor/index.blade.php +++ b/resources/views/koperasi/kelembagaan-aktor/index.blade.php @@ -1,306 +1,101 @@ @extends('layout.main') @section('content') -
- +@php + $policy = app(\App\Services\Koperasi\OperasionalKoperasiService::class); + $context = $policy->context($pengurus); +@endphp +
@include('koperasi.profil.header', ['pengurus' => $pengurus]) -
-
-
+
+
+ @csrf - + -
KELEMBAGAAN & AKTOR SISTEM
-

Memetakan potensi dan kesiapan koperasi sebagai agregator atau mitra pengelola rantai nilai komoditas.

- -
KOPERASI & BUMDes
-
Keberadaan Koperasi
- -
-
Bagaimana sejarah awal pendirian koperasi/CU bapak/ibu? Apa tujuan/cita-cita bersama yang ingin dicapai saat itu?
- +
Dokumen Kelembagaan
+
+ +
-
-
-
Apakah koperasi ini memiliki fokus pada pengembangan komoditas pertanian tertentu?
-
- fokus_komoditas) == 'Tidak' ? 'checked' : '' }}> - -
-
- fokus_komoditas) == 'Ya' ? 'checked' : '' }}> - - -
-
-
-
Apakah ada dokumen kelembagaan (AD/ART)?
-
- ad_art) == 'Ada, lengkap' ? 'checked' : '' }}> - - -
-
ad_art) == 'Ada tapi tidak lengkap' ? 'checked' : '' }}>
-
ad_art) == 'Tidak ada' ? 'checked' : '' }}>
-
ad_art) == 'Tidak tahu' ? 'checked' : '' }}>
-
+
Layanan Koperasi
+
+ +
-
Ketersediaan Layanan
-
-
Apa saja layanan yang disediakan oleh koperasi bapak/ibu untuk anggota koperasi?
-
layanan_koperasi ?? '', 'Simpan pinjam') ? 'checked' : '' }}>
-
layanan_koperasi ?? '', 'Pengadaan input produksi') ? 'checked' : '' }}>
-
layanan_koperasi ?? '', 'Pemasaran hasil') ? 'checked' : '' }}>
-
- layanan_koperasi ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
+
Pelatihan Anggota
+
+ + + + + +
-
-
Apakah terdapat program/pelatihan untuk meningkatkan kapasitas anggota koperasi?
-
- - - - - - - - - - - -
Jenis PelatihanYaTidak
Pelatihan produksi pertanianpelatihan_produksi) == 'Ya' ? 'checked' : '' }}>pelatihan_produksi) == 'Tidak' ? 'checked' : '' }}>
Pelatihan penanganan pascapanenpelatihan_pascapanen) == 'Ya' ? 'checked' : '' }}>pelatihan_pascapanen) == 'Tidak' ? 'checked' : '' }}>
Pelatihan pengolahan produk (setengah jadi/jadi)pelatihan_pengolahan) == 'Ya' ? 'checked' : '' }}>pelatihan_pengolahan) == 'Tidak' ? 'checked' : '' }}>
Pelatihan manajemen keuangan/pembukuanpelatihan_keuangan) == 'Ya' ? 'checked' : '' }}>pelatihan_keuangan) == 'Tidak' ? 'checked' : '' }}>
Pelatihan pengemasan dan pemasaranpelatihan_pemasaran) == 'Ya' ? 'checked' : '' }}>pelatihan_pemasaran) == 'Tidak' ? 'checked' : '' }}>
-
-
- - -
-
- -
Kapasitas Pengelolaan
-
-
-
Apakah sudah ada pencatatan/pembukuan keuangan?
-
status_pembukuan) == 'Sudah' ? 'checked' : '' }}>
-
status_pembukuan) == 'Belum ada' ? 'checked' : '' }}>
-
-
-
Format pembukuan yang dilakukan:
-
format_pembukuan) == 'Manual/buku kas' ? 'checked' : '' }}>
-
format_pembukuan) == 'Digital/file Excel' ? 'checked' : '' }}>
-
format_pembukuan) == 'Aplikasi khusus' ? 'checked' : '' }}>
-
format_pembukuan) == 'Tidak ada' ? 'checked' : '' }}>
-
-
-
-
Berapa kali diadakan pertemuan rutin anggota koperasi untuk membahas
rencana kerja dan pertanggungjawaban keuangan?
-
pertemuan_rutin) == 'Satu bulan sekali' ? 'checked' : '' }}>
-
pertemuan_rutin) == 'Satu tahun sekali' ? 'checked' : '' }}>
-
- pertemuan_rutin) == 'Lainnya' ? 'checked' : '' }}> - - -
-
- -
-
Khusus Koperasi Simpan Pinjam
-
-
- - -
-
- -
sanksi_kredit_macet) == 'Ada' ? 'checked' : '' }}>
-
sanksi_kredit_macet) == 'Tidak ada' ? 'checked' : '' }}>
-
-
- - -
-
-
- -
-
Apa faktor utama yang menyebabkan manajemen pengelolaan dalam koperasi efektif/tidak efektif?
-
faktor_manajemen ?? '', 'Rasa saling percaya') ? 'checked' : '' }}>
-
faktor_manajemen ?? '', 'Kepemimpinan koperasi') ? 'checked' : '' }}>
-
faktor_manajemen ?? '', 'Jumlah anggota') ? 'checked' : '' }}>
-
faktor_manajemen ?? '', 'Catatan/pembukuan rapi') ? 'checked' : '' }}>
-
- faktor_manajemen ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
-
- -
Pengalaman dalam mengelola rantai nilai komoditas pertanian
-
- -
-
keterlibatan_rantai_nilai ?? '', 'Penyediaan input') ? 'checked' : '' }}>
-
keterlibatan_rantai_nilai ?? '', 'Agregasi') ? 'checked' : '' }}>
-
keterlibatan_rantai_nilai ?? '', 'Pemasaran mentah') ? 'checked' : '' }}>
-
keterlibatan_rantai_nilai ?? '', 'Pengolahan barang jadi') ? 'checked' : '' }}>
-
keterlibatan_rantai_nilai ?? '', 'Pemasaran olahan') ? 'checked' : '' }}>
-
keterlibatan_rantai_nilai ?? '', 'Jual langsung konsumen') ? 'checked' : '' }}>
-
keterlibatan_rantai_nilai ?? '', 'Tidak terlibat kegiatan di atas') ? 'checked' : '' }}>
-
-
- -
-
Jika terlibat dalam proses agregasi, komoditas apa saja yang di agregasi oleh koperasi?
- -
agregasi_komoditas ?? '', 'Kopi') ? 'checked' : '' }}>
-
agregasi_komoditas ?? '', 'Kakao') ? 'checked' : '' }}>
-
agregasi_komoditas ?? '', 'Vanili') ? 'checked' : '' }}>
-
agregasi_komoditas ?? '', 'Kemiri') ? 'checked' : '' }}>
- -
- agregasi_komoditas ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
- -
- agregasi_komoditas ?? '', 'Tidak terlibat') ? 'checked' : '' }}> - -
-
- -
-
-
Fasilitas pengolahan pascapanen apa saja yang dikelola oleh koperasi saat ini?
-
fasilitas_koperasi ?? '', 'Gudang penyimpanan') ? 'checked' : '' }}>
-
fasilitas_koperasi ?? '', 'Alat pengering') ? 'checked' : '' }}>
-
fasilitas_koperasi ?? '', 'Mesin pemecah') ? 'checked' : '' }}>
-
fasilitas_koperasi ?? '', 'Mesin penggiling') ? 'checked' : '' }}>
-
fasilitas_koperasi ?? '', 'Mesin sortasi') ? 'checked' : '' }}>
-
- fasilitas_koperasi ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
-
- -
-
Apakah koperasi melakukan pengolahan lanjutan untuk menaikkan nilai jual
(seperti pengeringan, fermentasi, penepungan, pengemasan)?
-
- pengolahan_lanjutan) == 'Ya' ? 'checked' : '' }}> - - -
-
- pengolahan_lanjutan) == 'Tidak' ? 'checked' : '' }}> - -
-
- -
-
Bagaimana cara pengurus koperasi menentukan harga beli di tingkat petani anggota? (Transparan/fluktuasi/kualitas mutu)
- -
- -
-
Adakah kontrak kerjasama antara koperasi dengan mitra pembeli utama (offtaker) atau perusahaan?
-
- kontrak_offtaker) == 'Ada' ? 'checked' : '' }}> - - -
-
- kontrak_offtaker) == 'Tidak ada' ? 'checked' : '' }}> - -
-
-
-
-
- -
- -
+
Kapasitas Pengelolaan
+
+ + + + + + + + +
+
-
+
+ +
+
+
+ @csrf + + +
Rantai Nilai dan Pascapanen
+
+ + + +
+
+ + + + + + + +
+ +
Peluang Nilai Tambah (Pengolahan Pascapanen)
+
+ + + + + + + + + +
+ + + +
+
@endsection - -@push('scripts') - -@endpush \ No newline at end of file diff --git a/resources/views/koperasi/profil/_form.blade.php b/resources/views/koperasi/profil/_form.blade.php index 78798c4..e32d7cc 100644 --- a/resources/views/koperasi/profil/_form.blade.php +++ b/resources/views/koperasi/profil/_form.blade.php @@ -1,149 +1,175 @@ -
Data Pribadi Responden
-
-
- - - @error('nama_lengkap')
{{ $message }}
@enderror +@php + $currentJenis = old('jenis_koperasi', $pengurus->jenis_koperasi); + $lainnyaValue = \App\Enums\Koperasi\JenisKoperasi::LAINNYA->value; + $isLainnya = filled($currentJenis) && (\App\Enums\Koperasi\JenisKoperasi::tryFrom($currentJenis) === null || $currentJenis === $lainnyaValue); + $jenisLainnya = old('jenis_koperasi_lainnya', $isLainnya && $currentJenis !== $lainnyaValue ? $currentJenis : ''); + $currentFokus = old('fokus_komoditas', $pengurus->fokus_komoditas?->value); + $fokusYa = $currentFokus === \App\Enums\Koperasi\FokusKomoditasKoperasi::YA->value; +@endphp + +
Data Responden
+
+
+ + + @error('nama_lengkap')
{{ $message }}
@enderror
-
- - + + @foreach(\App\Enums\Koperasi\PosisiKoperasi::cases() as $posisi) + + @endforeach - @error('posisi_dalam_koperasi')
{{ $message }}
@enderror + @error('posisi_dalam_koperasi')
{{ $message }}
@enderror
-
- - +
+ + + @error('tahun_bergabung')
{{ $message }}
@enderror
-
- - +
+ + + @error('no_hp')
{{ $message }}
@enderror
-
- - +
+
+
+ + + @error('alamat_tinggal')
{{ $message }}
@enderror
-
Profil Koperasi
-
- - - @error('nama_koperasi')
{{ $message }}
@enderror -
- -
- -
- @foreach(['Koperasi Produksi', 'Koperasi Simpan Pinjam', 'Koperasi Pemasaran', 'Koperasi Desa'] as $jenis) -
-
- jenis_koperasi ?? '') == $jenis ? 'checked' : '' }} required> - -
+
Identitas Koperasi
+@if($wilayahTersedia) +
+ @foreach([ + 'provinsi_id' => ['Provinsi', 'provinsiOptions', 'provinsi', null], + 'kabupaten_kota_id' => ['Kabupaten/Kota', 'kabupatenOptions', 'kabupaten', 'provinsi_id'], + 'kecamatan_id' => ['Kecamatan', 'kecamatanOptions', 'kecamatan', 'kabupaten_kota_id'], + 'desa_kelurahan_id' => ['Desa/Kelurahan', 'desaOptions', 'desa', 'kecamatan_id'], + ] as $field => [$label, $options, $selector, $parent]) +
+ + + @error($field)
{{ $message }}
@enderror
@endforeach
-
- @php - $isLainnya = isset($pengurus) && !in_array($pengurus->jenis_koperasi, ['Koperasi Produksi', 'Koperasi Simpan Pinjam', 'Koperasi Pemasaran', 'Koperasi Desa']) && $pengurus->jenis_koperasi != null; - @endphp -
- - -
- +@endif +
+
+ + + @error('nama_koperasi')
{{ $message }}
@enderror
-
- -
- - -
- -
- -
- @foreach(['Level Dusun', 'Lintas Dusun', 'Level Desa', 'Lintas Desa', 'Level Kecamatan', 'Lintas Kecamatan', 'Level Kabupaten', 'Lintas Kabupaten'] as $skala) -
-
- skala_operasi ?? '') == $skala ? 'checked' : '' }} required> - -
-
- @endforeach +
+ + + @error('tahun_berdiri')
{{ $message }}
@enderror
-
- -
-
- - -
-
- +
+
- + orang + @error('jumlah_anggota_awal')
{{ $message }}
@enderror
- -
-
- -
-
-
- - dewasa -
-
-
-
- - anak-anak -
-
-
+
+
+ + + @error('jenis_koperasi')
{{ $message }}
@enderror
-
- -
- - orang -
+
+ + + @error('skala_operasi')
{{ $message }}
@enderror
- -
- -
-
- -
- Rp. - -
-
-
- -
- Rp. - -
-
-
- -
- Rp. - -
-
+
+
+ + + @error('jenis_koperasi_lainnya')
{{ $message }}
@enderror
+
+
+ + + @error('alamat_koperasi')
{{ $message }}
@enderror +
+
+
+
+ + + @error('sejarah_lembaga')
{{ $message }}
@enderror +
+
+ + + @error('fokus_komoditas')
{{ $message }}
@enderror +
+
+ + + @error('detail_fokus_komoditas')
{{ $message }}
@enderror +
+
+
+ @foreach([ + 'jumlah_anggota_dewasa' => 'Jumlah Anggota Dewasa (per Desember 2025)', + 'jumlah_anggota_anak' => 'Jumlah Anggota Anak (per Desember 2025)', + 'jumlah_pengurus' => 'Jumlah Pengurus Sekarang', + ] as $field => $label) +
+ +
+ + orang + @error($field)
{{ $message }}
@enderror +
+
+ @endforeach +
+
+ @foreach([2025, 2024, 2023] as $tahun) +
+ +
+ Rp. + +
+
+ @endforeach +
diff --git a/resources/views/koperasi/profil/add.blade.php b/resources/views/koperasi/profil/add.blade.php deleted file mode 100644 index 2c5dca9..0000000 --- a/resources/views/koperasi/profil/add.blade.php +++ /dev/null @@ -1,35 +0,0 @@ -@extends('layout.main') - -@section('content') -
- @if ($errors->any()) -
-
    - @foreach ($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - - @include('koperasi.profil.header', ['pengurus' => null]) - -
-
-
Tambah Profil Dasar Koperasi
-
-
-
- @csrf - - @include('koperasi.profil._form', ['pengurus' => null]) - -
- Batal / Kembali - -
-
-
-
-
-@endsection \ No newline at end of file diff --git a/resources/views/koperasi/profil/create.blade.php b/resources/views/koperasi/profil/create.blade.php new file mode 100644 index 0000000..c4e89e8 --- /dev/null +++ b/resources/views/koperasi/profil/create.blade.php @@ -0,0 +1,19 @@ +@extends('layout.main') + +@section('content') +
+ @include('koperasi.profil.header', ['pengurus' => null]) + +
+
+
+ @csrf + + @include('koperasi.profil._form', ['pengurus' => $pengurus]) + + + +
+
+
+@endsection diff --git a/resources/views/koperasi/profil/edit.blade.php b/resources/views/koperasi/profil/edit.blade.php index ec8d894..5a28ede 100644 --- a/resources/views/koperasi/profil/edit.blade.php +++ b/resources/views/koperasi/profil/edit.blade.php @@ -1,38 +1,23 @@ @extends('layout.main') @section('content') -
- @if ($errors->any()) -
-
    - @foreach ($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - +
@include('koperasi.profil.header', ['pengurus' => $pengurus])
-
Edit Profil Dasar Koperasi
+
Edit Profil Koperasi
-
+ @csrf @method('PUT') @include('koperasi.profil._form', ['pengurus' => $pengurus]) -
- Batal / Kembali - -
+
-@endsection \ No newline at end of file +@endsection diff --git a/resources/views/koperasi/profil/header.blade.php b/resources/views/koperasi/profil/header.blade.php index b26b598..fded50a 100644 --- a/resources/views/koperasi/profil/header.blade.php +++ b/resources/views/koperasi/profil/header.blade.php @@ -1,191 +1,37 @@ - + -@php - $namaLengkap = isset($pengurus) ? $pengurus->nama_lengkap : 'Tambah Profil Baru'; - $namaKoperasi = isset($pengurus) ? $pengurus->nama_koperasi : 'Nama Koperasi'; - $jabatanLabel = isset($pengurus) ? $pengurus->posisi_dalam_koperasi : 'Posisi/Jabatan'; - $noHp = isset($pengurus) ? $pengurus->no_hp : 'Belum terisi'; - $domisili = isset($pengurus) ? $pengurus->alamat_tinggal : 'Alamat Domisili Pengurus'; -@endphp - -
-
-
-
-
Profil Pengurus Koperasi
-
-
- - - -
-
-
{{ $namaLengkap }}
- {{ $namaKoperasi }} - Jabatan: {{ $jabatanLabel }} -
-
- -
-
Kontak & Domisili
-

No. HP: {{ $noHp }}

-

{{ $domisili }}

-
-
-
- -
-
-
-
-
-
-
-
-

Koperasi

-
{{ $namaKoperasi }}
-

- {{ isset($pengurus) ? $pengurus->jenis_koperasi : 'Belum terisi' }} -

-
- - - - - -
-
-
-
-
-

Posisi

-
{{ $jabatanLabel }}
-

- Thn Gabung: {{ isset($pengurus) ? ($pengurus->tahun_bergabung ?: '-') : '-' }} -

-
- - - - - -
-
-
-
-
-

Skala Operasi

-
{{ isset($pengurus) ? $pengurus->skala_operasi : '-' }}
-
- - - - - -
-
-
-
-
-
- -
-
-
-

Alamat Koperasi

-
{{ isset($pengurus) ? ($pengurus->alamat_koperasi ?: '-') : '-' }}
-
-
-
-
-
+ - \ No newline at end of file +@push('scripts') + + +@endpush diff --git a/resources/views/koperasi/profil/index.blade.php b/resources/views/koperasi/profil/index.blade.php index d1cf472..5a22164 100644 --- a/resources/views/koperasi/profil/index.blade.php +++ b/resources/views/koperasi/profil/index.blade.php @@ -1,160 +1,195 @@ @extends('layout.main') @section('content') -
- -
-
- +
+ + +
+
+
-
- +
+
-
- +
+ +
+
+ +
+
+
+
+
+ +
+
+ Wilayah +
+
+ +
+ {{ preg_replace('/^(kabupaten|kota)\s+/i', '', $selectedKabupaten?->nama ?? 'Semua Kabupaten/Kota') }} +
+
+
+ +
+
+ +
+
+
+
+
+ Data +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+ + Reset +
+
+
+
+
-
Data Pengurus Koperasi
+
Data Koperasi
-
+ + @foreach($filters as $field => $value) + + @endforeach
- - + + - + data
-
-
- - -
- - Tambah - +
+ Tambah
- +
- - - - - - - @if($showEnumerator ?? true) - - @endif - + + + + + + + @if($showEnumerator)@endif + - @forelse($pengurus ?? [] as $item) - - - - - - - - - @if($showEnumerator ?? true) - - @endif - + @forelse($koperasi as $item) + @php($ringkasan = $ringkasanKoperasi[$item->id]) + + + + + + + + @if($showEnumerator)@endif @empty - - - + @endforelse
NoNama PengurusPosisiNama KoperasiJenis KoperasiSkala OperasiNama EnumeratorAksiNoKoperasiJenis KoperasiLayanan UtamaDokumen KelembagaanNama RespondenNama EnumeratorAksi
{{ (($pengurus->firstItem() ?? 1) - 1) + $loop->iteration }}{{ $item->nama_lengkap }}{{ $item->posisi_dalam_koperasi }}{{ $item->nama_koperasi }}{{ $item->jenis_koperasi }}{{ $item->skala_operasi }}{{ $item->creator?->name ?? '-' }}
{{ $koperasi->firstItem() + $loop->index }} + {{ $ringkasan['judul'] }} +
{{ $ringkasan['alamat'] }}
+ @if(!$item->desa_kelurahan_id)
Wilayah belum terisi
@endif +
{{ $ringkasan['jenis'] }} + @if(count($ringkasan['layanan'])) +
{{ implode(', ', array_slice($ringkasan['layanan'], 0, 2)) }}
+ @if(count($ringkasan['layanan']) > 2)+{{ count($ringkasan['layanan']) - 2 }} lainnya@endif + @else + Belum terisi + @endif +
{{ $item->ad_art?->label() ?? 'Belum terisi' }}{{ $ringkasan['responden'] }}{{ $item->creator?->name ?? 'Belum terdata' }}
- - - - - - + +
+ @csrf + @method('DELETE') + +
Tidak ada data ditemukan.
Tidak ada data ditemukan.
@endsection @push('scripts') - -@endpush \ No newline at end of file + + +@endpush diff --git a/resources/views/koperasi/rantai-nilai-akses-pasar/index.blade.php b/resources/views/koperasi/rantai-nilai-akses-pasar/index.blade.php index dc52f56..4d47397 100644 --- a/resources/views/koperasi/rantai-nilai-akses-pasar/index.blade.php +++ b/resources/views/koperasi/rantai-nilai-akses-pasar/index.blade.php @@ -1,302 +1,100 @@ @extends('layout.main') @section('content') -
- +@php + $policy = app(\App\Services\Koperasi\RantaiNilaiKoperasiService::class); + $periods = \App\Services\Koperasi\RantaiNilaiKoperasiService::PERIODS; +@endphp +
@include('koperasi.profil.header', ['pengurus' => $pengurus]) -
-
-
+
+
+ @csrf - + -
RANTAI NILAI & AKSES PASAR
-
Ditanyakan khusus untuk Koperasi yang melakukan kegiatan jual beli/agregasi.
- -
K2. Harga di Setiap Titik
- -
Saluran Pembelian/Sumber Pasokan
-
-
Dari mana Koperasi biasanya memperoleh pasokan komoditas ini? (Boleh pilih >1)
- -
sumber_pasokan ?? '', 'Anggota') ? 'checked' : '' }}>
-
sumber_pasokan ?? '', 'Petani non-anggota') ? 'checked' : '' }}>
-
sumber_pasokan ?? '', 'Pengumpul/sub-agen') ? 'checked' : '' }}>
-
sumber_pasokan ?? '', 'Campuran') ? 'checked' : '' }}>
- -
- sumber_pasokan ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
-
- -
-
- -
bentuk_pembelian ?? '', 'Buah segar hasil petik') ? 'checked' : '' }}>
-
bentuk_pembelian ?? '', 'Biji basah') ? 'checked' : '' }}>
-
bentuk_pembelian ?? '', 'Biji kering/kupas') ? 'checked' : '' }}>
-
bentuk_pembelian ?? '', 'Produk terfermentasi') ? 'checked' : '' }}>
-
-
- -
- Panen terakhir - -
-
- Tahun lalu - -
-
- Dua tahun lalu - -
-
+
Saluran Penjualan/Pembelian
+
+ + + + @foreach($periods as $period => $label) + + @endforeach
-
Saluran Penjualan Utama
- -
-
Kepada siapa Koperasi biasanya menjual hasil yang dikumpulkan? (Boleh pilih >1)
-
-
tujuan_penjualan ?? '', 'Offtaker/Eksportir') ? 'checked' : '' }}>
-
tujuan_penjualan ?? '', 'Pedagang besar') ? 'checked' : '' }}>
-
tujuan_penjualan ?? '', 'Koperasi/BUMDes lain') ? 'checked' : '' }}>
-
tujuan_penjualan ?? '', 'Eceran') ? 'checked' : '' }}>
-
tujuan_penjualan ?? '', 'Belum pernah') ? 'checked' : '' }}>
-
-
- tujuan_penjualan ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
-
-
+
+ + + + @foreach($periods as $period => $label) + + @endforeach
-
-
- -
bentuk_penjualan ?? '', 'Buah segar hasil petik') ? 'checked' : '' }}>
-
bentuk_penjualan ?? '', 'Biji basah') ? 'checked' : '' }}>
-
bentuk_penjualan ?? '', 'Biji kering/kupas') ? 'checked' : '' }}>
-
bentuk_penjualan ?? '', 'Produk terfermentasi') ? 'checked' : '' }}>
-
-
- -
- Panen terakhir - -
-
- Tahun lalu - -
-
- Dua tahun lalu - -
-
-
- -
Sifat Hubungan dengan Petani Pemasok
- -
- -
sifat_hubungan_bisnis ?? '', 'Bebas') ? 'checked' : '' }}>
-
sifat_hubungan_bisnis ?? '', 'Langganan tetap') ? 'checked' : '' }}>
-
sifat_hubungan_bisnis ?? '', 'Berbasis hutang') ? 'checked' : '' }}>
-
sifat_hubungan_bisnis ?? '', 'Titip jual') ? 'checked' : '' }}>
-
sifat_hubungan_bisnis ?? '', 'Komitmen volume') ? 'checked' : '' }}>
-
sifat_hubungan_bisnis ?? '', 'Semi-kontrak') ? 'checked' : '' }}>
-
- sifat_hubungan_bisnis ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
-
- -
-
- -
mekanisme_pembayaran) == 'Tunai' ? 'checked' : '' }}>
-
mekanisme_pembayaran) == 'Tempo' ? 'checked' : '' }}>
-
mekanisme_pembayaran) == 'Campuran' ? 'checked' : '' }}>
-
-
- -
status_perjanjian_tertulis) == 'Tertulis' ? 'checked' : '' }}>
-
status_perjanjian_tertulis) == 'Lisan' ? 'checked' : '' }}>
-
status_perjanjian_tertulis) == 'Tidak ada' ? 'checked' : '' }}>
-
-
- -
K2. Harga di Setiap Titik
-
-
Harga Beli Koperasi dari Petani (Rp/kg)
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
BENTUK PRODUKPANEN TERAKHIRTAHUN LALUDUA TAHUN LALU
Buah segar petik
Rp
Rp
Rp
Biji basah
Rp
Rp
Rp
Biji kering/kupas
Rp
Rp
Rp
Terfermentasi
Rp
Rp
Rp
-
-
- -
- -
cara_menetapkan_harga_pasar ?? '', 'Ikut pasar') ? 'checked' : '' }}>
-
cara_menetapkan_harga_pasar ?? '', 'Kalkulasi biaya') ? 'checked' : '' }}>
-
cara_menetapkan_harga_pasar ?? '', 'Harga offtaker') ? 'checked' : '' }}>
-
- cara_menetapkan_harga_pasar ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
-
- -
-
K3. Peluang Nilai Tambah (Pengolahan Pascapanen)
- -
Sortasi & Pemilihan Kualitas
- -
-
-
praktik_sortasi) == 'Ya rutin' ? 'checked' : '' }}>
-
praktik_sortasi) == 'Ya tidak konsisten' ? 'checked' : '' }}>
-
praktik_sortasi) == 'Belum pernah' ? 'checked' : '' }}>
- - -
- - -
-
- -
Praktik Pengeringan
- -
-
-
praktik_pengeringan) == 'Ya rutin' ? 'checked' : '' }}>
-
praktik_pengeringan) == 'Kadang' ? 'checked' : '' }}>
-
praktik_pengeringan) == 'Tidak' ? 'checked' : '' }}>
- -
- - -
-
- -
Praktik Fermentasi
- -
-
-
praktik_fermentasi) == 'Ya rutin' ? 'checked' : '' }}>
-
praktik_fermentasi) == 'Pernah coba' ? 'checked' : '' }}>
-
praktik_fermentasi) == 'Belum, direncakan' ? 'checked' : '' }}>
-
praktik_fermentasi) == 'Belum, tidak' ? 'checked' : '' }}>
- -
- -
model_fermentasi ?? '', 'Individual') ? 'checked' : '' }}>
-
model_fermentasi ?? '', 'Kolektif') ? 'checked' : '' }}>
-
model_fermentasi ?? '', 'Unit desa') ? 'checked' : '' }}>
-
model_fermentasi ?? '', 'Campuran') ? 'checked' : '' }}>
-
- - -
-
- -
-
Besaran Premium Harga
-

Menurut pengalaman/pengamatan Koperasi, berapa kenaikan harga yang dapat dicapai jika sortasi/pengeringan/fermentasi standar diterapkan?

-
-
- Rp - - /kg -
- ATAU -
- - % -
-
-
-
- -
- - Sebelumnya - - -
- -
+
+ + + +
+
-
+
+ +
+
+
+ @csrf + + +
Harga di Setiap Titik
+
Harga Beli Koperasi dari Petani (Rp/kg)
+

Harga beli per kilogram untuk setiap bentuk produk dalam tiga musim panen terakhir.

+
+ + + + + @foreach($periods as $label)@endforeach + + + + @foreach(\App\Enums\Koperasi\BentukProdukKoperasi::cases() as $product) + + + @foreach($periods as $period => $label) + @php $field = 'harga_'.$product->pricePrefix().'_'.$period; @endphp + + @endforeach + + @endforeach + +
Bentuk Produk{{ $label }}
{{ $product->label() }} +
+ Rp + + /kg +
+ @error($field)
{{ $message }}
@enderror +
+
+
+ + +
+ + + +
+
@endsection - -@push('scripts') - -@endpush \ No newline at end of file diff --git a/resources/views/koperasi/sosial-ekonomi/index.blade.php b/resources/views/koperasi/sosial-ekonomi/index.blade.php index 98cca0e..3e734d8 100644 --- a/resources/views/koperasi/sosial-ekonomi/index.blade.php +++ b/resources/views/koperasi/sosial-ekonomi/index.blade.php @@ -1,153 +1,43 @@ @extends('layout.main') @section('content') -
- +@php + $policy = app(\App\Services\Koperasi\SosialEkonomiKoperasiService::class); + $context = $policy->context($pengurus); +@endphp +
@include('koperasi.profil.header', ['pengurus' => $pengurus]) -
-
-
+
+
+ @csrf - -
SOSIAL-EKONOMI RUMAH TANGGA
-
-
bagian tentang kendala & tantangan
-

Kuesioner bagian ini ditujukan untuk memetakan kestabilan finansial anggota koperasi yang bersangkutan

+
Kondisi Ekonomi Rumah Tangga
+
+ + + +
-
K2 KONDISI EKONOMI RUMAH TANGGA
-
Stabilitas penghasilan sepanjang tahun
- -
-
- -
- Rp. - -
-
-
- - -
-
- -
- -
-
alasan_pinjaman ?? '', 'Produksi') ? 'checked' : '' }}>
-
alasan_pinjaman ?? '', 'Pendidikan') ? 'checked' : '' }}>
-
alasan_pinjaman ?? '', 'Kesehatan') ? 'checked' : '' }}>
-
alasan_pinjaman ?? '', 'Acara keluarga') ? 'checked' : '' }}>
-
alasan_pinjaman ?? '', 'Renovasi') ? 'checked' : '' }}>
-
alasan_pinjaman ?? '', 'Sekunder') ? 'checked' : '' }}>
-
-
- alasan_pinjaman ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
-
-
-
- -
-
K6 IKATAN KEUANGAN
-
Keterlibatan Koperasi Simpan Pinjam dengan Petani Anggota Koperasi
- -
- Tanyakan jika koperasi merupakan koperasi simpan pinjam
- Pertanyaan pada bagian ini sensitif untuk sebagian orang, enumerator perlu melakukan strategi penggalian data tertentu kepada responden yang diwawancarai -
- -
- -
pembayaran_selain_uang) == 'Ada' ? 'checked' : '' }}>
-
pembayaran_selain_uang) == 'Tidak ada' ? 'checked' : '' }}>
-
- -
-
- -
- bentuk_pembayaran_barang ?? '', 'Hasil bumi') ? 'checked' : '' }}> - -
-
bentuk_pembayaran_barang ?? '', 'Ganti jasa') ? 'checked' : '' }}>
-
- bentuk_pembayaran_barang ?? '', 'Lainnya') ? 'checked' : '' }}> - - -
-
- -
-
-
perhitungan_harga_pasar) == 'Ya' ? 'checked' : '' }}>
-
- perhitungan_harga_pasar) == 'Tidak' ? 'checked' : '' }}> - - -
-
-
-
- -
- - Sebelumnya - - -
- -
+ @if($policy->active('pembayaran_selain_uang', [], $context)) +
Ikatan Keuangan
+ @endif +
+ + + + +
+
@endsection - -@push('scripts') - -@endpush \ No newline at end of file diff --git a/resources/views/layout/sidebar.blade.php b/resources/views/layout/sidebar.blade.php index 1bf20f9..9204072 100644 --- a/resources/views/layout/sidebar.blade.php +++ b/resources/views/layout/sidebar.blade.php @@ -119,7 +119,7 @@ @endif diff --git a/routes/web.php b/routes/web.php index 4984706..50a041d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -290,13 +290,19 @@ Route::middleware('auth') Route::resource('bumdes', App\Http\Controllers\Web\Bumdes\PengurusBumdesController::class) ->parameters(['bumdes' => 'pengurusBumdes']); - Route::delete('/koperasi/pengurus/{id}', [\App\Http\Controllers\Web\Koperasi\PengurusKoperasiController::class, 'destroy'])->name('koperasi.pengurus.destroy'); - // --- KOPERASI GROUP --- Route::prefix('koperasi')->name('koperasi.')->group(function () { - Route::resource('pengurus', App\Http\Controllers\Web\Koperasi\PengurusKoperasiController::class); - Route::get('pengurus/{id}/survey', [App\Http\Controllers\Web\Koperasi\PengurusKoperasiController::class, 'survey'])->name('pengurus.survey'); - Route::post('pengurus/{id}/survey', [App\Http\Controllers\Web\Koperasi\PengurusKoperasiController::class, 'surveyStore'])->name('pengurus.survey.store'); + Route::resource('pengurus.kelembagaan-aktor', App\Http\Controllers\Web\Koperasi\KelembagaanKoperasiController::class) + ->only(['index', 'store'])->parameters(['pengurus' => 'pengurusKoperasi'])->shallow(); + Route::resource('pengurus.rantai-nilai-akses-pasar', App\Http\Controllers\Web\Koperasi\RantaiNilaiKoperasiController::class) + ->only(['index', 'store'])->parameters(['pengurus' => 'pengurusKoperasi'])->shallow(); + Route::resource('pengurus.sosial-ekonomi', App\Http\Controllers\Web\Koperasi\SosialEkonomiKoperasiController::class) + ->only(['index', 'store'])->parameters(['pengurus' => 'pengurusKoperasi'])->shallow(); + Route::resource('pengurus.survey', App\Http\Controllers\Web\Koperasi\SurveyKoperasiController::class) + ->only(['index', 'store'])->parameters(['pengurus' => 'pengurusKoperasi']) + ->names(['index' => 'pengurus.survey', 'store' => 'pengurus.survey.store']); + Route::resource('pengurus', App\Http\Controllers\Web\Koperasi\PengurusKoperasiController::class) + ->except(['show'])->parameters(['pengurus' => 'pengurusKoperasi']); }); }); diff --git a/tests/Feature/KoperasiProfileTest.php b/tests/Feature/KoperasiProfileTest.php new file mode 100644 index 0000000..5e01ff1 --- /dev/null +++ b/tests/Feature/KoperasiProfileTest.php @@ -0,0 +1,1321 @@ + false]); + $this->withoutMiddleware(CatatKunjungan::class); + (require database_path('migrations/0001_01_01_000000_create_users_table.php'))->up(); + (require database_path('migrations/2026_02_12_110653_create_wilayah_table.php'))->up(); + (require database_path('migrations/2026_08_05_170222_create_pengurus_koperasi_table.php'))->up(); + (require database_path('migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php'))->up(); + Event::forget('composing: *'); + $user = new User(['name' => 'Admin Test', 'email' => 'admin@example.test', 'role' => 'admin']); + $user->id = 7; + $user->uid = 'test-admin'; + $user->username = 'admin-test'; + $user->password = 'test-password'; + $user->saveQuietly(); + $this->be($user); + $this->input = [ + 'nama_lengkap' => 'Budi Santoso', + 'posisi_dalam_koperasi' => 'Anggota koperasi', + 'nama_koperasi' => 'Koperasi Makmur', + 'jenis_koperasi' => 'Koperasi Produksi', + 'skala_operasi' => 'Level Desa', + 'tahun_bergabung' => 2020, + 'tahun_berdiri' => 2015, + 'jumlah_anggota_awal' => 0, + 'jumlah_anggota_dewasa' => 20, + 'jumlah_anggota_anak' => 0, + 'jumlah_pengurus' => 3, + 'shu_2025' => 1200000, + ]; + $this->dom = function (string $html): DOMXPath { + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($html); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + + return new DOMXPath($dom); + }; + $this->location = function (string $suffix = '1'): array { + $province = Provinsi::create(['kode' => $suffix, 'nama' => 'Provinsi '.$suffix, 'iso' => 'ID-'.$suffix]); + $district = KabupatenKota::create(['kode' => $suffix, 'nama' => 'Kabupaten '.$suffix, 'provinsi_id' => $province->id]); + $subdistrict = Kecamatan::create(['kode' => $suffix, 'nama' => 'Kecamatan '.$suffix, 'kabupaten_kota_id' => $district->id]); + $village = DesaKelurahan::create(['kode' => $suffix, 'nama' => 'Desa '.$suffix, 'kecamatan_id' => $subdistrict->id]); + + return ['provinsi_id' => $province->id, 'kabupaten_kota_id' => $district->id, 'kecamatan_id' => $subdistrict->id, 'desa_kelurahan_id' => $village->id]; + }; +}); + +test('koperasi creation renders the profile form without a redundant title and locks later tabs', function () { + $response = $this->get(route('koperasi.pengurus.create'))->assertOk() + ->assertViewIs('koperasi.profil.create')->assertDontSee('Tambah Profil Koperasi') + ->assertSee('Data Responden')->assertSee('Identitas Koperasi')->assertSee('assets/js/koperasi.js'); + $dom = ($this->dom)($response->getContent()); + expect($dom->query('//a[@data-koperasi-locked-tab and @aria-disabled="true" and @data-bs-original-title="Simpan Profil terlebih dahulu"]')->length)->toBe(3) + ->and($dom->query('//form[@data-koperasi-profile]//select')->length)->toBe(8) + ->and($dom->query('//a[contains(@class,"nav-link") and contains(@class,"active") and contains(.,"Profil")]')->length)->toBe(1) + ->and($dom->query('//section[@id="koperasi-profile-summary"]')->length)->toBe(1) + ->and(trim($dom->query('//*[@id="koperasi-profile-summary"]//*[@data-summary-field="anggota"]')->item(0)->textContent))->toBe('Belum terisi') + ->and(trim($dom->query('//*[@id="koperasi-profile-summary"]//*[@data-summary-field="diperbarui"]')->item(0)->textContent))->toBe('Belum tersimpan'); +}); + +test('koperasi list presents cooperative identity services and institutional documents', function () { + PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => 'Koperasi Makmur', 'jenis_koperasi' => 'Koperasi Simpan Pinjam', + 'alamat_koperasi' => 'Desa Maju', 'ad_art' => 'Ada, lengkap', + 'layanan_koperasi' => ['Simpan pinjam', 'Pengadaan input produksi', 'Pemasaran hasil', 'Lainnya'], 'layanan_lainnya' => 'Sewa alat', + ]); + $latest = PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => 'Koperasi Sejahtera', 'nama_lengkap' => 'Siti Responden', + 'ad_art' => 'Tidak tahu', + ]); + $response = $this->get(route('koperasi.pengurus.index'))->assertOk()->assertSee('Data Koperasi') + ->assertDontSee('Data Pengurus Koperasi')->assertViewHas('totalKoperasi', 2) + ->assertViewHas('totalJenisKoperasi', 2)->assertViewHas('totalDokumenLengkap', 1); + $dom = ($this->dom)($response->getContent()); + $table = '//table[contains(@class,"koperasi-list-table")]'; + $headings = array_map(fn ($node) => trim($node->textContent), iterator_to_array($dom->query($table.'/thead/tr/th'))); + expect($headings)->toBe(['No', 'Koperasi', 'Jenis Koperasi', 'Layanan Utama', 'Dokumen Kelembagaan', 'Nama Responden', 'Nama Enumerator', 'Aksi']) + ->and($dom->query($table.'/tbody/tr[1]/td[2]/a[@href="'.route('koperasi.pengurus.edit', $latest).'"]')->item(0)->textContent)->toBe('Koperasi Sejahtera') + ->and(trim($dom->query($table.'/tbody/tr[1]/td[5]')->item(0)->textContent))->toBe('Tidak tahu') + ->and(trim($dom->query($table.'/tbody/tr[2]/td[5]')->item(0)->textContent))->toBe('Ada dan lengkap') + ->and($dom->query($table.'//div[contains(text(),"Tahun Berdiri")]')->length)->toBe(0) + ->and($dom->query($table.'/tbody/tr[2]/td[4]//div[@title="Simpan pinjam, Pengadaan input produksi, Pemasaran hasil, Lainnya: Sewa alat"]')->length)->toBe(1) + ->and($dom->query($table.'/tbody/tr[2]/td[4]//span[text()="+2 lainnya"]')->length)->toBe(1) + ->and($dom->query('//form[@data-koperasi-delete and @data-nama="Koperasi Sejahtera"]//input[@name="_method" and @value="DELETE"]')->length)->toBe(1) + ->and($dom->query('//form[@data-koperasi-delete]//input[@name="_token"]')->length)->toBe(2); + $latest->update(['ad_art' => null]); + $dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent()); + expect(trim($dom->query($table.'/tbody/tr[1]/td[5]')->item(0)->textContent))->toBe('Belum terisi'); +}); + +test('koperasi list searches organization identity location type and respondent without case sensitivity', function (string $search) { + $match = PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => 'Koperasi Cahaya', 'alamat_koperasi' => 'Desa Bajawa', + 'jenis_koperasi' => 'Koperasi Pemasaran', 'nama_lengkap' => 'Siti Responden', + ]); + PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Koperasi Lain', 'nama_lengkap' => 'Budi']); + $response = $this->get(route('koperasi.pengurus.index', ['search' => $search]))->assertOk() + ->assertViewHas('totalKoperasi', 2)->assertViewHas('totalJenisKoperasi', 2); + expect($response->viewData('koperasi')->total())->toBe(1) + ->and($response->viewData('koperasi')->first()->id)->toBe($match->id); +})->with(['cahaya', 'BAJAWA', 'pemasaran', 'siti responden']); + +test('koperasi list pagination preserves searches and numbers rows across pages', function () { + $location = ($this->location)(); + for ($i = 1; $i <= 12; $i++) { + PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => 'Koperasi Paginasi '.$i, + 'layanan_koperasi' => ['Pengadaan input produksi'], 'ad_art' => 'Tidak tahu', 'alamat_koperasi' => 'Desa Maju, Kecamatan Sejahtera', + 'desa_kelurahan_id' => $location['desa_kelurahan_id'], + ]); + } + $filters = [ + 'search' => 'Paginasi', 'jenis_koperasi' => 'Koperasi Produksi', 'layanan' => 'Pengadaan input produksi', + 'ad_art' => 'Tidak tahu', 'sort_by' => 'nama_asc', ...array_diff_key($location, ['provinsi_id' => true]), + ]; + $url = route('koperasi.pengurus.index', [...$filters, 'per_page' => 10, 'page' => 2]); + $response = $this->get($url)->assertOk(); + expect($response->viewData('koperasi')->count())->toBe(2)->and($response->viewData('koperasi')->total())->toBe(12); + $dom = ($this->dom)($response->getContent()); + expect(trim($dom->query('//table/tbody/tr[1]/td[1]')->item(0)->textContent))->toBe('11') + ->and($dom->query('//form[@id="koperasi-filter-form"]//input[@name="search" and @value="Paginasi"]')->length)->toBe(1) + ->and($dom->query('//a[contains(@href,"search=Paginasi") and contains(@href,"per_page=10") and contains(@href,"page=1")]')->length)->toBeGreaterThan(0); + foreach ($filters as $name => $value) { + expect($dom->query('//form[@id="koperasi-pagination-form"]//input[@type="hidden" and @name="'.$name.'" and @value="'.$value.'"]')->length)->toBe(1); + } + $response = $this->get(route('koperasi.pengurus.index', ['per_page' => 25]))->assertOk(); + expect($response->viewData('koperasi')->perPage())->toBe(25)->and($response->viewData('koperasi')->count())->toBe(12); +}); + +test('koperasi list validates filter values', function (array $input, string $field) { + $this->getJson(route('koperasi.pengurus.index', $input))->assertUnprocessable()->assertJsonValidationErrors($field); +})->with([ + 'unexpected page size' => [['per_page' => 100], 'per_page'], + 'non numeric page size' => [['per_page' => 'semua'], 'per_page'], + 'array search' => [['search' => ['invalid']], 'search'], + 'array district' => [['kabupaten_kota_id' => ['invalid']], 'kabupaten_kota_id'], + 'unknown village' => [['desa_kelurahan_id' => 999], 'desa_kelurahan_id'], + 'overlong search' => [['search' => str_repeat('a', 256)], 'search'], + 'array cooperative type' => [['jenis_koperasi' => ['invalid']], 'jenis_koperasi'], + 'unknown service' => [['layanan' => 'invalid'], 'layanan'], + 'unknown institutional document status' => [['ad_art' => 'invalid'], 'ad_art'], + 'unknown sort order' => [['sort_by' => 'nama_koperasi desc; DROP TABLE pengurus_koperasi'], 'sort_by'], +]); + +test('koperasi data filters combine with searches and support saved custom cooperative types', function () { + $base = [ + ...$this->input, 'jenis_koperasi' => 'Koperasi Jasa', 'layanan_koperasi' => ['Pengadaan input produksi'], + 'ad_art' => 'Ada, lengkap', 'nama_koperasi' => 'Koperasi Bersama', + ]; + $match = PengurusKoperasi::create($base); + PengurusKoperasi::create([...$base, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']); + PengurusKoperasi::create([...$base, 'layanan_koperasi' => ['Pemasaran hasil']]); + PengurusKoperasi::create([...$base, 'ad_art' => 'Tidak ada']); + PengurusKoperasi::create([...$base, 'nama_koperasi' => 'Koperasi Lain']); + $filters = ['jenis_koperasi' => 'Koperasi Jasa', 'layanan' => 'Pengadaan input produksi', 'ad_art' => 'Ada, lengkap', 'search' => 'Bersama']; + $response = $this->get(route('koperasi.pengurus.index', $filters))->assertOk() + ->assertViewHas('totalKoperasi', 5)->assertViewHas('totalJenisKoperasi', 2)->assertViewHas('totalDokumenLengkap', 4); + expect($response->viewData('koperasi')->modelKeys())->toBe([$match->id]) + ->and($response->viewData('jenisKoperasiOptions')->all())->toContain('Koperasi Jasa'); + $dom = ($this->dom)($response->getContent()); + foreach (array_diff_key($filters, ['search' => true]) as $name => $value) { + expect($dom->query('//form[@id="koperasi-filter-form"]//select[@name="'.$name.'"]/option[@value="'.$value.'" and @selected]')->length)->toBe(1); + } + $otherTypes = $this->get(route('koperasi.pengurus.index', [...$filters, 'jenis_koperasi' => 'Lainnya']))->assertOk(); + expect($otherTypes->viewData('koperasi')->modelKeys())->toBe([$match->id]); +}); + +test('koperasi document summary counts only complete documents while filters leave totals unchanged', function () { + foreach (['Ada, lengkap', 'Ada tapi tidak lengkap', 'Tidak ada', 'Tidak tahu'] as $document) { + PengurusKoperasi::create([...$this->input, 'ad_art' => $document]); + } + $response = $this->get(route('koperasi.pengurus.index', ['ad_art' => 'Tidak ada']))->assertOk() + ->assertViewHas('totalKoperasi', 4)->assertViewHas('totalDokumenLengkap', 1); + expect($response->viewData('koperasi')->total())->toBe(1); +}); + +test('koperasi sorts names and complete member counts while unknown totals stay last', function (string $sort, array $expected) { + foreach ([['beta', 20, 0], ['Alpha', 0, 0], ['Gamma', 5, 0], ['Delta', null, 100]] as [$name, $adults, $children]) { + PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => $name, 'jumlah_anggota_dewasa' => $adults, 'jumlah_anggota_anak' => $children, + ]); + } + $response = $this->get(route('koperasi.pengurus.index', ['sort_by' => $sort]))->assertOk(); + expect($response->viewData('koperasi')->pluck('nama_koperasi')->all())->toBe($expected); +})->with([ + 'name ascending' => ['nama_asc', ['Alpha', 'beta', 'Delta', 'Gamma']], + 'name descending' => ['nama_desc', ['Gamma', 'Delta', 'beta', 'Alpha']], + 'fewest members' => ['anggota_asc', ['Alpha', 'Gamma', 'beta', 'Delta']], + 'most members' => ['anggota_desc', ['beta', 'Gamma', 'Alpha', 'Delta']], + 'latest entries' => ['terbaru', ['Delta', 'Gamma', 'Alpha', 'beta']], +]); + +test('koperasi list displays an empty state spanning the cooperative columns', function () { + $response = $this->get(route('koperasi.pengurus.index'))->assertOk()->assertViewHas('totalKoperasi', 0) + ->assertViewHas('totalJenisKoperasi', 0)->assertViewHas('totalDokumenLengkap', 0); + $dom = ($this->dom)($response->getContent()); + expect($dom->query('//table/tbody/tr/td[@colspan="8" and normalize-space(.)="Tidak ada data ditemukan."]')->length)->toBe(1); + expect($dom->query('//select[@name="jenis_koperasi"]/option')->length)->toBe(6); +}); + +test('koperasi regional dropdowns use saved location relationships rather than guesses from addresses', function (array $keys) { + $location = ($this->location)('1'); + $other = ($this->location)('2'); + $match = PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => 'Koperasi Cahaya', 'jenis_koperasi' => 'Koperasi Pemasaran', + 'desa_kelurahan_id' => $location['desa_kelurahan_id'], 'alamat_koperasi' => 'Alamat bebas', 'ad_art' => 'Ada, lengkap', + ]); + PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => 'Koperasi Cahaya', 'jenis_koperasi' => 'Koperasi Pemasaran', + 'desa_kelurahan_id' => $other['desa_kelurahan_id'], 'alamat_koperasi' => 'Desa 1, Kecamatan 1, Kabupaten 1', 'ad_art' => 'Ada, lengkap', + ]); + $unassigned = PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => 'Koperasi Cahaya', 'jenis_koperasi' => 'Koperasi Pemasaran', + 'alamat_koperasi' => 'Desa 1, Kecamatan 1, Kabupaten 1', 'ad_art' => 'Ada, lengkap', + ]); + $filters = array_intersect_key($location, array_flip($keys)); + $response = $this->get(route('koperasi.pengurus.index', [ + ...$filters, 'jenis_koperasi' => 'Koperasi Pemasaran', 'ad_art' => 'Ada, lengkap', 'search' => 'Cahaya', + ]))->assertOk()->assertViewHas('totalKoperasi', 3); + expect($response->viewData('koperasi')->modelKeys())->toBe(count($keys) === 1 ? [$unassigned->id, $match->id] : [$match->id]); + $dom = ($this->dom)($response->getContent()); + foreach ($filters as $field => $value) { + $control = $field === 'kabupaten_kota_id' + ? '//input[@type="hidden" and @name="'.$field.'" and @value="'.$value.'"]' + : '//select[@name="'.$field.'"]/option[@value="'.$value.'" and @selected]'; + expect($dom->query('//form[@id="koperasi-filter-form"]'.$control)->length)->toBe(1) + ->and($dom->query('//form[@id="koperasi-pagination-form"]//input[@type="hidden" and @name="'.$field.'" and @value="'.$value.'"]')->length)->toBe(1); + } +})->with([ + 'district' => [['kabupaten_kota_id']], + 'subdistrict' => [['kabupaten_kota_id', 'kecamatan_id']], + 'village' => [['kabupaten_kota_id', 'kecamatan_id', 'desa_kelurahan_id']], +]); + +test('koperasi captures the creator on create and retains that enumerator when another user updates location', function () { + $location = ($this->location)('1'); + $other = ($this->location)('2'); + $this->post(route('koperasi.pengurus.store'), [...$this->input, ...$location, 'created_by' => 999]) + ->assertSessionHasNoErrors()->assertRedirect(); + $profile = PengurusKoperasi::firstOrFail(); + expect($profile->created_by)->toBe(7)->and($profile->desa_kelurahan_id)->toBe($location['desa_kelurahan_id']); + $editor = User::create(['uid' => 'editor', 'username' => 'editor', 'name' => 'Editor Lain', 'email' => 'editor@example.test', 'password' => 'test', 'role' => 'admin']); + $this->be($editor); + $this->put(route('koperasi.pengurus.update', $profile), [...$this->input, ...$other, 'created_by' => $editor->id]) + ->assertSessionHasNoErrors(); + expect($profile->fresh()->created_by)->toBe(7)->and($profile->fresh()->desa_kelurahan_id)->toBe($other['desa_kelurahan_id']); + $dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent()); + expect(trim($dom->query('//table/tbody/tr[1]/td[7]')->item(0)->textContent))->toBe('Admin Test'); + $dom = ($this->dom)($this->get(route('koperasi.pengurus.edit', $profile))->assertOk()->getContent()); + foreach ($other as $field => $value) { + expect($dom->query('//form[@data-koperasi-profile]//select[@name="'.$field.'"]/option[@value="'.$value.'" and @selected]')->length)->toBe(1); + } + $this->put(route('koperasi.pengurus.update', $profile), [...$this->input, 'provinsi_id' => ''])->assertSessionHasNoErrors(); + expect($profile->fresh()->desa_kelurahan_id)->toBeNull()->and($profile->fresh()->created_by)->toBe(7); +}); + +test('koperasi defaults to Ngada and retains the selected district like the other profile filters', function () { + $location = ($this->location)('1'); + $other = ($this->location)('2'); + KabupatenKota::findOrFail($location['kabupaten_kota_id'])->update(['nama' => 'Kabupaten Ngada']); + $match = PengurusKoperasi::create([...$this->input, 'desa_kelurahan_id' => $location['desa_kelurahan_id']]); + $outside = PengurusKoperasi::create([...$this->input, 'desa_kelurahan_id' => $other['desa_kelurahan_id']]); + $legacy = PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Koperasi Lama Tanpa Desa']); + $response = $this->get(route('koperasi.pengurus.index'))->assertOk(); + expect($response->viewData('koperasi')->modelKeys())->toBe([$legacy->id, $match->id]); + $dom = ($this->dom)($response->getContent()); + expect($dom->query('//form[@id="koperasi-filter-form"]//input[@type="hidden" and @name="kabupaten_kota_id" and @value="'.$location['kabupaten_kota_id'].'"]')->length)->toBe(1) + ->and(trim($dom->query('//*[@role="text" and @aria-label="Kabupaten/Kota"]')->item(0)->textContent))->toBe('Ngada'); + $response = $this->get(route('koperasi.pengurus.index', ['kabupaten_kota_id' => $other['kabupaten_kota_id']]))->assertOk(); + expect($response->viewData('koperasi')->modelKeys())->toBe([$legacy->id, $outside->id]); +}); + +test('koperasi unassigned regions stay discoverable while data filters and selected subdistricts remain strict', function () { + $location = ($this->location)('1'); + KabupatenKota::findOrFail($location['kabupaten_kota_id'])->update(['nama' => 'Ngada']); + for ($i = 1; $i <= 12; $i++) { + PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Koperasi Lama '.$i]); + } + $mapped = PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Koperasi Terpetakan', 'desa_kelurahan_id' => $location['desa_kelurahan_id']]); + $response = $this->get(route('koperasi.pengurus.index', ['kabupaten_kota_id' => $location['kabupaten_kota_id'], 'page' => 2]))->assertOk(); + expect($response->viewData('koperasi')->total())->toBe(13)->and($response->viewData('koperasi')->count())->toBe(3); + $response = $this->get(route('koperasi.pengurus.index', ['jenis_koperasi' => 'Koperasi Simpan Pinjam']))->assertOk(); + expect($response->viewData('koperasi')->total())->toBe(0); + $response = $this->get(route('koperasi.pengurus.index', ['search' => 'Tidak cocok']))->assertOk(); + expect($response->viewData('koperasi')->total())->toBe(0); + $response = $this->get(route('koperasi.pengurus.index', ['kabupaten_kota_id' => $location['kabupaten_kota_id'], 'kecamatan_id' => $location['kecamatan_id']]))->assertOk(); + expect($response->viewData('koperasi')->modelKeys())->toBe([$mapped->id]); +}); + +test('koperasi rejects incomplete or mismatched regional hierarchy', function (string $scenario, string $field) { + $location = ($this->location)('1'); + $other = ($this->location)('2'); + $input = match ($scenario) { + 'incomplete' => ['provinsi_id' => $location['provinsi_id']], + 'province' => [...$location, 'provinsi_id' => $other['provinsi_id']], + 'district' => [...$location, 'kecamatan_id' => $other['kecamatan_id']], + 'village' => [...$location, 'desa_kelurahan_id' => $other['desa_kelurahan_id']], + }; + $this->post(route('koperasi.pengurus.store'), [...$this->input, ...$input])->assertSessionHasErrors($field); + if (! in_array($scenario, ['incomplete', 'province'], true)) { + $this->getJson(route('koperasi.pengurus.index', $input))->assertUnprocessable()->assertJsonValidationErrors($field); + } + expect(PengurusKoperasi::count())->toBe(0); +})->with([ + 'incomplete profile location' => ['incomplete', 'desa_kelurahan_id'], + 'wrong province for district' => ['province', 'kabupaten_kota_id'], + 'wrong district for subdistrict' => ['district', 'kecamatan_id'], + 'wrong subdistrict for village' => ['village', 'desa_kelurahan_id'], +]); + +test('koperasi shows missing enumerators honestly and follows the same role visibility as other profiles', function () { + PengurusKoperasi::create($this->input); + $dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent()); + expect(trim($dom->query('//table/tbody/tr[1]/td[7]')->item(0)->textContent))->toBe('Belum terdata'); + auth()->user()->role = 'enumerator'; + $dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent()); + expect($dom->query('//table/thead/tr/th[text()="Nama Enumerator"]')->length)->toBe(0) + ->and($dom->query('//table/tbody/tr[1]/td')->length)->toBe(7); +}); + +test('koperasi location dropdowns use existing master endpoints outside the active program scope', function () { + $location = ($this->location)('1'); + app()->instance('SystemProgram', (object) ['id' => 999, 'nama' => 'Program Test']); + foreach ([ + ['kabupatenkota.byprovinsi', $location['provinsi_id'], $location['kabupaten_kota_id']], + ['kecamatan.bykabupatenkota', $location['kabupaten_kota_id'], $location['kecamatan_id']], + ['desakelurahan.bykecamatan', $location['kecamatan_id'], $location['desa_kelurahan_id']], + ] as [$route, $parent, $expected]) { + $this->getJson(route($route, $parent).'?all_wilayah=1')->assertOk()->assertJsonFragment(['id' => $expected]); + } + $this->get(route('koperasi.pengurus.create'))->assertOk(); +}); + +test('koperasi keeps existing pages and saves working before the metadata migration is applied', function () { + (require database_path('migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php'))->down(); + $this->post(route('koperasi.pengurus.store'), $this->input)->assertSessionHasNoErrors(); + $profile = PengurusKoperasi::firstOrFail(); + $dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent()); + expect($dom->query('//select[@data-wilayah-kecamatan and @disabled]')->length)->toBe(1); + $dom = ($this->dom)($this->get(route('koperasi.pengurus.edit', $profile))->assertOk()->getContent()); + expect($dom->query('//*[@data-koperasi-wilayah]')->length)->toBe(0); + $this->put(route('koperasi.pengurus.update', $profile), [...$this->input, 'nama_lengkap' => 'Perubahan tetap tersimpan']) + ->assertSessionHasNoErrors(); + expect($profile->fresh()->nama_lengkap)->toBe('Perubahan tetap tersimpan'); +}); + +test('koperasi metadata migration preserves existing data and recovers only proven creation actors', function () { + $migration = require database_path('migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php'); + $migration->down(); + $original = PengurusKoperasi::create($this->input); + $editedOnly = PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Hanya Log Perubahan']); + $unknownActor = PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Penginput Tidak Diketahui']); + Schema::create('activity_log', function (Blueprint $table) { + $table->id(); + $table->string('subject_type'); + $table->unsignedBigInteger('subject_id'); + $table->string('causer_type'); + $table->unsignedBigInteger('causer_id'); + $table->string('event'); + }); + foreach ([[$original->id, 'created', 7], [$original->id, 'updated', 999], [$editedOnly->id, 'updated', 7], [$unknownActor->id, 'created', 999]] as [$subject, $event, $actor]) { + DB::table('activity_log')->insert([ + 'subject_type' => PengurusKoperasi::class, 'subject_id' => $subject, + 'causer_type' => User::class, 'causer_id' => $actor, 'event' => $event, + ]); + } + $migration->up(); + expect($original->fresh()->created_by)->toBe(7)->and($original->fresh()->desa_kelurahan_id)->toBeNull() + ->and($editedOnly->fresh()->created_by)->toBeNull()->and($unknownActor->fresh()->created_by)->toBeNull() + ->and(PengurusKoperasi::count())->toBe(3); + $migration->down(); + expect(PengurusKoperasi::count())->toBe(3)->and($original->fresh()->nama_koperasi)->toBe('Koperasi Makmur'); +}); + +test('koperasi summary shows organization activities and respondent contacts on every tab', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => 'Koperasi Makmur Bersama', 'jenis_koperasi' => 'Koperasi Jasa', + 'alamat_koperasi' => 'Desa Maju, Kecamatan Sejahtera', 'no_hp' => '081234567890', + 'jumlah_anggota_dewasa' => 1200, 'jumlah_anggota_anak' => 30, 'jumlah_pengurus' => 7, + 'fokus_komoditas' => 'Ya', 'detail_fokus_komoditas' => "Kopi, Kakao; Vanili\nKemiri, Kopi", + 'layanan_koperasi' => ['Simpan pinjam', 'Pengadaan input produksi', 'Pemasaran hasil', 'Lainnya'], + 'layanan_lainnya' => 'Sewa alat', 'shu_2025' => 25000000, 'shu_2024' => 10000000, + ]); + $routes = ['koperasi.pengurus.edit', ...array_map(fn ($tab) => $tab->routePrefix().'.index', TabKoperasi::cases())]; + foreach ($routes as $route) { + $response = $this->get(route($route, $profile))->assertOk(); + $dom = ($this->dom)($response->getContent()); + $card = '//*[@id="koperasi-profile-summary"]'; + foreach ([ + 'judul' => 'Koperasi Makmur Bersama', 'jenis' => 'Koperasi Jasa', + 'alamat' => 'Desa Maju, Kecamatan Sejahtera', 'tahun_berdiri' => '2015', + 'anggota' => '1.230', 'pengurus' => '7', + 'responden' => 'Budi Santoso', 'jabatan' => 'Anggota koperasi', 'kontak' => '081234567890', + ] as $field => $value) { + expect(trim($dom->query($card.'//*[@data-summary-field="'.$field.'"]')->item(0)->textContent))->toBe($value); + } + expect($dom->query($card.'//*[@data-summary-field="diperbarui"]//time[@datetime]')->length)->toBe(1) + ->and($dom->query($card.'//*[@data-summary-field="shu" or @data-summary-field="shu_label"]')->length)->toBe(0) + ->and($dom->query($card.'//*[@data-summary-field="komoditas" or @data-summary-field="skala"]')->length)->toBe(0) + ->and($dom->query($card.'//button[@data-bs-target="#koperasi-summary-extra-layanan" and normalize-space(.)="+2 lainnya"]')->length)->toBe(1) + ->and($dom->query($card.'//*[@id="koperasi-summary-extra-layanan"]//span[text()="Lainnya: Sewa alat"]')->length)->toBe(1); + } +}); + +test('koperasi summary distinguishes missing counts from zero', function (array $input, array $expected) { + $profile = PengurusKoperasi::create([...$this->input, ...$input]); + $response = $this->get(route('koperasi.pengurus.edit', $profile))->assertOk(); + $dom = ($this->dom)($response->getContent()); + foreach ($expected as $field => $value) { + expect(trim($dom->query('//*[@id="koperasi-profile-summary"]//*[@data-summary-field="'.$field.'"]')->item(0)->textContent))->toBe($value); + } +})->with([ + 'zero members remain visible' => [ + ['jumlah_anggota_dewasa' => 0, 'jumlah_anggota_anak' => 0, 'jumlah_pengurus' => 0], + ['anggota' => '0', 'pengurus' => '0'], + ], + 'incomplete counts' => [ + ['jumlah_anggota_dewasa' => 20, 'jumlah_anggota_anak' => null, 'jumlah_pengurus' => null], + ['anggota' => 'Belum terisi', 'pengurus' => 'Belum terisi'], + ], + 'no counts' => [ + ['jumlah_anggota_dewasa' => null, 'jumlah_anggota_anak' => null, 'jumlah_pengurus' => null], + ['anggota' => 'Belum terisi', 'pengurus' => 'Belum terisi'], + ], +]); + +test('koperasi summary escapes custom labels', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'nama_koperasi' => '', + 'fokus_komoditas' => 'Tidak', 'detail_fokus_komoditas' => 'Kopi, Kakao', + 'layanan_koperasi' => ['Lainnya'], 'layanan_lainnya' => '', + ]); + $response = $this->get(route('koperasi.pengurus.edit', $profile))->assertOk(); + $dom = ($this->dom)($response->getContent()); + $card = '//*[@id="koperasi-profile-summary"]'; + expect($dom->query($card.'//script | '.$card.'//img')->length)->toBe(0) + ->and($dom->query($card.'//*[@data-summary-field="komoditas"]//span[contains(@class,"badge")]')->length)->toBe(0) + ->and($dom->query($card.'//button[@data-bs-toggle="collapse"]')->length)->toBe(0); +}); + +test('koperasi profile saves custom types and updates the bound record without changing survey data', function () { + $this->post(route('koperasi.pengurus.store'), [ + ...$this->input, 'jenis_koperasi' => 'Lainnya', 'jenis_koperasi_lainnya' => 'Koperasi Jasa', + ])->assertRedirect()->assertSessionHas('success', 'Profil koperasi berhasil ditambahkan.'); + $profile = PengurusKoperasi::firstOrFail(); + expect($profile->jenis_koperasi)->toBe('Koperasi Jasa') + ->and($profile->posisi_dalam_koperasi)->toBe(PosisiKoperasi::ANGGOTA) + ->and($profile->skala_operasi)->toBe(SkalaOperasiKoperasi::LEVEL_DESA) + ->and($profile->jumlah_anggota_awal)->toBe(0) + ->and($profile->profilLengkap())->toBeTrue(); + $profile->update(['ad_art' => 'Ada, lengkap']); + + $this->put(route('koperasi.pengurus.update', $profile->id), [ + ...$this->input, 'nama_lengkap' => 'Budi Baru', 'sejarah_lembaga' => 'Riwayat koperasi', 'ad_art' => 'Di luar tab profil', + ])->assertRedirect(route('koperasi.pengurus.edit', $profile->id))->assertSessionHas('success', 'Profil koperasi berhasil diperbarui.'); + expect($profile->fresh()->nama_lengkap)->toBe('Budi Baru') + ->and($profile->fresh()->jenis_koperasi)->toBe('Koperasi Produksi') + ->and($profile->fresh()->sejarah_lembaga)->toBe('Riwayat koperasi') + ->and($profile->fresh()->ad_art->value)->toBe('Ada, lengkap') + ->and(PengurusKoperasi::count())->toBe(1); + $this->get(route('koperasi.pengurus.edit', 999))->assertNotFound(); + $this->put(route('koperasi.pengurus.update', 999), $this->input)->assertNotFound(); + $this->get(route('koperasi.pengurus.index'))->assertOk()->assertSee('Data Koperasi')->assertSee('Koperasi Makmur')->assertSee('Koperasi Produksi'); +}); + +test('koperasi profile rejects invalid selections with Indonesian validation messages', function () { + $response = $this->from(route('koperasi.pengurus.create'))->post(route('koperasi.pengurus.store'), [ + ...$this->input, 'nama_lengkap' => '', 'posisi_dalam_koperasi' => 'Pilihan salah', + 'jenis_koperasi' => 'Pilihan salah', 'skala_operasi' => 'Pilihan salah', 'shu_2025' => 'abc', + ]); + $response->assertRedirect(route('koperasi.pengurus.create'))->assertSessionHasErrors([ + 'nama_lengkap' => 'Nama responden wajib diisi.', + 'posisi_dalam_koperasi' => 'Pilihan Posisi dalam koperasi tidak valid.', + 'jenis_koperasi' => 'Pilihan Jenis koperasi tidak valid.', + 'skala_operasi' => 'Pilihan Skala operasi koperasi tidak valid.', + 'shu_2025' => 'SHU 2025 harus berupa angka.', + ]); + expect(PengurusKoperasi::count())->toBe(0); +}); + +test('koperasi editing restores custom type and gives old form input precedence after validation fails', function () { + $profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Jasa']); + $editUrl = route('koperasi.pengurus.edit', $profile->id); + $dom = ($this->dom)($this->get($editUrl)->assertOk()->getContent()); + expect($dom->query('//input[@name="jenis_koperasi_lainnya" and @value="Koperasi Jasa" and not(@disabled)]')->length)->toBe(1) + ->and($dom->query('//select[@name="jenis_koperasi"]/option[@value="Lainnya" and @selected]')->length)->toBe(1) + ->and($dom->query('//*[@aria-disabled="true"]')->length)->toBe(0); + + $this->from($editUrl)->put(route('koperasi.pengurus.update', $profile->id), [ + ...$this->input, 'nama_lengkap' => 'Nama Koreksi', 'jenis_koperasi' => 'Koperasi Desa', 'shu_2025' => 'abc', + ])->assertRedirect($editUrl)->assertSessionHasErrors('shu_2025'); + $dom = ($this->dom)($this->get($editUrl)->assertOk()->getContent()); + expect($dom->query('//input[@name="nama_lengkap" and @value="Nama Koreksi"]')->length)->toBe(1) + ->and($dom->query('//select[@name="jenis_koperasi"]/option[@value="Koperasi Desa" and @selected]')->length)->toBe(1) + ->and($dom->query('//input[@name="jenis_koperasi_lainnya" and @disabled]')->length)->toBe(1) + ->and($profile->fresh()->jenis_koperasi)->toBe('Koperasi Jasa'); +}); + +test('koperasi later tabs enforce profile completion for direct reads and writes', function () { + $profile = PengurusKoperasi::create([...$this->input, 'nama_lengkap' => '']); + foreach (TabKoperasi::cases() as $tab) { + $this->get(route('koperasi.pengurus.survey', [$profile->id, 'step' => $tab->value]))->assertForbidden(); + $this->post(route('koperasi.pengurus.survey.store', [$profile->id, 'step' => $tab->value]), [])->assertForbidden(); + $this->get(route($tab->routePrefix().'.index', $profile))->assertForbidden(); + $this->post(route($tab->routePrefix().'.store', $profile), [])->assertForbidden(); + } + $profile->update(['nama_lengkap' => 'Budi Santoso']); + foreach (TabKoperasi::cases() as $tab) { + $url = route($tab->routePrefix().'.index', $profile); + $this->get(route('koperasi.pengurus.survey', [$profile->id, 'step' => $tab->value]))->assertRedirect($url); + $this->get($url)->assertOk(); + } +}); + +test('koperasi save stays on each active tab and history and commodity focus belong to profile', function () { + $response = $this->post(route('koperasi.pengurus.store'), [ + ...$this->input, 'sejarah_lembaga' => 'Didirikan untuk pemasaran bersama', + 'fokus_komoditas' => 'Ya', 'detail_fokus_komoditas' => 'Kopi, Vanili', + ]); + $profile = PengurusKoperasi::firstOrFail(); + $response->assertRedirect(route('koperasi.pengurus.edit', $profile)); + expect($profile->fokus_komoditas)->toBe(FokusKomoditasKoperasi::YA); + $this->get(route('koperasi.pengurus.edit', $profile))->assertOk() + ->assertSee('Sejarah dan Tujuan Pendirian')->assertSee('Fokus Komoditas Pertanian')->assertSee('Kopi, Vanili'); + + foreach (TabKoperasi::cases() as $tab) { + $url = route($tab->routePrefix().'.index', $profile); + $this->post(route($tab->routePrefix().'.store', $profile), [ + 'nama_lengkap' => 'Tidak boleh ditimpa', 'sejarah_lembaga' => 'Tidak boleh dipindah lagi', + 'fokus_komoditas' => 'Tidak', 'detail_fokus_komoditas' => 'Tidak boleh ditimpa', + ])->assertRedirect($url)->assertSessionHas('success'); + $dom = ($this->dom)($this->get($url)->assertOk()->getContent()); + $actionCount = $tab === TabKoperasi::SOSIAL_EKONOMI ? 1 : 2; + expect($dom->query('//a[@href="'.route('koperasi.pengurus.index').'" and normalize-space(.)="Kembali"]')->length)->toBe($actionCount) + ->and($dom->query('//button[@type="submit" and normalize-space(.)="Simpan"]')->length)->toBe($actionCount) + ->and($dom->query('//*[@name="sejarah_lembaga" or @name="fokus_komoditas" or @name="detail_fokus_komoditas"]')->length)->toBe(0) + ->and($dom->query('//a[contains(@class,"nav-link") and contains(@class,"active") and @href="'.$url.'"]')->length)->toBe(1); + } + expect($profile->fresh()->nama_lengkap)->toBe('Budi Santoso') + ->and($profile->fresh()->sejarah_lembaga)->toBe('Didirikan untuk pemasaran bersama') + ->and($profile->fresh()->fokus_komoditas)->toBe(FokusKomoditasKoperasi::YA) + ->and($profile->fresh()->detail_fokus_komoditas)->toBe('Kopi, Vanili'); +}); + +test('all koperasi rupiah inputs accept thousands separators and render formatted persisted values', function () { + $this->post(route('koperasi.pengurus.store'), [ + ...$this->input, 'shu_2025' => '1.234.567', 'shu_2024' => '0', 'shu_2023' => '-1.250.000', + ])->assertSessionHasNoErrors(); + $profile = PengurusKoperasi::firstOrFail(); + $profileDom = ($this->dom)($this->get(route('koperasi.pengurus.edit', $profile))->assertOk()->getContent()); + expect($profileDom->query('//input[@data-koperasi-rupiah and @name="shu_2025" and @value="1.234.567" and @type="text"]')->length)->toBe(1) + ->and((int) $profile->shu_2025)->toBe(1234567) + ->and((int) $profile->shu_2024)->toBe(0) + ->and((int) $profile->shu_2023)->toBe(-1250000); + + $prices = []; + foreach (['segar', 'basah', 'kering', 'fermentasi'] as $bentuk) { + foreach (['sekarang', '1_tahun', '2_tahun'] as $periode) { + $prices['harga_'.$bentuk.'_'.$periode] = '1.234.567'; + } + } + $this->post(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.store', $profile), [ + ...$prices, 'volume_beli_sekarang' => 1000, + 'bentuk_pembelian' => array_map(fn ($case) => $case->value, BentukProdukKoperasi::cases()), + ])->assertRedirect(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.index', $profile))->assertSessionHasNoErrors(); + foreach ($prices as $field => $formatted) { + expect((int) $profile->fresh()->{$field})->toBe((int) str_replace('.', '', $formatted)); + } + $dom = ($this->dom)($this->get(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@data-koperasi-rupiah]')->length)->toBe(12) + ->and($dom->query('//input[@name="harga_segar_sekarang" and @value="1.234.567"]')->length)->toBe(1) + ->and($dom->query('//input[@name="volume_beli_sekarang" and not(@data-koperasi-rupiah)]')->length)->toBe(1) + ->and($dom->query('//input[@name="harga_premium_persen"]')->length)->toBe(0); + $this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), [ + 'praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Ukuran biji', 'harga_premium_rupiah' => '10.000', + ])->assertSessionHasNoErrors(); + expect((int) $profile->fresh()->harga_premium_rupiah)->toBe(10000); + $opDom = ($this->dom)($this->get(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.index', $profile))->assertOk()->getContent()); + expect($opDom->query('//input[@name="harga_premium_rupiah" and @data-koperasi-rupiah and @value="10.000"]')->length)->toBe(1) + ->and($opDom->query('//input[@name="harga_premium_persen" and not(@data-koperasi-rupiah)]')->length)->toBe(1); + $this->post(route(TabKoperasi::SOSIAL_EKONOMI->routePrefix().'.store', $profile), ['simpanan_terbesar' => '2.500.000'])->assertSessionHasNoErrors(); + expect((int) $profile->fresh()->simpanan_terbesar)->toBe(2500000); + $this->get(route(TabKoperasi::SOSIAL_EKONOMI->routePrefix().'.index', $profile))->assertOk()->assertSee('2.500.000'); +}); + +test('koperasi tab validation rejects bad rupiah and clears unchecked saved selections', function () { + $profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']); + $route = TabKoperasi::SOSIAL_EKONOMI->routePrefix(); + $this->post(route($route.'.store', $profile), ['alasan_pinjaman' => ['Produksi'], 'simpanan_terbesar' => '100.000'])->assertSessionHasNoErrors(); + $this->from(route($route.'.index', $profile))->post(route($route.'.store', $profile), [ + 'simpanan_terbesar' => '1.23.000', 'alasan_pinjaman' => ['Pendidikan'], + ])->assertSessionHasErrors('simpanan_terbesar'); + expect((int) $profile->fresh()->simpanan_terbesar)->toBe(100000); + $dom = ($this->dom)($this->get(route($route.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@name="alasan_pinjaman[]" and @value="Pendidikan" and @checked]')->length)->toBe(1) + ->and($dom->query('//input[@name="alasan_pinjaman[]" and @value="Produksi" and @checked]')->length)->toBe(0); + $this->post(route($route.'.store', $profile), [])->assertRedirect(route($route.'.index', $profile)); + expect($profile->fresh()->alasan_pinjaman)->toHaveCount(0); + $this->post(route('koperasi.pengurus.survey.store', [$profile->id, 'step' => 3]), ['simpanan_terbesar' => '1.000.000']) + ->assertRedirect(route($route.'.index', $profile))->assertSessionHasNoErrors(); + expect((int) $profile->fresh()->simpanan_terbesar)->toBe(1000000); +}); + +test('koperasi profile hides and clears commodity details when its focus is disabled', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'fokus_komoditas' => 'Ya', 'detail_fokus_komoditas' => 'Kopi', + ]); + $this->put(route('koperasi.pengurus.update', $profile), [ + ...$this->input, 'fokus_komoditas' => 'Tidak', 'detail_fokus_komoditas' => 'Data lama', + ])->assertRedirect(route('koperasi.pengurus.edit', $profile))->assertSessionHasNoErrors(); + expect($profile->fresh()->detail_fokus_komoditas)->toBeNull(); + $dom = ($this->dom)($this->get(route('koperasi.pengurus.edit', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@name="detail_fokus_komoditas" and @disabled]')->length)->toBe(1); +}); + +test('social covers savings loan needs and payment ties with the farmer group social icon', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'ad_art' => 'Tidak ada', 'harga_segar_sekarang' => 12000, + ]); + $prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix(); + $input = [ + 'simpanan_terbesar' => '2.500.000', 'momen_simpanan_terbesar' => 'Juni, saat panen kopi', + 'alasan_pinjaman' => ['Produksi', 'Pendidikan', 'Kesehatan', 'Acara keluarga', 'Renovasi', 'Sekunder', 'Lainnya'], + 'alasan_pinjaman_lainnya' => 'Modal dagang', 'pembayaran_selain_uang' => 'Ada', + 'bentuk_pembayaran_barang' => ['Hasil bumi', 'Ganti jasa', 'Lainnya'], 'bentuk_pembayaran_lainnya' => 'Barang dagangan', + 'perhitungan_harga_pasar' => 'Tidak', 'alasan_bukan_harga_pasar' => 'Harga disepakati pada awal pinjaman', + ]; + $this->post(route($prefix.'.store', $profile), [...$input, 'ad_art' => 'Ada, lengkap', 'harga_segar_sekarang' => 1]) + ->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile)); + $profile->refresh(); + foreach ($input as $field => $expected) { + $actual = $profile->{$field}; + if ($actual instanceof Collection) { + $actual = $actual->map(fn ($case) => $case->value)->all(); + } elseif ($actual instanceof BackedEnum) { + $actual = $actual->value; + } + if ($field === 'simpanan_terbesar') { + $actual = (int) $actual; + $expected = 2500000; + } + expect($actual)->toBe($expected); + } + expect($profile->ad_art->value)->toBe('Tidak ada')->and((int) $profile->harga_segar_sekarang)->toBe(12000) + ->and(json_decode($profile->getRawOriginal('alasan_pinjaman'), true))->toBe($input['alasan_pinjaman']) + ->and(json_decode($profile->getRawOriginal('bentuk_pembayaran_barang'), true))->toBe($input['bentuk_pembayaran_barang']); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//a[@href="'.route($prefix.'.index', $profile).'"]//i[contains(@class,"tabler-heart-handshake")]')->length)->toBe(1) + ->and($dom->query('//form[@data-koperasi-sosial-ekonomi]//input[@type="checkbox" and @checked and not(@disabled)]')->length)->toBe(10) + ->and($dom->query('//input[@name="simpanan_terbesar" and @data-koperasi-rupiah and @value="2.500.000"]')->length)->toBe(1) + ->and($dom->query('//select[@name="perhitungan_harga_pasar"]//option[@value="Tidak" and @selected]')->length)->toBe(1) + ->and($dom->query('//textarea[@name="alasan_bukan_harga_pasar" and @required and not(@disabled)]')->length)->toBe(1); + foreach ($dom->query('//form[@data-koperasi-sosial-ekonomi]//label | //form[@data-koperasi-sosial-ekonomi]//legend') as $label) { + expect($label->textContent)->not->toContain('?'); + } + foreach ($dom->query('//form[@data-koperasi-sosial-ekonomi]//h5') as $heading) { + expect(trim($heading->textContent))->not->toMatch('/^(K\d|\d+[.])/'); + } +}); + +test('social follow ups and choices validate only applicable questions', function (array $input, string $field) { + $profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']); + $this->post(route(TabKoperasi::SOSIAL_EKONOMI->routePrefix().'.store', $profile), $input)->assertSessionHasErrors($field); + expect($profile->fresh()->pembayaran_selain_uang)->toBeNull()->and($profile->fresh()->simpanan_terbesar)->toBeNull(); +})->with([ + 'other loan need requires detail' => [['alasan_pinjaman' => ['Lainnya']], 'alasan_pinjaman_lainnya'], + 'non cash payments require forms' => [['pembayaran_selain_uang' => 'Ada'], 'bentuk_pembayaran_barang'], + 'other payment form requires detail' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Lainnya']], 'bentuk_pembayaran_lainnya'], + 'harvest payments require price basis' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi']], 'perhitungan_harga_pasar'], + 'not using market price requires reason' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi'], 'perhitungan_harga_pasar' => 'Tidak'], 'alasan_bukan_harga_pasar'], + 'invalid loan need' => [['alasan_pinjaman' => ['Pilihan salah']], 'alasan_pinjaman.0'], + 'invalid payment mechanism' => [['pembayaran_selain_uang' => 'Pilihan salah'], 'pembayaran_selain_uang'], + 'invalid payment form' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Pilihan salah']], 'bentuk_pembayaran_barang.0'], + 'invalid price basis' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi'], 'perhitungan_harga_pasar' => 'Pilihan salah'], 'perhitungan_harga_pasar'], + 'negative savings' => [['simpanan_terbesar' => -1000], 'simpanan_terbesar'], +]); + +test('social loan questions use saved cooperative type rather than services or a forged request type', function (string $type, array $services, bool $isCreditCooperative) { + $profile = PengurusKoperasi::create([ + ...$this->input, 'jenis_koperasi' => $type, 'layanan_koperasi' => $services, + 'alasan_pinjaman' => ['Lainnya'], 'alasan_pinjaman_lainnya' => 'Kebutuhan lama', + 'pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi', 'Lainnya'], + 'bentuk_pembayaran_lainnya' => 'Barang lama', 'perhitungan_harga_pasar' => 'Tidak', 'alasan_bukan_harga_pasar' => 'Alasan lama', + ]); + $prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix(); + $input = $isCreditCooperative ? [ + 'jenis_koperasi' => 'Koperasi Produksi', 'alasan_pinjaman' => ['Pendidikan'], 'pembayaran_selain_uang' => 'Tidak ada', + ] : [ + 'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'alasan_pinjaman' => 'Pilihan salah', 'alasan_pinjaman_lainnya' => ['data tersembunyi'], + 'pembayaran_selain_uang' => 'Pilihan salah', 'bentuk_pembayaran_barang' => ['Pilihan salah'], + 'bentuk_pembayaran_lainnya' => ['data tersembunyi'], 'perhitungan_harga_pasar' => 'Pilihan salah', 'alasan_bukan_harga_pasar' => ['data tersembunyi'], + ]; + $this->post(route($prefix.'.store', $profile), [...$input, 'simpanan_terbesar' => '125.000', 'momen_simpanan_terbesar' => 'Panen']) + ->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile)); + $profile->refresh(); + expect($profile->jenis_koperasi)->toBe($type)->and((int) $profile->simpanan_terbesar)->toBe(125000) + ->and($profile->momen_simpanan_terbesar)->toBe('Panen') + ->and($profile->alasan_pinjaman->map(fn ($case) => $case->value)->all())->toBe($isCreditCooperative ? ['Pendidikan'] : []) + ->and($profile->pembayaran_selain_uang?->value)->toBe($isCreditCooperative ? 'Tidak ada' : null) + ->and($profile->alasan_pinjaman_lainnya)->toBeNull()->and($profile->bentuk_pembayaran_barang)->toHaveCount(0) + ->and($profile->bentuk_pembayaran_lainnya)->toBeNull()->and($profile->perhitungan_harga_pasar)->toBeNull() + ->and($profile->alasan_bukan_harga_pasar)->toBeNull(); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@name="alasan_pinjaman[]" and not(@disabled)]')->length)->toBe($isCreditCooperative ? 7 : 0) + ->and($dom->query('//select[@name="pembayaran_selain_uang" and not(@disabled)]')->length)->toBe($isCreditCooperative ? 1 : 0) + ->and($dom->query('//h5[text()="Ikatan Keuangan"]')->length)->toBe($isCreditCooperative ? 1 : 0); +})->with([ + 'production cooperative with credit service' => ['Koperasi Produksi', ['Simpan pinjam'], false], + 'credit cooperative without selected services' => ['Koperasi Simpan Pinjam', [], true], +]); + +test('social stops payment follow ups when non cash payments are unavailable', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam', + 'pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi', 'Lainnya'], + 'bentuk_pembayaran_lainnya' => 'Barang', 'perhitungan_harga_pasar' => 'Tidak', 'alasan_bukan_harga_pasar' => 'Kesepakatan', + ]); + $prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix(); + $this->post(route($prefix.'.store', $profile), [ + 'alasan_pinjaman' => ['Kesehatan'], 'pembayaran_selain_uang' => 'Tidak ada', + 'bentuk_pembayaran_barang' => 'data tersembunyi', 'bentuk_pembayaran_lainnya' => ['data tersembunyi'], + 'perhitungan_harga_pasar' => ['data tersembunyi'], 'alasan_bukan_harga_pasar' => ['data tersembunyi'], + ])->assertSessionHasNoErrors(); + $profile->refresh(); + expect($profile->pembayaran_selain_uang->value)->toBe('Tidak ada')->and($profile->bentuk_pembayaran_barang)->toHaveCount(0) + ->and($profile->bentuk_pembayaran_lainnya)->toBeNull()->and($profile->perhitungan_harga_pasar)->toBeNull() + ->and($profile->alasan_bukan_harga_pasar)->toBeNull()->and($profile->alasan_pinjaman->first()->value)->toBe('Kesehatan'); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@name="bentuk_pembayaran_barang[]" and @disabled]')->length)->toBe(3) + ->and($dom->query('//select[@name="perhitungan_harga_pasar" and @disabled]')->length)->toBe(1) + ->and($dom->query('//textarea[@name="alasan_bukan_harga_pasar" and @disabled]')->length)->toBe(1); +}); + +test('social skips harvest prices for services and clears old price reasons when market prices apply', function (array $input, ?string $expectedPrice) { + $profile = PengurusKoperasi::create([ + ...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam', + 'bentuk_pembayaran_lainnya' => 'Barang lama', 'alasan_bukan_harga_pasar' => 'Kesepakatan lama', + ]); + $prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix(); + $this->post(route($prefix.'.store', $profile), [ + 'pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_lainnya' => ['data tersembunyi'], + 'alasan_bukan_harga_pasar' => ['data tersembunyi'], ...$input, + ])->assertSessionHasNoErrors(); + $profile->refresh(); + expect($profile->perhitungan_harga_pasar?->value)->toBe($expectedPrice) + ->and($profile->bentuk_pembayaran_lainnya)->toBeNull()->and($profile->alasan_bukan_harga_pasar)->toBeNull(); +})->with([ + 'services only' => [['bentuk_pembayaran_barang' => ['Ganti jasa'], 'perhitungan_harga_pasar' => ['data tersembunyi']], null], + 'harvest valued at market prices' => [['bentuk_pembayaran_barang' => ['Hasil bumi'], 'perhitungan_harga_pasar' => 'Ya'], 'Ya'], +]); + +test('social restores active draft payment answers and Indonesian validation errors', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'pembayaran_selain_uang' => 'Tidak ada', + ]); + $prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix(); + $this->from(route($prefix.'.index', $profile))->post(route($prefix.'.store', $profile), [ + 'pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi'], 'perhitungan_harga_pasar' => 'Tidak', + ])->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasErrors('alasan_bukan_harga_pasar'); + $response = $this->get(route($prefix.'.index', $profile))->assertOk()->assertSee('Alasan Tidak Menggunakan Harga Pasar wajib diisi.'); + $dom = ($this->dom)($response->getContent()); + expect($dom->query('//select[@name="pembayaran_selain_uang"]/option[@value="Ada" and @selected]')->length)->toBe(1) + ->and($dom->query('//input[@name="bentuk_pembayaran_barang[]" and @value="Hasil bumi" and @checked and not(@disabled)]')->length)->toBe(1) + ->and($dom->query('//select[@name="perhitungan_harga_pasar"]/option[@value="Tidak" and @selected]')->length)->toBe(1) + ->and($dom->query('//textarea[@name="alasan_bukan_harga_pasar" and @required and not(@disabled) and contains(@class,"is-invalid")]')->length)->toBe(1) + ->and($profile->fresh()->pembayaran_selain_uang->value)->toBe('Tidak ada'); +}); + +test('koperasi operational choices keep their stored values and render concise field labels', function () { + $profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']); + $prefix = TabKoperasi::KELEMBAGAAN->routePrefix(); + $input = [ + 'ad_art' => 'Ada, lengkap', 'tahun_ad_art' => 2024, + 'layanan_koperasi' => ['Simpan pinjam', 'Lainnya'], 'layanan_lainnya' => 'Pendampingan usaha', + 'pelatihan_produksi' => 'Ya', 'pelatihan_pascapanen' => 'Tidak', + 'pelatihan_pengolahan' => 'Ya', 'pelatihan_keuangan' => 'Ya', 'pelatihan_pemasaran' => 'Tidak', + 'status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Digital/file Excel', + 'pertemuan_rutin' => 'Lainnya', 'pertemuan_lainnya' => 'Tiga bulan sekali', + 'kasus_kredit_macet' => '<10%', 'sanksi_kredit_macet' => 'Ada', 'efektivitas_sanksi' => 'Sangat efektif', + 'faktor_manajemen' => ['Kepemimpinan koperasi', 'Lainnya'], 'faktor_manajemen_lainnya' => 'Komunikasi', + 'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi', 'Lainnya'], + 'agregasi_komoditas_lainnya' => 'Padi', 'fasilitas_koperasi' => ['Gudang penyimpanan', 'Lainnya'], + 'fasilitas_lainnya' => 'Timbangan', 'pengolahan_lanjutan' => 'Ya', + 'detail_pengolahan_lanjutan' => 'Fermentasi', 'cara_penentuan_harga' => 'Harga pasar dan mutu produk', + 'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Kopi', + ]; + $this->post(route($prefix.'.store', $profile), $input) + ->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasNoErrors(); + $profile->refresh(); + foreach ($input as $field => $value) { + $stored = $profile->{$field}; + if ($stored instanceof BackedEnum) { + $stored = $stored->value; + } + if ($stored instanceof Collection) { + $stored = $stored->map(fn ($item) => $item->value)->all(); + } + expect($stored)->toBe($value); + } + $response = $this->get(route($prefix.'.index', $profile))->assertOk() + ->assertSee('Dokumen Kelembagaan')->assertDontSee('Dokumen Kelembagaan (AD/ART)') + ->assertDontSee('Data dokumen, layanan, pengelolaan, dan kegiatan usaha koperasi.'); + $dom = ($this->dom)($response->getContent()); + foreach ($dom->query('//form[@data-koperasi-operasional]//label | //form[@data-koperasi-operasional]//legend') as $label) { + expect($label->textContent)->not->toContain('?'); + } + expect($dom->query('//select[@name="ad_art"]/option[@value="Ada, lengkap" and @selected]')->length)->toBe(1) + ->and($dom->query('//input[@name="layanan_koperasi[]" and @value="Simpan pinjam" and @checked]')->length)->toBe(1) + ->and($dom->query('//input[@name="tahun_ad_art" and @value="2024" and not(@disabled)]')->length)->toBe(1); + $this->post(route($prefix.'.store', $profile), [])->assertSessionHasNoErrors(); + expect($profile->fresh()->layanan_koperasi)->toHaveCount(0); +}); + +test('operational separates management and post harvest into independent cards with actions', function () { + $profile = PengurusKoperasi::create($this->input); + $prefix = TabKoperasi::KELEMBAGAAN->routePrefix(); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + foreach (['pengelolaan', 'rantai-nilai'] as $section) { + $form = '//section[@id="section-operasional-'.$section.'"]//form'; + expect($dom->query($form)->length)->toBe(1) + ->and($dom->query($form.'//input[@name="section" and @value="'.$section.'"]')->length)->toBe(1) + ->and($dom->query($form.'//button[@type="submit"]')->length)->toBe(1) + ->and($dom->query($form.'//a[@href="'.route('koperasi.pengurus.index').'"]')->length)->toBe(1); + } + expect($dom->query('//form//form')->length)->toBe(0) + ->and($dom->query('//section[@id="section-operasional-pengelolaan"]//select[@name="ad_art"]')->length)->toBe(1) + ->and($dom->query('//section[@id="section-operasional-pengelolaan"]//input[@name="keterlibatan_rantai_nilai[]"]')->length)->toBe(0) + ->and($dom->query('//section[@id="section-operasional-rantai-nilai"]//h5[text()="Rantai Nilai dan Pascapanen"]')->length)->toBe(1) + ->and($dom->query('//section[@id="section-operasional-rantai-nilai"]//select[@name="praktik_sortasi"]')->length)->toBe(1) + ->and($dom->query('//section[@id="section-operasional-rantai-nilai"]//input[@name="harga_premium_rupiah"]')->length)->toBe(1); +}); + +test('operational saves each card without changing or validating answers from the other card', function (string $section, array $input, string $changedField, string $changedValue) { + $management = [ + 'ad_art' => 'Ada, lengkap', 'tahun_ad_art' => 2024, + 'layanan_koperasi' => ['Lainnya'], 'layanan_lainnya' => 'Pendampingan', 'pelatihan_produksi' => 'Ya', + 'status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Digital/file Excel', + 'pertemuan_rutin' => 'Lainnya', 'pertemuan_lainnya' => 'Tiga bulan sekali', + 'faktor_manajemen' => ['Lainnya'], 'faktor_manajemen_lainnya' => 'Komunikasi', + ]; + $postHarvest = [ + 'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Lainnya'], 'agregasi_komoditas_lainnya' => 'Padi', + 'fasilitas_koperasi' => ['Lainnya'], 'fasilitas_lainnya' => 'Timbangan', + 'pengolahan_lanjutan' => 'Ya', 'detail_pengolahan_lanjutan' => 'Fermentasi', + 'cara_penentuan_harga' => 'Harga pasar', 'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Padi', + 'praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', + 'praktik_pengeringan' => 'Kadang', 'cara_pengeringan' => 'Lantai jemur', + 'praktik_fermentasi' => 'Pernah coba', 'model_fermentasi' => ['Kolektif'], 'proses_fermentasi' => 'Kelompok', + 'harga_premium_rupiah' => 12500, + ]; + $profile = PengurusKoperasi::create([...$this->input, ...$management, ...$postHarvest]); + $preserved = $section === 'pengelolaan' ? $postHarvest : $management; + $original = array_intersect_key($profile->fresh()->getRawOriginal(), $preserved); + $prefix = TabKoperasi::KELEMBAGAAN->routePrefix(); + $this->post(route($prefix.'.store', $profile), ['section' => $section, ...$input]) + ->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile).'#section-operasional-rantai-nilai'); + $profile->refresh(); + expect(array_intersect_key($profile->getRawOriginal(), $preserved))->toBe($original) + ->and($profile->{$changedField}->value)->toBe($changedValue); +})->with([ + 'management preserves post harvest including premiums' => [ + 'pengelolaan', [ + 'status_pembukuan' => 'Belum ada', 'layanan_koperasi' => ['Simpan pinjam'], + 'keterlibatan_rantai_nilai' => ['Agregasi', 'Tidak terlibat kegiatan di atas'], + 'praktik_sortasi' => 'Pilihan salah', 'harga_premium_rupiah' => -1, + ], 'status_pembukuan', 'Belum ada', + ], + 'post harvest preserves documents services and management' => [ + 'rantai-nilai', [ + 'keterlibatan_rantai_nilai' => ['Tidak terlibat kegiatan di atas'], 'praktik_sortasi' => 'Belum pernah', + 'ad_art' => 'Pilihan salah', 'layanan_koperasi' => 'Pilihan salah', + 'status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Tidak ada', + ], 'praktik_sortasi', 'Belum pernah', + ], +]); + +test('operational validation stays at its card and keeps saved values visible in the other card', function (string $section, array $input, string $field) { + $profile = PengurusKoperasi::create([ + ...$this->input, 'layanan_koperasi' => ['Lainnya'], 'layanan_lainnya' => 'Pendampingan', + 'praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', + ]); + $original = $profile->fresh()->getRawOriginal(); + $prefix = TabKoperasi::KELEMBAGAAN->routePrefix(); + $this->post(route($prefix.'.store', $profile), ['section' => $section, ...$input]) + ->assertSessionHasErrors($field)->assertRedirect(route($prefix.'.index', $profile).'#section-operasional-'.$section); + expect($profile->fresh()->getRawOriginal())->toBe($original); + $otherField = $section === 'pengelolaan' ? 'standar_sortasi' : 'layanan_lainnya'; + $otherValue = $section === 'pengelolaan' ? 'Mutu' : 'Pendampingan'; + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@name="'.$otherField.'" and @value="'.$otherValue.'" and not(@disabled)]')->length)->toBe(1); +})->with([ + 'document needs year' => ['pengelolaan', ['ad_art' => 'Ada, lengkap'], 'tahun_ad_art'], + 'existing bookkeeping needs its format' => ['pengelolaan', ['status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Tidak ada'], 'format_pembukuan'], + 'aggregation needs commodities' => ['rantai-nilai', ['keterlibatan_rantai_nilai' => ['Agregasi']], 'agregasi_komoditas'], + 'sorting needs standards' => ['rantai-nilai', ['praktik_sortasi' => 'Ya rutin'], 'standar_sortasi'], + 'premium units are exclusive' => ['rantai-nilai', ['praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', 'harga_premium_rupiah' => 1000, 'harga_premium_persen' => 10], 'harga_premium_persen'], +]); + +test('operational rejects unknown card sections without saving', function () { + $profile = PengurusKoperasi::create($this->input); + $this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), [ + 'section' => 'invalid', 'pelatihan_produksi' => 'Ya', + ])->assertSessionHasErrors('section'); + expect($profile->fresh()->pelatihan_produksi)->toBeNull(); +}); + +test('koperasi operational validation preserves draft choices and marks invalid fields', function () { + $profile = PengurusKoperasi::create($this->input); + $prefix = TabKoperasi::KELEMBAGAAN->routePrefix(); + $url = route($prefix.'.index', $profile); + $this->from($url)->post(route($prefix.'.store', $profile), [ + 'ad_art' => 'Pilihan salah', 'tahun_ad_art' => 'bukan tahun', + 'layanan_koperasi' => ['Simpan pinjam'], 'fasilitas_koperasi' => ['Pilihan salah'], + 'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], + ])->assertRedirect($url)->assertSessionHasErrors(['ad_art', 'fasilitas_koperasi.0']); + $response = $this->get($url)->assertOk()->assertSee('Pilihan Status AD/ART tidak valid.'); + $dom = ($this->dom)($response->getContent()); + expect($dom->query('//input[@name="layanan_koperasi[]" and @value="Simpan pinjam" and @checked]')->length)->toBe(1) + ->and($dom->query('//select[@name="ad_art" and contains(@class,"is-invalid")]')->length)->toBe(1) + ->and($profile->fresh()->ad_art)->toBeNull(); +}); + +test('operational follow up details are required only for applicable answers', function (array $input, string $field) { + $profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']); + $url = route(TabKoperasi::KELEMBAGAAN->routePrefix().'.index', $profile); + $this->from($url)->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), $input) + ->assertRedirect($url)->assertSessionHasErrors($field); +})->with([ + 'complete documents need an update year' => [['ad_art' => 'Ada, lengkap'], 'tahun_ad_art'], + 'update year cannot be zero' => [['ad_art' => 'Ada, lengkap', 'tahun_ad_art' => 0], 'tahun_ad_art'], + 'other services need a description' => [['layanan_koperasi' => ['Lainnya']], 'layanan_lainnya'], + 'existing bookkeeping needs a format' => [['status_pembukuan' => 'Sudah'], 'format_pembukuan'], + 'existing bookkeeping cannot have no format' => [['status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Tidak ada'], 'format_pembukuan'], + 'other meeting frequency needs a description' => [['pertemuan_rutin' => 'Lainnya'], 'pertemuan_lainnya'], + 'sanctions need an effectiveness answer' => [['sanksi_kredit_macet' => 'Ada'], 'efektivitas_sanksi'], + 'other management factors need a description' => [['faktor_manajemen' => ['Lainnya']], 'faktor_manajemen_lainnya'], + 'aggregation role needs a commodity answer' => [['keterlibatan_rantai_nilai' => ['Agregasi']], 'agregasi_komoditas'], + 'other aggregation commodity needs a description' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Lainnya']], 'agregasi_komoditas_lainnya'], + 'other facilities need a description' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], 'fasilitas_koperasi' => ['Lainnya']], 'fasilitas_lainnya'], + 'processing needs a description when yes' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], 'pengolahan_lanjutan' => 'Ya'], 'detail_pengolahan_lanjutan'], + 'offtaker contract needs its commodity' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], 'kontrak_offtaker' => 'Ada'], 'kontrak_komoditas'], +]); + +test('credit questions use the saved cooperative type instead of selected services', function (string $jenis) { + $profile = PengurusKoperasi::create([ + ...$this->input, 'jenis_koperasi' => $jenis, 'kasus_kredit_macet' => '<10%', + 'sanksi_kredit_macet' => 'Ada', 'efektivitas_sanksi' => 'Efektif', + ]); + $prefix = TabKoperasi::KELEMBAGAAN->routePrefix(); + $this->post(route($prefix.'.store', $profile), [ + 'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'layanan_koperasi' => ['Simpan pinjam'], + 'kasus_kredit_macet' => 'Pilihan salah', 'sanksi_kredit_macet' => 'Ada', 'efektivitas_sanksi' => 'Pilihan salah', + ])->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasNoErrors(); + $profile->refresh(); + expect($profile->jenis_koperasi)->toBe($jenis)->and($profile->kasus_kredit_macet)->toBeNull() + ->and($profile->sanksi_kredit_macet)->toBeNull()->and($profile->efektivitas_sanksi)->toBeNull(); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//select[@name="kasus_kredit_macet" and @disabled]')->length)->toBe(1) + ->and($dom->query('//select[@name="sanksi_kredit_macet" and @disabled]')->length)->toBe(1); +})->with(['Koperasi Produksi', 'Koperasi Pemasaran', 'Koperasi Desa', 'Koperasi Jasa']); + +test('savings cooperatives see credit questions without selecting savings services and no sanctions skips effectiveness', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'layanan_koperasi' => [], + 'sanksi_kredit_macet' => 'Ada', 'efektivitas_sanksi' => 'Efektif', + ]); + $prefix = TabKoperasi::KELEMBAGAAN->routePrefix(); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//select[@name="kasus_kredit_macet" and not(@disabled)]')->length)->toBe(1); + $this->post(route($prefix.'.store', $profile), [ + 'kasus_kredit_macet' => '10-25%', 'sanksi_kredit_macet' => 'Tidak ada', 'efektivitas_sanksi' => ['data lama'], + ])->assertSessionHasNoErrors(); + expect($profile->fresh()->kasus_kredit_macet->value)->toBe('10-25%') + ->and($profile->fresh()->efektivitas_sanksi)->toBeNull(); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//select[@name="efektivitas_sanksi" and @disabled]')->length)->toBe(1); +}); + +test('changing operational answers clears obsolete details including malicious hidden inputs', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'ad_art' => 'Ada, lengkap', 'tahun_ad_art' => 2024, + 'layanan_koperasi' => ['Lainnya'], 'layanan_lainnya' => 'Layanan lama', + 'status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Digital/file Excel', + 'pertemuan_rutin' => 'Lainnya', 'pertemuan_lainnya' => 'Per tiga bulan', + 'faktor_manajemen' => ['Lainnya'], 'faktor_manajemen_lainnya' => 'Faktor lama', + 'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Lainnya'], 'agregasi_komoditas_lainnya' => 'Padi', + 'fasilitas_koperasi' => ['Lainnya'], 'fasilitas_lainnya' => 'Alat lama', + 'pengolahan_lanjutan' => 'Ya', 'detail_pengolahan_lanjutan' => 'Fermentasi', + 'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Kopi', + ]); + $this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), [ + 'ad_art' => 'Ada tapi tidak lengkap', 'tahun_ad_art' => 'bukan tahun', + 'layanan_koperasi' => ['Pemasaran hasil'], 'layanan_lainnya' => ['payload tersembunyi'], + 'status_pembukuan' => 'Belum ada', 'format_pembukuan' => 'Pilihan salah', + 'pertemuan_rutin' => 'Satu tahun sekali', 'pertemuan_lainnya' => ['payload tersembunyi'], + 'faktor_manajemen' => ['Rasa saling percaya'], 'faktor_manajemen_lainnya' => ['payload tersembunyi'], + 'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], 'agregasi_komoditas_lainnya' => ['payload tersembunyi'], + 'fasilitas_koperasi' => ['Gudang penyimpanan'], 'fasilitas_lainnya' => ['payload tersembunyi'], + 'pengolahan_lanjutan' => 'Tidak', 'detail_pengolahan_lanjutan' => ['payload tersembunyi'], + 'kontrak_offtaker' => 'Tidak ada', 'kontrak_komoditas' => ['payload tersembunyi'], + ])->assertSessionHasNoErrors(); + $profile->refresh(); + foreach (['tahun_ad_art', 'layanan_lainnya', 'pertemuan_lainnya', 'faktor_manajemen_lainnya', + 'agregasi_komoditas_lainnya', 'fasilitas_lainnya', 'detail_pengolahan_lanjutan', 'kontrak_komoditas'] as $field) { + expect($profile->{$field})->toBeNull(); + } + expect($profile->format_pembukuan->value)->toBe('Tidak ada'); +}); + +test('no aggregation skips remaining operational questions without a notification or changing save destination', function (array $answers) { + $profile = PengurusKoperasi::create([ + ...$this->input, 'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], + 'fasilitas_koperasi' => ['Gudang penyimpanan'], 'fasilitas_lainnya' => 'Teks lama', + 'pengolahan_lanjutan' => 'Ya', 'detail_pengolahan_lanjutan' => 'Fermentasi', + 'cara_penentuan_harga' => 'Harga pasar', 'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Kopi', + ]); + $prefix = TabKoperasi::KELEMBAGAAN->routePrefix(); + $this->post(route($prefix.'.store', $profile), [ + ...$answers, 'fasilitas_koperasi' => ['Pilihan salah'], 'pengolahan_lanjutan' => 'Ya', + 'detail_pengolahan_lanjutan' => 'Teks lama', 'cara_penentuan_harga' => 'Teks lama', + 'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Teks lama', + ])->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasNoErrors(); + $profile->refresh(); + expect($profile->fasilitas_koperasi)->toHaveCount(0)->and($profile->pengolahan_lanjutan)->toBeNull() + ->and($profile->detail_pengolahan_lanjutan)->toBeNull()->and($profile->cara_penentuan_harga)->toBeNull() + ->and($profile->kontrak_offtaker)->toBeNull()->and($profile->kontrak_komoditas)->toBeNull(); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + $d6 = route(TabKoperasi::RANTAI_NILAI->routePrefix().'.index', $profile); + expect($dom->query('//*[@data-operasional-skip-agregasi]')->length)->toBe(0) + ->and($dom->query('//a[@href="'.$d6.'" and contains(@class,"nav-link")]')->length)->toBe(1) + ->and($dom->query('//select[@name="pengolahan_lanjutan" and @disabled]')->length)->toBe(1); + $this->get($d6)->assertOk(); +})->with([ + 'explicitly does not aggregate' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Tidak terlibat']]], + 'different value chain role' => [['keterlibatan_rantai_nilai' => ['Penyediaan input']]], + 'not involved in value chain' => [['keterlibatan_rantai_nilai' => ['Tidak terlibat kegiatan di atas']]], +]); + +test('exclusive negative operational answers cannot be combined with positive choices', function (array $input, string $field) { + $profile = PengurusKoperasi::create($this->input); + $this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), $input) + ->assertSessionHasErrors($field); + expect($profile->fresh()->keterlibatan_rantai_nilai)->toBeNull(); +})->with([ + 'contradictory value chain roles' => [['keterlibatan_rantai_nilai' => ['Agregasi', 'Tidak terlibat kegiatan di atas']], 'keterlibatan_rantai_nilai'], + 'contradictory aggregation answer' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi', 'Tidak terlibat']], 'agregasi_komoditas'], +]); + +test('D6 covers purchase sales supplier relationships and prices with the shared profile icon', function () { + $profile = PengurusKoperasi::create($this->input); + $prefix = TabKoperasi::RANTAI_NILAI->routePrefix(); + $input = [ + 'sumber_pasokan' => ['Anggota', 'Petani non-anggota', 'Pengumpul/sub-agen', 'Campuran', 'Lainnya'], + 'sumber_pasokan_lainnya' => 'Kebun sendiri', + 'bentuk_pembelian' => ['Buah segar hasil petik', 'Biji basah', 'Biji kering/kupas', 'Produk terfermentasi'], + 'tujuan_penjualan' => ['Offtaker/Eksportir', 'Pedagang besar', 'Koperasi/BUMDes lain', 'Eceran', 'Lainnya'], + 'tujuan_penjualan_lainnya' => 'Hotel', 'bentuk_penjualan' => ['Biji basah', 'Biji kering/kupas'], + 'sifat_hubungan_bisnis' => ['Bebas', 'Langganan tetap', 'Berbasis hutang', 'Titip jual', 'Komitmen volume', 'Semi-kontrak', 'Lainnya'], + 'sifat_hubungan_lainnya' => 'Kemitraan', 'mekanisme_pembayaran' => 'Campuran', 'status_perjanjian_tertulis' => 'Lisan', + 'cara_menetapkan_harga_pasar' => ['Ikut pasar', 'Kalkulasi biaya', 'Harga offtaker', 'Lainnya'], + 'cara_menetapkan_harga_lainnya' => 'Musyawarah', + 'volume_beli_sekarang' => 1200, 'volume_beli_1_tahun' => 1000, 'volume_beli_2_tahun' => 900, + 'volume_jual_sekarang' => 1100, 'volume_jual_1_tahun' => 900, 'volume_jual_2_tahun' => 800, + 'harga_segar_sekarang' => '12.000', 'harga_basah_1_tahun' => '15.000', 'harga_kering_2_tahun' => '25.000', + ]; + $this->post(route($prefix.'.store', $profile), $input)->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasNoErrors(); + $profile->refresh(); + foreach ($input as $field => $expected) { + $stored = $profile->{$field}; + if ($stored instanceof BackedEnum) { + $stored = $stored->value; + } + if ($stored instanceof Collection) { + $stored = $stored->map(fn ($case) => $case->value)->all(); + } + if (str_starts_with($field, 'harga_')) { + $expected = (int) str_replace('.', '', $expected); + } + expect($stored)->toBe($expected); + } + $response = $this->get(route($prefix.'.index', $profile))->assertOk() + ->assertSee('Saluran Penjualan/Pembelian')->assertSee('Harga di Setiap Titik') + ->assertDontSee('Peluang Nilai Tambah'); + $dom = ($this->dom)($response->getContent()); + expect($dom->query('//a[@href="'.route($prefix.'.index', $profile).'"]//i[contains(@class,"tabler-arrows-exchange")]')->length)->toBe(1) + ->and($dom->query('//input[@name="harga_segar_sekarang" and @value="12.000" and not(@disabled)]')->length)->toBe(1) + ->and($dom->query('//*[@name="praktik_sortasi" or @name="harga_premium_rupiah"]')->length)->toBe(0); + $conditions = json_decode($dom->query('//*[@data-koperasi-survey-field="bentuk_penjualan"]')->item(0)->getAttribute('data-koperasi-conditions')); + expect($conditions[0]->values)->toBeArray(); +}); + +test('D6 other choices require details and never sold cannot be combined with buyers', function (array $input, string $field) { + $profile = PengurusKoperasi::create($this->input); + $this->post(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.store', $profile), $input)->assertSessionHasErrors($field); +})->with([ + 'other supplier' => [['sumber_pasokan' => ['Lainnya']], 'sumber_pasokan_lainnya'], + 'other buyer' => [['tujuan_penjualan' => ['Lainnya']], 'tujuan_penjualan_lainnya'], + 'other relationship' => [['sifat_hubungan_bisnis' => ['Lainnya']], 'sifat_hubungan_lainnya'], + 'other price setting' => [['cara_menetapkan_harga_pasar' => ['Lainnya']], 'cara_menetapkan_harga_lainnya'], + 'never sold with a buyer' => [['tujuan_penjualan' => ['Belum pernah', 'Eceran']], 'tujuan_penjualan'], +]); + +test('D6 never sold clears obsolete sales answers while retaining purchase information', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'tujuan_penjualan' => ['Lainnya'], 'tujuan_penjualan_lainnya' => 'Hotel', + 'bentuk_penjualan' => ['Biji basah'], 'volume_jual_sekarang' => 500, + 'volume_jual_1_tahun' => 400, 'volume_jual_2_tahun' => 300, + 'sumber_pasokan_lainnya' => 'Teks lama', 'sifat_hubungan_lainnya' => 'Teks lama', 'cara_menetapkan_harga_lainnya' => 'Teks lama', + ]); + $prefix = TabKoperasi::RANTAI_NILAI->routePrefix(); + $this->post(route($prefix.'.store', $profile), [ + 'sumber_pasokan' => ['Anggota'], 'sumber_pasokan_lainnya' => ['payload tersembunyi'], + 'bentuk_pembelian' => ['Biji basah'], 'volume_beli_sekarang' => 600, 'harga_basah_sekarang' => '12.000', + 'tujuan_penjualan' => ['Belum pernah'], 'tujuan_penjualan_lainnya' => ['payload tersembunyi'], + 'bentuk_penjualan' => ['Pilihan salah'], 'volume_jual_sekarang' => 'bukan angka', + 'sifat_hubungan_lainnya' => ['payload tersembunyi'], 'cara_menetapkan_harga_lainnya' => ['payload tersembunyi'], + ])->assertSessionHasNoErrors(); + $profile->refresh(); + expect($profile->bentuk_penjualan)->toHaveCount(0)->and($profile->volume_jual_sekarang)->toBeNull() + ->and($profile->volume_jual_1_tahun)->toBeNull()->and($profile->volume_jual_2_tahun)->toBeNull() + ->and($profile->tujuan_penjualan_lainnya)->toBeNull()->and($profile->sumber_pasokan_lainnya)->toBeNull() + ->and($profile->sifat_hubungan_lainnya)->toBeNull()->and($profile->cara_menetapkan_harga_lainnya)->toBeNull() + ->and($profile->volume_beli_sekarang)->toBe(600)->and((int) $profile->harga_basah_sekarang)->toBe(12000); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@name="bentuk_penjualan[]" and @disabled]')->length)->toBe(4) + ->and($dom->query('//input[@name="volume_jual_sekarang" and @disabled]')->length)->toBe(1); +}); + +test('D6 keeps all historical product prices editable regardless of current purchase forms', function () { + $profile = PengurusKoperasi::create([...$this->input, 'bentuk_pembelian' => ['Biji basah'], 'harga_basah_sekarang' => 12000]); + $prefix = TabKoperasi::RANTAI_NILAI->routePrefix(); + $this->post(route($prefix.'.store', $profile), [ + 'bentuk_pembelian' => ['Buah segar hasil petik'], 'harga_segar_sekarang' => '15.000', + 'harga_basah_sekarang' => '12.000', 'harga_kering_1_tahun' => '25.000', + ])->assertSessionHasNoErrors(); + expect((int) $profile->fresh()->harga_segar_sekarang)->toBe(15000) + ->and((int) $profile->fresh()->harga_basah_sekarang)->toBe(12000)->and((int) $profile->fresh()->harga_kering_1_tahun)->toBe(25000); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@name="harga_segar_sekarang" and not(@disabled)]')->length)->toBe(1) + ->and($dom->query('//input[@name="harga_basah_sekarang" and not(@disabled)]')->length)->toBe(1) + ->and($dom->query('//input[starts-with(@name,"harga_") and not(@disabled)]')->length)->toBe(12); + $this->post(route($prefix.'.store', $profile), ['bentuk_pembelian' => ['Buah segar hasil petik'], 'harga_segar_sekarang' => -1])->assertSessionHasErrors('harga_segar_sekarang'); + expect((int) $profile->fresh()->harga_segar_sekarang)->toBe(15000); +}); + +test('D6 has two independent cards with actions and the complete three harvest price table', function () { + $profile = PengurusKoperasi::create($this->input); + $prefix = TabKoperasi::RANTAI_NILAI->routePrefix(); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + foreach (['saluran', 'harga'] as $section) { + $form = '//section[@id="section-rantai-'.$section.'"]//form'; + expect($dom->query($form)->length)->toBe(1) + ->and($dom->query($form.'//input[@name="section" and @value="'.$section.'"]')->length)->toBe(1) + ->and($dom->query($form.'//button[@type="submit"]')->length)->toBe(1) + ->and($dom->query($form.'//a[@href="'.route('koperasi.pengurus.index').'"]')->length)->toBe(1); + } + expect($dom->query('//form//form')->length)->toBe(0) + ->and($dom->query('//h6[text()="Saluran Pembelian/Sumber Pasokan" or text()="Saluran Penjualan Utama" or text()="Hubungan dengan Petani Pemasok"]')->length)->toBe(0) + ->and($dom->query('//section[@id="section-rantai-saluran"]//input[starts-with(@name,"harga_")]')->length)->toBe(0) + ->and($dom->query('//section[@id="section-rantai-harga"]//table/tbody/tr')->length)->toBe(4) + ->and($dom->query('//section[@id="section-rantai-harga"]//table//input[@data-koperasi-rupiah and not(@disabled)]')->length)->toBe(12) + ->and($dom->query('//section[@id="section-rantai-harga"]//table//span[text()="/kg"]')->length)->toBe(12); +}); + +test('D6 saves channels then jumps to prices while preserving every price answer', function () { + $prices = ['cara_menetapkan_harga_pasar' => ['Lainnya'], 'cara_menetapkan_harga_lainnya' => 'Musyawarah']; + foreach (BentukProdukKoperasi::cases() as $product) { + foreach (['sekarang', '1_tahun', '2_tahun'] as $period) { + $prices['harga_'.$product->pricePrefix().'_'.$period] = 12500; + } + } + $profile = PengurusKoperasi::create([...$this->input, ...$prices]); + $prefix = TabKoperasi::RANTAI_NILAI->routePrefix(); + $this->post(route($prefix.'.store', $profile), [ + 'section' => 'saluran', 'sumber_pasokan' => ['Anggota'], 'bentuk_pembelian' => ['Biji basah'], + 'tujuan_penjualan' => ['Belum pernah'], 'volume_beli_sekarang' => 200, + 'harga_segar_sekarang' => -1, 'cara_menetapkan_harga_pasar' => ['Lainnya'], + ])->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile).'#section-rantai-harga'); + $profile->refresh(); + expect($profile->sumber_pasokan->first()->value)->toBe('Anggota')->and($profile->volume_beli_sekarang)->toBe(200) + ->and($profile->cara_menetapkan_harga_pasar->first()->value)->toBe('Lainnya') + ->and($profile->cara_menetapkan_harga_lainnya)->toBe('Musyawarah'); + foreach (array_keys($prices) as $field) { + if (str_starts_with($field, 'harga_')) { + expect((int) $profile->{$field})->toBe(12500); + } + } +}); + +test('D6 saves prices independently without changing channel answers or validating their submitted values', function () { + $channels = [ + 'sumber_pasokan' => ['Lainnya'], 'sumber_pasokan_lainnya' => 'Kebun sendiri', + 'bentuk_pembelian' => ['Biji basah'], 'tujuan_penjualan' => ['Lainnya'], 'tujuan_penjualan_lainnya' => 'Hotel', + 'bentuk_penjualan' => ['Biji kering/kupas'], 'sifat_hubungan_bisnis' => ['Lainnya'], + 'sifat_hubungan_lainnya' => 'Kemitraan', 'mekanisme_pembayaran' => 'Tunai', 'status_perjanjian_tertulis' => 'Lisan', + 'volume_beli_sekarang' => 200, 'volume_beli_1_tahun' => 150, 'volume_beli_2_tahun' => 100, + 'volume_jual_sekarang' => 180, 'volume_jual_1_tahun' => 120, 'volume_jual_2_tahun' => 90, + ]; + $profile = PengurusKoperasi::create([...$this->input, ...$channels]); + $original = array_intersect_key($profile->fresh()->getRawOriginal(), $channels); + $prefix = TabKoperasi::RANTAI_NILAI->routePrefix(); + $this->post(route($prefix.'.store', $profile), [ + 'section' => 'harga', 'harga_fermentasi_2_tahun' => '1.250.000', 'cara_menetapkan_harga_pasar' => ['Ikut pasar'], + 'sumber_pasokan' => 'Pilihan salah', 'tujuan_penjualan' => ['Belum pernah', 'Eceran'], 'volume_beli_sekarang' => -1, + ])->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile).'#section-rantai-harga'); + $profile->refresh(); + expect(array_intersect_key($profile->getRawOriginal(), $channels))->toBe($original) + ->and((int) $profile->harga_fermentasi_2_tahun)->toBe(1250000) + ->and($profile->cara_menetapkan_harga_pasar->first()->value)->toBe('Ikut pasar'); +}); + +test('D6 validation returns to the submitted card and keeps the other card saved values', function (string $section, array $input, string $field) { + $profile = PengurusKoperasi::create([ + ...$this->input, 'sumber_pasokan' => ['Lainnya'], 'sumber_pasokan_lainnya' => 'Kebun sendiri', + 'cara_menetapkan_harga_pasar' => ['Lainnya'], 'cara_menetapkan_harga_lainnya' => 'Musyawarah', 'harga_segar_sekarang' => 12500, + ]); + $original = $profile->fresh()->getRawOriginal(); + $prefix = TabKoperasi::RANTAI_NILAI->routePrefix(); + $this->post(route($prefix.'.store', $profile), ['section' => $section, ...$input]) + ->assertSessionHasErrors($field)->assertRedirect(route($prefix.'.index', $profile).'#section-rantai-'.$section); + expect($profile->fresh()->getRawOriginal())->toBe($original); + $otherField = $section === 'saluran' ? 'cara_menetapkan_harga_lainnya' : 'sumber_pasokan_lainnya'; + $otherValue = $section === 'saluran' ? 'Musyawarah' : 'Kebun sendiri'; + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@name="'.$otherField.'" and @value="'.$otherValue.'" and not(@disabled)]')->length)->toBe(1); +})->with([ + 'supplier detail' => ['saluran', ['sumber_pasokan' => ['Lainnya']], 'sumber_pasokan_lainnya'], + 'negative price' => ['harga', ['harga_segar_sekarang' => -1], 'harga_segar_sekarang'], + 'price setting detail' => ['harga', ['cara_menetapkan_harga_pasar' => ['Lainnya']], 'cara_menetapkan_harga_lainnya'], +]); + +test('D6 rejects unknown form sections without writing any answers', function () { + $profile = PengurusKoperasi::create($this->input); + $this->post(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.store', $profile), [ + 'section' => 'invalid', 'harga_segar_sekarang' => 10000, + ])->assertSessionHasErrors('section'); + expect($profile->fresh()->harga_segar_sekarang)->toBeNull(); +}); + +test('K3 existing data moves to operational and D6 writes cannot overwrite post harvest answers', function () { + $profile = PengurusKoperasi::create([ + ...$this->input, 'praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Ukuran biji', + 'praktik_pengeringan' => 'Kadang', 'cara_pengeringan' => 'Lantai jemur', + 'praktik_fermentasi' => 'Pernah coba', 'model_fermentasi' => ['Kolektif'], 'proses_fermentasi' => 'Fermentasi kelompok', + 'harga_premium_rupiah' => 12500, 'harga_segar_sekarang' => 10000, + ]); + $op = TabKoperasi::KELEMBAGAAN->routePrefix(); + $d6 = TabKoperasi::RANTAI_NILAI->routePrefix(); + $response = $this->get(route($op.'.index', $profile))->assertOk()->assertSee('Peluang Nilai Tambah (Pengolahan Pascapanen)'); + $dom = ($this->dom)($response->getContent()); + expect($dom->query('//input[@name="harga_premium_rupiah" and @value="12.500"]')->length)->toBe(1) + ->and($dom->query('//input[@name="model_fermentasi[]" and @value="Kolektif" and @checked]')->length)->toBe(1); + $this->post(route($d6.'.store', $profile), [ + 'bentuk_pembelian' => ['Buah segar hasil petik'], 'praktik_sortasi' => 'Belum pernah', + 'model_fermentasi' => [], 'harga_premium_rupiah' => 0, + ])->assertSessionHasNoErrors(); + expect($profile->fresh()->praktik_sortasi->value)->toBe('Ya rutin') + ->and((int) $profile->fresh()->harga_premium_rupiah)->toBe(12500) + ->and($profile->fresh()->model_fermentasi)->toHaveCount(1); + $profile->update(['harga_segar_sekarang' => 10000]); + $this->post(route($op.'.store', $profile), [ + 'praktik_sortasi' => 'Belum pernah', 'praktik_pengeringan' => 'Tidak', 'praktik_fermentasi' => 'Belum, direncakan', + 'standar_sortasi' => ['payload tersembunyi'], 'cara_pengeringan' => ['payload tersembunyi'], + 'model_fermentasi' => ['Pilihan salah'], 'proses_fermentasi' => ['payload tersembunyi'], + 'harga_premium_rupiah' => 'bukan angka', 'harga_premium_persen' => 999, + ])->assertRedirect(route($op.'.index', $profile))->assertSessionHasNoErrors(); + $profile->refresh(); + expect($profile->standar_sortasi)->toBeNull()->and($profile->cara_pengeringan)->toBeNull() + ->and($profile->model_fermentasi)->toHaveCount(0)->and($profile->proses_fermentasi)->toBeNull() + ->and($profile->harga_premium_rupiah)->toBeNull()->and($profile->harga_premium_persen)->toBeNull() + ->and((int) $profile->harga_segar_sekarang)->toBe(10000); +}); + +test('K3 follow ups validate actual practices while planned or absent processing skips them', function (array $input, string $field) { + $profile = PengurusKoperasi::create($this->input); + $this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), $input)->assertSessionHasErrors($field); +})->with([ + 'routine sorting needs criteria' => [['praktik_sortasi' => 'Ya rutin'], 'standar_sortasi'], + 'inconsistent sorting needs criteria' => [['praktik_sortasi' => 'Ya tidak konsisten'], 'standar_sortasi'], + 'routine drying needs its method' => [['praktik_pengeringan' => 'Ya rutin'], 'cara_pengeringan'], + 'occasional drying needs its method' => [['praktik_pengeringan' => 'Kadang'], 'cara_pengeringan'], + 'routine fermentation needs a model' => [['praktik_fermentasi' => 'Ya rutin', 'proses_fermentasi' => 'Proses kelompok'], 'model_fermentasi'], + 'trial fermentation needs its process' => [['praktik_fermentasi' => 'Pernah coba', 'model_fermentasi' => ['Individual']], 'proses_fermentasi'], + 'premium units are alternatives' => [['praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', 'harga_premium_rupiah' => '1.000', 'harga_premium_persen' => 10], 'harga_premium_persen'], + 'premium percentage is bounded' => [['praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', 'harga_premium_persen' => 101], 'harga_premium_persen'], + 'premium rupiah cannot be negative' => [['praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', 'harga_premium_rupiah' => -1], 'harga_premium_rupiah'], +]); + +test('K3 premium is available for other actual post harvest processing such as packaging', function () { + $profile = PengurusKoperasi::create($this->input); + $prefix = TabKoperasi::KELEMBAGAAN->routePrefix(); + $this->post(route($prefix.'.store', $profile), [ + 'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], + 'pengolahan_lanjutan' => 'Ya', 'detail_pengolahan_lanjutan' => 'Pengemasan', + 'praktik_sortasi' => 'Belum pernah', 'praktik_pengeringan' => 'Tidak', 'praktik_fermentasi' => 'Belum, tidak', + 'harga_premium_persen' => 10, + ])->assertSessionHasNoErrors(); + expect((int) $profile->fresh()->harga_premium_persen)->toBe(10); + $dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent()); + expect($dom->query('//input[@name="harga_premium_persen" and not(@disabled)]')->length)->toBe(1); +});