From c55ff109a0988381c04d134034f1029bcf889649 Mon Sep 17 00:00:00 2001 From: SyaharaniSalsabilla Date: Thu, 5 Nov 2026 19:20:05 +0700 Subject: [PATCH] Update BUMDes --- app/Enums/Bumdes/AsetProduktifBumdes.php | 30 + app/Enums/Bumdes/BentukProdukBumdes.php | 26 + app/Enums/Bumdes/CakupanLayananBumdes.php | 22 + app/Enums/Bumdes/DukunganDesaBumdes.php | 28 + app/Enums/Bumdes/FormatPembukuanBumdes.php | 26 + app/Enums/Bumdes/HambatanUsahaBumdes.php | 32 + app/Enums/Bumdes/HubunganPemasokBumdes.php | 32 + .../Bumdes/InfrastrukturPascapanenBumdes.php | 30 + .../Bumdes/JabatanPengurusIntiBumdes.php | 32 + app/Enums/Bumdes/KemitraanEksternalBumdes.php | 30 + .../Bumdes/KeterlibatanKoordinasiBumdes.php | 24 + .../Bumdes/KondisiAsetProduktifBumdes.php | 43 + .../Bumdes/KondisiInfrastrukturBumdes.php | 22 + app/Enums/Bumdes/KondisiKeuanganBumdes.php | 26 + app/Enums/Bumdes/KontribusiPadesBumdes.php | 24 + app/Enums/Bumdes/LayananPetaniBumdes.php | 34 + .../Bumdes/MekanismePembayaranBumdes.php | 28 + app/Enums/Bumdes/ModelFermentasiBumdes.php | 26 + app/Enums/Bumdes/PencatatanPetaniBumdes.php | 24 + app/Enums/Bumdes/PenetapanHargaBeliBumdes.php | 26 + app/Enums/Bumdes/PengalamanOfftakerBumdes.php | 24 + app/Enums/Bumdes/PeranRantaiNilaiBumdes.php | 26 + app/Enums/Bumdes/PeriodePanenBumdes.php | 24 + app/Enums/Bumdes/PerjanjianPetaniBumdes.php | 24 + .../Bumdes/PertanggungjawabanMusdesBumdes.php | 24 + app/Enums/Bumdes/SaluranPenjualanBumdes.php | 30 + app/Enums/Bumdes/SifatAuditBumdes.php | 22 + app/Enums/Bumdes/StatusAdArtBumdes.php | 26 + app/Enums/Bumdes/StatusBadanHukumBumdes.php | 26 + app/Enums/Bumdes/StatusFermentasiBumdes.php | 26 + app/Enums/Bumdes/StatusKeaktifanBumdes.php | 26 + .../Bumdes/StatusKepemilikanAsetBumdes.php | 24 + app/Enums/Bumdes/StatusPembukuanBumdes.php | 24 + app/Enums/Bumdes/StatusPengeringanBumdes.php | 24 + app/Enums/Bumdes/StatusSortasiBumdes.php | 24 + .../Bumdes/StatusUnitKomoditasBumdes.php | 24 + app/Enums/Bumdes/SumberPasokanBumdes.php | 28 + app/Enums/Bumdes/UnitUsahaBumdes.php | 32 + .../KelembagaanAktorBumdesController.php | 29 + .../Bumdes/OperasionalBumdesController.php | 29 + .../Web/Bumdes/PengurusBumdesController.php | 573 +------ .../Bumdes/RantaiNilaiBumdesController.php | 34 + app/Http/Requests/Bumdes/BumdesRequest.php | 174 ++ .../Bumdes/IndexPengurusBumdesRequest.php | 27 + .../StoreKelembagaanAktorBumdesRequest.php | 11 + .../Bumdes/StoreOperasionalBumdesRequest.php | 252 +++ .../Bumdes/StorePengurusBumdesRequest.php | 66 + .../Bumdes/StoreRantaiNilaiBumdesRequest.php | 172 ++ .../Bumdes/UpdatePengurusBumdesRequest.php | 5 + app/Models/Bumdes/KelembagaanAktorBumdes.php | 252 ++- app/Models/Bumdes/PengurusBumdes.php | 47 +- app/Services/Bumdes/PengurusBumdesService.php | 633 +++++++ ...stem_to_bumdes_kelembagaan_aktor_table.php | 32 + ...anan_to_bumdes_kelembagaan_aktor_table.php | 72 + ...ilai_to_bumdes_kelembagaan_aktor_table.php | 56 + ...mbah_to_bumdes_kelembagaan_aktor_table.php | 40 + public/assets/css/demo.css | 174 +- public/assets/js/bumdes.js | 375 ++++ .../bumdes/kelembagaan-aktor/index.blade.php | 567 +------ .../views/bumdes/operasional/index.blade.php | 656 +++++++ resources/views/bumdes/profil/_form.blade.php | 220 +-- .../views/bumdes/profil/_keaktifan.blade.php | 26 + .../bumdes/profil/_kepengurusan.blade.php | 89 + resources/views/bumdes/profil/add.blade.php | 34 - .../views/bumdes/profil/create.blade.php | 27 + resources/views/bumdes/profil/edit.blade.php | 49 +- .../views/bumdes/profil/header.blade.php | 265 ++- resources/views/bumdes/profil/index.blade.php | 222 ++- .../views/bumdes/rantai-nilai/index.blade.php | 449 +++++ .../views/components/bumdes-alert.blade.php | 36 + .../components/bumdes-form-actions.blade.php | 6 + routes/web.php | 13 +- tests/Feature/BumdesProfileTest.php | 1512 +++++++++++++++++ 73 files changed, 6699 insertions(+), 1548 deletions(-) create mode 100644 app/Enums/Bumdes/AsetProduktifBumdes.php create mode 100644 app/Enums/Bumdes/BentukProdukBumdes.php create mode 100644 app/Enums/Bumdes/CakupanLayananBumdes.php create mode 100644 app/Enums/Bumdes/DukunganDesaBumdes.php create mode 100644 app/Enums/Bumdes/FormatPembukuanBumdes.php create mode 100644 app/Enums/Bumdes/HambatanUsahaBumdes.php create mode 100644 app/Enums/Bumdes/HubunganPemasokBumdes.php create mode 100644 app/Enums/Bumdes/InfrastrukturPascapanenBumdes.php create mode 100644 app/Enums/Bumdes/JabatanPengurusIntiBumdes.php create mode 100644 app/Enums/Bumdes/KemitraanEksternalBumdes.php create mode 100644 app/Enums/Bumdes/KeterlibatanKoordinasiBumdes.php create mode 100644 app/Enums/Bumdes/KondisiAsetProduktifBumdes.php create mode 100644 app/Enums/Bumdes/KondisiInfrastrukturBumdes.php create mode 100644 app/Enums/Bumdes/KondisiKeuanganBumdes.php create mode 100644 app/Enums/Bumdes/KontribusiPadesBumdes.php create mode 100644 app/Enums/Bumdes/LayananPetaniBumdes.php create mode 100644 app/Enums/Bumdes/MekanismePembayaranBumdes.php create mode 100644 app/Enums/Bumdes/ModelFermentasiBumdes.php create mode 100644 app/Enums/Bumdes/PencatatanPetaniBumdes.php create mode 100644 app/Enums/Bumdes/PenetapanHargaBeliBumdes.php create mode 100644 app/Enums/Bumdes/PengalamanOfftakerBumdes.php create mode 100644 app/Enums/Bumdes/PeranRantaiNilaiBumdes.php create mode 100644 app/Enums/Bumdes/PeriodePanenBumdes.php create mode 100644 app/Enums/Bumdes/PerjanjianPetaniBumdes.php create mode 100644 app/Enums/Bumdes/PertanggungjawabanMusdesBumdes.php create mode 100644 app/Enums/Bumdes/SaluranPenjualanBumdes.php create mode 100644 app/Enums/Bumdes/SifatAuditBumdes.php create mode 100644 app/Enums/Bumdes/StatusAdArtBumdes.php create mode 100644 app/Enums/Bumdes/StatusBadanHukumBumdes.php create mode 100644 app/Enums/Bumdes/StatusFermentasiBumdes.php create mode 100644 app/Enums/Bumdes/StatusKeaktifanBumdes.php create mode 100644 app/Enums/Bumdes/StatusKepemilikanAsetBumdes.php create mode 100644 app/Enums/Bumdes/StatusPembukuanBumdes.php create mode 100644 app/Enums/Bumdes/StatusPengeringanBumdes.php create mode 100644 app/Enums/Bumdes/StatusSortasiBumdes.php create mode 100644 app/Enums/Bumdes/StatusUnitKomoditasBumdes.php create mode 100644 app/Enums/Bumdes/SumberPasokanBumdes.php create mode 100644 app/Enums/Bumdes/UnitUsahaBumdes.php create mode 100644 app/Http/Controllers/Web/Bumdes/KelembagaanAktorBumdesController.php create mode 100644 app/Http/Controllers/Web/Bumdes/OperasionalBumdesController.php create mode 100644 app/Http/Controllers/Web/Bumdes/RantaiNilaiBumdesController.php create mode 100644 app/Http/Requests/Bumdes/BumdesRequest.php create mode 100644 app/Http/Requests/Bumdes/IndexPengurusBumdesRequest.php create mode 100644 app/Http/Requests/Bumdes/StoreKelembagaanAktorBumdesRequest.php create mode 100644 app/Http/Requests/Bumdes/StoreOperasionalBumdesRequest.php create mode 100644 app/Http/Requests/Bumdes/StorePengurusBumdesRequest.php create mode 100644 app/Http/Requests/Bumdes/StoreRantaiNilaiBumdesRequest.php create mode 100644 app/Http/Requests/Bumdes/UpdatePengurusBumdesRequest.php create mode 100644 app/Services/Bumdes/PengurusBumdesService.php create mode 100644 database/migrations/2026_11_04_000001_add_dukungan_ekosistem_to_bumdes_kelembagaan_aktor_table.php create mode 100644 database/migrations/2026_11_04_000002_add_pengelolaan_layanan_to_bumdes_kelembagaan_aktor_table.php create mode 100644 database/migrations/2026_11_04_000003_add_rantai_nilai_to_bumdes_kelembagaan_aktor_table.php create mode 100644 database/migrations/2026_11_05_000001_add_peluang_nilai_tambah_to_bumdes_kelembagaan_aktor_table.php create mode 100644 public/assets/js/bumdes.js create mode 100644 resources/views/bumdes/operasional/index.blade.php create mode 100644 resources/views/bumdes/profil/_keaktifan.blade.php create mode 100644 resources/views/bumdes/profil/_kepengurusan.blade.php delete mode 100644 resources/views/bumdes/profil/add.blade.php create mode 100644 resources/views/bumdes/profil/create.blade.php create mode 100644 resources/views/bumdes/rantai-nilai/index.blade.php create mode 100644 resources/views/components/bumdes-alert.blade.php create mode 100644 resources/views/components/bumdes-form-actions.blade.php create mode 100644 tests/Feature/BumdesProfileTest.php diff --git a/app/Enums/Bumdes/AsetProduktifBumdes.php b/app/Enums/Bumdes/AsetProduktifBumdes.php new file mode 100644 index 0000000..020fe59 --- /dev/null +++ b/app/Enums/Bumdes/AsetProduktifBumdes.php @@ -0,0 +1,30 @@ + 'Bangunan / kantor', + self::GUDANG_PENYIMPANAN => 'Gudang penyimpanan', + self::KENDARAAN_OPERASIONAL_ANGKUT => 'Kendaraan operasional/angkut', + self::LAHAN_USAHA => 'Lahan usaha', + self::ALAT_PENGOLAHAN_KOMODITAS => 'Alat pengolahan komoditas', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/BentukProdukBumdes.php b/app/Enums/Bumdes/BentukProdukBumdes.php new file mode 100644 index 0000000..186a55e --- /dev/null +++ b/app/Enums/Bumdes/BentukProdukBumdes.php @@ -0,0 +1,26 @@ + 'Buah segar hasil petik', + self::BIJI_BASAH => 'Biji basah', + self::BIJI_KERING => 'Biji kering/biji kupas', + self::TERFERMENTASI => 'Terfermentasi', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/CakupanLayananBumdes.php b/app/Enums/Bumdes/CakupanLayananBumdes.php new file mode 100644 index 0000000..9b635d8 --- /dev/null +++ b/app/Enums/Bumdes/CakupanLayananBumdes.php @@ -0,0 +1,22 @@ + 'Hanya melayani desa sendiri', + self::DESA_LAIN => 'Melayani desa sendiri dan desa lain', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/DukunganDesaBumdes.php b/app/Enums/Bumdes/DukunganDesaBumdes.php new file mode 100644 index 0000000..30feec9 --- /dev/null +++ b/app/Enums/Bumdes/DukunganDesaBumdes.php @@ -0,0 +1,28 @@ + 'Pendampingan teknis', + self::REGULASI_PERDES => 'Regulasi/Perdes pendukung', + self::FASILITASI_JEJARING => 'Fasilitasi jejaring/kemitraan', + self::TIDAK_ADA => 'Tidak ada dukungan khusus', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/FormatPembukuanBumdes.php b/app/Enums/Bumdes/FormatPembukuanBumdes.php new file mode 100644 index 0000000..d2686cc --- /dev/null +++ b/app/Enums/Bumdes/FormatPembukuanBumdes.php @@ -0,0 +1,26 @@ + 'Manual/buku kas', + self::DIGITAL_EXCEL => 'Digital/file Excel', + self::APLIKASI => 'Aplikasi keuangan khusus', + self::TIDAK_ADA => 'Tidak ada', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/HambatanUsahaBumdes.php b/app/Enums/Bumdes/HambatanUsahaBumdes.php new file mode 100644 index 0000000..2fa7e64 --- /dev/null +++ b/app/Enums/Bumdes/HambatanUsahaBumdes.php @@ -0,0 +1,32 @@ + 'Keterbatasan modal', + self::SDM => 'Keterbatasan SDM/kapasitas pengurus', + self::INFRASTRUKTUR => 'Keterbatasan infrastruktur/alat', + self::PASOKAN => 'Kesulitan mendapat pasokan konsisten dari produsen', + self::PERSAINGAN => 'Persaingan dengan tengkulak/pedagang', + self::AKSES_PASAR => 'Belum ada akses ke offtaker/pasar premium', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/HubunganPemasokBumdes.php b/app/Enums/Bumdes/HubunganPemasokBumdes.php new file mode 100644 index 0000000..e130ea9 --- /dev/null +++ b/app/Enums/Bumdes/HubunganPemasokBumdes.php @@ -0,0 +1,32 @@ + 'Bebas/tidak terikat: petani bebas menjual ke BUMDes atau pihak lain kapan saja, tanpa ikatan apa pun', + self::LANGGANAN => 'Langganan tetap: petani rutin menjual ke BUMDes tanpa perjanjian formal', + self::PINJAMAN => 'Berbasis hutang/pinjaman: petani menjual ke BUMDes karena memiliki pinjaman kepada BUMDes', + self::KONSINYASI => 'Titip jual (konsinyasi): petani dibayar setelah hasil panen terjual', + self::KOMITMEN_VOLUME => 'Ada komitmen volume: kesepakatan volume tertentu yang dipenuhi secara berkala', + self::SEMI_KONTRAK => 'Semi-kontrak: kesepakatan tertulis/lisan mengenai harga, volume, dan waktu penyerahan', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/InfrastrukturPascapanenBumdes.php b/app/Enums/Bumdes/InfrastrukturPascapanenBumdes.php new file mode 100644 index 0000000..6db3b15 --- /dev/null +++ b/app/Enums/Bumdes/InfrastrukturPascapanenBumdes.php @@ -0,0 +1,30 @@ + 'Gudang penyimpanan hasil panen', + self::ALAT_MESIN_PENGERING => 'Alat/mesin pengering', + self::MESIN_PULPER_HULLER => 'Mesin pulper/huller', + self::TIMBANGAN => 'Timbangan', + self::ALAT_SORTASI => 'Alat sortasi', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/JabatanPengurusIntiBumdes.php b/app/Enums/Bumdes/JabatanPengurusIntiBumdes.php new file mode 100644 index 0000000..e2b2c09 --- /dev/null +++ b/app/Enums/Bumdes/JabatanPengurusIntiBumdes.php @@ -0,0 +1,32 @@ + 'Penasihat (Kepala Desa)', + self::KETUA_PENGAWAS => 'Ketua Pengawas', + self::DIREKTUR_KETUA_BUMDES => 'Direktur/Ketua BUMDes', + self::SEKRETARIS => 'Sekretaris', + self::BENDAHARA => 'Bendahara', + self::KEPALA_UNIT_USAHA_KOMODITAS => 'Kepala Unit Usaha Komoditas', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/KemitraanEksternalBumdes.php b/app/Enums/Bumdes/KemitraanEksternalBumdes.php new file mode 100644 index 0000000..e6ca83d --- /dev/null +++ b/app/Enums/Bumdes/KemitraanEksternalBumdes.php @@ -0,0 +1,30 @@ + 'NGO/lembaga pemberdayaan', + self::PEMERINTAH_KABUPATEN => 'Dinas/pemerintah kabupaten', + self::SWASTA_OFFTAKER => 'Perusahaan swasta/offtaker', + self::PROGRAM_PEMERINTAH_LAIN => 'Program pemerintah lain', + self::BELUM_ADA => 'Belum ada kemitraan eksternal', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/KeterlibatanKoordinasiBumdes.php b/app/Enums/Bumdes/KeterlibatanKoordinasiBumdes.php new file mode 100644 index 0000000..ecc760e --- /dev/null +++ b/app/Enums/Bumdes/KeterlibatanKoordinasiBumdes.php @@ -0,0 +1,24 @@ + 'Selalu dilibatkan', + self::KADANG => 'Kadang dilibatkan', + self::TIDAK_PERNAH => 'Tidak pernah dilibatkan', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/KondisiAsetProduktifBumdes.php b/app/Enums/Bumdes/KondisiAsetProduktifBumdes.php new file mode 100644 index 0000000..a15b76a --- /dev/null +++ b/app/Enums/Bumdes/KondisiAsetProduktifBumdes.php @@ -0,0 +1,43 @@ + 'Berfungsi', + self::PERLU_PERBAIKAN => 'Perlu perbaikan/evaluasi', + self::RUSAK => 'Rusak/tidak bisa dipakai', + self::MANGKRAK => 'Mangkrak', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } + + public static function normalizeLegacy(mixed $value): ?string + { + if (! is_scalar($value) || trim((string) $value) === '') { + return null; + } + + $value = trim((string) $value); + + return match (mb_strtolower($value)) { + 'baik', 'berfungsi', 'baik dan berfungsi', 'baik_berfungsi' => self::BERFUNGSI->value, + 'rusak ringan', 'perlu perbaikan', 'perlu perbaikan/evaluasi' => self::PERLU_PERBAIKAN->value, + 'rusak', 'rusak/tidak terpakai', 'rusak_tidak_terpakai', 'rusak/tidak bisa dipakai' => self::RUSAK->value, + 'mangkrak' => self::MANGKRAK->value, + default => $value, + }; + } +} diff --git a/app/Enums/Bumdes/KondisiInfrastrukturBumdes.php b/app/Enums/Bumdes/KondisiInfrastrukturBumdes.php new file mode 100644 index 0000000..75e0330 --- /dev/null +++ b/app/Enums/Bumdes/KondisiInfrastrukturBumdes.php @@ -0,0 +1,22 @@ + 'Baik dan berfungsi', + self::RUSAK_TIDAK_TERPAKAI => 'Rusak/tidak terpakai', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/KondisiKeuanganBumdes.php b/app/Enums/Bumdes/KondisiKeuanganBumdes.php new file mode 100644 index 0000000..ab0dc9c --- /dev/null +++ b/app/Enums/Bumdes/KondisiKeuanganBumdes.php @@ -0,0 +1,26 @@ + 'Untung/surplus', + self::IMPAS => 'Impas', + self::RUGI_DEFISIT => 'Rugi/defisit', + self::TIDAK_MENJAWAB => 'Tidak tahu/tidak mau menjawab', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/KontribusiPadesBumdes.php b/app/Enums/Bumdes/KontribusiPadesBumdes.php new file mode 100644 index 0000000..79f7140 --- /dev/null +++ b/app/Enums/Bumdes/KontribusiPadesBumdes.php @@ -0,0 +1,24 @@ + 'Ya, rutin', + self::TIDAK_RUTIN => 'Ya, tapi tidak rutin', + self::BELUM_PERNAH => 'Belum pernah', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/LayananPetaniBumdes.php b/app/Enums/Bumdes/LayananPetaniBumdes.php new file mode 100644 index 0000000..bb6c496 --- /dev/null +++ b/app/Enums/Bumdes/LayananPetaniBumdes.php @@ -0,0 +1,34 @@ + 'Simpan pinjam', + self::PENGADAAN_INPUT => 'Pengadaan input (pupuk, bibit, dll)', + self::PEMASARAN_PANEN => 'Pembelian/pemasaran hasil panen', + self::PENYEWAAN_ALAT => 'Penyewaan alat pengolahan', + self::JASA_PENGOLAHAN => 'Jasa pengolahan/fermentasi', + self::PELATIHAN => 'Pelatihan/pendampingan', + self::BELUM_ADA => 'Belum ada layanan terkait komoditas ini', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/MekanismePembayaranBumdes.php b/app/Enums/Bumdes/MekanismePembayaranBumdes.php new file mode 100644 index 0000000..efc5250 --- /dev/null +++ b/app/Enums/Bumdes/MekanismePembayaranBumdes.php @@ -0,0 +1,28 @@ + 'Tunai langsung', + self::TEMPO => 'Sistem tempo/dibayar kemudian', + self::CAMPURAN => 'Campuran, tergantung situasi', + self::IJON => 'BUMDes membayar di muka sebelum panen, sebagai bagian dari ikatan pinjaman kepada petani (ijon)', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/ModelFermentasiBumdes.php b/app/Enums/Bumdes/ModelFermentasiBumdes.php new file mode 100644 index 0000000..ade0438 --- /dev/null +++ b/app/Enums/Bumdes/ModelFermentasiBumdes.php @@ -0,0 +1,26 @@ + 'Individual (masing-masing petani)', + self::KOLEKTIF_KELOMPOK => 'Kolektif kelompok tani', + self::UNIT_BUMDES => 'Unit pengolah BUMDes/desa', + self::CAMPURAN => 'Campuran', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/PencatatanPetaniBumdes.php b/app/Enums/Bumdes/PencatatanPetaniBumdes.php new file mode 100644 index 0000000..c912f46 --- /dev/null +++ b/app/Enums/Bumdes/PencatatanPetaniBumdes.php @@ -0,0 +1,24 @@ + 'Ada, tercatat rapi', + self::TIDAK_RAPI => 'Ada tapi tidak rapi', + self::TIDAK_ADA => 'Tidak ada pencatatan', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/PenetapanHargaBeliBumdes.php b/app/Enums/Bumdes/PenetapanHargaBeliBumdes.php new file mode 100644 index 0000000..1099a2e --- /dev/null +++ b/app/Enums/Bumdes/PenetapanHargaBeliBumdes.php @@ -0,0 +1,26 @@ + 'Mengikuti harga pasar/tengkulak setempat', + self::KALKULASI_BIAYA => 'Ditetapkan sendiri berdasarkan kalkulasi biaya', + self::HARGA_MITRA => 'Mengikuti harga dari offtaker/mitra', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/PengalamanOfftakerBumdes.php b/app/Enums/Bumdes/PengalamanOfftakerBumdes.php new file mode 100644 index 0000000..6ad7439 --- /dev/null +++ b/app/Enums/Bumdes/PengalamanOfftakerBumdes.php @@ -0,0 +1,24 @@ + 'Sedang berjalan', + self::BERHENTI => 'Pernah, tapi sudah berhenti', + self::BELUM_PERNAH => 'Belum pernah', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/PeranRantaiNilaiBumdes.php b/app/Enums/Bumdes/PeranRantaiNilaiBumdes.php new file mode 100644 index 0000000..b1373cb --- /dev/null +++ b/app/Enums/Bumdes/PeranRantaiNilaiBumdes.php @@ -0,0 +1,26 @@ + 'Membeli langsung dari petani', + self::PERANTARA => 'Menjadi perantara/mengumpulkan untuk dijual ke pihak lain', + self::MENGOLAH_SENDIRI => 'Mengolah sendiri (sortasi/fermentasi/dll) sebelum dijual', + self::BELUM_BERPERAN => 'Belum berperan dalam rantai nilai komoditas ini', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/PeriodePanenBumdes.php b/app/Enums/Bumdes/PeriodePanenBumdes.php new file mode 100644 index 0000000..9f1c67c --- /dev/null +++ b/app/Enums/Bumdes/PeriodePanenBumdes.php @@ -0,0 +1,24 @@ + 'Panen Terakhir', + self::TAHUN_LALU => 'Tahun Lalu', + self::DUA_TAHUN_LALU => 'Dua Tahun Lalu', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/PerjanjianPetaniBumdes.php b/app/Enums/Bumdes/PerjanjianPetaniBumdes.php new file mode 100644 index 0000000..853f501 --- /dev/null +++ b/app/Enums/Bumdes/PerjanjianPetaniBumdes.php @@ -0,0 +1,24 @@ + 'Ada, tertulis', + self::LISAN => 'Ada, kesepakatan lisan saja', + self::TIDAK_ADA => 'Tidak ada perjanjian', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/PertanggungjawabanMusdesBumdes.php b/app/Enums/Bumdes/PertanggungjawabanMusdesBumdes.php new file mode 100644 index 0000000..4102fdf --- /dev/null +++ b/app/Enums/Bumdes/PertanggungjawabanMusdesBumdes.php @@ -0,0 +1,24 @@ + 'Ya, setiap tahun', + self::TIDAK_RUTIN => 'Tidak rutin', + self::BELUM_PERNAH => 'Belum pernah', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/SaluranPenjualanBumdes.php b/app/Enums/Bumdes/SaluranPenjualanBumdes.php new file mode 100644 index 0000000..4c254e0 --- /dev/null +++ b/app/Enums/Bumdes/SaluranPenjualanBumdes.php @@ -0,0 +1,30 @@ + 'Offtaker/eksportir langsung', + self::PEDAGANG_KABUPATEN => 'Pedagang besar/pengepul kabupaten', + self::KOPERASI_BUMDES => 'Koperasi atau BUMDes lain', + self::PASAR_LOKAL => 'Dijual eceran/pasar lokal', + self::BELUM_MENJUAL => 'Belum pernah menjual (masih disimpan/diproses)', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/SifatAuditBumdes.php b/app/Enums/Bumdes/SifatAuditBumdes.php new file mode 100644 index 0000000..7483d74 --- /dev/null +++ b/app/Enums/Bumdes/SifatAuditBumdes.php @@ -0,0 +1,22 @@ + 'Internal (pengurus/pengawas BUMDes sendiri)', + self::EKSTERNAL => 'Eksternal (Inspektorat, Dinas PMD, akuntan publik/auditor independen)', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/StatusAdArtBumdes.php b/app/Enums/Bumdes/StatusAdArtBumdes.php new file mode 100644 index 0000000..a3816a1 --- /dev/null +++ b/app/Enums/Bumdes/StatusAdArtBumdes.php @@ -0,0 +1,26 @@ + 'Ada, lengkap', + self::ADA_TIDAK_LENGKAP => 'Ada tapi tidak lengkap', + self::TIDAK_ADA => 'Tidak ada', + self::TIDAK_TAHU => 'Tidak tahu', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/StatusBadanHukumBumdes.php b/app/Enums/Bumdes/StatusBadanHukumBumdes.php new file mode 100644 index 0000000..083f091 --- /dev/null +++ b/app/Enums/Bumdes/StatusBadanHukumBumdes.php @@ -0,0 +1,26 @@ + 'Sudah berbadan hukum (terdaftar Kemenkumham)', + self::PROSES_PENDAFTARAN => 'Proses pendaftaran', + self::BELUM_BERBADAN_HUKUM => 'Belum berbadan hukum', + self::TIDAK_TAHU => 'Tidak tahu', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/StatusFermentasiBumdes.php b/app/Enums/Bumdes/StatusFermentasiBumdes.php new file mode 100644 index 0000000..33a6691 --- /dev/null +++ b/app/Enums/Bumdes/StatusFermentasiBumdes.php @@ -0,0 +1,26 @@ + 'Ya, rutin', + self::PERNAH_MENCOBA => 'Pernah mencoba', + self::DIRENCANAKAN => 'Belum pernah, direncanakan', + self::TIDAK_DIRENCANAKAN => 'Belum pernah, tidak direncanakan', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/StatusKeaktifanBumdes.php b/app/Enums/Bumdes/StatusKeaktifanBumdes.php new file mode 100644 index 0000000..04add4e --- /dev/null +++ b/app/Enums/Bumdes/StatusKeaktifanBumdes.php @@ -0,0 +1,26 @@ + 'Aktif beroperasi penuh', + self::AKTIF_TERBATAS => 'Aktif tapi terbatas (sebagian unit usaha berjalan)', + self::TIDAK_AKTIF_VAKUM => 'Tidak aktif/vakum', + self::BERDIRI_BELUM_BEROPERASI => 'Sudah berdiri tapi belum pernah beroperasi', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/StatusKepemilikanAsetBumdes.php b/app/Enums/Bumdes/StatusKepemilikanAsetBumdes.php new file mode 100644 index 0000000..31f85ab --- /dev/null +++ b/app/Enums/Bumdes/StatusKepemilikanAsetBumdes.php @@ -0,0 +1,24 @@ + 'Milik BUMDes sendiri', + self::MILIK_DESA_DIPINJAMKAN_DIKELOLA => 'Milik desa (dipinjamkan/dikelola)', + self::SEWA_PINJAM_PIHAK_LAIN => 'Sewa/pinjam pihak lain', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/StatusPembukuanBumdes.php b/app/Enums/Bumdes/StatusPembukuanBumdes.php new file mode 100644 index 0000000..8155f6e --- /dev/null +++ b/app/Enums/Bumdes/StatusPembukuanBumdes.php @@ -0,0 +1,24 @@ + 'Ya, rutin dan tertib', + self::TIDAK_RUTIN => 'Ada tapi tidak rutin/tidak rapi', + self::TIDAK_ADA => 'Tidak ada pembukuan', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/StatusPengeringanBumdes.php b/app/Enums/Bumdes/StatusPengeringanBumdes.php new file mode 100644 index 0000000..d867756 --- /dev/null +++ b/app/Enums/Bumdes/StatusPengeringanBumdes.php @@ -0,0 +1,24 @@ + 'Ya, rutin', + self::KADANG => 'Kadang-kadang', + self::TIDAK_PERNAH => 'Tidak pernah, produk dijual dalam kondisi diterima', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/StatusSortasiBumdes.php b/app/Enums/Bumdes/StatusSortasiBumdes.php new file mode 100644 index 0000000..c8dbab2 --- /dev/null +++ b/app/Enums/Bumdes/StatusSortasiBumdes.php @@ -0,0 +1,24 @@ + 'Ya, rutin dengan standar tertentu', + self::TIDAK_KONSISTEN => 'Ya, tapi tidak konsisten', + self::BELUM_PERNAH => 'Belum pernah dilakukan', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/StatusUnitKomoditasBumdes.php b/app/Enums/Bumdes/StatusUnitKomoditasBumdes.php new file mode 100644 index 0000000..a08a3c9 --- /dev/null +++ b/app/Enums/Bumdes/StatusUnitKomoditasBumdes.php @@ -0,0 +1,24 @@ + 'Ya, ada unit khusus', + self::DITANGANI_PENGURUS_INTI => 'Tidak ada unit khusus, ditangani langsung pengurus inti', + self::DIRENCANAKAN => 'Belum ada, masih direncanakan', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/SumberPasokanBumdes.php b/app/Enums/Bumdes/SumberPasokanBumdes.php new file mode 100644 index 0000000..a125fe4 --- /dev/null +++ b/app/Enums/Bumdes/SumberPasokanBumdes.php @@ -0,0 +1,28 @@ + 'Anggota kelompok tani langsung', + self::PETANI_NON_ANGGOTA => 'Petani non-anggota', + self::PENGUMPUL_DESA => 'Pengumpul/sub-agen desa', + self::CAMPURAN => 'Campuran', + self::LAINNYA => 'Lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Enums/Bumdes/UnitUsahaBumdes.php b/app/Enums/Bumdes/UnitUsahaBumdes.php new file mode 100644 index 0000000..854b624 --- /dev/null +++ b/app/Enums/Bumdes/UnitUsahaBumdes.php @@ -0,0 +1,32 @@ + 'Simpan pinjam', + self::PERDAGANGAN_TOKO_DESA => 'Perdagangan/toko desa', + self::PENGELOLAAN_AIR_BERSIH => 'Pengelolaan air bersih', + self::WISATA_DESA_AGROWISATA => 'Wisata desa/agrowisata', + self::PENGELOLAAN_HASIL_PERTANIAN_KOMODITAS => 'Pengelolaan hasil pertanian/komoditas', + self::PENYEWAAN_ALAT_ASET => 'Penyewaan alat/aset', + self::JASA_LAINNYA => 'Jasa lainnya', + }; + } + + public static function options(): array + { + return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all(); + } +} diff --git a/app/Http/Controllers/Web/Bumdes/KelembagaanAktorBumdesController.php b/app/Http/Controllers/Web/Bumdes/KelembagaanAktorBumdesController.php new file mode 100644 index 0000000..64d7e31 --- /dev/null +++ b/app/Http/Controllers/Web/Bumdes/KelembagaanAktorBumdesController.php @@ -0,0 +1,29 @@ +service->sectionData($pengurusBumdes)); + } + + public function store(StoreKelembagaanAktorBumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse + { + $this->service->saveSection($pengurusBumdes, $request->validated(), 'kelembagaan'); + + return redirect()->route('bumdes.kelembagaan-aktor.index', $pengurusBumdes) + ->with('success', 'Data Kelembagaan berhasil disimpan.'); + } +} diff --git a/app/Http/Controllers/Web/Bumdes/OperasionalBumdesController.php b/app/Http/Controllers/Web/Bumdes/OperasionalBumdesController.php new file mode 100644 index 0000000..ad1f0f6 --- /dev/null +++ b/app/Http/Controllers/Web/Bumdes/OperasionalBumdesController.php @@ -0,0 +1,29 @@ +service->sectionData($pengurusBumdes)); + } + + public function store(StoreOperasionalBumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse + { + $this->service->saveSection($pengurusBumdes, $request->validated(), 'operasional'); + + return redirect()->route('bumdes.operasional.index', $pengurusBumdes) + ->with('success', 'Data Operasional berhasil disimpan.'); + } +} diff --git a/app/Http/Controllers/Web/Bumdes/PengurusBumdesController.php b/app/Http/Controllers/Web/Bumdes/PengurusBumdesController.php index 2146c49..dfdf7ab 100644 --- a/app/Http/Controllers/Web/Bumdes/PengurusBumdesController.php +++ b/app/Http/Controllers/Web/Bumdes/PengurusBumdesController.php @@ -2,579 +2,66 @@ namespace App\Http\Controllers\Web\Bumdes; -use App\Enums\Bumdes\JabatanBumdes; -use App\Enums\Pendidikan; use App\Http\Controllers\Controller; -use App\Models\Bumdes\KelembagaanAktorBumdes; +use App\Http\Requests\Bumdes\BumdesRequest; +use App\Http\Requests\Bumdes\IndexPengurusBumdesRequest; +use App\Http\Requests\Bumdes\StorePengurusBumdesRequest; +use App\Http\Requests\Bumdes\UpdatePengurusBumdesRequest; use App\Models\Bumdes\PengurusBumdes; -use App\Models\Wilayah\Provinsi; -use Carbon\Carbon; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Gate; -use Illuminate\Support\Str; -use Illuminate\Validation\Rule; +use App\Services\Bumdes\PengurusBumdesService; +use Illuminate\Http\RedirectResponse; +use Illuminate\Support\Facades\DB; +use Illuminate\View\View; class PengurusBumdesController extends Controller { - public function index(Request $request) + public function __construct(private readonly PengurusBumdesService $service) {} + + public function index(IndexPengurusBumdesRequest $request): View { - if (! Gate::any(['super_admin', 'admin', 'enumerator'])) { - abort(403); - } - - $query = PengurusBumdes::with([ - ...$this->wilayahRelationsWithoutProgramScope(), - 'creator:id,name', - ]) - ->orderBy('created_at', 'desc'); - - if ($request->search) { - $query->where(function ($query) use ($request) { - $query->where('nama', 'like', '%' . $request->search . '%') - ->orWhere('jabatan', 'like', '%' . $request->search . '%') - ->orWhere('jabatan_lainnya', 'like', '%' . $request->search . '%'); - }); - } - - if (auth()->user()->role->value === 'enumerator') { - $query->where('created_by', auth()->id()); - } - - $summaryQuery = clone $query; - $totalPengurus = (clone $summaryQuery)->count(); - $totalLakiLaki = (clone $summaryQuery)->where('jenis_kelamin', true)->count(); - $totalPerempuan = (clone $summaryQuery)->where('jenis_kelamin', false)->count(); - $totalDesaKelurahan = (clone $summaryQuery)->distinct('desa_kelurahan_id')->count('desa_kelurahan_id'); - $totalJabatan = (clone $summaryQuery)->distinct('jabatan')->count('jabatan'); - - $pengurus = $query->paginate($request->per_page ?? 10); - $userRole = auth()->user()->role->value; - $showEnumerator = in_array($userRole, ['super_admin', 'admin']); - - return view('bumdes.profil.index', compact( - 'pengurus', - 'totalPengurus', - 'totalLakiLaki', - 'totalPerempuan', - 'totalDesaKelurahan', - 'totalJabatan', - 'showEnumerator' - )); + return view('bumdes.profil.index', $this->service->indexData($request->validated())); } - public function create() + public function create(BumdesRequest $request): View { - if (! Gate::any(['super_admin', 'admin', 'enumerator'])) { - abort(403); - } - - $provinsi = $this->provinsiOptions(); - $jabatan = JabatanBumdes::options(); - $pendidikan = Pendidikan::options(); - - return view('bumdes.profil.add', compact('provinsi', 'jabatan', 'pendidikan')); + return view('bumdes.profil.create', $this->service->profileData()); } - public function store(Request $request) + public function store(StorePengurusBumdesRequest $request): RedirectResponse { - if (! Gate::any(['super_admin', 'admin', 'enumerator'])) { - abort(403); - } + $pengurus = $this->service->storeProfile($request->validated()); - $validatedData = $this->validatedData($request); - - $pengurus = PengurusBumdes::create([ - ...$this->formatData($validatedData), - 'uid' => 'BMD-' . strtoupper(Str::random(10)), - 'created_by' => auth()->id(), - 'updated_by' => auth()->id(), - ]); - - return redirect()->route('bumdes.edit', $pengurus->id)->with('success', 'Data pengurus BUMDes berhasil ditambahkan.'); + return $this->profileSavedResponse($request, $pengurus); } - public function show(string $id) + public function show(BumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse { - return redirect()->route('bumdes.edit', $id); + return redirect()->route('bumdes.edit', $pengurusBumdes); } - public function edit(string $id) + public function edit(BumdesRequest $request, PengurusBumdes $pengurusBumdes): View { - if (! Gate::any(['super_admin', 'admin', 'enumerator'])) { - abort(403); - } - - $pengurus = $this->pengurusQuery() - ->with($this->wilayahRelationsWithoutProgramScope()) - ->findOrFail($id); - $provinsi = $this->provinsiOptions(); - $jabatan = JabatanBumdes::options(); - $pendidikan = Pendidikan::options(); - - return view('bumdes.profil.edit', compact('pengurus', 'provinsi', 'jabatan', 'pendidikan')); + return view('bumdes.profil.edit', $this->service->profileData($pengurusBumdes)); } - public function update(Request $request, string $id) + public function update(UpdatePengurusBumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse { - if (! Gate::any(['super_admin', 'admin', 'enumerator'])) { - abort(403); - } + $this->service->updateProfile($pengurusBumdes, $request->validated()); - $pengurus = $this->pengurusQuery()->findOrFail($id); - $validatedData = $this->validatedData($request); - - $pengurus->update([ - ...$this->formatData($validatedData), - 'updated_by' => auth()->id(), - ]); - - return redirect()->route('bumdes.edit', $pengurus->id)->with('success', 'Data pengurus BUMDes berhasil diupdate.'); + return $this->profileSavedResponse($request, $pengurusBumdes); } - public function destroy(string $id) + public function destroy(BumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse { - if (! Gate::any(['super_admin', 'admin', 'enumerator'])) { - abort(403); - } + DB::transaction(fn () => $pengurusBumdes->delete()); - $pengurus = $this->pengurusQuery()->findOrFail($id); - $pengurus->update(['deleted_by' => auth()->id()]); - $pengurus->delete(); - - return response()->json(['message' => 'Data pengurus BUMDes berhasil dihapus.']); + return redirect()->route('bumdes.index')->with('success', 'Data profil berhasil dihapus.'); } - public function kelembagaanAktor(string $id) + private function profileSavedResponse(BumdesRequest $request, PengurusBumdes $pengurus): RedirectResponse { - if (! Gate::any(['super_admin', 'admin', 'enumerator'])) { - abort(403); - } + $fragment = $request->input('redirect_fragment') === 'section-kepengurusan' ? '#section-kepengurusan' : ''; - $pengurus = $this->pengurusQuery() - ->with([ - ...$this->wilayahRelationsWithoutProgramScope(), - 'kelembagaanAktor', - ]) - ->findOrFail($id); - $kelembagaanAktor = $pengurus->kelembagaanAktor; - - return view('bumdes.kelembagaan-aktor.index', array_merge( - compact('pengurus', 'kelembagaanAktor'), - $this->kelembagaanAktorOptions() - )); - } - - public function storeKelembagaanAktor(Request $request, string $id) - { - if (! Gate::any(['super_admin', 'admin', 'enumerator'])) { - abort(403); - } - - $pengurus = $this->pengurusQuery()->findOrFail($id); - $validatedData = $this->validatedKelembagaanAktorData($request); - - KelembagaanAktorBumdes::updateOrCreate( - ['pengurus_bumdes_id' => $pengurus->id], - [ - ...$this->formatKelembagaanAktorData($validatedData), - 'created_by' => $pengurus->kelembagaanAktor?->created_by ?? auth()->id(), - 'updated_by' => auth()->id(), - ] - ); - - return redirect() - ->route('bumdes.kelembagaan-aktor.index', $pengurus->id) - ->with('success', 'Data kelembagaan dan aktor BUMDes berhasil disimpan.'); - } - - private function pengurusQuery() - { - $query = PengurusBumdes::query(); - - if (auth()->user()->role->value === 'enumerator') { - $query->where('created_by', auth()->id()); - } - - return $query; - } - - private function provinsiOptions() - { - return Provinsi::withoutGlobalScope('program') - ->select('id', 'kode', 'nama') - ->orderBy('kode', 'asc') - ->get(); - } - - private function wilayahRelationsWithoutProgramScope(): array - { - return [ - 'desaKelurahan' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama', 'kecamatan_id'), - 'desaKelurahan.kecamatan' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama', 'kabupaten_kota_id'), - 'desaKelurahan.kecamatan.kabupatenKota' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama', 'provinsi_id'), - 'desaKelurahan.kecamatan.kabupatenKota.provinsi' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama'), - ]; - } - - private function validatedData(Request $request): array - { - return $request->validate([ - 'nama' => ['required', 'string', 'max:255'], - 'jenis_kelamin' => ['required', 'boolean'], - 'tempat_lahir' => ['required', 'string', 'max:255'], - 'tanggal_lahir' => ['required', 'date_format:d-m-Y'], - 'pendidikan_terakhir' => ['required', Rule::enum(Pendidikan::class)], - 'alamat' => ['required', 'string', 'max:500'], - 'desa_kelurahan_id' => ['required', 'exists:master_desa_kelurahan,id'], - 'nomor_telepon' => ['nullable', 'digits_between:11,13'], - 'jabatan' => ['required', Rule::enum(JabatanBumdes::class)], - 'jabatan_lainnya' => ['nullable', 'required_if:jabatan,' . JabatanBumdes::LAINNYA->value, 'string', 'max:255'], - 'lama_menjabat_tahun' => ['required', 'integer', 'min:0', 'max:80'], - ], [ - 'nama.required' => 'Nama harus diisi.', - 'jenis_kelamin.required' => 'Jenis kelamin harus dipilih.', - 'tempat_lahir.required' => 'Tempat lahir harus diisi.', - 'tanggal_lahir.required' => 'Tanggal lahir harus diisi.', - 'tanggal_lahir.date_format' => 'Tanggal lahir harus menggunakan format tanggal yang valid.', - 'pendidikan_terakhir.required' => 'Pendidikan terakhir harus dipilih.', - 'alamat.required' => 'Alamat harus diisi.', - 'desa_kelurahan_id.required' => 'Desa kelurahan harus dipilih.', - 'desa_kelurahan_id.exists' => 'Desa kelurahan tidak ditemukan.', - 'nomor_telepon.digits_between' => 'Nomor telepon harus berupa angka 11 sampai 13 digit.', - 'jabatan.required' => 'Jabatan/peran harus dipilih.', - 'jabatan_lainnya.required_if' => 'Jabatan lainnya harus diisi.', - 'lama_menjabat_tahun.required' => 'Lama menjabat harus diisi.', - 'lama_menjabat_tahun.integer' => 'Lama menjabat harus berupa angka tahun.', - ]); - } - - private function formatData(array $data): array - { - $data['tanggal_lahir'] = Carbon::createFromFormat('d-m-Y', $data['tanggal_lahir'])->format('Y-m-d'); - $data['tanggal_mulai_menjabat'] = Carbon::now() - ->subYears((int) $data['lama_menjabat_tahun']) - ->format('Y-m-d'); - - if (($data['jabatan'] ?? null) !== JabatanBumdes::LAINNYA->value) { - $data['jabatan_lainnya'] = null; - } - - return $data; - } - - private function validatedKelembagaanAktorData(Request $request): array - { - $this->normalizeKelembagaanAktorInput($request); - $maxYear = now()->year + 1; - - return $request->validate([ - 'status_badan_hukum' => ['nullable', Rule::in(array_keys($this->statusBadanHukumOptions()))], - 'nomor_dasar_hukum' => ['nullable', 'string', 'max:255'], - 'tahun_dasar_hukum' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:' . $maxYear], - 'status_keaktifan' => ['nullable', Rule::in(array_keys($this->statusKeaktifanOptions()))], - 'alasan_tidak_aktif' => ['nullable', 'string'], - 'unit_usaha' => ['nullable', 'array'], - 'unit_usaha.*' => ['string', Rule::in(array_keys($this->unitUsahaOptions()))], - 'unit_usaha_jasa_lainnya' => ['nullable', 'string', 'max:255'], - 'status_unit_komoditas' => ['nullable', Rule::in(array_keys($this->statusUnitKomoditasOptions()))], - 'nama_unit_komoditas' => ['nullable', 'string', 'max:255'], - 'status_ad_art' => ['nullable', Rule::in(array_keys($this->statusAdArtOptions()))], - 'tahun_ad_art' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:' . $maxYear], - 'pengurus_inti' => ['nullable', 'array'], - 'pengurus_inti.*.jabatan' => ['nullable', 'string', 'max:255'], - 'pengurus_inti.*.nama' => ['nullable', 'string', 'max:255'], - 'pengurus_inti.*.aktif' => ['nullable', 'boolean'], - 'memiliki_staf' => ['nullable', 'boolean'], - 'jumlah_staf' => ['nullable', 'integer', 'min:0'], - 'unit_usaha_staf' => ['nullable', 'string'], - 'modal_awal' => ['nullable', 'integer', 'min:0'], - 'tahun_modal_awal' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:' . $maxYear], - 'pernah_penambahan_modal' => ['nullable', 'boolean'], - 'penambahan_modal' => ['nullable', 'array'], - 'penambahan_modal.*.tahun' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:' . $maxYear], - 'penambahan_modal.*.jumlah' => ['nullable', 'integer', 'min:0'], - 'aset_produktif' => ['nullable', 'array'], - 'aset_produktif.*.aset' => ['nullable', 'string', 'max:255'], - 'aset_produktif.*.dipilih' => ['nullable', 'boolean'], - 'aset_produktif.*.jumlah' => ['nullable', 'string', 'max:255'], - 'aset_produktif.*.kondisi' => ['nullable', 'string', 'max:255'], - 'aset_produktif_lainnya' => ['nullable', 'string', 'max:255'], - 'status_kepemilikan_aset' => ['nullable', Rule::in(array_keys($this->statusKepemilikanAsetOptions()))], - 'infrastruktur_pascapanen' => ['nullable', 'array'], - 'infrastruktur_pascapanen.*.infrastruktur' => ['nullable', 'string', 'max:255'], - 'infrastruktur_pascapanen.*.dipilih' => ['nullable', 'boolean'], - 'infrastruktur_pascapanen.*.jumlah' => ['nullable', 'string', 'max:255'], - 'infrastruktur_pascapanen.*.kondisi' => ['nullable', Rule::in(array_keys($this->kondisiInfrastrukturOptions()))], - 'infrastruktur_pascapanen_lainnya' => ['nullable', 'string', 'max:255'], - ], $this->kelembagaanAktorValidationMessages(), $this->kelembagaanAktorValidationAttributes()); - } - - private function normalizeKelembagaanAktorInput(Request $request): void - { - foreach (['modal_awal'] as $field) { - if ($request->has($field)) { - $request->merge([$field => preg_replace('/\D/', '', (string) $request->input($field))]); - } - } - - $penambahanModal = collect($request->input('penambahan_modal', [])) - ->map(function ($row) { - if (isset($row['jumlah'])) { - $row['jumlah'] = preg_replace('/\D/', '', (string) $row['jumlah']); - } - - return $row; - }) - ->all(); - - if ($request->has('penambahan_modal')) { - $request->merge(['penambahan_modal' => $penambahanModal]); - } - } - - private function formatKelembagaanAktorData(array $data): array - { - $payload = array_merge([ - 'status_badan_hukum' => null, - 'nomor_dasar_hukum' => null, - 'tahun_dasar_hukum' => null, - 'status_keaktifan' => null, - 'alasan_tidak_aktif' => null, - 'unit_usaha' => [], - 'unit_usaha_jasa_lainnya' => null, - 'status_unit_komoditas' => null, - 'nama_unit_komoditas' => null, - 'status_ad_art' => null, - 'tahun_ad_art' => null, - 'pengurus_inti' => [], - 'memiliki_staf' => null, - 'jumlah_staf' => null, - 'unit_usaha_staf' => null, - 'modal_awal' => null, - 'tahun_modal_awal' => null, - 'pernah_penambahan_modal' => null, - 'penambahan_modal' => [], - 'aset_produktif' => [], - 'aset_produktif_lainnya' => null, - 'status_kepemilikan_aset' => null, - 'infrastruktur_pascapanen' => [], - 'infrastruktur_pascapanen_lainnya' => null, - ], $data); - - $payload['unit_usaha'] = array_values($payload['unit_usaha'] ?? []); - $payload['pengurus_inti'] = $this->filterRows($payload['pengurus_inti'] ?? [], ['nama', 'aktif']); - $payload['penambahan_modal'] = $this->filterRows($payload['penambahan_modal'] ?? [], ['tahun', 'jumlah']); - $payload['aset_produktif'] = $this->filterRows($payload['aset_produktif'] ?? [], ['dipilih', 'jumlah', 'kondisi']); - $payload['infrastruktur_pascapanen'] = $this->filterRows($payload['infrastruktur_pascapanen'] ?? [], ['dipilih', 'jumlah', 'kondisi']); - - if (($payload['status_keaktifan'] ?? null) !== 'tidak_aktif_vakum') { - $payload['alasan_tidak_aktif'] = null; - } - - if (! in_array('jasa_lainnya', $payload['unit_usaha'], true)) { - $payload['unit_usaha_jasa_lainnya'] = null; - } - - if (($payload['status_unit_komoditas'] ?? null) !== 'ya') { - $payload['nama_unit_komoditas'] = null; - } - - if (($payload['status_ad_art'] ?? null) !== 'ada_lengkap') { - $payload['tahun_ad_art'] = null; - } - - if (! (bool) ($payload['memiliki_staf'] ?? false)) { - $payload['jumlah_staf'] = null; - $payload['unit_usaha_staf'] = null; - } - - if (! (bool) ($payload['pernah_penambahan_modal'] ?? false)) { - $payload['penambahan_modal'] = null; - } - - if (! collect($payload['aset_produktif'] ?? [])->contains(fn ($row) => ($row['aset'] ?? null) === 'lainnya')) { - $payload['aset_produktif_lainnya'] = null; - } - - if (! collect($payload['infrastruktur_pascapanen'] ?? [])->contains(fn ($row) => ($row['infrastruktur'] ?? null) === 'lainnya')) { - $payload['infrastruktur_pascapanen_lainnya'] = null; - } - - return $payload; - } - - private function filterRows(array $rows, array $valueKeys): array - { - return collect($rows) - ->map(function ($row) { - return collect($row) - ->map(fn ($value) => $value === '' ? null : $value) - ->all(); - }) - ->filter(function ($row) use ($valueKeys) { - foreach ($valueKeys as $key) { - if (filled($row[$key] ?? null)) { - return true; - } - } - - return false; - }) - ->values() - ->all(); - } - - private function kelembagaanAktorOptions(): array - { - return [ - 'statusBadanHukumOptions' => $this->statusBadanHukumOptions(), - 'statusKeaktifanOptions' => $this->statusKeaktifanOptions(), - 'unitUsahaOptions' => $this->unitUsahaOptions(), - 'statusUnitKomoditasOptions' => $this->statusUnitKomoditasOptions(), - 'statusAdArtOptions' => $this->statusAdArtOptions(), - 'pengurusIntiRows' => $this->pengurusIntiRows(), - 'asetProduktifRows' => $this->asetProduktifRows(), - 'statusKepemilikanAsetOptions' => $this->statusKepemilikanAsetOptions(), - 'infrastrukturPascapanenRows' => $this->infrastrukturPascapanenRows(), - 'kondisiInfrastrukturOptions' => $this->kondisiInfrastrukturOptions(), - ]; - } - - private function statusBadanHukumOptions(): array - { - return [ - 'sudah_berbadan_hukum' => 'Sudah berbadan hukum (terdaftar Kemenkumham)', - 'proses_pendaftaran' => 'Proses pendaftaran', - 'belum_berbadan_hukum' => 'Belum berbadan hukum', - 'tidak_tahu' => 'Tidak tahu', - ]; - } - - private function statusKeaktifanOptions(): array - { - return [ - 'aktif_penuh' => 'Aktif beroperasi penuh', - 'aktif_terbatas' => 'Aktif tapi terbatas (sebagian unit usaha berjalan)', - 'tidak_aktif_vakum' => 'Tidak aktif/vakum', - 'berdiri_belum_beroperasi' => 'Sudah berdiri tapi belum pernah beroperasi', - ]; - } - - private function unitUsahaOptions(): array - { - return [ - 'simpan_pinjam' => 'Simpan pinjam', - 'perdagangan_toko_desa' => 'Perdagangan/toko desa', - 'pengelolaan_air_bersih' => 'Pengelolaan air bersih', - 'wisata_desa_agrowisata' => 'Wisata desa/agrowisata', - 'pengelolaan_hasil_pertanian_komoditas' => 'Pengelolaan hasil pertanian/komoditas', - 'penyewaan_alat_aset' => 'Penyewaan alat/aset', - 'jasa_lainnya' => 'Jasa lainnya', - ]; - } - - private function statusUnitKomoditasOptions(): array - { - return [ - 'ya' => 'Ya, ada unit khusus', - 'ditangani_pengurus_inti' => 'Tidak ada unit khusus, ditangani langsung pengurus inti', - 'direncanakan' => 'Belum ada, masih direncanakan', - ]; - } - - private function statusAdArtOptions(): array - { - return [ - 'ada_lengkap' => 'Ada, lengkap', - 'ada_tidak_lengkap' => 'Ada tapi tidak lengkap', - 'tidak_ada' => 'Tidak ada', - 'tidak_tahu' => 'Tidak tahu', - ]; - } - - private function pengurusIntiRows(): array - { - return [ - 'penasihat_kepala_desa' => 'Penasihat (Kepala Desa)', - 'ketua_pengawas' => 'Ketua Pengawas', - 'direktur_ketua_bumdes' => 'Direktur/Ketua BUMDes', - 'sekretaris' => 'Sekretaris', - 'bendahara' => 'Bendahara', - 'kepala_unit_usaha_komoditas' => 'Kepala Unit Usaha Komoditas', - 'lainnya' => 'Lainnya', - ]; - } - - private function asetProduktifRows(): array - { - return [ - 'bangunan_kantor' => 'Bangunan / kantor', - 'gudang_penyimpanan' => 'Gudang penyimpanan', - 'kendaraan_operasional_angkut' => 'Kendaraan operasional/angkut', - 'lahan_usaha' => 'Lahan usaha', - 'alat_pengolahan_komoditas' => 'Alat pengolahan komoditas', - 'lainnya' => 'Lainnya', - ]; - } - - private function statusKepemilikanAsetOptions(): array - { - return [ - 'milik_bumdes_sendiri' => 'Milik BUMDes sendiri', - 'milik_desa_dipinjamkan_dikelola' => 'Milik desa (dipinjamkan/dikelola)', - 'sewa_pinjam_pihak_lain' => 'Sewa/pinjam pihak lain', - ]; - } - - private function infrastrukturPascapanenRows(): array - { - return [ - 'gudang_penyimpanan_hasil_panen' => 'Gudang penyimpanan hasil panen', - 'alat_mesin_pengering' => 'Alat/mesin pengering', - 'mesin_pulper_huller' => 'Mesin pulper/huller', - 'timbangan' => 'Timbangan', - 'alat_sortasi' => 'Alat sortasi', - 'lainnya' => 'Lainnya', - ]; - } - - private function kondisiInfrastrukturOptions(): array - { - return [ - 'baik_berfungsi' => 'Baik dan berfungsi', - 'rusak_tidak_terpakai' => 'Rusak/tidak terpakai', - ]; - } - - private function kelembagaanAktorValidationMessages(): array - { - return [ - 'integer' => ':attribute harus berupa angka.', - 'digits' => ':attribute harus terdiri dari :digits digit.', - 'min' => ':attribute tidak boleh kurang dari :min.', - 'max' => ':attribute tidak valid.', - 'in' => ':attribute tidak valid.', - 'array' => ':attribute harus berupa daftar data.', - 'boolean' => ':attribute harus dipilih.', - ]; - } - - private function kelembagaanAktorValidationAttributes(): array - { - return [ - 'status_badan_hukum' => 'Status badan hukum', - 'nomor_dasar_hukum' => 'Nomor dasar hukum', - 'tahun_dasar_hukum' => 'Tahun dasar hukum', - 'status_keaktifan' => 'Status keaktifan', - 'unit_usaha' => 'Unit usaha', - 'status_unit_komoditas' => 'Unit usaha komoditas', - 'status_ad_art' => 'Dokumen AD/ART', - 'memiliki_staf' => 'Status staf/karyawan', - 'jumlah_staf' => 'Jumlah staf', - 'modal_awal' => 'Modal awal', - 'tahun_modal_awal' => 'Tahun modal awal', - 'pernah_penambahan_modal' => 'Status penambahan modal', - 'status_kepemilikan_aset' => 'Status kepemilikan aset', - ]; + return redirect(route('bumdes.edit', $pengurus).$fragment)->with('success', 'Data Profil berhasil disimpan.'); } } diff --git a/app/Http/Controllers/Web/Bumdes/RantaiNilaiBumdesController.php b/app/Http/Controllers/Web/Bumdes/RantaiNilaiBumdesController.php new file mode 100644 index 0000000..16b7473 --- /dev/null +++ b/app/Http/Controllers/Web/Bumdes/RantaiNilaiBumdesController.php @@ -0,0 +1,34 @@ +service->sectionData($pengurusBumdes)); + } + + public function store(StoreRantaiNilaiBumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse + { + $this->service->saveSection($pengurusBumdes, $request->validated(), 'rantai-nilai'); + + $nextSection = $request->validated('redirect_fragment'); + $postharvestSection = in_array($nextSection, ['section-rantai-pengeringan', 'section-rantai-fermentasi', 'section-rantai-premium-harga'], true); + $canContinue = $request->boolean($postharvestSection ? 'melakukan_pengolahan_pascapanen' : 'melakukan_jual_beli'); + $nextSection = $canContinue ? $nextSection : null; + + return redirect()->to(route('bumdes.rantai-nilai.index', $pengurusBumdes).($nextSection ? '#'.$nextSection : '')) + ->with('success', 'Data Rantai Nilai & Akses Pasar berhasil disimpan.'); + } +} diff --git a/app/Http/Requests/Bumdes/BumdesRequest.php b/app/Http/Requests/Bumdes/BumdesRequest.php new file mode 100644 index 0000000..715702a --- /dev/null +++ b/app/Http/Requests/Bumdes/BumdesRequest.php @@ -0,0 +1,174 @@ +route('pengurusBumdes'); + if ($pengurus instanceof PengurusBumdes && $this->user()->role === Role::ENUMERATOR) { + abort_unless((int) $pengurus->created_by === (int) $this->user()->id, 404); + } + + if ($pengurus instanceof PengurusBumdes && $this->routeIs('bumdes.operasional.*', 'bumdes.kelembagaan-aktor.*', 'bumdes.rantai-nilai.*')) { + abort_unless($pengurus->profilLengkap(), 403); + } + + return true; + } + + public function rules(): array + { + return []; + } + + protected function sectionRules(string $section): array + { + $maxYear = now()->year + 1; + + $rules = [ + 'status_badan_hukum' => ['nullable', Rule::enum(StatusBadanHukumBumdes::class)], + 'nomor_dasar_hukum' => [Rule::excludeIf($this->input('status_badan_hukum') !== 'sudah_berbadan_hukum'), 'required_if:status_badan_hukum,sudah_berbadan_hukum', 'string', 'max:255'], + 'tahun_dasar_hukum' => [Rule::excludeIf($this->input('status_badan_hukum') !== 'sudah_berbadan_hukum'), 'required_if:status_badan_hukum,sudah_berbadan_hukum', 'integer', 'digits:4', 'min:1900', 'max:'.$maxYear], + 'status_keaktifan' => ['nullable', Rule::enum(StatusKeaktifanBumdes::class)], + 'alasan_tidak_aktif' => ['nullable', 'string'], + 'unit_usaha' => ['nullable', 'array'], + 'unit_usaha.*' => ['string', Rule::enum(UnitUsahaBumdes::class)], + 'unit_usaha_jasa_lainnya' => ['nullable', 'string', 'max:255'], + 'status_unit_komoditas' => ['nullable', Rule::enum(StatusUnitKomoditasBumdes::class)], + 'nama_unit_komoditas' => [Rule::excludeIf($this->input('status_unit_komoditas') !== 'ya'), 'required_if:status_unit_komoditas,ya', 'string', 'max:255'], + 'status_ad_art' => ['nullable', Rule::enum(StatusAdArtBumdes::class)], + 'tahun_ad_art' => [Rule::excludeIf($this->input('status_ad_art') !== 'ada_lengkap'), 'required_if:status_ad_art,ada_lengkap', 'integer', 'digits:4', 'min:1900', 'max:'.$maxYear], + 'pengurus_inti' => ['nullable', 'array'], + 'pengurus_inti.*' => ['array'], + 'pengurus_inti.*.jabatan' => ['nullable', Rule::enum(JabatanPengurusIntiBumdes::class)], + 'pengurus_inti.*.nama' => ['nullable', 'string', 'max:255'], + 'pengurus_inti.*.aktif' => ['nullable', 'boolean'], + 'memiliki_staf' => ['nullable', 'boolean'], + 'jumlah_staf' => ['nullable', 'integer', 'min:0'], + 'unit_usaha_staf' => ['nullable', 'string'], + 'modal_awal' => ['nullable', 'integer', 'min:0'], + 'tahun_modal_awal' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:'.$maxYear], + 'pernah_penambahan_modal' => ['nullable', 'boolean'], + 'penambahan_modal' => [Rule::excludeIf(! $this->boolean('pernah_penambahan_modal')), 'nullable', 'array'], + 'penambahan_modal.*' => [Rule::excludeIf(! $this->boolean('pernah_penambahan_modal')), 'array'], + 'penambahan_modal.*.tahun' => [Rule::excludeIf(! $this->boolean('pernah_penambahan_modal')), 'nullable', 'integer', 'digits:4', 'min:1900', 'max:'.$maxYear], + 'penambahan_modal.*.jumlah' => [Rule::excludeIf(! $this->boolean('pernah_penambahan_modal')), 'nullable', 'integer', 'min:0'], + 'aset_produktif' => ['nullable', 'array'], + 'aset_produktif.*' => ['array'], + 'aset_produktif.*.aset' => ['nullable', Rule::enum(AsetProduktifBumdes::class)], + 'aset_produktif.*.dipilih' => ['nullable', 'boolean'], + 'aset_produktif.*.jumlah' => ['nullable', 'integer', 'min:0'], + 'aset_produktif.*.kondisi' => ['nullable', 'string', 'max:255'], + 'aset_produktif_lainnya' => ['nullable', 'string', 'max:255'], + 'status_kepemilikan_aset' => ['nullable', Rule::enum(StatusKepemilikanAsetBumdes::class)], + 'infrastruktur_pascapanen' => ['nullable', 'array'], + 'infrastruktur_pascapanen.*' => ['array'], + 'infrastruktur_pascapanen.*.infrastruktur' => ['nullable', Rule::enum(InfrastrukturPascapanenBumdes::class)], + 'infrastruktur_pascapanen.*.dipilih' => ['nullable', 'boolean'], + 'infrastruktur_pascapanen.*.jumlah' => ['nullable', 'integer', 'min:0'], + 'infrastruktur_pascapanen.*.kondisi' => ['nullable', Rule::enum(KondisiInfrastrukturBumdes::class)], + 'infrastruktur_pascapanen_lainnya' => ['nullable', 'string', 'max:255'], + ]; + $fields = KelembagaanAktorBumdes::sectionFields($section); + $rules = array_filter($rules, fn ($field) => in_array(Str::before($field, '.'), $fields, true), ARRAY_FILTER_USE_KEY); + + return $rules; + } + + public function messages(): array + { + return [ + 'nomor_dasar_hukum.required_if' => 'Nomor Peraturan Desa/SK wajib diisi.', + 'tahun_dasar_hukum.required_if' => 'Tahun Ditetapkan wajib diisi.', + 'nama_unit_komoditas.required_if' => 'Nama Unit Usaha Komoditas wajib diisi.', + 'tahun_ad_art.required_if' => 'Tahun Terakhir Diperbarui wajib diisi.', + 'string' => ':attribute harus berupa teks.', + 'required' => ':attribute wajib diisi.', + 'exists' => ':attribute tidak ditemukan.', + 'integer' => ':attribute harus berupa angka.', + 'digits' => ':attribute harus terdiri dari :digits digit.', + 'min' => ':attribute tidak boleh kurang dari :min.', + 'max' => ':attribute tidak valid.', + 'enum' => 'Pilihan :attribute tidak valid.', + 'in' => ':attribute tidak valid.', + 'array' => ':attribute harus berupa daftar data.', + 'boolean' => ':attribute harus dipilih.', + ]; + } + + public function attributes(): array + { + return [ + 'search' => 'Pencarian', + 'kabupaten_kota_id' => 'Kabupaten/Kota', 'kabupaten_id' => 'Kabupaten/Kota', + 'provinsi_id' => 'Provinsi', 'kecamatan_id' => 'Kecamatan', 'desa_kelurahan_id' => 'Desa/Kelurahan', + 'jenis_kelamin' => 'Jenis kelamin', 'per_page' => 'Jumlah data per halaman', + 'nama' => 'Nama', 'tempat_lahir' => 'Tempat lahir', 'tanggal_lahir' => 'Tanggal lahir', + 'pendidikan_terakhir' => 'Pendidikan terakhir', 'alamat' => 'Alamat', 'nomor_telepon' => 'Nomor telepon', + 'jabatan' => 'Jabatan/peran', 'jabatan_lainnya' => 'Jabatan lainnya', 'lama_menjabat_tahun' => 'Lama menjabat', + 'pengurus_inti' => 'Daftar Pengurus', 'pengurus_inti.*.jabatan' => 'Jabatan pengurus', + 'pengurus_inti.*.nama' => 'Nama pengurus', 'pengurus_inti.*.aktif' => 'Keaktifan pengurus', + 'status_badan_hukum' => 'Status badan hukum', + 'nomor_dasar_hukum' => 'Nomor Peraturan Desa/SK', + 'tahun_dasar_hukum' => 'Tahun Ditetapkan', + 'nama_unit_komoditas' => 'Nama Unit Usaha Komoditas', + 'tahun_ad_art' => 'Tahun Terakhir Diperbarui', + 'status_keaktifan' => 'Status keaktifan', + 'unit_usaha' => 'Unit usaha', + 'status_unit_komoditas' => 'Unit usaha komoditas', + 'status_ad_art' => 'Dokumen AD/ART', + 'memiliki_staf' => 'Status staf/karyawan', + 'jumlah_staf' => 'Jumlah staf', + 'modal_awal' => 'Modal awal', + 'tahun_modal_awal' => 'Tahun modal awal', + 'pernah_penambahan_modal' => 'Status penambahan modal', + 'penambahan_modal' => 'Rincian penambahan modal', + 'penambahan_modal.*.tahun' => 'Tahun penambahan modal pada baris :position', + 'penambahan_modal.*.jumlah' => 'Jumlah penambahan modal pada baris :position', + 'status_kepemilikan_aset' => 'Status kepemilikan aset', + 'alasan_tidak_aktif' => 'Alasan tidak aktif', + 'unit_usaha.*' => 'Pilihan unit usaha', + 'unit_usaha_jasa_lainnya' => 'Jasa lainnya', + 'unit_usaha_staf' => 'Unit usaha staf', + 'pengurus_inti.*' => 'Data pengurus pada baris :position', + 'penambahan_modal.*' => 'Penambahan modal pada baris :position', + 'aset_produktif' => 'Aset produktif', + 'aset_produktif.*' => 'Data aset produktif pada baris :position', + 'aset_produktif.*.aset' => 'Jenis aset produktif', + 'aset_produktif.*.dipilih' => 'Pilihan aset produktif', + 'aset_produktif.*.jumlah' => 'Jumlah aset produktif', + 'aset_produktif.*.kondisi' => 'Kondisi aset produktif', + 'aset_produktif_lainnya' => 'Aset produktif lainnya', + 'infrastruktur_pascapanen' => 'Infrastruktur pascapanen', + 'infrastruktur_pascapanen.*' => 'Data infrastruktur pada baris :position', + 'infrastruktur_pascapanen.*.infrastruktur' => 'Jenis infrastruktur', + 'infrastruktur_pascapanen.*.dipilih' => 'Pilihan infrastruktur', + 'infrastruktur_pascapanen.*.jumlah' => 'Jumlah infrastruktur', + 'infrastruktur_pascapanen.*.kondisi' => 'Kondisi infrastruktur', + 'infrastruktur_pascapanen_lainnya' => 'Infrastruktur lainnya', + ]; + } +} diff --git a/app/Http/Requests/Bumdes/IndexPengurusBumdesRequest.php b/app/Http/Requests/Bumdes/IndexPengurusBumdesRequest.php new file mode 100644 index 0000000..ce0ca10 --- /dev/null +++ b/app/Http/Requests/Bumdes/IndexPengurusBumdesRequest.php @@ -0,0 +1,27 @@ + ['nullable', 'string', 'max:255'], + 'kabupaten_kota_id' => ['nullable', 'integer', 'exists:master_kabupaten_kota,id'], + 'kecamatan_id' => ['nullable', 'integer', 'exists:master_kecamatan,id'], + 'desa_kelurahan_id' => ['nullable', 'integer', 'exists:master_desa_kelurahan,id'], + 'status_keaktifan' => ['nullable', Rule::enum(StatusKeaktifanBumdes::class)], + 'sort_by' => ['nullable', 'string'], + 'per_page' => ['nullable', 'integer', 'in:10,25'], + ]; + } + + public function messages(): array + { + return [...parent::messages(), 'exists' => ':attribute tidak ditemukan.', 'string' => ':attribute harus berupa teks.', 'max' => ':attribute terlalu panjang.', 'in' => 'Pilihan :attribute tidak valid.']; + } +} diff --git a/app/Http/Requests/Bumdes/StoreKelembagaanAktorBumdesRequest.php b/app/Http/Requests/Bumdes/StoreKelembagaanAktorBumdesRequest.php new file mode 100644 index 0000000..03120d4 --- /dev/null +++ b/app/Http/Requests/Bumdes/StoreKelembagaanAktorBumdesRequest.php @@ -0,0 +1,11 @@ +sectionRules('kelembagaan'); + } +} diff --git a/app/Http/Requests/Bumdes/StoreOperasionalBumdesRequest.php b/app/Http/Requests/Bumdes/StoreOperasionalBumdesRequest.php new file mode 100644 index 0000000..7b0c649 --- /dev/null +++ b/app/Http/Requests/Bumdes/StoreOperasionalBumdesRequest.php @@ -0,0 +1,252 @@ +sectionRules('operasional'), ...$this->pengelolaanRules()]; + $dukungan = $this->input('dukungan_desa', []); + $kemitraan = $this->input('kemitraan_eksternal', []); + $dukunganLainnya = is_array($dukungan) && in_array(DukunganDesaBumdes::LAINNYA->value, $dukungan, true); + $kemitraanLainnya = is_array($kemitraan) && in_array(KemitraanEksternalBumdes::LAINNYA->value, $kemitraan, true); + + $rules = [...$rules, + 'ada_alokasi_desa' => ['nullable', 'boolean'], + 'jumlah_alokasi_desa' => [Rule::excludeIf(! $this->boolean('ada_alokasi_desa')), 'required_if:ada_alokasi_desa,1', 'integer', 'min:0', 'max:9999999999999999'], + 'dukungan_desa' => ['nullable', 'array'], + 'dukungan_desa.*' => ['string', 'distinct', Rule::enum(DukunganDesaBumdes::class)], + 'dukungan_desa_lainnya' => [Rule::excludeIf(! $dukunganLainnya), Rule::requiredIf($dukunganLainnya), 'string', 'max:255'], + 'keterlibatan_koordinasi_desa' => ['nullable', Rule::enum(KeterlibatanKoordinasiBumdes::class)], + 'kemitraan_eksternal' => ['nullable', 'array'], + 'kemitraan_eksternal.*' => ['string', 'distinct', Rule::enum(KemitraanEksternalBumdes::class)], + 'kemitraan_eksternal_lainnya' => [Rule::excludeIf(! $kemitraanLainnya), Rule::requiredIf($kemitraanLainnya), 'string', 'max:255'], + ]; + $saved = collect($this->route('pengurusBumdes')?->kelembagaanAktor?->aset_produktif ?? [])->keyBy('aset'); + $rows = $this->input('aset_produktif', []); + $hasOtherAsset = false; + + foreach (is_array($rows) ? $rows : [] as $index => $row) { + if (! is_array($row)) { + continue; + } + $exists = in_array($row['dipilih'] ?? null, [true, 1, '1'], true); + $assetKey = is_string($row['aset'] ?? null) ? $row['aset'] : ''; + $hasOtherAsset = $hasOtherAsset || ($exists && ($row['aset'] ?? null) === 'lainnya'); + $conditions = array_keys(KondisiAsetProduktifBumdes::options()); + // Kondisi teks lama tetap dapat ditampilkan dan disimpan untuk aset yang sama. + $legacy = KondisiAsetProduktifBumdes::normalizeLegacy(data_get($saved->get($assetKey), 'kondisi')); + if ($legacy !== null) { + $conditions[] = $legacy; + } + $prefix = 'aset_produktif.'.$index.'.'; + $rules[$prefix.'status_kepemilikan_aset'] = [Rule::excludeIf(! $exists), Rule::requiredIf($exists), Rule::enum(StatusKepemilikanAsetBumdes::class)]; + $rules[$prefix.'kondisi'] = [Rule::excludeIf(! $exists), Rule::requiredIf($exists), Rule::in($conditions)]; + $rules[$prefix.'jumlah'] = [Rule::excludeIf(! $exists), 'nullable', 'integer', 'min:0']; + } + $rules['aset_produktif_lainnya'] = [Rule::excludeIf(! $hasOtherAsset), Rule::requiredIf($hasOtherAsset), 'string', 'max:255']; + + $rows = $this->input('infrastruktur_pascapanen', []); + $hasOtherInfrastructure = false; + foreach (is_array($rows) ? $rows : [] as $index => $row) { + if (! is_array($row)) { + continue; + } + $exists = in_array($row['dipilih'] ?? null, [true, 1, '1'], true); + $hasOtherInfrastructure = $hasOtherInfrastructure || ($exists && ($row['infrastruktur'] ?? null) === 'lainnya'); + $prefix = 'infrastruktur_pascapanen.'.$index.'.'; + $rules[$prefix.'kondisi'] = [Rule::excludeIf(! $exists), Rule::requiredIf($exists), Rule::enum(KondisiInfrastrukturBumdes::class)]; + $rules[$prefix.'jumlah'] = [Rule::excludeIf(! $exists), 'nullable', 'integer', 'min:0']; + } + $rules['infrastruktur_pascapanen_lainnya'] = [Rule::excludeIf(! $hasOtherInfrastructure), Rule::requiredIf($hasOtherInfrastructure), 'string', 'max:255']; + + return $rules; + } + + private function pengelolaanRules(): array + { + $hasAudit = $this->boolean('pernah_audit'); + $annualReport = $this->input('pertanggungjawaban_musdes') === PertanggungjawabanMusdesBumdes::TAHUNAN->value; + $hasPades = in_array($this->input('kontribusi_pades'), [KontribusiPadesBumdes::RUTIN->value, KontribusiPadesBumdes::TIDAK_RUTIN->value], true); + $otherVillage = $this->input('cakupan_layanan') === CakupanLayananBumdes::DESA_LAIN->value; + $activePartnership = $this->input('pengalaman_offtaker') === PengalamanOfftakerBumdes::BERJALAN->value; + $stoppedPartnership = $this->input('pengalaman_offtaker') === PengalamanOfftakerBumdes::BERHENTI->value; + $hasOther = fn (string $field, string $value) => is_array($selected = $this->input($field)) && in_array($value, $selected, true); + $otherService = $hasOther('layanan_petani', LayananPetaniBumdes::LAINNYA->value); + $otherObstacle = $hasOther('hambatan_usaha', HambatanUsahaBumdes::LAINNYA->value); + $year = ['integer', 'digits:4', 'min:1900', 'max:'.now()->year]; + + return [ + 'status_pembukuan' => ['nullable', Rule::enum(StatusPembukuanBumdes::class)], + 'format_pembukuan' => ['nullable', Rule::enum(FormatPembukuanBumdes::class)], + 'pernah_audit' => ['nullable', 'boolean'], + 'sifat_audit' => [Rule::excludeIf(! $hasAudit), Rule::requiredIf($hasAudit), 'array', 'min:1'], + 'sifat_audit.*' => [Rule::excludeIf(! $hasAudit), 'string', 'distinct', Rule::enum(SifatAuditBumdes::class)], + 'pelaksana_audit' => [Rule::excludeIf(! $hasAudit), Rule::requiredIf($hasAudit), 'string', 'max:255'], + 'tahun_audit' => [Rule::excludeIf(! $hasAudit), Rule::requiredIf($hasAudit), ...$year], + 'pertanggungjawaban_musdes' => ['nullable', Rule::enum(PertanggungjawabanMusdesBumdes::class)], + 'tahun_laporan_musdes' => [Rule::excludeIf(! $annualReport), Rule::requiredIf($annualReport), ...$year], + 'kondisi_keuangan' => ['nullable', Rule::enum(KondisiKeuanganBumdes::class)], + 'kontribusi_pades' => ['nullable', Rule::enum(KontribusiPadesBumdes::class)], + 'nominal_kontribusi_pades' => [Rule::excludeIf(! $hasPades), Rule::requiredIf($hasPades), 'integer', 'min:0', 'max:9999999999999999'], + 'cakupan_layanan' => ['nullable', Rule::enum(CakupanLayananBumdes::class)], + 'desa_lain_dilayani' => [Rule::excludeIf(! $otherVillage), Rule::requiredIf($otherVillage), 'string', 'max:2000'], + 'jumlah_petani_desa_sendiri' => ['nullable', 'integer', 'min:0', 'max:2147483647'], + 'jumlah_petani_desa_lain' => [Rule::excludeIf(! $otherVillage), Rule::requiredIf($otherVillage), 'integer', 'min:0', 'max:2147483647'], + 'pencatatan_petani' => ['nullable', Rule::enum(PencatatanPetaniBumdes::class)], + 'layanan_petani' => ['nullable', 'array'], + 'layanan_petani.*' => ['string', 'distinct', Rule::enum(LayananPetaniBumdes::class)], + 'layanan_petani_lainnya' => [Rule::excludeIf(! $otherService), Rule::requiredIf($otherService), 'string', 'max:255'], + 'peran_rantai_nilai' => ['nullable', Rule::enum(PeranRantaiNilaiBumdes::class)], + 'pengalaman_offtaker' => ['nullable', Rule::enum(PengalamanOfftakerBumdes::class)], + 'nama_offtaker' => [Rule::excludeIf(! $activePartnership), Rule::requiredIf($activePartnership), 'string', 'max:255'], + 'tahun_mulai_offtaker' => [Rule::excludeIf(! $activePartnership), Rule::requiredIf($activePartnership), ...$year], + 'alasan_berhenti_offtaker' => [Rule::excludeIf(! $stoppedPartnership), Rule::requiredIf($stoppedPartnership), 'string', 'max:2000'], + 'hambatan_usaha' => ['nullable', 'array'], + 'hambatan_usaha.*' => ['string', 'distinct', Rule::enum(HambatanUsahaBumdes::class)], + 'hambatan_usaha_lainnya' => [Rule::excludeIf(! $otherObstacle), Rule::requiredIf($otherObstacle), 'string', 'max:255'], + ]; + } + + protected function prepareForValidation(): void + { + foreach (['jumlah_alokasi_desa', 'nominal_kontribusi_pades'] as $field) { + $amount = $this->input($field); + if (is_string($amount) && preg_match('/^\d{1,3}(\.\d{3})+$/D', $amount)) { + $this->merge([$field => str_replace('.', '', $amount)]); + } + } + $normalize = static fn ($value) => is_scalar($value) ? preg_replace('/\D/', '', (string) $value) : $value; + $data = []; + if ($this->has('modal_awal')) { + $data['modal_awal'] = $normalize($this->input('modal_awal')); + } + if (is_array($this->input('penambahan_modal'))) { + $data['penambahan_modal'] = array_map(function ($row) use ($normalize) { + if (is_array($row) && array_key_exists('jumlah', $row)) { + $row['jumlah'] = $normalize($row['jumlah']); + } + + return $row; + }, $this->input('penambahan_modal')); + } + if (is_array($this->input('aset_produktif'))) { + $legacyOwnership = $this->input('status_kepemilikan_aset') + ?? $this->route('pengurusBumdes')?->kelembagaanAktor?->status_kepemilikan_aset?->value; + $data['aset_produktif'] = array_map(function ($row) use ($legacyOwnership) { + if (! is_array($row)) { + return $row; + } + if (! array_key_exists('status_kepemilikan_aset', $row)) { + $row['status_kepemilikan_aset'] = $legacyOwnership; + } + if (is_scalar($row['kondisi'] ?? null)) { + $row['kondisi'] = KondisiAsetProduktifBumdes::normalizeLegacy($row['kondisi']); + } + + return $row; + }, $this->input('aset_produktif')); + } + $this->merge($data); + } + + public function after(): array + { + return [function (Validator $validator) { + foreach (['dukungan_desa' => DukunganDesaBumdes::TIDAK_ADA->value, 'kemitraan_eksternal' => KemitraanEksternalBumdes::BELUM_ADA->value, 'layanan_petani' => LayananPetaniBumdes::BELUM_ADA->value] as $field => $none) { + $selected = $this->input($field, []); + if (is_array($selected) && in_array($none, $selected, true) && count($selected) > 1) { + $validator->errors()->add($field, match ($field) { + 'dukungan_desa' => 'Tidak ada dukungan khusus tidak boleh dipilih bersama bentuk dukungan lainnya.', + 'kemitraan_eksternal' => 'Belum ada kemitraan eksternal tidak boleh dipilih bersama jenis kemitraan lainnya.', + 'layanan_petani' => 'Belum ada layanan tidak boleh dipilih bersama jenis layanan lainnya.', + }); + } + } + }]; + } + + public function messages(): array + { + return [...parent::messages(), + 'jumlah_alokasi_desa.required_if' => 'Besaran alokasi APBDes/Dana Desa wajib diisi jika ada alokasi tahun berjalan.', + 'jumlah_alokasi_desa.max' => 'Besaran alokasi APBDes/Dana Desa tidak boleh melebihi Rp 9.999.999.999.999.999.', + 'dukungan_desa_lainnya.required' => 'Bentuk dukungan lainnya wajib diisi jika memilih Lainnya.', + 'kemitraan_eksternal_lainnya.required' => 'Jenis kemitraan lainnya wajib diisi jika memilih Lainnya.', + 'nominal_kontribusi_pades.max' => 'Nominal kontribusi PADes tidak boleh melebihi Rp 9.999.999.999.999.999.', + 'sifat_audit.min' => 'Pilih minimal satu sifat audit/pemeriksaan.', + 'distinct' => ':attribute tidak boleh dipilih lebih dari satu kali.', + 'aset_produktif.*.status_kepemilikan_aset.required' => 'Status Kepemilikan wajib diisi jika aset dipilih Ada.', + 'aset_produktif.*.kondisi.required' => 'Kondisi wajib diisi jika aset dipilih Ada.', + 'aset_produktif_lainnya.required' => 'Nama aset lainnya wajib diisi jika dipilih Ada.', + 'infrastruktur_pascapanen.*.kondisi.required' => 'Kondisi wajib diisi jika infrastruktur dipilih Ada.', + 'infrastruktur_pascapanen_lainnya.required' => 'Nama infrastruktur lainnya wajib diisi jika dipilih Ada.', + ]; + } + + public function attributes(): array + { + return [...parent::attributes(), + 'status_pembukuan' => 'Sistem pembukuan dan laporan keuangan', + 'format_pembukuan' => 'Format pembukuan', + 'pernah_audit' => 'Status audit/pemeriksaan', + 'sifat_audit' => 'Sifat audit/pemeriksaan', + 'pelaksana_audit' => 'Pelaksana audit/pemeriksaan', + 'tahun_audit' => 'Tahun terakhir audit/pemeriksaan', + 'pertanggungjawaban_musdes' => 'Pertanggungjawaban ke musyawarah desa', + 'tahun_laporan_musdes' => 'Tahun terakhir laporan ke musyawarah desa', + 'kondisi_keuangan' => 'Kondisi keuangan', + 'kontribusi_pades' => 'Kontribusi ke PADes', + 'nominal_kontribusi_pades' => 'Nominal kontribusi ke PADes', + 'cakupan_layanan' => 'Cakupan wilayah layanan', + 'desa_lain_dilayani' => 'Desa lain yang dilayani', + 'jumlah_petani_desa_sendiri' => 'Jumlah petani dari desa sendiri', + 'jumlah_petani_desa_lain' => 'Jumlah petani dari desa lain', + 'pencatatan_petani' => 'Pencatatan petani yang dilayani', + 'layanan_petani' => 'Layanan bagi petani', + 'layanan_petani_lainnya' => 'Layanan lainnya', + 'peran_rantai_nilai' => 'Peran dalam rantai nilai', + 'pengalaman_offtaker' => 'Pengalaman kemitraan dengan offtaker', + 'nama_offtaker' => 'Nama offtaker', + 'tahun_mulai_offtaker' => 'Tahun mulai kemitraan', + 'alasan_berhenti_offtaker' => 'Alasan kemitraan berhenti', + 'hambatan_usaha' => 'Hambatan usaha', + 'hambatan_usaha_lainnya' => 'Hambatan lainnya', + 'sifat_audit.*' => 'Pilihan sifat audit/pemeriksaan', + 'layanan_petani.*' => 'Pilihan layanan bagi petani', + 'hambatan_usaha.*' => 'Pilihan hambatan usaha', + 'ada_alokasi_desa' => 'Status alokasi APBDes/Dana Desa', + 'jumlah_alokasi_desa' => 'Besaran alokasi APBDes/Dana Desa', + 'dukungan_desa' => 'Bentuk dukungan pemerintah desa', + 'dukungan_desa.*' => 'Pilihan dukungan pemerintah desa', + 'dukungan_desa_lainnya' => 'Bentuk dukungan lainnya', + 'keterlibatan_koordinasi_desa' => 'Keterlibatan dalam rapat/koordinasi desa', + 'kemitraan_eksternal' => 'Kemitraan eksternal', + 'kemitraan_eksternal.*' => 'Pilihan kemitraan eksternal', + 'kemitraan_eksternal_lainnya' => 'Jenis kemitraan lainnya', + 'aset_produktif.*.status_kepemilikan_aset' => 'Status Kepemilikan aset', + ]; + } +} diff --git a/app/Http/Requests/Bumdes/StorePengurusBumdesRequest.php b/app/Http/Requests/Bumdes/StorePengurusBumdesRequest.php new file mode 100644 index 0000000..1ab8b96 --- /dev/null +++ b/app/Http/Requests/Bumdes/StorePengurusBumdesRequest.php @@ -0,0 +1,66 @@ + ['required', 'string', 'max:255'], + 'jenis_kelamin' => ['required', 'boolean'], + 'tempat_lahir' => ['required', 'string', 'max:255'], + 'tanggal_lahir' => ['required', 'date_format:d-m-Y'], + 'pendidikan_terakhir' => ['required', Rule::enum(Pendidikan::class)], + 'alamat' => ['required', 'string', 'max:500'], + 'provinsi_id' => ['bail', 'required', 'integer', 'exists:master_provinsi,id'], + 'kabupaten_id' => ['bail', 'required', 'integer', 'exists:master_kabupaten_kota,id'], + 'kecamatan_id' => ['bail', 'required', 'integer', 'exists:master_kecamatan,id'], + 'desa_kelurahan_id' => ['bail', 'required', 'integer', 'exists:master_desa_kelurahan,id'], + 'nomor_telepon' => ['nullable', 'digits_between:11,13'], + 'jabatan' => ['required', Rule::enum(JabatanBumdes::class)], + 'jabatan_lainnya' => ['nullable', 'required_if:jabatan,'.JabatanBumdes::LAINNYA->value, 'string', 'max:255'], + 'lama_menjabat_tahun' => ['required', 'integer', 'min:0', 'max:80'], + ...$this->sectionRules('profil'), + ]; + } + + public function messages(): array + { + return [ + ...parent::messages(), + 'nama.required' => 'Nama harus diisi.', + 'jenis_kelamin.required' => 'Jenis kelamin harus dipilih.', + 'tempat_lahir.required' => 'Tempat lahir harus diisi.', + 'tanggal_lahir.required' => 'Tanggal lahir harus diisi.', + 'tanggal_lahir.date_format' => 'Tanggal lahir harus menggunakan format tanggal yang valid.', + 'pendidikan_terakhir.required' => 'Pendidikan terakhir harus dipilih.', + 'alamat.required' => 'Alamat harus diisi.', + 'provinsi_id.required' => 'Provinsi wajib dipilih.', + 'kabupaten_id.required' => 'Kabupaten/Kota wajib dipilih.', + 'kecamatan_id.required' => 'Kecamatan wajib dipilih.', + 'provinsi_id.integer' => 'Pilihan Provinsi tidak valid.', + 'kabupaten_id.integer' => 'Pilihan Kabupaten/Kota tidak valid.', + 'kecamatan_id.integer' => 'Pilihan Kecamatan tidak valid.', + 'desa_kelurahan_id.integer' => 'Pilihan Desa/Kelurahan tidak valid.', + 'provinsi_id.exists' => 'Provinsi tidak ditemukan.', + 'kabupaten_id.exists' => 'Kabupaten/Kota tidak ditemukan.', + 'kecamatan_id.exists' => 'Kecamatan tidak ditemukan.', + 'desa_kelurahan_id.required' => 'Desa kelurahan harus dipilih.', + 'desa_kelurahan_id.exists' => 'Desa kelurahan tidak ditemukan.', + 'nomor_telepon.digits_between' => 'Nomor telepon harus berupa angka 11 sampai 13 digit.', + 'jabatan.required' => 'Jabatan/peran harus dipilih.', + 'jabatan_lainnya.required_if' => 'Jabatan lainnya harus diisi.', + 'lama_menjabat_tahun.required' => 'Lama menjabat harus diisi.', + 'lama_menjabat_tahun.integer' => 'Lama menjabat harus berupa angka tahun.', + 'string' => ':attribute harus berupa teks.', + 'max' => ':attribute tidak valid.', + 'min' => ':attribute tidak boleh kurang dari :min.', + 'enum' => 'Pilihan :attribute tidak valid.', + ]; + } +} diff --git a/app/Http/Requests/Bumdes/StoreRantaiNilaiBumdesRequest.php b/app/Http/Requests/Bumdes/StoreRantaiNilaiBumdesRequest.php new file mode 100644 index 0000000..5994dd6 --- /dev/null +++ b/app/Http/Requests/Bumdes/StoreRantaiNilaiBumdesRequest.php @@ -0,0 +1,172 @@ +boolean('melakukan_jual_beli'); + $hasChoice = fn (string $field, string $value) => is_array($selected = $this->input($field)) && in_array($value, $selected, true); + $notSold = $hasChoice('saluran_penjualan', SaluranPenjualanBumdes::BELUM_MENJUAL->value); + $otherSource = $trading && $hasChoice('sumber_pasokan', SumberPasokanBumdes::LAINNYA->value); + $otherChannel = $trading && $hasChoice('saluran_penjualan', SaluranPenjualanBumdes::LAINNYA->value); + $otherRelationship = $trading && $hasChoice('hubungan_petani_pemasok', HubunganPemasokBumdes::LAINNYA->value); + $otherPayment = $trading && $this->input('mekanisme_pembayaran_petani') === MekanismePembayaranBumdes::LAINNYA->value; + $otherPrice = $trading && $this->input('penetapan_harga_beli') === PenetapanHargaBeliBumdes::LAINNYA->value; + $periods = implode(',', array_keys(PeriodePanenBumdes::options())); + $products = implode(',', array_keys(BentukProdukBumdes::options())); + $rules = [...$this->pascapanenRules(), + 'melakukan_jual_beli' => ['required', 'boolean'], + 'redirect_fragment' => ['nullable', Rule::in(['section-rantai-penjualan', 'section-rantai-hubungan-pemasok', 'section-rantai-harga-beli', 'section-rantai-sortasi', 'section-rantai-pengeringan', 'section-rantai-fermentasi', 'section-rantai-premium-harga'])], + 'sumber_pasokan' => [Rule::excludeIf(! $trading), 'nullable', 'array'], + 'sumber_pasokan.*' => [Rule::excludeIf(! $trading), 'string', 'distinct', Rule::enum(SumberPasokanBumdes::class)], + 'sumber_pasokan_lainnya' => [Rule::excludeIf(! $otherSource), Rule::requiredIf($otherSource), 'string', 'max:255'], + 'bentuk_produk_dibeli' => [Rule::excludeIf(! $trading), 'nullable', 'array'], + 'bentuk_produk_dibeli.*' => [Rule::excludeIf(! $trading), 'string', 'distinct', Rule::enum(BentukProdukBumdes::class)], + 'volume_pembelian' => [Rule::excludeIf(! $trading), 'nullable', 'array:'.$periods], + 'volume_pembelian.*' => [Rule::excludeIf(! $trading), 'nullable', 'numeric', 'decimal:0,3', 'min:0', 'max:999999999999.999'], + 'saluran_penjualan' => [Rule::excludeIf(! $trading), 'nullable', 'array'], + 'saluran_penjualan.*' => [Rule::excludeIf(! $trading), 'string', 'distinct', Rule::enum(SaluranPenjualanBumdes::class)], + 'saluran_penjualan_lainnya' => [Rule::excludeIf(! $otherChannel), Rule::requiredIf($otherChannel), 'string', 'max:255'], + 'bentuk_produk_dijual' => [Rule::excludeIf(! $trading || $notSold), 'nullable', 'array'], + 'bentuk_produk_dijual.*' => [Rule::excludeIf(! $trading || $notSold), 'string', 'distinct', Rule::enum(BentukProdukBumdes::class)], + 'volume_penjualan' => [Rule::excludeIf(! $trading || $notSold), 'nullable', 'array:'.$periods], + 'volume_penjualan.*' => [Rule::excludeIf(! $trading || $notSold), 'nullable', 'numeric', 'decimal:0,3', 'min:0', 'max:999999999999.999'], + 'hubungan_petani_pemasok' => [Rule::excludeIf(! $trading), 'nullable', 'array'], + 'hubungan_petani_pemasok.*' => [Rule::excludeIf(! $trading), 'string', 'distinct', Rule::enum(HubunganPemasokBumdes::class)], + 'hubungan_petani_pemasok_lainnya' => [Rule::excludeIf(! $otherRelationship), Rule::requiredIf($otherRelationship), 'string', 'max:255'], + 'mekanisme_pembayaran_petani' => [Rule::excludeIf(! $trading), 'nullable', Rule::enum(MekanismePembayaranBumdes::class)], + 'mekanisme_pembayaran_lainnya' => [Rule::excludeIf(! $otherPayment), Rule::requiredIf($otherPayment), 'string', 'max:255'], + 'perjanjian_petani' => [Rule::excludeIf(! $trading), 'nullable', Rule::enum(PerjanjianPetaniBumdes::class)], + 'harga_beli_petani' => [Rule::excludeIf(! $trading), 'nullable', 'array:'.$products], + 'penetapan_harga_beli' => [Rule::excludeIf(! $trading), 'nullable', Rule::enum(PenetapanHargaBeliBumdes::class)], + 'penetapan_harga_beli_lainnya' => [Rule::excludeIf(! $otherPrice), Rule::requiredIf($otherPrice), 'string', 'max:255'], + ]; + foreach (BentukProdukBumdes::cases() as $product) { + $enabled = $trading && $hasChoice('bentuk_produk_dibeli', $product->value); + $prefix = 'harga_beli_petani.'.$product->value; + $rules[$prefix] = [Rule::excludeIf(! $enabled), 'nullable', 'array:'.$periods]; + $rules[$prefix.'.*'] = [Rule::excludeIf(! $enabled), 'nullable', 'integer', 'min:0', 'max:9999999999999999']; + } + + return $rules; + } + + private function pascapanenRules(): array + { + $processing = $this->boolean('melakukan_pengolahan_pascapanen'); + $sorting = $processing && in_array($this->input('status_sortasi'), [StatusSortasiBumdes::RUTIN_STANDAR->value, StatusSortasiBumdes::TIDAK_KONSISTEN->value], true); + $drying = $processing && in_array($this->input('status_pengeringan'), [StatusPengeringanBumdes::RUTIN->value, StatusPengeringanBumdes::KADANG->value], true); + $fermenting = $processing && in_array($this->input('status_fermentasi'), [StatusFermentasiBumdes::RUTIN->value, StatusFermentasiBumdes::PERNAH_MENCOBA->value], true); + + return [ + 'melakukan_pengolahan_pascapanen' => ['required', 'boolean'], + 'status_sortasi' => [Rule::excludeIf(! $processing), 'nullable', Rule::enum(StatusSortasiBumdes::class)], + 'kriteria_sortasi' => [Rule::excludeIf(! $sorting), Rule::requiredIf($sorting), 'string', 'max:2000'], + 'status_pengeringan' => [Rule::excludeIf(! $processing), 'nullable', Rule::enum(StatusPengeringanBumdes::class)], + 'cara_tempat_pengeringan' => [Rule::excludeIf(! $drying), Rule::requiredIf($drying), 'string', 'max:2000'], + 'status_fermentasi' => [Rule::excludeIf(! $processing), 'nullable', Rule::enum(StatusFermentasiBumdes::class)], + 'model_fermentasi' => [Rule::excludeIf(! $fermenting), Rule::requiredIf($fermenting), 'array', 'min:1'], + 'model_fermentasi.*' => [Rule::excludeIf(! $fermenting), 'string', 'distinct', Rule::enum(ModelFermentasiBumdes::class)], + 'proses_fermentasi' => [Rule::excludeIf(! $fermenting), Rule::requiredIf($fermenting), 'string', 'max:2000'], + 'premium_harga_pascapanen' => [Rule::excludeIf(! $processing), 'nullable', 'integer', 'min:0', 'max:9999999999999999'], + ]; + } + + protected function prepareForValidation(): void + { + $premium = $this->input('premium_harga_pascapanen'); + if (is_string($premium) && preg_match('/^\d{1,3}(\.\d{3})+$/D', $premium)) { + $this->merge(['premium_harga_pascapanen' => str_replace('.', '', $premium)]); + } + $prices = $this->input('harga_beli_petani'); + if (! is_array($prices)) { + return; + } + foreach ($prices as &$row) { + if (! is_array($row)) { + continue; + } + foreach ($row as &$value) { + if (is_string($value) && preg_match('/^\d{1,3}(\.\d{3})+$/D', $value)) { + $value = str_replace('.', '', $value); + } + } + unset($value); + } + unset($row); + $this->merge(['harga_beli_petani' => $prices]); + } + + public function after(): array + { + return [function (Validator $validator) { + $channels = $this->input('saluran_penjualan'); + if ($this->boolean('melakukan_jual_beli') && is_array($channels) && count($channels) > 1 && in_array(SaluranPenjualanBumdes::BELUM_MENJUAL->value, $channels, true)) { + $validator->errors()->add('saluran_penjualan', 'Belum pernah menjual tidak boleh dipilih bersama saluran penjualan lainnya.'); + } + }]; + } + + public function messages(): array + { + return [...parent::messages(), + 'melakukan_pengolahan_pascapanen.required' => 'Kegiatan pengolahan pascapanen wajib dipilih.', + 'melakukan_jual_beli.required' => 'Kegiatan jual beli komoditas wajib dipilih.', + 'model_fermentasi.min' => 'Pilih minimal satu model fermentasi.', + 'premium_harga_pascapanen.max' => 'Premium harga tidak boleh melebihi Rp 9.999.999.999.999.999 per kg.', + 'numeric' => ':attribute harus berupa angka.', + 'decimal' => ':attribute boleh memiliki maksimal 3 angka di belakang koma.', + 'distinct' => ':attribute tidak boleh dipilih lebih dari satu kali.', + 'harga_beli_petani.*.*.max' => 'Harga beli tidak boleh melebihi Rp 9.999.999.999.999.999 per kg.', + ]; + } + + public function attributes(): array + { + return [...parent::attributes(), + 'melakukan_pengolahan_pascapanen' => 'Kegiatan pengolahan pascapanen', + 'status_sortasi' => 'Praktik sortasi', + 'kriteria_sortasi' => 'Standar/kriteria sortasi', + 'status_pengeringan' => 'Praktik pengeringan', + 'cara_tempat_pengeringan' => 'Cara dan tempat pengeringan', + 'status_fermentasi' => 'Praktik fermentasi/pengepresan', + 'model_fermentasi' => 'Model fermentasi', + 'proses_fermentasi' => 'Proses fermentasi', + 'premium_harga_pascapanen' => 'Premium harga per kg', + 'model_fermentasi.*' => 'Pilihan model fermentasi', + 'melakukan_jual_beli' => 'Kegiatan jual beli komoditas', + 'redirect_fragment' => 'Tujuan setelah penyimpanan', + 'sumber_pasokan' => 'Sumber pasokan', 'sumber_pasokan.*' => 'Pilihan sumber pasokan', + 'sumber_pasokan_lainnya' => 'Sumber pasokan lainnya', + 'bentuk_produk_dibeli' => 'Bentuk produk yang dibeli', 'bentuk_produk_dibeli.*' => 'Pilihan bentuk produk yang dibeli', + 'volume_pembelian' => 'Volume pembelian', 'volume_pembelian.*' => 'Volume pembelian pada musim panen terkait', + 'saluran_penjualan' => 'Saluran penjualan', 'saluran_penjualan.*' => 'Pilihan saluran penjualan', + 'saluran_penjualan_lainnya' => 'Saluran penjualan lainnya', + 'bentuk_produk_dijual' => 'Bentuk produk yang dijual', 'bentuk_produk_dijual.*' => 'Pilihan bentuk produk yang dijual', + 'volume_penjualan' => 'Volume penjualan', 'volume_penjualan.*' => 'Volume penjualan pada musim panen terkait', + 'hubungan_petani_pemasok' => 'Hubungan dengan petani pemasok', 'hubungan_petani_pemasok.*' => 'Pilihan hubungan dengan petani pemasok', + 'hubungan_petani_pemasok_lainnya' => 'Hubungan dengan petani pemasok lainnya', + 'mekanisme_pembayaran_petani' => 'Mekanisme pembayaran kepada petani', 'mekanisme_pembayaran_lainnya' => 'Mekanisme pembayaran lainnya', + 'perjanjian_petani' => 'Perjanjian dengan petani', + 'harga_beli_petani' => 'Harga beli dari petani', 'harga_beli_petani.*' => 'Harga beli untuk bentuk produk terkait', 'harga_beli_petani.*.*' => 'Harga beli pada musim panen terkait', + 'penetapan_harga_beli' => 'Penetapan harga beli', 'penetapan_harga_beli_lainnya' => 'Penetapan harga beli lainnya', + ]; + } +} diff --git a/app/Http/Requests/Bumdes/UpdatePengurusBumdesRequest.php b/app/Http/Requests/Bumdes/UpdatePengurusBumdesRequest.php new file mode 100644 index 0000000..bcbd605 --- /dev/null +++ b/app/Http/Requests/Bumdes/UpdatePengurusBumdesRequest.php @@ -0,0 +1,5 @@ + 'integer', - 'tahun_ad_art' => 'integer', - 'unit_usaha' => 'array', - 'pengurus_inti' => 'array', - 'memiliki_staf' => 'boolean', - 'jumlah_staf' => 'integer', - 'modal_awal' => 'decimal:2', - 'tahun_modal_awal' => 'integer', - 'pernah_penambahan_modal' => 'boolean', - 'penambahan_modal' => 'array', - 'aset_produktif' => 'array', - 'infrastruktur_pascapanen' => 'array', - ]; + protected function casts(): array + { + return [ + 'melakukan_pengolahan_pascapanen' => 'boolean', + 'status_sortasi' => StatusSortasiBumdes::class, + 'status_pengeringan' => StatusPengeringanBumdes::class, + 'status_fermentasi' => StatusFermentasiBumdes::class, + 'model_fermentasi' => AsEnumCollection::of(ModelFermentasiBumdes::class), + 'premium_harga_pascapanen' => 'decimal:2', + 'melakukan_jual_beli' => 'boolean', + 'sumber_pasokan' => AsEnumCollection::of(SumberPasokanBumdes::class), + 'bentuk_produk_dibeli' => AsEnumCollection::of(BentukProdukBumdes::class), + 'volume_pembelian' => 'array', + 'saluran_penjualan' => AsEnumCollection::of(SaluranPenjualanBumdes::class), + 'bentuk_produk_dijual' => AsEnumCollection::of(BentukProdukBumdes::class), + 'volume_penjualan' => 'array', + 'hubungan_petani_pemasok' => AsEnumCollection::of(HubunganPemasokBumdes::class), + 'mekanisme_pembayaran_petani' => MekanismePembayaranBumdes::class, + 'perjanjian_petani' => PerjanjianPetaniBumdes::class, + 'harga_beli_petani' => 'array', + 'penetapan_harga_beli' => PenetapanHargaBeliBumdes::class, + 'status_pembukuan' => StatusPembukuanBumdes::class, + 'format_pembukuan' => FormatPembukuanBumdes::class, + 'pernah_audit' => 'boolean', + 'sifat_audit' => AsEnumCollection::of(SifatAuditBumdes::class), + 'tahun_audit' => 'integer', + 'pertanggungjawaban_musdes' => PertanggungjawabanMusdesBumdes::class, + 'tahun_laporan_musdes' => 'integer', + 'kondisi_keuangan' => KondisiKeuanganBumdes::class, + 'kontribusi_pades' => KontribusiPadesBumdes::class, + 'nominal_kontribusi_pades' => 'decimal:2', + 'cakupan_layanan' => CakupanLayananBumdes::class, + 'jumlah_petani_desa_sendiri' => 'integer', + 'jumlah_petani_desa_lain' => 'integer', + 'pencatatan_petani' => PencatatanPetaniBumdes::class, + 'layanan_petani' => AsEnumCollection::of(LayananPetaniBumdes::class), + 'peran_rantai_nilai' => PeranRantaiNilaiBumdes::class, + 'pengalaman_offtaker' => PengalamanOfftakerBumdes::class, + 'tahun_mulai_offtaker' => 'integer', + 'hambatan_usaha' => AsEnumCollection::of(HambatanUsahaBumdes::class), + 'ada_alokasi_desa' => 'boolean', + 'jumlah_alokasi_desa' => 'decimal:2', + 'dukungan_desa' => AsEnumCollection::of(DukunganDesaBumdes::class), + 'keterlibatan_koordinasi_desa' => KeterlibatanKoordinasiBumdes::class, + 'kemitraan_eksternal' => AsEnumCollection::of(KemitraanEksternalBumdes::class), + 'status_badan_hukum' => StatusBadanHukumBumdes::class, + 'status_keaktifan' => StatusKeaktifanBumdes::class, + 'status_unit_komoditas' => StatusUnitKomoditasBumdes::class, + 'status_ad_art' => StatusAdArtBumdes::class, + 'status_kepemilikan_aset' => StatusKepemilikanAsetBumdes::class, + 'tahun_dasar_hukum' => 'integer', + 'tahun_ad_art' => 'integer', + 'unit_usaha' => AsEnumCollection::of(UnitUsahaBumdes::class), + 'pengurus_inti' => 'array', + 'memiliki_staf' => 'boolean', + 'jumlah_staf' => 'integer', + 'modal_awal' => 'decimal:2', + 'tahun_modal_awal' => 'integer', + 'pernah_penambahan_modal' => 'boolean', + 'penambahan_modal' => 'array', + 'aset_produktif' => 'array', + 'infrastruktur_pascapanen' => 'array', + ]; + } + + public static function sectionFields(string $section): array + { + if ($section === 'rantai-nilai') { + return [ + 'melakukan_pengolahan_pascapanen', + 'status_sortasi', + 'kriteria_sortasi', + 'status_pengeringan', + 'cara_tempat_pengeringan', + 'status_fermentasi', + 'model_fermentasi', + 'proses_fermentasi', + 'premium_harga_pascapanen', + 'melakukan_jual_beli', + 'sumber_pasokan', + 'sumber_pasokan_lainnya', + 'bentuk_produk_dibeli', + 'volume_pembelian', + 'saluran_penjualan', + 'saluran_penjualan_lainnya', + 'bentuk_produk_dijual', + 'volume_penjualan', + 'hubungan_petani_pemasok', + 'hubungan_petani_pemasok_lainnya', + 'mekanisme_pembayaran_petani', + 'mekanisme_pembayaran_lainnya', + 'perjanjian_petani', + 'harga_beli_petani', + 'penetapan_harga_beli', + 'penetapan_harga_beli_lainnya', + ]; + } + + if ($section === 'profil') { + return [ + 'status_keaktifan', 'alasan_tidak_aktif', 'pengurus_inti', 'memiliki_staf', + 'jumlah_staf', 'unit_usaha_staf', + ]; + } + + return $section === 'operasional' ? [ + 'modal_awal', 'tahun_modal_awal', + 'pernah_penambahan_modal', 'penambahan_modal', 'aset_produktif', + 'aset_produktif_lainnya', 'status_kepemilikan_aset', 'infrastruktur_pascapanen', + 'infrastruktur_pascapanen_lainnya', + 'ada_alokasi_desa', 'jumlah_alokasi_desa', 'dukungan_desa', + 'dukungan_desa_lainnya', 'keterlibatan_koordinasi_desa', + 'kemitraan_eksternal', 'kemitraan_eksternal_lainnya', + 'status_pembukuan', + 'format_pembukuan', + 'pernah_audit', + 'sifat_audit', + 'pelaksana_audit', + 'tahun_audit', + 'pertanggungjawaban_musdes', + 'tahun_laporan_musdes', + 'kondisi_keuangan', + 'kontribusi_pades', + 'nominal_kontribusi_pades', + 'cakupan_layanan', + 'desa_lain_dilayani', + 'jumlah_petani_desa_sendiri', + 'jumlah_petani_desa_lain', + 'pencatatan_petani', + 'layanan_petani', + 'layanan_petani_lainnya', + 'peran_rantai_nilai', + 'pengalaman_offtaker', + 'nama_offtaker', + 'tahun_mulai_offtaker', + 'alasan_berhenti_offtaker', + 'hambatan_usaha', + 'hambatan_usaha_lainnya', + ] : [ + 'status_badan_hukum', 'nomor_dasar_hukum', 'tahun_dasar_hukum', 'unit_usaha', + 'unit_usaha_jasa_lainnya', 'status_unit_komoditas', 'nama_unit_komoditas', + 'status_ad_art', 'tahun_ad_art', + ]; + } public function pengurusBumdes(): BelongsTo { diff --git a/app/Models/Bumdes/PengurusBumdes.php b/app/Models/Bumdes/PengurusBumdes.php index 890b6de..1565dad 100644 --- a/app/Models/Bumdes/PengurusBumdes.php +++ b/app/Models/Bumdes/PengurusBumdes.php @@ -2,9 +2,10 @@ namespace App\Models\Bumdes; -use App\Models\Concerns\CatatAktivitas; use App\Enums\Bumdes\JabatanBumdes; use App\Enums\Pendidikan; +use App\Models\Concerns\CatatAktivitas; +use App\Models\Concerns\HasDeletedBy; use App\Models\User; use App\Models\Wilayah\DesaKelurahan; use Carbon\Carbon; @@ -15,7 +16,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; class PengurusBumdes extends Model { - use SoftDeletes, CatatAktivitas; + use CatatAktivitas, HasDeletedBy, SoftDeletes; protected $table = 'bumdes_pengurus'; @@ -38,13 +39,33 @@ class PengurusBumdes extends Model 'deleted_by', ]; - protected $casts = [ - 'jenis_kelamin' => 'boolean', - 'tanggal_lahir' => 'date', - 'pendidikan_terakhir' => Pendidikan::class, - 'tanggal_mulai_menjabat' => 'date', - 'jabatan' => JabatanBumdes::class, - ]; + protected function casts(): array + { + return [ + 'jenis_kelamin' => 'boolean', + 'tanggal_lahir' => 'date', + 'pendidikan_terakhir' => Pendidikan::class, + 'tanggal_mulai_menjabat' => 'date', + 'jabatan' => JabatanBumdes::class, + ]; + } + + protected static function booted(): void + { + static::deleting(function (self $pengurus) { + if (! $pengurus->isForceDeleting()) { + $pengurus->kelembagaanAktor()->each(fn ($data) => $data->delete()); + } + }); + } + + public function profilLengkap(): bool + { + return $this->exists && collect([ + 'nama', 'tempat_lahir', 'tanggal_lahir', 'pendidikan_terakhir', 'alamat', + 'desa_kelurahan_id', 'jabatan', 'lama_menjabat_tahun', + ])->every(fn ($field) => filled($this->{$field})) && $this->jenis_kelamin !== null; + } public function getUsiaAttribute(): ?int { @@ -58,7 +79,7 @@ class PengurusBumdes extends Model public function getLamaMenjabatAttribute(): string { if (! is_null($this->lama_menjabat_tahun)) { - return $this->lama_menjabat_tahun . ' tahun'; + return $this->lama_menjabat_tahun.' tahun'; } if (! $this->tanggal_mulai_menjabat) { @@ -75,15 +96,15 @@ class PengurusBumdes extends Model $bagian = []; if ($selisih->y > 0) { - $bagian[] = $selisih->y . ' tahun'; + $bagian[] = $selisih->y.' tahun'; } if ($selisih->m > 0) { - $bagian[] = $selisih->m . ' bulan'; + $bagian[] = $selisih->m.' bulan'; } if (empty($bagian)) { - return $selisih->d . ' hari'; + return $selisih->d.' hari'; } return implode(' ', $bagian); diff --git a/app/Services/Bumdes/PengurusBumdesService.php b/app/Services/Bumdes/PengurusBumdesService.php new file mode 100644 index 0000000..9844bd8 --- /dev/null +++ b/app/Services/Bumdes/PengurusBumdesService.php @@ -0,0 +1,633 @@ +select('id', 'nama') + ->whereRaw('lower(nama) like ?', ['%ngada%'])->orderBy('nama')->first(); + $filters = [ + 'search' => $validated['search'] ?? null, + 'kabupaten_kota_id' => $validated['kabupaten_kota_id'] ?? $defaultKabupaten?->id, + 'kecamatan_id' => $validated['kecamatan_id'] ?? null, + 'desa_kelurahan_id' => $validated['desa_kelurahan_id'] ?? null, + 'status_keaktifan' => $validated['status_keaktifan'] ?? null, + 'sort_by' => in_array($validated['sort_by'] ?? null, ['nama_asc', 'nama_desc'], true) + ? $validated['sort_by'] : null, + ]; + + $query = $this->pengurusQuery()->with([ + ...$this->wilayahRelationsWithoutProgramScope(), + 'creator:id,name', + 'kelembagaanAktor', + ]); + + if (filled($filters['search'])) { + $search = '%'.Str::lower($filters['search']).'%'; + $query->where(function ($query) use ($search) { + $query->whereRaw('lower(nama) like ?', [$search]) + ->orWhereHas('desaKelurahan', function ($query) use ($search) { + $query->withoutGlobalScope('program')->where(function ($query) use ($search) { + $query->whereRaw('lower(nama) like ?', [$search]) + ->orWhereHas('kecamatan', fn ($query) => $query->withoutGlobalScope('program')->whereRaw('lower(nama) like ?', [$search])); + }); + }); + }); + } + + $query->when($filters['kabupaten_kota_id'], function ($query, $id) { + $query->whereHas('desaKelurahan', function ($query) use ($id) { + $query->withoutGlobalScope('program')->whereHas('kecamatan', function ($query) use ($id) { + $query->withoutGlobalScope('program')->where('kabupaten_kota_id', $id); + }); + }); + })->when($filters['kecamatan_id'], function ($query, $id) { + $query->whereHas('desaKelurahan', fn ($query) => $query->withoutGlobalScope('program')->where('kecamatan_id', $id)); + })->when($filters['desa_kelurahan_id'], fn ($query, $id) => $query->where('desa_kelurahan_id', $id)); + + $query->when($filters['status_keaktifan'], fn ($query, $value) => $query + ->whereHas('kelembagaanAktor', fn ($query) => $query->where('status_keaktifan', $value))); + + $summaryQuery = clone $query; + $totalBumdes = (clone $summaryQuery)->count(); + $totalBumdesAktif = (clone $summaryQuery)->whereHas('kelembagaanAktor', fn ($query) => $query + ->whereIn('status_keaktifan', [StatusKeaktifanBumdes::AKTIF_PENUH->value, StatusKeaktifanBumdes::AKTIF_TERBATAS->value]))->count(); + $totalBerbadanHukum = (clone $summaryQuery)->whereHas('kelembagaanAktor', fn ($query) => $query + ->where('status_badan_hukum', StatusBadanHukumBumdes::SUDAH_BERBADAN_HUKUM->value))->count(); + $totalDesaKelurahan = (clone $summaryQuery)->distinct('desa_kelurahan_id')->count('desa_kelurahan_id'); + + if ($filters['sort_by']) { + $namaDesa = DesaKelurahan::withoutGlobalScope('program')->select('nama') + ->whereColumn('master_desa_kelurahan.id', 'bumdes_pengurus.desa_kelurahan_id')->limit(1); + $query->orderBy($namaDesa, $filters['sort_by'] === 'nama_asc' ? 'asc' : 'desc'); + } else { + $query->latest(); + } + $perPage = (int) ($validated['per_page'] ?? 10); + $pengurus = $query->orderByDesc('id')->paginate($perPage)->appends([...$filters, 'per_page' => $perPage]); + $ringkasanBumdes = $pengurus->getCollection() + ->mapWithKeys(fn (PengurusBumdes $item) => [$item->id => $this->profileSummary($item)]); + $selectedKabupaten = $filters['kabupaten_kota_id'] + ? KabupatenKota::withoutGlobalScope('program')->select('id', 'nama')->find($filters['kabupaten_kota_id']) : null; + $kecamatanOptions = $filters['kabupaten_kota_id'] + ? Kecamatan::withoutGlobalScope('program')->select('id', 'nama')->where('kabupaten_kota_id', $filters['kabupaten_kota_id'])->orderBy('nama')->get() + : collect(); + $desaOptions = $filters['kecamatan_id'] + ? DesaKelurahan::withoutGlobalScope('program')->select('id', 'nama')->where('kecamatan_id', $filters['kecamatan_id'])->orderBy('nama')->get() + : collect(); + $statusKeaktifanOptions = StatusKeaktifanBumdes::options(); + $userRole = auth()->user()->role->value; + $showEnumerator = in_array($userRole, ['super_admin', 'admin']); + + return compact( + 'pengurus', + 'ringkasanBumdes', + 'totalBumdes', + 'totalBumdesAktif', + 'totalBerbadanHukum', + 'totalDesaKelurahan', + 'showEnumerator', 'filters', 'selectedKabupaten', 'kecamatanOptions', 'desaOptions', + 'statusKeaktifanOptions' + ); + } + + public function profileData(?PengurusBumdes $pengurus = null): array + { + $pengurus?->loadMissing([...$this->wilayahRelationsWithoutProgramScope(), 'kelembagaanAktor']); + + return [ + 'pengurus' => $pengurus, + 'ringkasanProfil' => $this->profileSummary($pengurus), + 'provinsi' => $this->provinsiOptions(), + 'jabatan' => JabatanBumdes::options(), + 'pendidikan' => Pendidikan::options(), + ...$this->profileLocationOptions($pengurus), + ...$this->profileBumdesOptions($pengurus), + ]; + } + + public function sectionData(PengurusBumdes $pengurus): array + { + $pengurus->loadMissing([...$this->wilayahRelationsWithoutProgramScope(), 'kelembagaanAktor']); + + return [ + 'pengurus' => $pengurus, + 'ringkasanProfil' => $this->profileSummary($pengurus), + 'kelembagaanAktor' => $pengurus->kelembagaanAktor?->attributesToArray(), + ...$this->kelembagaanAktorOptions(), + ]; + } + + private function profileSummary(?PengurusBumdes $pengurus): array + { + $data = $pengurus?->kelembagaanAktor; + $desa = $pengurus?->desaKelurahan; + $keaktifan = $data?->status_keaktifan; + $hukum = $data?->status_badan_hukum; + $unitUsaha = collect($data?->unit_usaha ?? [])->map(function (UnitUsahaBumdes $unit) use ($data) { + return $unit === UnitUsahaBumdes::JASA_LAINNYA && filled($data->unit_usaha_jasa_lainnya) + ? $unit->label().': '.$data->unit_usaha_jasa_lainnya : $unit->label(); + })->all(); + $updatedAt = collect([ + $pengurus?->updated_at ?? $pengurus?->created_at, + $data?->updated_at ?? $data?->created_at, + ])->filter()->sortByDesc(fn ($date) => $date->getTimestamp())->first(); + + return [ + 'judul' => $pengurus + ? ($desa?->nama ? 'BUMDes · '.$desa->nama : 'Profil BUMDes') : 'Profil BUMDes Baru', + 'lokasi' => collect([ + $desa?->kecamatan?->nama, $desa?->kecamatan?->kabupatenKota?->nama, + $desa?->kecamatan?->kabupatenKota?->provinsi?->nama, + ])->filter(fn ($value) => filled($value))->implode(' · ') ?: 'Lokasi belum terisi', + 'alamat' => $pengurus?->alamat ?: 'Belum terisi', + 'keaktifan' => [ + 'label' => $keaktifan?->label() ?? 'Belum terisi', + 'warna' => match ($keaktifan) { + StatusKeaktifanBumdes::AKTIF_PENUH => 'success', + StatusKeaktifanBumdes::AKTIF_TERBATAS => 'warning', + StatusKeaktifanBumdes::TIDAK_AKTIF_VAKUM => 'danger', + default => 'secondary', + }, + ], + 'hukum' => [ + 'label' => $hukum?->label() ?? 'Belum terisi', + 'warna' => match ($hukum) { + StatusBadanHukumBumdes::SUDAH_BERBADAN_HUKUM => 'success', + StatusBadanHukumBumdes::PROSES_PENDAFTARAN => 'warning', + default => 'secondary', + }, + ], + 'unit_usaha' => $unitUsaha, + 'responden' => $pengurus?->nama ?: 'Belum terisi', + 'jabatan' => $pengurus ? $pengurus->nama_jabatan : 'Belum terisi', + 'nomor_telepon' => $pengurus?->nomor_telepon ?: 'Belum terisi', + 'diperbarui' => $updatedAt, + ]; + } + + public function storeProfile(array $data): PengurusBumdes + { + return DB::transaction(function () use ($data) { + $pengurus = PengurusBumdes::create([ + ...$this->respondentData($data), + 'uid' => 'BMD-'.strtoupper(Str::random(10)), + 'created_by' => auth()->id(), 'updated_by' => auth()->id(), + ]); + $this->saveSection($pengurus, $data, 'profil'); + + return $pengurus; + }); + } + + public function updateProfile(PengurusBumdes $pengurus, array $data): void + { + DB::transaction(function () use ($pengurus, $data) { + $pengurus->update([...$this->respondentData($data), 'updated_by' => auth()->id()]); + $this->saveSection($pengurus, $data, 'profil'); + }); + } + + private function respondentData(array $data): array + { + $data = array_intersect_key($data, array_flip([ + 'nama', 'jenis_kelamin', 'tempat_lahir', 'tanggal_lahir', 'pendidikan_terakhir', + 'alamat', 'desa_kelurahan_id', 'nomor_telepon', 'jabatan', 'jabatan_lainnya', 'lama_menjabat_tahun', + ])); + + return $this->formatData($data); + } + + public function saveSection(PengurusBumdes $pengurus, array $validatedData, string $section): void + { + $data = array_intersect_key( + $section === 'rantai-nilai' ? $this->formatRantaiNilaiData($validatedData) : $this->formatKelembagaanAktorData($validatedData), + array_flip(KelembagaanAktorBumdes::sectionFields($section)) + ); + + // Kolom kepemilikan lama tetap dipertahankan untuk data sebelum kepemilikan dipisah per aset. + if ($section === 'operasional' && ! array_key_exists('status_kepemilikan_aset', $validatedData)) { + unset($data['status_kepemilikan_aset']); + } + + KelembagaanAktorBumdes::updateOrCreate( + ['pengurus_bumdes_id' => $pengurus->id], + [ + ...$data, + 'created_by' => $pengurus->kelembagaanAktor?->created_by ?? auth()->id(), + 'updated_by' => auth()->id(), + ] + ); + } + + private function profileBumdesOptions(?PengurusBumdes $pengurus = null): array + { + return [ + 'kelembagaanAktor' => $pengurus?->kelembagaanAktor?->attributesToArray(), + 'statusKeaktifanOptions' => StatusKeaktifanBumdes::options(), + 'pengurusIntiRows' => JabatanPengurusIntiBumdes::options(), + ]; + } + + private function pengurusQuery() + { + $query = PengurusBumdes::query(); + + if (auth()->user()->role === Role::ENUMERATOR) { + $query->where('created_by', auth()->id()); + } + + return $query; + } + + // Master wilayah BUMDes tidak dibatasi lokasi program, sama seperti halaman profilnya. + private function provinsiOptions() + { + return Provinsi::withoutGlobalScope('program') + ->select('id', 'kode', 'nama') + ->orderBy('kode', 'asc') + ->get(); + } + + private function wilayahRelationsWithoutProgramScope(): array + { + return [ + 'desaKelurahan' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama', 'kecamatan_id'), + 'desaKelurahan.kecamatan' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama', 'kabupaten_kota_id'), + 'desaKelurahan.kecamatan.kabupatenKota' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama', 'provinsi_id'), + 'desaKelurahan.kecamatan.kabupatenKota.provinsi' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama'), + ]; + } + + private function profileLocationOptions(?PengurusBumdes $pengurus = null): array + { + $locationId = static fn ($value) => is_scalar($value) && filter_var($value, FILTER_VALIDATE_INT) !== false ? (int) $value : null; + $provinsiId = $locationId(old('provinsi_id', $pengurus?->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi_id)); + $kabupatenId = $locationId(old('kabupaten_id', $pengurus?->desaKelurahan?->kecamatan?->kabupaten_kota_id)); + $kecamatanId = $locationId(old('kecamatan_id', $pengurus?->desaKelurahan?->kecamatan_id)); + + return [ + 'kabupatenOptions' => $provinsiId ? KabupatenKota::withoutGlobalScope('program')->where('provinsi_id', $provinsiId)->orderBy('nama')->get() : collect(), + 'kecamatanOptions' => $kabupatenId ? Kecamatan::withoutGlobalScope('program')->where('kabupaten_kota_id', $kabupatenId)->orderBy('nama')->get() : collect(), + 'desaOptions' => $kecamatanId ? DesaKelurahan::withoutGlobalScope('program')->where('kecamatan_id', $kecamatanId)->orderBy('nama')->get() : collect(), + ]; + } + + private function formatData(array $data): array + { + $data['tanggal_lahir'] = Carbon::createFromFormat('d-m-Y', $data['tanggal_lahir'])->format('Y-m-d'); + $data['tanggal_mulai_menjabat'] = Carbon::now() + ->subYears((int) $data['lama_menjabat_tahun']) + ->format('Y-m-d'); + + if (($data['jabatan'] ?? null) !== JabatanBumdes::LAINNYA->value) { + $data['jabatan_lainnya'] = null; + } + + return $data; + } + + private function formatRantaiNilaiData(array $data): array + { + $defaults = array_fill_keys(KelembagaanAktorBumdes::sectionFields('rantai-nilai'), null); + foreach (['sumber_pasokan', 'bentuk_produk_dibeli', 'volume_pembelian', 'saluran_penjualan', 'bentuk_produk_dijual', 'volume_penjualan', 'hubungan_petani_pemasok', 'harga_beli_petani'] as $field) { + $defaults[$field] = []; + } + $postharvest = $this->formatPascapanenData($data); + $defaults['melakukan_jual_beli'] = in_array($data['melakukan_jual_beli'] ?? null, [true, 1, '1'], true); + if (! $defaults['melakukan_jual_beli']) { + return array_replace($defaults, $postharvest); + } + $payload = array_replace($defaults, $data); + foreach (['sumber_pasokan', 'bentuk_produk_dibeli', 'saluran_penjualan', 'bentuk_produk_dijual', 'hubungan_petani_pemasok'] as $field) { + $payload[$field] = array_values($payload[$field] ?? []); + } + foreach (['sumber_pasokan' => 'sumber_pasokan_lainnya', 'saluran_penjualan' => 'saluran_penjualan_lainnya', 'hubungan_petani_pemasok' => 'hubungan_petani_pemasok_lainnya'] as $field => $other) { + if (! in_array('lainnya', $payload[$field], true)) { + $payload[$other] = null; + } + } + if ($payload['mekanisme_pembayaran_petani'] !== MekanismePembayaranBumdes::LAINNYA->value) { + $payload['mekanisme_pembayaran_lainnya'] = null; + } + if ($payload['penetapan_harga_beli'] !== PenetapanHargaBeliBumdes::LAINNYA->value) { + $payload['penetapan_harga_beli_lainnya'] = null; + } + $periods = array_keys(PeriodePanenBumdes::options()); + foreach (['volume_pembelian', 'volume_penjualan'] as $field) { + $payload[$field] = collect($payload[$field] ?? [])->only($periods)->filter(fn ($value) => $value !== null && $value !== '')->all(); + } + if (in_array(SaluranPenjualanBumdes::BELUM_MENJUAL->value, $payload['saluran_penjualan'], true)) { + $payload['bentuk_produk_dijual'] = []; + $payload['volume_penjualan'] = []; + } + $payload['harga_beli_petani'] = collect($payload['harga_beli_petani'] ?? [])->only($payload['bentuk_produk_dibeli']) + ->map(fn ($row) => collect($row)->only($periods)->filter(fn ($value) => $value !== null && $value !== '')->all()) + ->filter(fn ($row) => count($row) > 0)->all(); + + return array_replace($payload, $postharvest); + } + + private function formatPascapanenData(array $data): array + { + $processing = in_array($data['melakukan_pengolahan_pascapanen'] ?? null, [true, 1, '1'], true); + $payload = [ + 'melakukan_pengolahan_pascapanen' => ($data['melakukan_pengolahan_pascapanen'] ?? null) === null ? null : $processing, + 'status_sortasi' => null, 'kriteria_sortasi' => null, + 'status_pengeringan' => null, 'cara_tempat_pengeringan' => null, + 'status_fermentasi' => null, 'model_fermentasi' => [], + 'proses_fermentasi' => null, 'premium_harga_pascapanen' => null, + ]; + if (! $processing) { + return $payload; + } + $payload = array_replace($payload, array_intersect_key($data, $payload)); + if (! in_array($payload['status_sortasi'], [StatusSortasiBumdes::RUTIN_STANDAR->value, StatusSortasiBumdes::TIDAK_KONSISTEN->value], true)) { + $payload['kriteria_sortasi'] = null; + } + if (! in_array($payload['status_pengeringan'], [StatusPengeringanBumdes::RUTIN->value, StatusPengeringanBumdes::KADANG->value], true)) { + $payload['cara_tempat_pengeringan'] = null; + } + if (! in_array($payload['status_fermentasi'], [StatusFermentasiBumdes::RUTIN->value, StatusFermentasiBumdes::PERNAH_MENCOBA->value], true)) { + $payload['model_fermentasi'] = []; + $payload['proses_fermentasi'] = null; + } + $payload['model_fermentasi'] = array_values($payload['model_fermentasi'] ?? []); + + return $payload; + } + + private function formatKelembagaanAktorData(array $data): array + { + $payload = array_merge([ + 'status_badan_hukum' => null, + 'nomor_dasar_hukum' => null, + 'tahun_dasar_hukum' => null, + 'status_keaktifan' => null, + 'alasan_tidak_aktif' => null, + 'unit_usaha' => [], + 'unit_usaha_jasa_lainnya' => null, + 'status_unit_komoditas' => null, + 'nama_unit_komoditas' => null, + 'status_ad_art' => null, + 'tahun_ad_art' => null, + 'pengurus_inti' => [], + 'memiliki_staf' => null, + 'jumlah_staf' => null, + 'unit_usaha_staf' => null, + 'modal_awal' => null, + 'tahun_modal_awal' => null, + 'pernah_penambahan_modal' => null, + 'penambahan_modal' => [], + 'aset_produktif' => [], + 'aset_produktif_lainnya' => null, + 'status_kepemilikan_aset' => null, + 'infrastruktur_pascapanen' => [], + 'infrastruktur_pascapanen_lainnya' => null, + 'status_pembukuan' => null, + 'format_pembukuan' => null, + 'pernah_audit' => null, + 'sifat_audit' => [], + 'pelaksana_audit' => null, + 'tahun_audit' => null, + 'pertanggungjawaban_musdes' => null, + 'tahun_laporan_musdes' => null, + 'kondisi_keuangan' => null, + 'kontribusi_pades' => null, + 'nominal_kontribusi_pades' => null, + 'cakupan_layanan' => null, + 'desa_lain_dilayani' => null, + 'jumlah_petani_desa_sendiri' => null, + 'jumlah_petani_desa_lain' => null, + 'pencatatan_petani' => null, + 'layanan_petani' => [], + 'layanan_petani_lainnya' => null, + 'peran_rantai_nilai' => null, + 'pengalaman_offtaker' => null, + 'nama_offtaker' => null, + 'tahun_mulai_offtaker' => null, + 'alasan_berhenti_offtaker' => null, + 'hambatan_usaha' => [], + 'hambatan_usaha_lainnya' => null, + 'ada_alokasi_desa' => null, + 'jumlah_alokasi_desa' => null, + 'dukungan_desa' => [], + 'dukungan_desa_lainnya' => null, + 'keterlibatan_koordinasi_desa' => null, + 'kemitraan_eksternal' => [], + 'kemitraan_eksternal_lainnya' => null, + ], $data); + + foreach (['sifat_audit', 'layanan_petani', 'hambatan_usaha'] as $field) { + $payload[$field] = array_values($payload[$field] ?? []); + } + if (! (bool) $payload['pernah_audit']) { + $payload['sifat_audit'] = []; + $payload['pelaksana_audit'] = null; + $payload['tahun_audit'] = null; + } + if ($payload['pertanggungjawaban_musdes'] !== PertanggungjawabanMusdesBumdes::TAHUNAN->value) { + $payload['tahun_laporan_musdes'] = null; + } + if (! in_array($payload['kontribusi_pades'], [KontribusiPadesBumdes::RUTIN->value, KontribusiPadesBumdes::TIDAK_RUTIN->value], true)) { + $payload['nominal_kontribusi_pades'] = null; + } + if ($payload['cakupan_layanan'] !== CakupanLayananBumdes::DESA_LAIN->value) { + $payload['desa_lain_dilayani'] = null; + $payload['jumlah_petani_desa_lain'] = null; + } + if (! in_array(LayananPetaniBumdes::LAINNYA->value, $payload['layanan_petani'], true)) { + $payload['layanan_petani_lainnya'] = null; + } + if (! in_array(HambatanUsahaBumdes::LAINNYA->value, $payload['hambatan_usaha'], true)) { + $payload['hambatan_usaha_lainnya'] = null; + } + if ($payload['pengalaman_offtaker'] !== PengalamanOfftakerBumdes::BERJALAN->value) { + $payload['nama_offtaker'] = null; + $payload['tahun_mulai_offtaker'] = null; + } + if ($payload['pengalaman_offtaker'] !== PengalamanOfftakerBumdes::BERHENTI->value) { + $payload['alasan_berhenti_offtaker'] = null; + } + + $payload['dukungan_desa'] = array_values($payload['dukungan_desa'] ?? []); + $payload['kemitraan_eksternal'] = array_values($payload['kemitraan_eksternal'] ?? []); + if (! (bool) $payload['ada_alokasi_desa']) { + $payload['jumlah_alokasi_desa'] = null; + } + if (! in_array(DukunganDesaBumdes::LAINNYA->value, $payload['dukungan_desa'], true)) { + $payload['dukungan_desa_lainnya'] = null; + } + if (! in_array(KemitraanEksternalBumdes::LAINNYA->value, $payload['kemitraan_eksternal'], true)) { + $payload['kemitraan_eksternal_lainnya'] = null; + } + + $payload['unit_usaha'] = array_values($payload['unit_usaha'] ?? []); + $payload['pengurus_inti'] = $this->filterRows($payload['pengurus_inti'] ?? [], ['nama']); + $payload['penambahan_modal'] = $this->filterRows($payload['penambahan_modal'] ?? [], ['tahun', 'jumlah']); + foreach (['aset_produktif', 'infrastruktur_pascapanen'] as $field) { + $payload[$field] = collect($payload[$field] ?? []) + ->filter(fn ($row) => in_array($row['dipilih'] ?? null, [true, 1, '1'], true)) + ->values()->all(); + } + + if (($payload['status_badan_hukum'] ?? null) !== 'sudah_berbadan_hukum') { + $payload['nomor_dasar_hukum'] = null; + $payload['tahun_dasar_hukum'] = null; + } + + if (($payload['status_keaktifan'] ?? null) !== 'tidak_aktif_vakum') { + $payload['alasan_tidak_aktif'] = null; + } + + if (! in_array('jasa_lainnya', $payload['unit_usaha'], true)) { + $payload['unit_usaha_jasa_lainnya'] = null; + } + + if (($payload['status_unit_komoditas'] ?? null) !== 'ya') { + $payload['nama_unit_komoditas'] = null; + } + + if (($payload['status_ad_art'] ?? null) !== 'ada_lengkap') { + $payload['tahun_ad_art'] = null; + } + + if (! (bool) ($payload['memiliki_staf'] ?? false)) { + $payload['jumlah_staf'] = null; + $payload['unit_usaha_staf'] = null; + } + + if (! (bool) ($payload['pernah_penambahan_modal'] ?? false)) { + $payload['penambahan_modal'] = null; + } + + if (! collect($payload['aset_produktif'] ?? [])->contains(fn ($row) => ($row['aset'] ?? null) === 'lainnya')) { + $payload['aset_produktif_lainnya'] = null; + } + + if (! collect($payload['infrastruktur_pascapanen'] ?? [])->contains(fn ($row) => ($row['infrastruktur'] ?? null) === 'lainnya')) { + $payload['infrastruktur_pascapanen_lainnya'] = null; + } + + return $payload; + } + + private function filterRows(array $rows, array $valueKeys): array + { + return collect($rows) + ->map(function ($row) { + return collect($row) + ->map(fn ($value) => $value === '' ? null : $value) + ->all(); + }) + ->filter(function ($row) use ($valueKeys) { + foreach ($valueKeys as $key) { + if (filled($row[$key] ?? null)) { + return true; + } + } + + return false; + }) + ->values() + ->all(); + } + + private function kelembagaanAktorOptions(): array + { + return [ + 'statusSortasiOptions' => StatusSortasiBumdes::options(), + 'statusPengeringanOptions' => StatusPengeringanBumdes::options(), + 'statusFermentasiOptions' => StatusFermentasiBumdes::options(), + 'modelFermentasiOptions' => ModelFermentasiBumdes::options(), + 'sumberPasokanOptions' => SumberPasokanBumdes::options(), + 'bentukProdukOptions' => BentukProdukBumdes::options(), + 'periodePanenOptions' => PeriodePanenBumdes::options(), + 'saluranPenjualanOptions' => SaluranPenjualanBumdes::options(), + 'hubunganPemasokOptions' => HubunganPemasokBumdes::options(), + 'mekanismePembayaranOptions' => MekanismePembayaranBumdes::options(), + 'perjanjianPetaniOptions' => PerjanjianPetaniBumdes::options(), + 'penetapanHargaBeliOptions' => PenetapanHargaBeliBumdes::options(), + 'statusBadanHukumOptions' => StatusBadanHukumBumdes::options(), + 'statusKeaktifanOptions' => StatusKeaktifanBumdes::options(), + 'unitUsahaOptions' => UnitUsahaBumdes::options(), + 'statusUnitKomoditasOptions' => StatusUnitKomoditasBumdes::options(), + 'statusAdArtOptions' => StatusAdArtBumdes::options(), + 'pengurusIntiRows' => JabatanPengurusIntiBumdes::options(), + 'asetProduktifRows' => AsetProduktifBumdes::options(), + 'kondisiAsetProduktifOptions' => KondisiAsetProduktifBumdes::options(), + 'statusKepemilikanAsetOptions' => StatusKepemilikanAsetBumdes::options(), + 'infrastrukturPascapanenRows' => InfrastrukturPascapanenBumdes::options(), + 'statusPembukuanOptions' => StatusPembukuanBumdes::options(), + 'formatPembukuanOptions' => FormatPembukuanBumdes::options(), + 'sifatAuditOptions' => SifatAuditBumdes::options(), + 'pertanggungjawabanMusdesOptions' => PertanggungjawabanMusdesBumdes::options(), + 'kondisiKeuanganOptions' => KondisiKeuanganBumdes::options(), + 'kontribusiPadesOptions' => KontribusiPadesBumdes::options(), + 'cakupanLayananOptions' => CakupanLayananBumdes::options(), + 'pencatatanPetaniOptions' => PencatatanPetaniBumdes::options(), + 'layananPetaniOptions' => LayananPetaniBumdes::options(), + 'peranRantaiNilaiOptions' => PeranRantaiNilaiBumdes::options(), + 'pengalamanOfftakerOptions' => PengalamanOfftakerBumdes::options(), + 'hambatanUsahaOptions' => HambatanUsahaBumdes::options(), + 'dukunganDesaOptions' => DukunganDesaBumdes::options(), + 'keterlibatanKoordinasiOptions' => KeterlibatanKoordinasiBumdes::options(), + 'kemitraanEksternalOptions' => KemitraanEksternalBumdes::options(), + 'kondisiInfrastrukturOptions' => KondisiInfrastrukturBumdes::options(), + ]; + } +} diff --git a/database/migrations/2026_11_04_000001_add_dukungan_ekosistem_to_bumdes_kelembagaan_aktor_table.php b/database/migrations/2026_11_04_000001_add_dukungan_ekosistem_to_bumdes_kelembagaan_aktor_table.php new file mode 100644 index 0000000..9cf8700 --- /dev/null +++ b/database/migrations/2026_11_04_000001_add_dukungan_ekosistem_to_bumdes_kelembagaan_aktor_table.php @@ -0,0 +1,32 @@ +boolean('ada_alokasi_desa')->nullable(); + $table->decimal('jumlah_alokasi_desa', 18, 2)->nullable(); + $table->json('dukungan_desa')->nullable(); + $table->string('dukungan_desa_lainnya')->nullable(); + $table->string('keterlibatan_koordinasi_desa')->nullable(); + $table->json('kemitraan_eksternal')->nullable(); + $table->string('kemitraan_eksternal_lainnya')->nullable(); + }); + } + + public function down(): void + { + Schema::table('bumdes_kelembagaan_aktor', function (Blueprint $table) { + $table->dropColumn([ + 'ada_alokasi_desa', 'jumlah_alokasi_desa', 'dukungan_desa', + 'dukungan_desa_lainnya', 'keterlibatan_koordinasi_desa', + 'kemitraan_eksternal', 'kemitraan_eksternal_lainnya', + ]); + }); + } +}; diff --git a/database/migrations/2026_11_04_000002_add_pengelolaan_layanan_to_bumdes_kelembagaan_aktor_table.php b/database/migrations/2026_11_04_000002_add_pengelolaan_layanan_to_bumdes_kelembagaan_aktor_table.php new file mode 100644 index 0000000..9ee22f5 --- /dev/null +++ b/database/migrations/2026_11_04_000002_add_pengelolaan_layanan_to_bumdes_kelembagaan_aktor_table.php @@ -0,0 +1,72 @@ +string('status_pembukuan')->nullable(); + $table->string('format_pembukuan')->nullable(); + $table->boolean('pernah_audit')->nullable(); + $table->json('sifat_audit')->nullable(); + $table->string('pelaksana_audit')->nullable(); + $table->unsignedSmallInteger('tahun_audit')->nullable(); + $table->string('pertanggungjawaban_musdes')->nullable(); + $table->unsignedSmallInteger('tahun_laporan_musdes')->nullable(); + $table->string('kondisi_keuangan')->nullable(); + $table->string('kontribusi_pades')->nullable(); + $table->decimal('nominal_kontribusi_pades', 18, 2)->nullable(); + $table->string('cakupan_layanan')->nullable(); + $table->text('desa_lain_dilayani')->nullable(); + $table->unsignedInteger('jumlah_petani_desa_sendiri')->nullable(); + $table->unsignedInteger('jumlah_petani_desa_lain')->nullable(); + $table->string('pencatatan_petani')->nullable(); + $table->json('layanan_petani')->nullable(); + $table->string('layanan_petani_lainnya')->nullable(); + $table->string('peran_rantai_nilai')->nullable(); + $table->string('pengalaman_offtaker')->nullable(); + $table->string('nama_offtaker')->nullable(); + $table->unsignedSmallInteger('tahun_mulai_offtaker')->nullable(); + $table->text('alasan_berhenti_offtaker')->nullable(); + $table->json('hambatan_usaha')->nullable(); + $table->string('hambatan_usaha_lainnya')->nullable(); + }); + } + + public function down(): void + { + Schema::table('bumdes_kelembagaan_aktor', function (Blueprint $table) { + $table->dropColumn([ + 'status_pembukuan', + 'format_pembukuan', + 'pernah_audit', + 'sifat_audit', + 'pelaksana_audit', + 'tahun_audit', + 'pertanggungjawaban_musdes', + 'tahun_laporan_musdes', + 'kondisi_keuangan', + 'kontribusi_pades', + 'nominal_kontribusi_pades', + 'cakupan_layanan', + 'desa_lain_dilayani', + 'jumlah_petani_desa_sendiri', + 'jumlah_petani_desa_lain', + 'pencatatan_petani', + 'layanan_petani', + 'layanan_petani_lainnya', + 'peran_rantai_nilai', + 'pengalaman_offtaker', + 'nama_offtaker', + 'tahun_mulai_offtaker', + 'alasan_berhenti_offtaker', + 'hambatan_usaha', + 'hambatan_usaha_lainnya', + ]); + }); + } +}; diff --git a/database/migrations/2026_11_04_000003_add_rantai_nilai_to_bumdes_kelembagaan_aktor_table.php b/database/migrations/2026_11_04_000003_add_rantai_nilai_to_bumdes_kelembagaan_aktor_table.php new file mode 100644 index 0000000..4b2777f --- /dev/null +++ b/database/migrations/2026_11_04_000003_add_rantai_nilai_to_bumdes_kelembagaan_aktor_table.php @@ -0,0 +1,56 @@ +boolean('melakukan_jual_beli')->nullable(); + $table->json('sumber_pasokan')->nullable(); + $table->string('sumber_pasokan_lainnya')->nullable(); + $table->json('bentuk_produk_dibeli')->nullable(); + $table->json('volume_pembelian')->nullable(); + $table->json('saluran_penjualan')->nullable(); + $table->string('saluran_penjualan_lainnya')->nullable(); + $table->json('bentuk_produk_dijual')->nullable(); + $table->json('volume_penjualan')->nullable(); + $table->json('hubungan_petani_pemasok')->nullable(); + $table->string('hubungan_petani_pemasok_lainnya')->nullable(); + $table->string('mekanisme_pembayaran_petani')->nullable(); + $table->string('mekanisme_pembayaran_lainnya')->nullable(); + $table->string('perjanjian_petani')->nullable(); + $table->json('harga_beli_petani')->nullable(); + $table->string('penetapan_harga_beli')->nullable(); + $table->string('penetapan_harga_beli_lainnya')->nullable(); + }); + } + + public function down(): void + { + Schema::table('bumdes_kelembagaan_aktor', function (Blueprint $table) { + $table->dropColumn([ + 'melakukan_jual_beli', + 'sumber_pasokan', + 'sumber_pasokan_lainnya', + 'bentuk_produk_dibeli', + 'volume_pembelian', + 'saluran_penjualan', + 'saluran_penjualan_lainnya', + 'bentuk_produk_dijual', + 'volume_penjualan', + 'hubungan_petani_pemasok', + 'hubungan_petani_pemasok_lainnya', + 'mekanisme_pembayaran_petani', + 'mekanisme_pembayaran_lainnya', + 'perjanjian_petani', + 'harga_beli_petani', + 'penetapan_harga_beli', + 'penetapan_harga_beli_lainnya', + ]); + }); + } +}; diff --git a/database/migrations/2026_11_05_000001_add_peluang_nilai_tambah_to_bumdes_kelembagaan_aktor_table.php b/database/migrations/2026_11_05_000001_add_peluang_nilai_tambah_to_bumdes_kelembagaan_aktor_table.php new file mode 100644 index 0000000..ff0c13e --- /dev/null +++ b/database/migrations/2026_11_05_000001_add_peluang_nilai_tambah_to_bumdes_kelembagaan_aktor_table.php @@ -0,0 +1,40 @@ +boolean('melakukan_pengolahan_pascapanen')->nullable(); + $table->string('status_sortasi')->nullable(); + $table->text('kriteria_sortasi')->nullable(); + $table->string('status_pengeringan')->nullable(); + $table->text('cara_tempat_pengeringan')->nullable(); + $table->string('status_fermentasi')->nullable(); + $table->json('model_fermentasi')->nullable(); + $table->text('proses_fermentasi')->nullable(); + $table->decimal('premium_harga_pascapanen', 18, 2)->nullable(); + }); + } + + public function down(): void + { + Schema::table('bumdes_kelembagaan_aktor', function (Blueprint $table) { + $table->dropColumn([ + 'melakukan_pengolahan_pascapanen', + 'status_sortasi', + 'kriteria_sortasi', + 'status_pengeringan', + 'cara_tempat_pengeringan', + 'status_fermentasi', + 'model_fermentasi', + 'proses_fermentasi', + 'premium_harga_pascapanen', + ]); + }); + } +}; diff --git a/public/assets/css/demo.css b/public/assets/css/demo.css index 1d0ceb8..245295f 100644 --- a/public/assets/css/demo.css +++ b/public/assets/css/demo.css @@ -186,18 +186,62 @@ } #layout-menu .menu-sub .sidebar-icon > .ti { width: 19px; height: 19px; font-size: 19px; } -/* Profil Kelompok Tani */ -.kelompok-tani-page { +/* Select2 sizes the floating dropdown itself; its body wrapper must not span the viewport. */ +body > .select2-container--open { + inline-size: auto !important; +} + +/* Tipografi profil Kelompok Tani dan BUMDes */ +.kelompok-tani-page, +.bumdes-page { font-family: var(--bs-body-font-family); font-size: var(--bs-body-font-size); font-weight: var(--bs-body-font-weight); line-height: var(--bs-body-line-height); } -.kelompok-tani-page :is(.form-control, .form-select, .form-check-label, .select2-container) { +.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) { font-family: var(--bs-body-font-family); } +.bumdes-page form :is(.form-control, .form-select, .form-check-label, .select2-container) { + font-size: var(--bs-body-font-size); + font-weight: var(--bs-body-font-weight); + line-height: var(--bs-body-line-height); +} + +#bumdes-notifications, +.bumdes-page #section-kepengurusan { + scroll-margin-top: 7rem; +} + +.bumdes-page .pengurus-status-switch-row { + display: flex; + align-items: center; + gap: 12px; + white-space: nowrap; +} + +.bumdes-page .pengurus-status-switch-row .switch { + width: 52px; + min-width: 52px; + max-width: 52px; + padding-left: 0; + margin: 0; + position: relative; + display: inline-block; + flex: 0 0 52px; +} + +.bumdes-page .pengurus-status-switch-row .switch-toggle-slider { + left: 0; +} + +.bumdes-page select.is-invalid + .select2-container .select2-selection { + border-color: var(--bs-form-invalid-border-color); +} + .kelompok-tani-page select.is-invalid + .select2-container .select2-selection { border-color: var(--bs-form-invalid-border-color); } @@ -254,3 +298,127 @@ .kelompok-tani-page .select2-container--disabled .select2-selection::after { color: var(--bs-secondary-color); } + +/* Daftar dan navigasi profil BUMDes. */ + + .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 { + 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 { + 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 { + display: flex; align-items: center; min-height: 31px; + padding: .25rem .75rem; font-size: .8125rem; + background-color: var(--bs-paper-bg); + border: 1px solid var(--bs-border-color); + 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) { + 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; } + } + + + @media (min-width: 768px) { + .bumdes-page .nav-pills-custom-spacing { + display: flex; + width: 100%; + gap: 0.5rem; + flex-wrap: nowrap !important; + overflow-x: auto; + } + + .bumdes-page .nav-pills-custom-spacing::-webkit-scrollbar { + display: none; + } + + .bumdes-page .nav-pills-custom-spacing .nav-item { + margin-right: 0 !important; + flex: 1; + } + + .bumdes-page .nav-pills-custom-spacing .nav-link { + display: flex; + justify-content: center; + align-items: center; + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + width: 100%; + white-space: nowrap; + font-size: var(--bs-body-font-size) !important; + } + + .bumdes-page .nav-pills-custom-spacing .nav-link i { + margin-right: 0.4rem !important; + } + } + +/* Tabel dan ringkasan profil BUMDes. */ +.bumdes-list-table .bumdes-list-identity { + min-inline-size: 12rem; + max-inline-size: 16rem; +} + +.bumdes-list-table .bumdes-list-status { + min-inline-size: 10rem; + max-inline-size: 16rem; +} + +.bumdes-list-table .bumdes-list-units { + min-inline-size: 12rem; + max-inline-size: 18rem; +} + +.bumdes-list-table .badge { + max-inline-size: 100%; + line-height: 1.45; + white-space: normal; + text-align: start; + overflow-wrap: anywhere; +} + +.bumdes-profile-summary .bumdes-summary-content { + min-inline-size: 0; +} + +.bumdes-profile-summary .bumdes-summary-label { + color: var(--bs-secondary-color); + font-size: 0.8125rem; + font-weight: 500; +} + +.bumdes-profile-summary .badge { + max-inline-size: 100%; + line-height: 1.45; + white-space: normal; + text-align: start; + overflow-wrap: anywhere; +} + +.bumdes-profile-summary .bumdes-summary-more { + min-block-size: 1.5rem; + font-size: 0.8125rem; +} diff --git a/public/assets/js/bumdes.js b/public/assets/js/bumdes.js new file mode 100644 index 0000000..4ba8fc2 --- /dev/null +++ b/public/assets/js/bumdes.js @@ -0,0 +1,375 @@ +/* Profil, kelembagaan, dan operasional BUMDes. */ +'use strict'; +$(document).ready(function () { + const $page = $('.bumdes-page'); + if (!$page.length) return; + const profileForm = document.querySelector('[data-bumdes-profile]'); + const ajaxHeaders = { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, + 'Accept': 'application/json' + }; + if (profileForm) { + const prefix = '#' + profileForm.dataset.prefix; + const lainnyaValue = profileForm.dataset.jabatanLainnya; + + $(`${prefix}-jenis-kelamin, ${prefix}-pendidikan-terakhir, ${prefix}-jabatan`).select2(); + $(`${prefix}-tanggal-lahir`).flatpickr({ + dateFormat: 'd-m-Y' + }); + + function toggleJabatanLainnya() { + const isLainnya = $(`${prefix}-jabatan`).val() === lainnyaValue; + $(`${prefix}-jabatan-lainnya-wrapper`).toggleClass('d-none', !isLainnya); + $(`${prefix}-jabatan-lainnya`).prop('disabled', !isLainnya).prop('required', isLainnya); + + if (!isLainnya) { + $(`${prefix}-jabatan-lainnya`).val(''); + } + } + + $(`${prefix}-jabatan`).on('change', toggleJabatanLainnya); + toggleJabatanLainnya(); + + $(`${prefix}-provinsi`).select2().change(function(e) { + e.preventDefault(); + const provinsiId = $(this).val(); + $(`${prefix}-kabupaten`).prop('disabled', !provinsiId).val('').trigger('change'); + $(`${prefix}-kecamatan`).prop('disabled', true).val('').trigger('change'); + $(`${prefix}-desa`).prop('disabled', true).val('').trigger('change'); + + if (provinsiId) { + $.ajax({ + url: profileForm.dataset.kabupatenUrl.replace(':id', provinsiId) + '?all_wilayah=1', + method: 'GET', + headers: ajaxHeaders, + success: function(data) { + $(`${prefix}-kabupaten`).empty().append(''); + data.forEach(function(kabupaten) { + $(`${prefix}-kabupaten`).append(new Option(kabupaten.nama, kabupaten.id)); + }); + } + }); + } + }); + + $(`${prefix}-kabupaten`).select2().change(function(e) { + e.preventDefault(); + const kabupatenId = $(this).val(); + $(`${prefix}-kecamatan`).prop('disabled', !kabupatenId).val('').trigger('change'); + $(`${prefix}-desa`).prop('disabled', true).val('').trigger('change'); + + if (kabupatenId) { + $.ajax({ + url: profileForm.dataset.kecamatanUrl.replace(':id', kabupatenId) + '?all_wilayah=1', + method: 'GET', + headers: ajaxHeaders, + success: function(data) { + $(`${prefix}-kecamatan`).empty().append(''); + data.forEach(function(kecamatan) { + $(`${prefix}-kecamatan`).append(new Option(kecamatan.nama, kecamatan.id)); + }); + } + }); + } + }); + + $(`${prefix}-kecamatan`).select2().change(function(e) { + e.preventDefault(); + const kecamatanId = $(this).val(); + $(`${prefix}-desa`).prop('disabled', !kecamatanId).val('').trigger('change'); + + if (kecamatanId) { + $.ajax({ + url: profileForm.dataset.desaUrl.replace(':id', kecamatanId) + '?all_wilayah=1', + method: 'GET', + headers: ajaxHeaders, + success: function(data) { + $(`${prefix}-desa`).empty().append(''); + data.forEach(function(desa) { + $(`${prefix}-desa`).append(new Option(desa.nama, desa.id)); + }); + } + }); + } + }); + + $(`${prefix}-desa`).select2(); + } + + if (profileForm) { + function syncProfileDetails() { + const inactive = $('input[name="status_keaktifan"]:checked').val() === 'tidak_aktif_vakum'; + $('#alasan-tidak-aktif').prop('disabled', !inactive); + const hasStaff = $('input[name="memiliki_staf"]:checked').val() === '1'; + $('#staf-detail-wrapper').find('input').prop('disabled', !hasStaff); + } + $('input[name="status_keaktifan"], input[name="memiliki_staf"]').on('change', syncProfileDetails); + syncProfileDetails(); + } + + if (document.getElementById('kelembagaanAktorForm')) { + $('#status-badan-hukum, #status-unit-komoditas, #status-ad-art').select2({ width: '100%' }); + + function setRequired(selector, enabled) { + $(selector).prop('disabled', !enabled).prop('required', enabled).each(function () { + $(`[data-required-for="${this.id}"]`).toggleClass('d-none', !enabled); + }); + } + + function syncDetails() { + const jasaLainnya = $('#unit-usaha-jasa_lainnya').is(':checked'); + $('#unit-usaha-jasa-lainnya-wrapper').toggleClass('d-none', !jasaLainnya).find('input').prop('disabled', !jasaLainnya); + setRequired('#nomor-dasar-hukum, #tahun-dasar-hukum', $('#status-badan-hukum').val() === 'sudah_berbadan_hukum'); + setRequired('#nama-unit-komoditas', $('#status-unit-komoditas').val() === 'ya'); + setRequired('#tahun-ad-art', $('#status-ad-art').val() === 'ada_lengkap'); + } + + $('input[name="unit_usaha[]"], #status-badan-hukum, #status-unit-komoditas, #status-ad-art').on('change', syncDetails); + syncDetails(); + } + + if (document.getElementById('operasionalBumdesForm')) { + function setFieldsEnabled($wrapper, enabled) { + $wrapper.find('input, textarea, select').prop('disabled', !enabled); + + if (!enabled) { + $wrapper.find('input:not([type="radio"]):not([type="checkbox"]), textarea').val(''); + $wrapper.find('input[type="radio"], input[type="checkbox"]').prop('checked', false); + } + } + + function syncPenambahanModal() { + const enabled = $('input[name="pernah_penambahan_modal"]:checked').val() === '1'; + const $wrapper = $('#penambahan-modal-wrapper'); + setFieldsEnabled($wrapper, enabled); + } + + function syncInventarisRow(row) { + const $row = $(row); + const exists = $row.find('.bumdes-inventaris-status-radio[value="1"]').is(':checked'); + $row.find('.bumdes-inventaris-detail').prop('disabled', !exists); + $row.find('.bumdes-inventaris-required').prop('required', exists); + if (!exists) { + $row.find('.bumdes-inventaris-detail').val('').removeClass('is-invalid').removeAttr('aria-invalid aria-describedby'); + } + } + + $('input[name="pernah_penambahan_modal"]').on('change', syncPenambahanModal); + $('.bumdes-inventaris-status-radio').on('change', function () { + syncInventarisRow($(this).closest('tr')); + }); + + syncPenambahanModal(); + $('[data-bumdes-aset], [data-bumdes-infrastruktur]').each(function () { syncInventarisRow(this); }); + } + + const $moneyForms = $('#operasionalBumdesForm, #rantaiNilaiBumdesForm'); + if ($moneyForms.length) { + function onlyDigits(value) { + return String(value || '').replace(/\D/g, ''); + } + + function formatRupiah(value) { + return onlyDigits(value).replace(/\B(?=(\d{3})+(?!\d))/g, '.'); + } + + $moneyForms.on('input', '.rupiah-input', function () { + const cursorAtEnd = this.selectionStart === this.value.length; + this.value = formatRupiah(this.value); + + if (cursorAtEnd) { + this.setSelectionRange(this.value.length, this.value.length); + } + }); + + $moneyForms.find('.rupiah-input').each(function () { + this.value = formatRupiah(this.value); + }); + + $moneyForms.on('submit', function () { + $(this).find('.rupiah-input').each(function () { + this.value = onlyDigits(this.value); + }); + }); + } + + const $choiceForms = $('#operasionalBumdesForm, #rantaiNilaiBumdesForm'); + if ($choiceForms.length) { + function syncChoiceField($input, enabled) { + $input.prop('disabled', !enabled).prop('required', enabled); + $choiceForms.find(`[data-required-for="${$input.attr('id')}"]`).toggleClass('d-none', !enabled); + if (!enabled) $input.val('').removeClass('is-invalid'); + } + + function syncChoiceDetails() { + syncChoiceField($('#jumlah-alokasi-desa'), $choiceForms.find('input[name="ada_alokasi_desa"]:checked').val() === '1'); + $choiceForms.find('[data-bumdes-choices]').each(function () { + const $group = $(this); + const $other = $group.find('[data-ekosistem-other-value]'); + const enabled = $group.find('input[type="checkbox"]').toArray().some(input => input.checked && input.value === $other.data('ekosistem-other-value')); + syncChoiceField($other, enabled); + }); + } + + $choiceForms.on('change', 'input[name="ada_alokasi_desa"]', syncChoiceDetails); + $choiceForms.on('change', '[data-bumdes-choices] input[type="checkbox"]', function () { + const $group = $(this).closest('[data-bumdes-choices]'); + const exclusive = $group.data('exclusive-value'); + if (this.checked) { + $group.find('input[type="checkbox"]').not(this).each((_, input) => { + if (this.value === exclusive || input.value === exclusive) input.checked = false; + }); + } + syncChoiceDetails(); + }); + syncChoiceDetails(); + + const $rantaiForm = $('#rantaiNilaiBumdesForm'); + if ($rantaiForm.length) { + function syncRantaiState() { + const trading = $rantaiForm.find('[name="melakukan_jual_beli"]:checked').val() === '1'; + const $tradeSections = $rantaiForm.find('[data-rantai-section]').prop('disabled', !trading).toggleClass('d-none', !trading); + $tradeSections.closest('.card').not('#section-rantai-pembelian').toggleClass('d-none', !trading); + const notSold = $rantaiForm.find('[name="saluran_penjualan[]"]:checked').toArray().some(input => input.value === $rantaiForm.data('belum-menjual-value')); + const $sales = $('#rantai-penjualan-details').prop('disabled', !trading || notSold); + if (notSold) { + $sales.find('input[type="checkbox"]').prop('checked', false); + $sales.find('input:not([type="checkbox"])').val(''); + } + $rantaiForm.find('[data-rantai-other-field]').each(function () { + const $group = $(this); + const value = $rantaiForm.find(`[name="${$group.data('rantai-other-field')}"]:checked`).val(); + syncChoiceField($group.find('input[type="text"]'), trading && value === $group.data('rantai-other-value')); + }); + const purchased = $rantaiForm.find('[name="bentuk_produk_dibeli[]"]:checked').toArray().map(input => input.value); + $rantaiForm.find('[data-rantai-price-product]').each(function () { + const enabled = trading && purchased.includes($(this).data('rantai-price-product')); + const $inputs = $(this).find('input').prop('disabled', !enabled); + if (!enabled) $inputs.val('').removeClass('is-invalid'); + }); + } + function syncPostharvestState() { + const processing = $rantaiForm.find('[name="melakukan_pengolahan_pascapanen"]:checked').val() === '1'; + const $processingSections = $rantaiForm.find('[data-postharvest-section]').prop('disabled', !processing).toggleClass('d-none', !processing); + $processingSections.closest('.card').not('#section-rantai-sortasi').toggleClass('d-none', !processing); + $rantaiForm.find('[data-postharvest-detail]').each(function () { + const $group = $(this); + const value = $rantaiForm.find(`[name="${$group.data('condition-field')}"]:checked`).val(); + const enabled = processing && $group.data('condition-values').includes(value); + const $inputs = $group.find('input, textarea'); + const $checkboxes = $inputs.filter('[type="checkbox"]'); + $inputs.prop('disabled', !enabled); + $inputs.not('[type="checkbox"]').prop('required', enabled); + $checkboxes.prop('required', false); + if (enabled && !$checkboxes.is(':checked')) $checkboxes.first().prop('required', true); + $group.find('[data-postharvest-required]').toggleClass('d-none', !enabled); + if (!enabled) { + $checkboxes.prop('checked', false); + $inputs.not('[type="checkbox"]').val(''); + $inputs.removeClass('is-invalid'); + } + }); + } + $rantaiForm.on('change', 'input', function () { + if (this.name === 'melakukan_jual_beli' && this.value === '0') { + const $fields = $rantaiForm.find('[data-rantai-section] input'); + $fields.filter('[type="radio"], [type="checkbox"]').prop('checked', false); + $fields.not('[type="radio"], [type="checkbox"]').val(''); + } + if (this.name === 'melakukan_pengolahan_pascapanen' && this.value === '0') { + const $fields = $rantaiForm.find('[data-postharvest-section] input, [data-postharvest-section] textarea'); + $fields.filter('[type="radio"], [type="checkbox"]').prop('checked', false); + $fields.not('[type="radio"], [type="checkbox"]').val(''); + } + syncChoiceDetails(); + syncRantaiState(); + syncPostharvestState(); + }); + syncRantaiState(); + syncPostharvestState(); + } + } + + const $operationalForm = $('#operasionalBumdesForm'); + if ($operationalForm.length) { + function syncOperationalConditions() { + $operationalForm.find('[data-operasional-condition]').each(function () { + const $group = $(this); + const field = $group.data('condition-field'); + const selected = $operationalForm.find(`[name="${field}"]:checked`).val(); + const enabled = $group.data('condition-values').includes(selected); + const $inputs = $group.find('input, textarea, select'); + const $checkboxes = $inputs.filter('[type="checkbox"]'); + $inputs.prop('disabled', !enabled); + $inputs.not('[type="checkbox"]').prop('required', enabled); + $checkboxes.prop('required', false); + if (enabled && !$checkboxes.is(':checked')) $checkboxes.first().prop('required', true); + $group.find('[data-operasional-required]').toggleClass('d-none', !enabled); + if (!enabled) { + $inputs.not('[type="checkbox"]').val(''); + $checkboxes.prop('checked', false); + $inputs.removeClass('is-invalid'); + } + }); + } + $operationalForm.on('change', 'input, select', syncOperationalConditions); + syncOperationalConditions(); + } + + const filterForm = document.getElementById('bumdes-filter-form'); + if (filterForm) { + const $desa = $('#filter-desa'); + let desaRequest; + $('#filter-kecamatan').on('change', function () { + if (desaRequest) desaRequest.abort(); + $desa.empty().append(new Option('Semua Desa', '')).prop('disabled', true); + if (!this.value) return; + desaRequest = $.ajax({ + url: filterForm.dataset.desaUrl.replace(':id', this.value), + data: {all_wilayah: 1}, headers: ajaxHeaders, dataType: 'json' + }).done(function (data) { + data.forEach(desa => $desa.append(new Option(desa.nama, desa.id))); + $desa.prop('disabled', false); + }).fail(function (xhr, status) { + if (status !== 'abort') $desa.empty().append(new Option('Gagal memuat desa', '')); + }); + }); + } + $page.on('click', '.bumdes-row', function (event) { + if (!$(event.target).closest('a, button, input, select, textarea, label').length) { + window.location.href = this.querySelector('.edit-record').href; + } + }); + $page.on('submit', '.bumdes-delete-form', function (event) { + event.preventDefault(); + const form = this; + Swal.fire({ + title: 'Apakah Anda yakin?', text: `Profil "${form.dataset.nama}" akan dihapus.`, + icon: 'warning', showCancelButton: true, confirmButtonText: 'Ya, Hapus!', cancelButtonText: 'Batal', + customClass: {confirmButton: 'btn btn-primary me-3', cancelButton: 'btn btn-label-secondary'}, + buttonsStyling: false + }).then(result => { if (result.isConfirmed) form.submit(); }); + }); + const scrollAfterSave = function () { + const notice = document.getElementById('bumdes-notifications'); + if (notice && notice.dataset.hasErrors !== '1') { + const target = document.getElementById(window.location.hash.slice(1)); + const profileSection = target?.id === 'section-kepengurusan'; + const trading = document.querySelector('#rantaiNilaiBumdesForm [name="melakukan_jual_beli"]:checked')?.value === '1'; + const processing = document.querySelector('#rantaiNilaiBumdesForm [name="melakukan_pengolahan_pascapanen"]:checked')?.value === '1'; + const postharvestTarget = target?.hasAttribute('data-postharvest-card'); + const eligibleForTarget = postharvestTarget ? (processing || (trading && target.id === 'section-rantai-sortasi')) : trading; + const valueChainSection = eligibleForTarget && target?.matches('#rantaiNilaiBumdesForm > .card'); + if ((profileSection || valueChainSection) && target.closest('.bumdes-page') && target.getClientRects().length) { + target.scrollIntoView({block: 'start'}); + return; + } + } + notice?.scrollIntoView({block: 'start'}); + }; + if (document.readyState === 'complete') { + scrollAfterSave(); + } else { + window.addEventListener('load', scrollAfterSave, {once: true}); + } +}); diff --git a/resources/views/bumdes/kelembagaan-aktor/index.blade.php b/resources/views/bumdes/kelembagaan-aktor/index.blade.php index ac8525b..b668667 100644 --- a/resources/views/bumdes/kelembagaan-aktor/index.blade.php +++ b/resources/views/bumdes/kelembagaan-aktor/index.blade.php @@ -1,530 +1,107 @@ @extends('layout.main') @php - $fieldValue = function (string $field, $default = null) use ($kelembagaanAktor) { - return old($field, data_get($kelembagaanAktor, $field, $default)); - }; - - $arrayValue = function (string $field) use ($fieldValue) { - return (array) $fieldValue($field, []); - }; - - $rowValue = function (string $field, string $keyName, string $key) use ($fieldValue) { - return collect((array) $fieldValue($field, []))->firstWhere($keyName, $key) ?? []; - }; - - $rupiahValue = function ($value) { - if (! filled($value)) { - return ''; - } - - if (is_numeric($value)) { - return number_format((float) $value, 0, ',', '.'); - } - - return preg_replace('/\D/', '', (string) $value); - }; + $fieldValue = fn (string $field, $default = null) => old($field, data_get($kelembagaanAktor, $field, $default)); + $unitUsahaSelected = (array) $fieldValue('unit_usaha', []); + $hasBadanHukum = $fieldValue('status_badan_hukum') === 'sudah_berbadan_hukum'; + $hasUnitKomoditas = $fieldValue('status_unit_komoditas') === 'ya'; + $hasAdArtLengkap = $fieldValue('status_ad_art') === 'ada_lengkap'; @endphp @section('content') -
- @if (session('success')) -
{{ session('success') }}
- @endif - - @if ($errors->any()) -
-
    - @foreach ($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - +
@include('bumdes.profil.header', ['pengurus' => $pengurus]) -
- @csrf - -
-
-
Status Keberadaan, Keaktifan, Kepengurusan dan Modal BUMDes
-
-
-
Status Keberadaan dan Keaktifan
- -
- -
- @foreach($statusBadanHukumOptions as $value => $label) -
-
- - -
-
- @endforeach -
- @error('status_badan_hukum')
{{ $message }}
@enderror -
+
+
+ + @csrf +
Status Legalitas BUMDes
-
- - +
+ + + @error('status_badan_hukum')
{{ $message }}
@enderror +
+
+ + @error('nomor_dasar_hukum')
{{ $message }}
@enderror
-
- - +
+ + @error('tahun_dasar_hukum')
{{ $message }}
@enderror
-
- -
- @foreach($statusKeaktifanOptions as $value => $label) -
-
- - -
-
- @endforeach -
- @error('status_keaktifan')
{{ $message }}
@enderror -
- -
- - - @error('alasan_tidak_aktif')
{{ $message }}
@enderror -
-
Unit Usaha dan Dokumen Kelembagaan
- -
- -
- @foreach($unitUsahaOptions as $value => $label) -
-
- - +
+
+ +

Boleh pilih lebih dari satu unit usaha.

+
+ @foreach($unitUsahaOptions as $value => $label) +
+
+ + +
-
- @endforeach + @endforeach +
+ @error('unit_usaha')
{{ $message }}
@enderror
- @error('unit_usaha')
{{ $message }}
@enderror -
- -
-
- +
+ @error('unit_usaha_jasa_lainnya')
{{ $message }}
@enderror
- -
- -
- @foreach($statusUnitKomoditasOptions as $value => $label) -
-
- - -
-
- @endforeach +
+
+ + + @error('status_unit_komoditas')
{{ $message }}
@enderror
- @error('status_unit_komoditas')
{{ $message }}
@enderror -
- -
-
- - +
+ + @error('nama_unit_komoditas')
{{ $message }}
@enderror
- -
- -
- @foreach($statusAdArtOptions as $value => $label) -
-
- - -
-
- @endforeach +
+
+ + + @error('status_ad_art')
{{ $message }}
@enderror
- @error('status_ad_art')
{{ $message }}
@enderror -
- -
-
- - +
+ + @error('tahun_ad_art')
{{ $message }}
@enderror
-
Kepengurusan dan Staf
- -
- - - - - - - - - - @foreach($pengurusIntiRows as $key => $label) - @php - $savedRow = $rowValue('pengurus_inti', 'jabatan', $key); - $aktifValue = old("pengurus_inti.$loop->index.aktif", data_get($savedRow, 'aktif')); - @endphp - - - - - - @endforeach - -
JabatanNamaKeaktifan
- - {{ $label }} - - index.nama", data_get($savedRow, 'nama')) }}"> - -
-
- - -
-
- - -
-
-
-
- -
- -
-
- - -
-
- - -
-
- @error('memiliki_staf')
{{ $message }}
@enderror -
- -
-
- -
- - orang -
- @error('jumlah_staf')
{{ $message }}
@enderror -
-
- - - @error('unit_usaha_staf')
{{ $message }}
@enderror -
-
- -
Modal dan Aset
-
-
- -
- Rp - -
- @error('modal_awal')
{{ $message }}
@enderror -
-
- - - @error('tahun_modal_awal')
{{ $message }}
@enderror -
-
- -
- -
-
- - -
-
- - -
-
- @error('pernah_penambahan_modal')
{{ $message }}
@enderror -
- -
- -
- - - - - - - - - @for($i = 0; $i < 3; $i++) - @php $savedRow = data_get($fieldValue('penambahan_modal', []), $i, []); @endphp - - - - - @endfor - -
TahunJumlah (Rp)
- - -
- Rp - -
-
-
-
- - -
- - - - - - - - - - @foreach($asetProduktifRows as $key => $label) - @php - $savedRow = $rowValue('aset_produktif', 'aset', $key); - $dipilih = old("aset_produktif.$loop->index.dipilih", data_get($savedRow, 'dipilih')); - @endphp - - - - - - @endforeach - -
Aset ProduktifJumlahKondisi
- -
- - -
- @if($key === 'lainnya') - - @endif -
- index.jumlah", data_get($savedRow, 'jumlah')) }}"> - - index.kondisi", data_get($savedRow, 'kondisi')) }}" placeholder="Contoh: Baik, rusak ringan"> -
-
- -
- -
- @foreach($statusKepemilikanAsetOptions as $value => $label) -
-
- - -
-
- @endforeach -
- @error('status_kepemilikan_aset')
{{ $message }}
@enderror -
- - -
- - - - - - - - - - @foreach($infrastrukturPascapanenRows as $key => $label) - @php - $savedRow = $rowValue('infrastruktur_pascapanen', 'infrastruktur', $key); - $dipilih = old("infrastruktur_pascapanen.$loop->index.dipilih", data_get($savedRow, 'dipilih')); - $kondisiValue = old("infrastruktur_pascapanen.$loop->index.kondisi", data_get($savedRow, 'kondisi')); - @endphp - - - - - - @endforeach - -
Infrastruktur PascapanenJumlahKondisi
- -
- - -
- @if($key === 'lainnya') - - @endif -
- index.jumlah", data_get($savedRow, 'jumlah')) }}"> - -
- @foreach($kondisiInfrastrukturOptions as $kondisi => $kondisiLabel) -
- - -
- @endforeach -
-
-
- -
- Kembali - -
-
+ +
- +
@endsection - -@push('scripts') - -@endpush diff --git a/resources/views/bumdes/operasional/index.blade.php b/resources/views/bumdes/operasional/index.blade.php new file mode 100644 index 0000000..9c57c6c --- /dev/null +++ b/resources/views/bumdes/operasional/index.blade.php @@ -0,0 +1,656 @@ +@extends('layout.main') + +@php + $fieldValue = function (string $field, $default = null) use ($kelembagaanAktor) { + return old($field, data_get($kelembagaanAktor, $field, $default)); + }; + $hasPenambahanModal = (string) $fieldValue('pernah_penambahan_modal') === '1'; + + $rowValue = function (string $field, string $keyName, string $key) use ($fieldValue) { + return collect((array) $fieldValue($field, []))->firstWhere($keyName, $key) ?? []; + }; + + $rupiahValue = function ($value) { + if (! filled($value) || ! is_scalar($value)) { + return ''; + } + + if (is_numeric($value)) { + return number_format((float) $value, 0, ',', '.'); + } + + return preg_replace('/\D/', '', (string) $value); + }; + $textValue = fn (string $field) => is_scalar($value = $fieldValue($field)) ? (string) $value : ''; + $hasAlokasi = in_array($fieldValue('ada_alokasi_desa'), [true, 1, '1'], true); + $dukunganSelected = (array) $fieldValue('dukungan_desa', []); + $kemitraanSelected = (array) $fieldValue('kemitraan_eksternal', []); + $hasDukunganLainnya = in_array(\App\Enums\Bumdes\DukunganDesaBumdes::LAINNYA->value, $dukunganSelected, true); + $hasKemitraanLainnya = in_array(\App\Enums\Bumdes\KemitraanEksternalBumdes::LAINNYA->value, $kemitraanSelected, true); + $alokasi = $fieldValue('jumlah_alokasi_desa'); + $alokasi = is_scalar($alokasi) ? (string) $alokasi : ''; + if (preg_match('/^\d+(\.00)?$/D', $alokasi)) { + $alokasi = preg_replace('/\B(?=(\d{3})+(?!\d))/', '.', explode('.', $alokasi)[0]); + } + $hasAudit = in_array($fieldValue('pernah_audit'), [true, 1, '1'], true); + $auditSelected = (array) $fieldValue('sifat_audit', []); + $annualReport = $fieldValue('pertanggungjawaban_musdes') === \App\Enums\Bumdes\PertanggungjawabanMusdesBumdes::TAHUNAN->value; + $hasPades = in_array($fieldValue('kontribusi_pades'), [\App\Enums\Bumdes\KontribusiPadesBumdes::RUTIN->value, \App\Enums\Bumdes\KontribusiPadesBumdes::TIDAK_RUTIN->value], true); + $otherVillage = $fieldValue('cakupan_layanan') === \App\Enums\Bumdes\CakupanLayananBumdes::DESA_LAIN->value; + $servicesSelected = (array) $fieldValue('layanan_petani', []); + $obstaclesSelected = (array) $fieldValue('hambatan_usaha', []); + $hasOtherService = in_array(\App\Enums\Bumdes\LayananPetaniBumdes::LAINNYA->value, $servicesSelected, true); + $hasOtherObstacle = in_array(\App\Enums\Bumdes\HambatanUsahaBumdes::LAINNYA->value, $obstaclesSelected, true); + $activePartnership = $fieldValue('pengalaman_offtaker') === \App\Enums\Bumdes\PengalamanOfftakerBumdes::BERJALAN->value; + $stoppedPartnership = $fieldValue('pengalaman_offtaker') === \App\Enums\Bumdes\PengalamanOfftakerBumdes::BERHENTI->value; + $nominalPades = $textValue('nominal_kontribusi_pades'); + if (preg_match('/^\d+(\.00)?$/D', $nominalPades)) { + $nominalPades = preg_replace('/\B(?=(\d{3})+(?!\d))/', '.', explode('.', $nominalPades)[0]); + } +@endphp + +@section('content') +
+ @include('bumdes.profil.header', ['pengurus' => $pengurus]) + +
+ @csrf + +
+
+
Modal dan Aset
+
+
+ +
+ Rp + +
+ @error('modal_awal')
{{ $message }}
@enderror +
+
+ + + @error('tahun_modal_awal')
{{ $message }}
@enderror +
+
+ +
+ +
+
+ + +
+
+ + +
+
+ @error('pernah_penambahan_modal')
{{ $message }}
@enderror +
+ +
+ +
+ + + + + + + + + @for($i = 0; $i < 3; $i++) + @php $savedRow = data_get($fieldValue('penambahan_modal', []), $i, []); @endphp + + + + + @endfor + +
TahunJumlah (Rp)
+ + @error("penambahan_modal.$i.tahun")
{{ $message }}
@enderror +
+
+ Rp + +
+ @error("penambahan_modal.$i.jumlah")
{{ $message }}
@enderror +
+
+
+ +
Aset Produktif Milik BUMDes
+

* Wajib diisi untuk aset yang dipilih Ada.

+
+ + + + + + + + + + + + @foreach($asetProduktifRows as $key => $label) + @php + $index = $loop->index; + $savedRow = $rowValue('aset_produktif', 'aset', $key); + $ada = (string) old("aset_produktif.$index.dipilih", data_get($savedRow, 'dipilih', '0')) === '1'; + $kepemilikan = old("aset_produktif.$index.status_kepemilikan_aset", data_get($savedRow, 'status_kepemilikan_aset', $fieldValue('status_kepemilikan_aset'))); + $kondisi = \App\Enums\Bumdes\KondisiAsetProduktifBumdes::normalizeLegacy(old("aset_produktif.$index.kondisi", data_get($savedRow, 'kondisi'))); + @endphp + + + + + + + + @endforeach + +
Jenis AsetAda/TidakJumlahStatus Kepemilikan Kondisi
+ + {{ $label }} + @if($key === 'lainnya') + + @error('aset_produktif_lainnya')
{{ $message }}
@enderror + @endif +
+
+ + +
+
+ + +
+
+ + @error("aset_produktif.$index.jumlah")
{{ $message }}
@enderror +
+ + @error("aset_produktif.$index.status_kepemilikan_aset")
{{ $message }}
@enderror +
+ + @error("aset_produktif.$index.kondisi")
{{ $message }}
@enderror +
+
+ +
Ketersediaan Infrastruktur Pascapanen
+

Untuk komoditas kopi/kakao/vanili/kemiri.

+

* Wajib diisi untuk infrastruktur yang dipilih Ada.

+
+ + + + + + + + + + + @foreach($infrastrukturPascapanenRows as $key => $label) + @php + $index = $loop->index; + $savedRow = $rowValue('infrastruktur_pascapanen', 'infrastruktur', $key); + $ada = (string) old("infrastruktur_pascapanen.$index.dipilih", data_get($savedRow, 'dipilih', '0')) === '1'; + $kondisiValue = old("infrastruktur_pascapanen.$index.kondisi", data_get($savedRow, 'kondisi')); + @endphp + + + + + + + @endforeach + +
Infrastruktur PascapanenAda/TidakJumlahKondisi
+ + {{ $label }} + @if($key === 'lainnya') + + @error('infrastruktur_pascapanen_lainnya')
{{ $message }}
@enderror + @endif +
+
+ + +
+
+ + +
+
+ + @error("infrastruktur_pascapanen.$index.jumlah")
{{ $message }}
@enderror +
+ + @error("infrastruktur_pascapanen.$index.kondisi")
{{ $message }}
@enderror +
+
+ + +
+
+ +
+
+
Dukungan Ekosistem
+
+ Alokasi APBDes/Dana Desa Tahun Berjalan +
+ + +
+
+ + +
+ @error('ada_alokasi_desa')
{{ $message }}
@enderror +
+
+ +
+ Rp + +
+ @error('jumlah_alokasi_desa')
{{ $message }}
@enderror +
+
+
+ +
+ Dukungan Lain dari Pemerintah Desa +

Boleh pilih lebih dari satu.

+
+ @foreach($dukunganDesaOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('dukungan_desa')
{{ $message }}
@enderror + @error('dukungan_desa.*')
{{ $message }}
@enderror +
+
+ + + @error('dukungan_desa_lainnya')
{{ $message }}
@enderror +
+
+
+ +
+ Keterlibatan dalam Rapat Desa Terkait Komoditas +
+ @foreach($keterlibatanKoordinasiOptions as $value => $label) +
+ + +
+ @endforeach +
+ @error('keterlibatan_koordinasi_desa')
{{ $message }}
@enderror +
+ +
+ Kemitraan Eksternal +

Boleh pilih lebih dari satu.

+
+ @foreach($kemitraanEksternalOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('kemitraan_eksternal')
{{ $message }}
@enderror + @error('kemitraan_eksternal.*')
{{ $message }}
@enderror +
+
+ + + @error('kemitraan_eksternal_lainnya')
{{ $message }}
@enderror +
+
+
+ + +
+
+
+
+
Kapasitas Pengelolaan Keuangan
+
+ Pembukuan dan Laporan Keuangan +
+ @foreach($statusPembukuanOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('status_pembukuan')
{{ $message }}
@enderror +
+
+ Format Pembukuan +
+ @foreach($formatPembukuanOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('format_pembukuan')
{{ $message }}
@enderror +
+
+ Audit/Pemeriksaan Keuangan +
+
+ + +
+
+ + +
+
+ @error('pernah_audit')
{{ $message }}
@enderror +
+
+
+ Jenis Audit/Pemeriksaan* +

Boleh pilih lebih dari satu.

+
+ @foreach($sifatAuditOptions as $value => $label) +
+
+ first && count($auditSelected) === 0)> + +
+
+ @endforeach +
+ @error('sifat_audit')
{{ $message }}
@enderror + @error('sifat_audit.*')
{{ $message }}
@enderror +
+
+ + + @error('pelaksana_audit')
{{ $message }}
@enderror +
+
+ + + @error('tahun_audit')
{{ $message }}
@enderror +
+
+
+ Pertanggungjawaban ke Musyawarah Desa +
+ @foreach($pertanggungjawabanMusdesOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('pertanggungjawaban_musdes')
{{ $message }}
@enderror +
+
+
+ + + @error('tahun_laporan_musdes')
{{ $message }}
@enderror +
+
+
+ Kondisi Keuangan Saat Ini +
+ @foreach($kondisiKeuanganOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('kondisi_keuangan')
{{ $message }}
@enderror +
+
+ Kontribusi ke Pendapatan Asli Desa (PADes) +
+ @foreach($kontribusiPadesOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('kontribusi_pades')
{{ $message }}
@enderror +
+
+
+ +
Rp
+ @error('nominal_kontribusi_pades')
{{ $message }}
@enderror +
+
+ + +
+
+
+
+
Jangkauan Layanan
+
+ Wilayah Layanan Komoditas +
+ @foreach($cakupanLayananOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('cakupan_layanan')
{{ $message }}
@enderror +
+
+
+ + + @error('desa_lain_dilayani')
{{ $message }}
@enderror +
+
+

Perkiraan Jumlah Petani yang Dilayani

+
+
+ +
orang
+ @error('jumlah_petani_desa_sendiri')
{{ $message }}
@enderror +
+
+
+
+ +
orang
+ @error('jumlah_petani_desa_lain')
{{ $message }}
@enderror +
+
+
+ Pencatatan Petani yang Dilayani +
+ @foreach($pencatatanPetaniOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('pencatatan_petani')
{{ $message }}
@enderror +
+ + +
+
+
+
+
Layanan bagi Petani Terkait Komoditas
+
+ Layanan untuk Petani Kopi/Kakao/Vanili/Kemiri +

Boleh pilih lebih dari satu.

+
+ @foreach($layananPetaniOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('layanan_petani')
{{ $message }}
@enderror + @error('layanan_petani.*')
{{ $message }}
@enderror +
+
+ + + @error('layanan_petani_lainnya')
{{ $message }}
@enderror +
+
+
+ + +
+
+
+
+
Pengalaman dalam Mengelola Rantai Nilai Komoditas Pertanian
+
+ Peran dalam Rantai Nilai Komoditas +
+ @foreach($peranRantaiNilaiOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('peran_rantai_nilai')
{{ $message }}
@enderror +
+
+ Pengalaman Kemitraan Offtaker +
+ @foreach($pengalamanOfftakerOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('pengalaman_offtaker')
{{ $message }}
@enderror +
+
+
+ + + @error('nama_offtaker')
{{ $message }}
@enderror +
+
+ + + @error('tahun_mulai_offtaker')
{{ $message }}
@enderror +
+
+
+
+ + + @error('alasan_berhenti_offtaker')
{{ $message }}
@enderror +
+
+ + +
+
+
+
+
Hambatan yang Dihadapi BUMDes
+
+ Hambatan Usaha +

Boleh pilih lebih dari satu.

+
+ @foreach($hambatanUsahaOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('hambatan_usaha')
{{ $message }}
@enderror + @error('hambatan_usaha.*')
{{ $message }}
@enderror +
+
+ + + @error('hambatan_usaha_lainnya')
{{ $message }}
@enderror +
+
+
+ + +
+
+
+
+@endsection diff --git a/resources/views/bumdes/profil/_form.blade.php b/resources/views/bumdes/profil/_form.blade.php index d33f55d..6f86441 100644 --- a/resources/views/bumdes/profil/_form.blade.php +++ b/resources/views/bumdes/profil/_form.blade.php @@ -1,23 +1,23 @@ @php $prefix = $mode . '-bumdes'; $currentProvinsiId = old('provinsi_id', $pengurus?->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi_id); - $currentKabupaten = $pengurus?->desaKelurahan?->kecamatan?->kabupatenKota; - $currentKecamatan = $pengurus?->desaKelurahan?->kecamatan; - $currentDesa = $pengurus?->desaKelurahan; + $currentKabupatenId = old('kabupaten_id', $pengurus?->desaKelurahan?->kecamatan?->kabupaten_kota_id); + $currentKecamatanId = old('kecamatan_id', $pengurus?->desaKelurahan?->kecamatan_id); + $currentDesaId = old('desa_kelurahan_id', $pengurus?->desa_kelurahan_id); $currentJabatan = $pengurus?->jabatan instanceof \App\Enums\Bumdes\JabatanBumdes ? $pengurus->jabatan->value : $pengurus?->jabatan; $currentPendidikan = $pengurus?->pendidikan_terakhir instanceof \App\Enums\Pendidikan ? $pengurus->pendidikan_terakhir->value : $pengurus?->pendidikan_terakhir; @endphp -
Data Pribadi
+
Data Responden
- - + + @error('nama')
{{ $message }}
@enderror
- @@ -26,19 +26,17 @@
- + @error('tempat_lahir')
{{ $message }}
@enderror
- + @error('tanggal_lahir')
{{ $message }}
@enderror
-
-
- @foreach($pendidikan as $value) @@ -46,13 +44,9 @@ @error('pendidikan_terakhir')
{{ $message }}
@enderror
-
- -
Peran Kepengurusan BUMDes
-
- @foreach($jabatan as $value) @@ -67,23 +61,27 @@
- + @error('lama_menjabat_tahun')
{{ $message }}
@enderror
+
+ + + @error('nomor_telepon')
{{ $message }}
@enderror +
-
Domisili dan Kontak
- + @error('alamat')
{{ $message }}
@enderror
- @foreach($provinsi as $p) @@ -93,188 +91,32 @@
- - @if($currentKabupaten) - - @endif + @foreach($kabupatenOptions as $option) + + @endforeach @error('kabupaten_id')
{{ $message }}
@enderror
- - @if($currentKecamatan) - - @endif + @foreach($kecamatanOptions as $option) + + @endforeach @error('kecamatan_id')
{{ $message }}
@enderror
- - @if($currentDesa) - - @endif + @foreach($desaOptions as $option) + + @endforeach @error('desa_kelurahan_id')
{{ $message }}
@enderror
- -
-
- - - @error('nomor_telepon')
{{ $message }}
@enderror -
-
- -@push('scripts') - -@endpush diff --git a/resources/views/bumdes/profil/_keaktifan.blade.php b/resources/views/bumdes/profil/_keaktifan.blade.php new file mode 100644 index 0000000..c325670 --- /dev/null +++ b/resources/views/bumdes/profil/_keaktifan.blade.php @@ -0,0 +1,26 @@ +@php + $fieldValue = fn (string $field, $default = null) => old($field, data_get($kelembagaanAktor, $field, $default)); +@endphp + +
Status Keaktifan BUMDes
+ +
+ +
+ @foreach($statusKeaktifanOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('status_keaktifan')
{{ $message }}
@enderror +
+ +
+ + + @error('alasan_tidak_aktif')
{{ $message }}
@enderror +
diff --git a/resources/views/bumdes/profil/_kepengurusan.blade.php b/resources/views/bumdes/profil/_kepengurusan.blade.php new file mode 100644 index 0000000..d802138 --- /dev/null +++ b/resources/views/bumdes/profil/_kepengurusan.blade.php @@ -0,0 +1,89 @@ +@php + $fieldValue = function (string $field, $default = null) use ($kelembagaanAktor) { + return old($field, data_get($kelembagaanAktor, $field, $default)); + }; + + $rowValue = function (string $field, string $keyName, string $key) use ($fieldValue) { + return collect((array) $fieldValue($field, []))->firstWhere($keyName, $key) ?? []; + }; + +@endphp + +
+
+
Kepengurusan dan Staf
+ +
+ + + + + + + + + + @foreach($pengurusIntiRows as $key => $label) + @php + $savedRow = $rowValue('pengurus_inti', 'jabatan', $key); + $aktifValue = old("pengurus_inti.$loop->index.aktif", data_get($savedRow, 'aktif')); + @endphp + + + + + + @endforeach + +
JabatanNamaKeaktifan
+ + {{ $label }} + + index.nama", data_get($savedRow, 'nama')) }}"> + +
+ + +
+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ @error('memiliki_staf')
{{ $message }}
@enderror +
+ +
+
+ +
+ + orang +
+ @error('jumlah_staf')
{{ $message }}
@enderror +
+
+ + + @error('unit_usaha_staf')
{{ $message }}
@enderror +
+
+ +
+
diff --git a/resources/views/bumdes/profil/add.blade.php b/resources/views/bumdes/profil/add.blade.php deleted file mode 100644 index 740c812..0000000 --- a/resources/views/bumdes/profil/add.blade.php +++ /dev/null @@ -1,34 +0,0 @@ -@extends('layout.main') - -@section('content') -
- @if ($errors->any()) -
-
    - @foreach ($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - - @include('bumdes.profil.header', ['pengurus' => null]) - -
-
-
Tambah Profil Pengurus BUMDes
-
-
-
- @csrf - @include('bumdes.profil._form', ['pengurus' => null, 'mode' => 'add']) - -
- Kembali - -
-
-
-
-
-@endsection diff --git a/resources/views/bumdes/profil/create.blade.php b/resources/views/bumdes/profil/create.blade.php new file mode 100644 index 0000000..a91f1f5 --- /dev/null +++ b/resources/views/bumdes/profil/create.blade.php @@ -0,0 +1,27 @@ +@extends('layout.main') + +@section('content') +
+ @include('bumdes.profil.header', ['pengurus' => null]) + +
+ @csrf + +
+
+ @include('bumdes.profil._form', ['pengurus' => null, 'mode' => 'add']) + @include('bumdes.profil._keaktifan') + + +
+
+ + @include('bumdes.profil._kepengurusan') +
+
+@endsection diff --git a/resources/views/bumdes/profil/edit.blade.php b/resources/views/bumdes/profil/edit.blade.php index bb27f75..e00b857 100644 --- a/resources/views/bumdes/profil/edit.blade.php +++ b/resources/views/bumdes/profil/edit.blade.php @@ -1,39 +1,28 @@ @extends('layout.main') @section('content') -
- @if (session('success')) -
{{ session('success') }}
- @endif - - @if ($errors->any()) -
-
    - @foreach ($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - +
@include('bumdes.profil.header', ['pengurus' => $pengurus]) -
-
-
Edit Profil Pengurus BUMDes
-
-
-
- @csrf - @method('PUT') - @include('bumdes.profil._form', ['pengurus' => $pengurus, 'mode' => 'edit']) + + @csrf + @method('PUT') -
- Kembali - -
-
+
+
+ @include('bumdes.profil._form', ['pengurus' => $pengurus, 'mode' => 'edit']) + @include('bumdes.profil._keaktifan') + + +
-
+ + @include('bumdes.profil._kepengurusan') +
@endsection diff --git a/resources/views/bumdes/profil/header.blade.php b/resources/views/bumdes/profil/header.blade.php index 64aee7f..348858e 100644 --- a/resources/views/bumdes/profil/header.blade.php +++ b/resources/views/bumdes/profil/header.blade.php @@ -1,144 +1,117 @@ - - -@php - $jenisKelamin = isset($pengurus) - ? ($pengurus->jenis_kelamin ? 'Laki-laki' : 'Perempuan') - : 'Jenis Kelamin'; - $jabatanLabel = isset($pengurus) ? $pengurus->nama_jabatan : 'Jabatan/Peran'; - $domisili = isset($pengurus) && $pengurus->desaKelurahan - ? $pengurus->desaKelurahan->nama . ', ' . $pengurus->desaKelurahan->kecamatan?->nama . ', ' . $pengurus->desaKelurahan->kecamatan?->kabupatenKota?->nama . ', ' . $pengurus->desaKelurahan->kecamatan?->kabupatenKota?->provinsi?->nama - : 'Desa/Kelurahan, Kecamatan, Kabupaten/Kota, Provinsi'; -@endphp - -
-
-
-
-
Profil Pengurus BUMDes
-
-
+
+
+
+
+
+
-
-
{{ isset($pengurus) ? $pengurus->nama : 'Tambah Pengurus BUMDes Baru' }}
- {{ $jenisKelamin }} - Jabatan: {{ $jabatanLabel }} -
-
- -
-
Domisili
-

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

-

{{ $domisili }}

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

Jenis Kelamin

-
{{ $jenisKelamin }}
-

- {{ isset($pengurus) ? ($pengurus->usia . ' tahun') : 'Belum terisi' }} -

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

Jabatan

-
{{ $jabatanLabel }}
-

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

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

Domisili

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

- {{ isset($pengurus) ? ($pengurus->desaKelurahan?->kecamatan?->nama ?? '-') : 'Belum terisi' }} -

-
- - - - - -
-
+ +
+
{{ $ringkasanProfil['judul'] }}
+

+ {{ $ringkasanProfil['lokasi'] }} +

+

Alamat: {{ $ringkasanProfil['alamat'] }}

-
- -
-
-
-

Alamat Domisili

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

{{ $domisili }}

+
+
+
+

Keaktifan

+ {{ $ringkasanProfil['keaktifan']['label'] }} +
+
+

Badan Hukum

+ {{ $ringkasanProfil['hukum']['label'] }} +
+
+

Unit Usaha

+ @if(count($ringkasanProfil['unit_usaha'])) +
+ @foreach(array_slice($ringkasanProfil['unit_usaha'], 0, 2) as $unit) + {{ $unit }} + @endforeach + @if(count($ringkasanProfil['unit_usaha']) > 2) + + @endif +
+ @if(count($ringkasanProfil['unit_usaha']) > 2) +
+
+ @foreach(array_slice($ringkasanProfil['unit_usaha'], 2) as $unit) + {{ $unit }} + @endforeach +
+
+ @endif + @else +

Belum terisi

+ @endif +
-
+ + diff --git a/resources/views/bumdes/profil/index.blade.php b/resources/views/bumdes/profil/index.blade.php index 97698c0..0a0b2f1 100644 --- a/resources/views/bumdes/profil/index.blade.php +++ b/resources/views/bumdes/profil/index.blade.php @@ -1,59 +1,132 @@ @extends('layout.main') + + @section('content') -
-
-
+
+ +
+
-
+
-
+
+
+
+
+
+
+ +
+
+ Wilayah +
+
+ +
+ {{ preg_replace('/^(kabupaten|kota)\s+/i', '', $selectedKabupaten?->nama ?? 'Semua Kabupaten/Kota') }} +
+
+
+ +
+
+ +
+
+ +
+ +
+
+ Data +
+
+ +
+
+ +
+
+
+ + +
+
+ + Reset +
+
+
+
+
+
+ +
-
Data Pengurus BUMDes
+
Data BUMDes
-
+ + @foreach($filters as $field => $value) + + @endforeach
-
-
- - -
+
Tambah @@ -62,16 +135,15 @@
- +
- - - - - - + + + + + @if($showEnumerator) @endif @@ -80,79 +152,55 @@ @forelse($pengurus as $item) - + @php($ringkasan = $ringkasanBumdes[$item->id]) + - - - - - - + + + + @if($showEnumerator) @endif @empty - + @endforelse
NoNamaJenis KelaminUsiaDesa/KelurahanJabatan/PeranLama MenjabatBUMDesKeaktifanBadan HukumUnit UsahaNama RespondenNama Enumerator
{{ ($pengurus->firstItem() ?? 0) + $loop->index }}{{ $item->nama }}{{ $item->jenis_kelamin ? 'Laki-laki' : 'Perempuan' }}{{ $item->usia }} tahun - - {{ $item->desaKelurahan?->nama }}, - {{ $item->desaKelurahan?->kecamatan?->nama }}, - {{ $item->desaKelurahan?->kecamatan?->kabupatenKota?->nama }}, - {{ $item->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi?->nama }} - + + {{ $ringkasan['judul'] }} + {{ $item->desaKelurahan?->kecamatan?->nama ?? 'Kecamatan belum terisi' }} {{ $item->nama_jabatan }}{{ $item->lama_menjabat }} + {{ $ringkasan['keaktifan']['label'] }} + + {{ $ringkasan['hukum']['label'] }} + + @if(count($ringkasan['unit_usaha'])) +
{{ implode(', ', array_slice($ringkasan['unit_usaha'], 0, 2)) }}
+ @if(count($ringkasan['unit_usaha']) > 2) + +{{ count($ringkasan['unit_usaha']) - 2 }} lainnya + @endif + @else + Belum terisi + @endif +
{{ $ringkasan['responden'] }}{{ $item->creator?->name ?? '-' }}
- - + + + @csrf + @method('DELETE') + +
Tidak ada data ditemukan.Tidak ada data ditemukan.
@endsection - -@push('scripts') - -@endpush diff --git a/resources/views/bumdes/rantai-nilai/index.blade.php b/resources/views/bumdes/rantai-nilai/index.blade.php new file mode 100644 index 0000000..c5d445e --- /dev/null +++ b/resources/views/bumdes/rantai-nilai/index.blade.php @@ -0,0 +1,449 @@ +@extends('layout.main') + +@php + $fieldValue = fn (string $field, $default = null) => old($field, data_get($kelembagaanAktor, $field, $default)); + $textValue = fn (string $field) => is_scalar($value = $fieldValue($field)) ? (string) $value : ''; + $moneyValue = function (string $field) use ($textValue) { + $value = $textValue($field); + return preg_match('/^\d+(\.00)?$/D', $value) ? preg_replace('/\B(?=(\d{3})+(?!\d))/', '.', explode('.', $value)[0]) : $value; + }; + $trading = in_array($fieldValue('melakukan_jual_beli'), [true, 1, '1'], true); + $sourcesSelected = (array) $fieldValue('sumber_pasokan', []); + $purchasedSelected = (array) $fieldValue('bentuk_produk_dibeli', []); + $channelsSelected = (array) $fieldValue('saluran_penjualan', []); + $soldSelected = (array) $fieldValue('bentuk_produk_dijual', []); + $relationshipsSelected = (array) $fieldValue('hubungan_petani_pemasok', []); + $notSold = in_array(\App\Enums\Bumdes\SaluranPenjualanBumdes::BELUM_MENJUAL->value, $channelsSelected, true); + $otherSource = $trading && in_array(\App\Enums\Bumdes\SumberPasokanBumdes::LAINNYA->value, $sourcesSelected, true); + $otherChannel = $trading && in_array(\App\Enums\Bumdes\SaluranPenjualanBumdes::LAINNYA->value, $channelsSelected, true); + $otherRelationship = $trading && in_array(\App\Enums\Bumdes\HubunganPemasokBumdes::LAINNYA->value, $relationshipsSelected, true); + $otherPayment = $trading && $fieldValue('mekanisme_pembayaran_petani') === \App\Enums\Bumdes\MekanismePembayaranBumdes::LAINNYA->value; + $otherPrice = $trading && $fieldValue('penetapan_harga_beli') === \App\Enums\Bumdes\PenetapanHargaBeliBumdes::LAINNYA->value; + $processing = in_array($fieldValue('melakukan_pengolahan_pascapanen'), [true, 1, '1'], true); + $sortingActive = $processing && in_array($fieldValue('status_sortasi'), [\App\Enums\Bumdes\StatusSortasiBumdes::RUTIN_STANDAR->value, \App\Enums\Bumdes\StatusSortasiBumdes::TIDAK_KONSISTEN->value], true); + $dryingActive = $processing && in_array($fieldValue('status_pengeringan'), [\App\Enums\Bumdes\StatusPengeringanBumdes::RUTIN->value, \App\Enums\Bumdes\StatusPengeringanBumdes::KADANG->value], true); + $fermentingActive = $processing && in_array($fieldValue('status_fermentasi'), [\App\Enums\Bumdes\StatusFermentasiBumdes::RUTIN->value, \App\Enums\Bumdes\StatusFermentasiBumdes::PERNAH_MENCOBA->value], true); + $fermentationSelected = (array) $fieldValue('model_fermentasi', []); +@endphp + +@section('content') +
+ @include('bumdes.profil.header', ['pengurus' => $pengurus]) + +
+ @csrf +
+
+
Saluran Pembelian/Sumber Pasokan
+
+ Kegiatan Jual Beli Komoditas* +
+
+ + +
+
+ + +
+
+ @error('melakukan_jual_beli')
{{ $message }}
@enderror +
+
!$trading]) data-rantai-section @disabled(!$trading)> +
+ Sumber Pasokan Komoditas +

Boleh pilih lebih dari satu.

+
+ @foreach($sumberPasokanOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('sumber_pasokan')
{{ $message }}
@enderror + @error('sumber_pasokan.*')
{{ $message }}
@enderror +
+
+ + + @error('sumber_pasokan_lainnya')
{{ $message }}
@enderror +
+
+
+
+ Bentuk Produk yang Dibeli +

Boleh pilih lebih dari satu.

+
+ @foreach($bentukProdukOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('bentuk_produk_dibeli')
{{ $message }}
@enderror + @error('bentuk_produk_dibeli.*')
{{ $message }}
@enderror +
+
+ Perkiraan Volume Pembelian Tiga Panen Terakhir +
+ @foreach($periodePanenOptions as $period => $label) +
+ +
+ + kg +
+ @error("volume_pembelian.$period")
{{ $message }}
@enderror +
+ @endforeach +
+ @error('volume_pembelian')
{{ $message }}
@enderror +
+
+ +
+
+
+
+
Saluran Penjualan Utama
+
!$trading]) data-rantai-section @disabled(!$trading)> +
+ Tujuan Penjualan +

Boleh pilih lebih dari satu.

+
+ @foreach($saluranPenjualanOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('saluran_penjualan')
{{ $message }}
@enderror + @error('saluran_penjualan.*')
{{ $message }}
@enderror +
+
+ + + @error('saluran_penjualan_lainnya')
{{ $message }}
@enderror +
+
+
+
+
+ Bentuk Produk yang Dijual +

Boleh pilih lebih dari satu.

+
+ @foreach($bentukProdukOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('bentuk_produk_dijual')
{{ $message }}
@enderror + @error('bentuk_produk_dijual.*')
{{ $message }}
@enderror +
+
+ Perkiraan Volume Penjualan Tiga Panen Terakhir +
+ @foreach($periodePanenOptions as $period => $label) +
+ +
+ + kg +
+ @error("volume_penjualan.$period")
{{ $message }}
@enderror +
+ @endforeach +
+ @error('volume_penjualan')
{{ $message }}
@enderror +
+
+
+ +
+
+
+
+
Sifat Hubungan dengan Petani Pemasok
+
!$trading]) data-rantai-section @disabled(!$trading)> +
+ Pola Hubungan dengan Petani Pemasok +

Boleh pilih lebih dari satu.

+
+ @foreach($hubunganPemasokOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('hubungan_petani_pemasok')
{{ $message }}
@enderror + @error('hubungan_petani_pemasok.*')
{{ $message }}
@enderror +
+
+ + + @error('hubungan_petani_pemasok_lainnya')
{{ $message }}
@enderror +
+
+
+
+ Cara Pembayaran kepada Petani +
+ @foreach($mekanismePembayaranOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('mekanisme_pembayaran_petani')
{{ $message }}
@enderror +
+
+
+ + + @error('mekanisme_pembayaran_lainnya')
{{ $message }}
@enderror +
+
+
+ Perjanjian Pembelian dengan Petani +
+ @foreach($perjanjianPetaniOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('perjanjian_petani')
{{ $message }}
@enderror +
+
+ +
+
+
+
+
Harga Beli BUMDes dari Petani
+
!$trading]) data-rantai-section @disabled(!$trading)> +

Harga Beli per Bentuk Produk (Tiga Panen Terakhir)

+

Isi harga per kg sesuai bentuk produk yang dibeli.

+ @error('harga_beli_petani')
{{ $message }}
@enderror +
+ + + + + @foreach($periodePanenOptions as $period => $label) + + @endforeach + + + + @foreach($bentukProdukOptions as $product => $productLabel) + + + @foreach($periodePanenOptions as $period => $periodLabel) + + @endforeach + + @endforeach + +
Bentuk Produk{{ $label }}
+ {{ $productLabel }} + @error("harga_beli_petani.$product")
{{ $message }}
@enderror +
+
+ Rp + + /kg +
+ @error("harga_beli_petani.$product.$period")
{{ $message }}
@enderror +
+
+
+ Dasar Penetapan Harga Beli +
+ @foreach($penetapanHargaBeliOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('penetapan_harga_beli')
{{ $message }}
@enderror +
+
+
+ + + @error('penetapan_harga_beli_lainnya')
{{ $message }}
@enderror +
+
+
+ +
+
+
+
+
Sortasi & Pemilihan Kualitas
+

Peluang Nilai Tambah

+
+ Kegiatan Pengolahan Pascapanen* +
+
+ + +
+
+ + +
+
+ @error('melakukan_pengolahan_pascapanen')
{{ $message }}
@enderror +
+
!$processing]) data-postharvest-section @disabled(!$processing)> +
+ Sortasi Hasil Panen Sebelum Dijual +
+ @foreach($statusSortasiOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('status_sortasi')
{{ $message }}
@enderror +
+
+
+ + + @error('kriteria_sortasi')
{{ $message }}
@enderror +
+
+
+ +
+
+
+
+
Praktik Pengeringan
+
!$processing]) data-postharvest-section @disabled(!$processing)> +
+ Pengeringan Hasil Panen +
+ @foreach($statusPengeringanOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('status_pengeringan')
{{ $message }}
@enderror +
+
+
+ + + @error('cara_tempat_pengeringan')
{{ $message }}
@enderror +
+
+
+ +
+
+
+
+
Praktik Fermentasi
+
!$processing]) data-postharvest-section @disabled(!$processing)> +
+ Fermentasi/Pengepresan Hasil Panen +
+ @foreach($statusFermentasiOptions as $value => $label) +
+
+ + +
+
+ @endforeach +
+ @error('status_fermentasi')
{{ $message }}
@enderror +
+
+
+ Model Fermentasi* +

Boleh pilih lebih dari satu.

+
+ @foreach($modelFermentasiOptions as $value => $label) +
+
+ first && count($fermentationSelected) === 0)> + +
+
+ @endforeach +
+ @error('model_fermentasi')
{{ $message }}
@enderror + @error('model_fermentasi.*')
{{ $message }}
@enderror +
+
+ + + @error('proses_fermentasi')
{{ $message }}
@enderror +
+
+
+ +
+
+
+
+
Besaran Premium Harga
+
!$processing]) data-postharvest-section @disabled(!$processing)> +
+ +

Dibandingkan hasil panen tanpa sortasi, pengeringan, atau fermentasi standar.

+
+
+
+ Rp + + /kg +
+ @error('premium_harga_pascapanen')
{{ $message }}
@enderror +
+
+
+
+ +
+
+
+
+@endsection diff --git a/resources/views/components/bumdes-alert.blade.php b/resources/views/components/bumdes-alert.blade.php new file mode 100644 index 0000000..91059e9 --- /dev/null +++ b/resources/views/components/bumdes-alert.blade.php @@ -0,0 +1,36 @@ +@php + $notificationTypes = [ + 'success' => 'success', 'error' => 'danger', 'warning' => 'warning', + 'info' => 'info', + ]; + $hasNotifications = $errors->any() || collect(array_keys($notificationTypes))->contains(fn ($key) => session()->has($key)); +@endphp + +@if($hasNotifications) +
+ @foreach($notificationTypes as $key => $type) + @if(session()->has($key)) + + @endif + @endforeach + @if($errors->any()) + + @endif +
+@endif + +@once + @push('scripts') + + @endpush +@endonce diff --git a/resources/views/components/bumdes-form-actions.blade.php b/resources/views/components/bumdes-form-actions.blade.php new file mode 100644 index 0000000..1ba2391 --- /dev/null +++ b/resources/views/components/bumdes-form-actions.blade.php @@ -0,0 +1,6 @@ +@props(['redirectFragment' => null]) + +
+ Kembali + +
diff --git a/routes/web.php b/routes/web.php index c89bbad..4984706 100644 --- a/routes/web.php +++ b/routes/web.php @@ -280,10 +280,15 @@ Route::middleware('auth') Route::resource('bantuan-penilaian', App\Http\Controllers\Web\Produktivitas\BantuanKelompokTaniController::class)->only(['index', 'store', 'destroy'])->parameters(['bantuan-penilaian' => 'bantuan']); }); - // BUMDES - Route::post('bumdes/{bumdes}/kelembagaan-aktor', [App\Http\Controllers\Web\Bumdes\PengurusBumdesController::class, 'storeKelembagaanAktor'])->name('bumdes.kelembagaan-aktor.store'); - Route::get('bumdes/{bumdes}/kelembagaan-aktor', [App\Http\Controllers\Web\Bumdes\PengurusBumdesController::class, 'kelembagaanAktor'])->name('bumdes.kelembagaan-aktor.index'); - Route::resource('bumdes', App\Http\Controllers\Web\Bumdes\PengurusBumdesController::class); + // BUMDes: profil dan tab memakai resource dengan parameter model yang sama. + Route::resource('bumdes.rantai-nilai', App\Http\Controllers\Web\Bumdes\RantaiNilaiBumdesController::class) + ->only(['index', 'store'])->parameters(['bumdes' => 'pengurusBumdes'])->shallow(); + Route::resource('bumdes.operasional', App\Http\Controllers\Web\Bumdes\OperasionalBumdesController::class) + ->only(['index', 'store'])->parameters(['bumdes' => 'pengurusBumdes'])->shallow(); + Route::resource('bumdes.kelembagaan-aktor', App\Http\Controllers\Web\Bumdes\KelembagaanAktorBumdesController::class) + ->only(['index', 'store'])->parameters(['bumdes' => 'pengurusBumdes'])->shallow(); + 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'); diff --git a/tests/Feature/BumdesProfileTest.php b/tests/Feature/BumdesProfileTest.php new file mode 100644 index 0000000..bec70ae --- /dev/null +++ b/tests/Feature/BumdesProfileTest.php @@ -0,0 +1,1512 @@ + null, + 'master_kabupaten_kota' => 'provinsi_id', + 'master_kecamatan' => 'kabupaten_kota_id', + 'master_desa_kelurahan' => 'kecamatan_id', + ] as $name => $parent) { + Schema::create($name, function (Blueprint $table) use ($parent) { + $table->id(); + $table->string('nama'); + $table->string('kode')->nullable(); + if ($parent) { + $table->integer($parent); + } + $table->softDeletes(); + }); + } + Schema::create('users', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->softDeletes(); + }); + Schema::create('activity_log', function (Blueprint $table) { + $table->id(); + $table->string('log_name')->nullable(); + $table->text('description'); + $table->nullableMorphs('subject'); + $table->string('event')->nullable(); + $table->nullableMorphs('causer'); + $table->json('properties')->nullable(); + $table->uuid('batch_uuid')->nullable(); + $table->timestamps(); + }); + Schema::create('bumdes_pengurus', function (Blueprint $table) { + $table->id(); + foreach (['uid', 'tempat_lahir', 'pendidikan_terakhir', 'alamat', 'nomor_telepon'] as $field) { + $table->string($field)->nullable(); + } + $table->date('tanggal_lahir')->nullable(); + $table->date('tanggal_mulai_menjabat')->nullable(); + $table->integer('lama_menjabat_tahun')->nullable(); + $table->integer('updated_by')->nullable(); + $table->integer('deleted_by')->nullable(); + $table->string('nama'); + $table->boolean('jenis_kelamin'); + $table->string('jabatan'); + $table->string('jabatan_lainnya')->nullable(); + $table->integer('desa_kelurahan_id'); + $table->integer('created_by'); + $table->timestamps(); + $table->softDeletes(); + }); + (require database_path('migrations/2026_08_20_000001_create_bumdes_kelembagaan_aktor_table.php'))->up(); + (require database_path('migrations/2026_11_04_000001_add_dukungan_ekosistem_to_bumdes_kelembagaan_aktor_table.php'))->up(); + (require database_path('migrations/2026_11_04_000002_add_pengelolaan_layanan_to_bumdes_kelembagaan_aktor_table.php'))->up(); + (require database_path('migrations/2026_11_04_000003_add_rantai_nilai_to_bumdes_kelembagaan_aktor_table.php'))->up(); + (require database_path('migrations/2026_11_05_000001_add_peluang_nilai_tambah_to_bumdes_kelembagaan_aktor_table.php'))->up(); + DB::table('users')->insert([ + ['id' => 7, 'name' => 'Admin Test'], ['id' => 8, 'name' => 'Enumerator'], + ]); + DB::table('master_provinsi')->insert(['id' => 1, 'nama' => 'NTT']); + DB::table('master_kabupaten_kota')->insert(['id' => 1, 'nama' => 'Kabupaten Ngada', 'provinsi_id' => 1]); + DB::table('master_kecamatan')->insert([ + ['id' => 1, 'nama' => 'Kecamatan A', 'kabupaten_kota_id' => 1], + ['id' => 2, 'nama' => 'Kecamatan B', 'kabupaten_kota_id' => 1], + ]); + DB::table('master_desa_kelurahan')->insert([ + ['id' => 1, 'nama' => 'Desa A', 'kecamatan_id' => 1], + ['id' => 2, 'nama' => 'Desa B', 'kecamatan_id' => 2], + ]); + foreach ([ + [1, 'Alpha', 1, 'direktur', 1, 7, null], + [2, 'Beta', 0, 'direktur_ketua', 1, 8, null], + [3, 'Citra', 0, 'sekretaris', 2, 7, null], + [4, 'Dihapus', 0, 'bendahara', 1, 7, '2026-01-01'], + ] as [$id, $nama, $gender, $jabatan, $desa, $creator, $deleted]) { + DB::table('bumdes_pengurus')->insert([ + 'tempat_lahir' => 'Ngada', 'tanggal_lahir' => '1990-01-01', 'pendidikan_terakhir' => '04_sma', + 'alamat' => 'Desa A', 'lama_menjabat_tahun' => 2, + 'id' => $id, 'nama' => $nama, 'jenis_kelamin' => $gender, 'jabatan' => $jabatan, + 'desa_kelurahan_id' => $desa, 'created_by' => $creator, + 'created_at' => "2026-01-0{$id}", 'deleted_at' => $deleted, + ]); + } + $user = new User(['name' => 'Admin Test', 'role' => 'admin']); + $user->id = 7; + $this->be($user); + Event::forget('composing: *'); + view()->share('errors', new ViewErrorBag); + $this->bumdesAction = function (string $action, ?Request $base = null, ?string $id = null) { + [$controllerClass, $method, $routeName] = match ($action) { + 'kelembagaan.index' => [KelembagaanAktorBumdesController::class, 'index', 'bumdes.kelembagaan-aktor.index'], + 'kelembagaan.store' => [KelembagaanAktorBumdesController::class, 'store', 'bumdes.kelembagaan-aktor.store'], + 'rantai-nilai.index' => [RantaiNilaiBumdesController::class, 'index', 'bumdes.rantai-nilai.index'], + 'rantai-nilai.store' => [RantaiNilaiBumdesController::class, 'store', 'bumdes.rantai-nilai.store'], + 'operasional.index' => [OperasionalBumdesController::class, 'index', 'bumdes.operasional.index'], + 'operasional.store' => [OperasionalBumdesController::class, 'store', 'bumdes.operasional.store'], + default => [PengurusBumdesController::class, $action, 'bumdes.'.$action], + }; + $base ??= Request::create('/bumdes'.($id ? '/'.$id : '')); + $base->setLaravelSession(app('session')->driver()); + $model = $id ? PengurusBumdes::findOrFail($id) : null; + $route = new Route(['GET', 'POST', 'PUT', 'DELETE'], 'bumdes/{pengurusBumdes?}', fn () => null); + $route->name($routeName)->bind(Request::create('/bumdes'.($id ? '/'.$id : ''))); + if ($model) { + $route->setParameter('pengurusBumdes', $model); + } + $requestClass = (new ReflectionMethod($controllerClass, $method))->getParameters()[0]->getType()->getName(); + $request = $requestClass::createFrom($base); + $request->setContainer($this->app)->setRedirector(app('redirect')); + $request->setUserResolver(fn () => auth()->user())->setRouteResolver(fn () => $route); + $this->app->instance('request', $request); + $request->validateResolved(); + $arguments = $model ? [$request, $model] : [$request]; + + return app($controllerClass)->$method(...$arguments); + }; + $this->bumdesList = function (array $filters = []) { + $request = Request::create('/bumdes', 'GET', $filters); + $request->setLaravelSession(app('session')->driver()); + $this->app->instance('request', $request); + + return ($this->bumdesAction)('index', $request); + }; + $this->profileInput = [ + 'nama' => 'Responden', 'jenis_kelamin' => '1', 'tempat_lahir' => 'Ngada', + 'tanggal_lahir' => '01-01-1990', 'pendidikan_terakhir' => '04_sma', + 'alamat' => 'Desa A', 'provinsi_id' => 1, 'kabupaten_id' => 1, 'kecamatan_id' => 1, + 'desa_kelurahan_id' => 1, 'jabatan' => 'sekretaris', 'lama_menjabat_tahun' => 2, + ]; +}); + +test('bumdes activity and location filters update rows and summaries', function () { + $data = ($this->bumdesList)()->getData(); + expect($data['totalBumdes'])->toBe(3)->and($data['totalBumdesAktif'])->toBe(0)->and($data['totalBerbadanHukum'])->toBe(0); + foreach ([ + [1, 'aktif_penuh', 'sudah_berbadan_hukum'], + [2, 'tidak_aktif_vakum', 'belum_berbadan_hukum'], + [3, 'aktif_terbatas', 'proses_pendaftaran'], + [4, 'aktif_penuh', 'sudah_berbadan_hukum'], + ] as [$id, $status, $hukum]) { + KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => $id, 'status_keaktifan' => $status, 'status_badan_hukum' => $hukum, + ]); + } + $data = ($this->bumdesList)()->getData(); + expect($data['totalBumdes'])->toBe(3)->and($data['totalBumdesAktif'])->toBe(2) + ->and($data['totalBerbadanHukum'])->toBe(1)->and($data['totalDesaKelurahan'])->toBe(2); + $data = ($this->bumdesList)(['status_keaktifan' => 'aktif_penuh'])->getData(); + expect($data['pengurus']->pluck('id')->all())->toBe([1]); + expect($data['totalBumdes'])->toBe(1)->and($data['totalDesaKelurahan'])->toBe(1) + ->and($data['totalBumdesAktif'])->toBe(1)->and($data['totalBerbadanHukum'])->toBe(1); + + $data = ($this->bumdesList)(['kecamatan_id' => 1, 'desa_kelurahan_id' => 1])->getData(); + expect($data['pengurus']->pluck('id')->all())->toBe([2, 1]); + expect($data['totalBumdes'])->toBe(2); + expect($data['desaOptions']->pluck('id')->all())->toBe([1]); + expect(($this->bumdesList)(['search' => 'aLpHa'])->getData()['pengurus']->pluck('id')->all())->toBe([1]); + expect(($this->bumdesList)(['search' => 'dEsA a'])->getData()['pengurus']->pluck('id')->all())->toBe([2, 1]); + KelembagaanAktorBumdes::where('pengurus_bumdes_id', 1)->firstOrFail()->delete(); + expect(($this->bumdesList)(['status_keaktifan' => 'aktif_penuh'])->getData()['pengurus']->count())->toBe(0); + $data = ($this->bumdesList)()->getData(); + expect($data['totalBumdesAktif'])->toBe(1)->and($data['totalBerbadanHukum'])->toBe(0); + $data = ($this->bumdesList)([ + 'jenis_kelamin' => '1', 'jabatan' => 'sekretaris', 'status_badan_hukum' => 'invalid', 'unit_usaha' => 'invalid', + ])->getData(); + expect($data['pengurus']->pluck('id')->all())->toBe([3, 2, 1]); + expect($data['filters'])->not->toHaveKeys(['jenis_kelamin', 'jabatan', 'status_badan_hukum', 'unit_usaha']); +}); + +test('bumdes search and summaries respect enumerator ownership', function () { + auth()->user()->role = 'enumerator'; + foreach ([1, 2] as $id) { + KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => $id, 'status_keaktifan' => 'aktif_penuh', 'status_badan_hukum' => 'sudah_berbadan_hukum', + ]); + } + $data = ($this->bumdesList)(['search' => 'Desa A', 'status_keaktifan' => 'aktif_penuh'])->getData(); + expect($data['pengurus']->pluck('id')->all())->toBe([1]); + expect($data['totalBumdes'])->toBe(1)->and($data['showEnumerator'])->toBeFalse() + ->and($data['totalBumdesAktif'])->toBe(1)->and($data['totalBerbadanHukum'])->toBe(1); +}); + +test('bumdes sorting and pagination preserve filters across all results', function () { + foreach (['nama_asc' => [2, 1, 3], 'nama_desc' => [3, 2, 1], 'unknown' => [3, 2, 1]] as $sort => $ids) { + expect(($this->bumdesList)(['sort_by' => $sort])->getData()['pengurus']->pluck('id')->all())->toBe($ids); + } + KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 2, 'status_keaktifan' => 'aktif_penuh']); + for ($i = 0; $i < 12; $i++) { + $id = DB::table('bumdes_pengurus')->insertGetId([ + 'nama' => "Tambahan {$i}", 'jenis_kelamin' => 0, 'jabatan' => 'sekretaris', + 'desa_kelurahan_id' => 1, 'created_by' => 7, + ]); + KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => $id, 'status_keaktifan' => 'aktif_penuh']); + } + $data = ($this->bumdesList)(['status_keaktifan' => 'aktif_penuh', 'sort_by' => 'nama_asc', 'per_page' => 10, 'page' => 2])->getData(); + expect($data['totalBumdes'])->toBe(13)->and($data['pengurus']->count())->toBe(3); + parse_str(parse_url($data['pengurus']->url(1), PHP_URL_QUERY), $params); + expect($params)->toMatchArray(['status_keaktifan' => 'aktif_penuh', 'sort_by' => 'nama_asc', 'per_page' => '10']); +}); + +test('bumdes filters reject malformed input', function (array $filters) { + expect(fn () => ($this->bumdesList)($filters))->toThrow(ValidationException::class); +})->with([ + [['status_keaktifan' => 'invalid']], [['kecamatan_id' => 999]], [['per_page' => 0]], [['search' => ['invalid']]], +]); + +test('bumdes list and respondent forms render successfully', function () { + KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'status_keaktifan' => 'aktif_penuh', + 'status_badan_hukum' => 'proses_pendaftaran', + 'unit_usaha' => ['simpan_pinjam', 'jasa_lainnya'], 'unit_usaha_jasa_lainnya' => 'Pengiriman hasil panen', + ]); + $view = ($this->bumdesList)(); + expect($view->getData()['pengurus']->every(fn ($item) => $item->relationLoaded('kelembagaanAktor')))->toBeTrue(); + $html = $view->render(); + expect($html)->toContain('bumdes-row', 'bumdes-filter-form', 'Semua Keaktifan', 'Data BUMDes', + 'BUMDes · Desa A', 'Aktif beroperasi penuh', 'Proses pendaftaran', 'Simpan pinjam', 'Pengiriman hasil panen', 'Belum terisi'); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($html); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + $overview = $xpath->query('//*[@id="bumdes-overview"]')->item(0); + expect($overview->textContent)->toContain('Total BUMDes', 'BUMDes Aktif', 'Berbadan Hukum', 'desa/kelurahan terdata') + ->not->toContain('Pengurus BUMDes', 'Jabatan/Peran', 'Laki-laki', 'Perempuan'); + expect($xpath->query('//form[@id="bumdes-filter-form"]//select[@name="status_badan_hukum" or @name="unit_usaha" or @name="jenis_kelamin" or @name="jabatan"]')->length)->toBe(0); + $header = $xpath->query('//table[contains(@class, "bumdes-list-table")]/thead')->item(0); + expect($header->textContent)->toContain('BUMDes', 'Keaktifan', 'Badan Hukum', 'Unit Usaha', 'Nama Responden') + ->not->toContain('Jenis Kelamin', 'Usia', 'Jabatan/Peran', 'Lama Menjabat'); + expect($xpath->query('//form[contains(@class, "bumdes-delete-form") and contains(@data-nama, "BUMDes")]')->length)->toBe(3); + + $controller = $this->bumdesAction; + foreach ([$controller('create'), $controller('edit', null, '1')] as $view) { + $html = $view->render(); + expect($html)->toContain('Data Responden', 'tabler-check me-2', '> Simpan') + ->not->toContain('Data Pribadi', 'Peran Kepengurusan BUMDes', 'Domisili dan Kontak', 'Edit Profil Pengurus BUMDes', 'Tambah Profil Pengurus BUMDes'); + } +}); + +test('bumdes institutional and operational forms display the correct fields and saved dropdown selections', function () { + KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'status_badan_hukum' => 'proses_pendaftaran', + 'unit_usaha' => ['simpan_pinjam', 'jasa_lainnya'], 'unit_usaha_jasa_lainnya' => 'Pengiriman', + 'status_ad_art' => 'ada_lengkap', 'tahun_ad_art' => 2025, + 'status_keaktifan' => 'tidak_aktif_vakum', 'alasan_tidak_aktif' => 'Kekurangan modal', + 'pengurus_inti' => [['jabatan' => 'sekretaris', 'nama' => 'Sekretaris Lama', 'aktif' => true]], + ]); + ($this->bumdesList)(); + $controller = $this->bumdesAction; + $html = $controller('kelembagaan.index', null, '1')->render(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($html); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + expect($xpath->query('//select[@name="status_badan_hukum"]/option[@selected]')->item(0)->getAttribute('value'))->toBe('proses_pendaftaran'); + expect($xpath->query('//input[@name="unit_usaha[]" and @type="checkbox" and @checked]')->length)->toBe(2); + expect($xpath->query('//select[@name="unit_usaha[]"]')->length)->toBe(0); + expect($xpath->query('//select[@name="status_unit_komoditas" or @name="status_ad_art"]')->length)->toBe(2); + expect($html)->toContain('Status Legalitas BUMDes', route('bumdes.operasional.index', 1)) + ->not->toContain('name="status_keaktifan"', 'Kepengurusan dan Staf', 'Modal dan Aset'); + + $html = $controller('operasional.index', null, '1')->render(); + expect($html)->toContain('Modal dan Aset', route('bumdes.operasional.store', 1)) + ->not->toContain('name="status_badan_hukum"', 'name="unit_usaha[]"', 'name="status_keaktifan"', 'Kepengurusan dan Staf'); + foreach ([$controller('edit', null, '1'), $controller('create')] as $view) { + $html = $view->render(); + expect($html)->toContain('Status Keaktifan BUMDes', 'Kepengurusan dan Staf', 'Daftar Pengurus', 'switch switch-lg', 'role="switch"') + ->not->toContain('Catat pengurus inti yang aktif menjabat saat ini', 'Modal dan Aset'); + } + expect($controller('edit', null, '1')->render())->toContain('Sekretaris Lama', 'Kekurangan modal'); +}); + +test('institutional conditional fields are required on the server', function (array $input, array $expectedErrors) { + $request = Request::create('/bumdes/1/kelembagaan-aktor', 'POST', $input); + $this->app->instance('request', $request); + try { + ($this->bumdesAction)('kelembagaan.store', $request, '1'); + $this->fail('Expected conditional validation errors.'); + } catch (ValidationException $exception) { + expect(array_keys($exception->errors()))->toBe($expectedErrors); + foreach ($exception->errors() as $messages) { + expect($messages[0])->toContain('wajib diisi.'); + } + } + expect(KelembagaanAktorBumdes::count())->toBe(0); +})->with([ + [['status_badan_hukum' => 'sudah_berbadan_hukum'], ['nomor_dasar_hukum', 'tahun_dasar_hukum']], + [['status_unit_komoditas' => 'ya'], ['nama_unit_komoditas']], + [['status_ad_art' => 'ada_lengkap'], ['tahun_ad_art']], + [[ + 'status_badan_hukum' => 'sudah_berbadan_hukum', 'status_unit_komoditas' => 'ya', 'status_ad_art' => 'ada_lengkap', + ], ['nomor_dasar_hukum', 'tahun_dasar_hukum', 'nama_unit_komoditas', 'tahun_ad_art']], +]); + +test('disabled institutional fields ignore submitted values and clear stale data', function () { + KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'status_badan_hukum' => 'sudah_berbadan_hukum', + 'nomor_dasar_hukum' => 'SK-01', 'tahun_dasar_hukum' => 2025, + 'status_unit_komoditas' => 'ya', 'nama_unit_komoditas' => 'Kopi', + 'status_ad_art' => 'ada_lengkap', 'tahun_ad_art' => 2025, + ]); + $request = Request::create('/bumdes/1/kelembagaan-aktor', 'POST', [ + 'status_badan_hukum' => 'proses_pendaftaran', 'nomor_dasar_hukum' => ['invalid'], 'tahun_dasar_hukum' => 'invalid', + 'status_unit_komoditas' => 'direncanakan', 'nama_unit_komoditas' => ['invalid'], + 'status_ad_art' => 'tidak_ada', 'tahun_ad_art' => 'invalid', + ]); + $this->app->instance('request', $request); + ($this->bumdesAction)('kelembagaan.store', $request, '1'); + $data = KelembagaanAktorBumdes::firstOrFail(); + expect($data->nomor_dasar_hukum)->toBeNull()->and($data->tahun_dasar_hukum)->toBeNull() + ->and($data->nama_unit_komoditas)->toBeNull()->and($data->tahun_ad_art)->toBeNull(); +}); + +test('institutional dependent inputs stay visible and have matching disabled required and asterisk states', function (bool $active) { + KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, + 'status_badan_hukum' => $active ? 'sudah_berbadan_hukum' : 'proses_pendaftaran', + 'status_unit_komoditas' => $active ? 'ya' : 'direncanakan', + 'status_ad_art' => $active ? 'ada_lengkap' : 'tidak_ada', + ]); + ($this->bumdesList)(); + $html = ($this->bumdesAction)('kelembagaan.index', null, '1')->render(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($html); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + expect($xpath->query('//form[@id="kelembagaanAktorForm" and @novalidate]')->length)->toBe(1); + foreach (['nomor-dasar-hukum', 'tahun-dasar-hukum', 'nama-unit-komoditas', 'tahun-ad-art'] as $id) { + $input = $xpath->query('//input[@id="'.$id.'"]')->item(0); + expect($input->hasAttribute('disabled'))->toBe(! $active) + ->and($input->hasAttribute('required'))->toBe($active); + $label = $xpath->query('//span[@data-required-for="'.$id.'"]')->item(0); + expect(str_contains($label->getAttribute('class'), 'd-none'))->toBe(! $active); + for ($node = $input; $node instanceof DOMElement; $node = $node->parentNode) { + expect(str_contains($node->getAttribute('class'), 'd-none'))->toBeFalse(); + } + } +})->with([true, false]); + +test('bumdes notifications collect required errors above the cards and preserve old profile locations', function () { + $request = Request::create('/bumdes', 'POST'); + $this->app->instance('request', $request); + try { + ($this->bumdesAction)('store', $request); + $this->fail('Expected profile validation errors.'); + } catch (ValidationException $exception) { + expect($exception->errors())->toHaveKeys(['nama', 'provinsi_id', 'kabupaten_id', 'kecamatan_id', 'desa_kelurahan_id']); + $errors = (new ViewErrorBag)->put('default', new MessageBag($exception->errors())); + view()->share('errors', $errors); + } + session()->flashInput(['provinsi_id' => 1, 'kabupaten_id' => 1, 'kecamatan_id' => 1, 'desa_kelurahan_id' => 1]); + ($this->bumdesList)(); + $controller = $this->bumdesAction; + foreach ([$controller('create'), $controller('kelembagaan.index', null, '1'), $controller('operasional.index', null, '1')] as $view) { + $html = $view->render(); + expect(substr_count($html, 'id="bumdes-notifications"'))->toBe(1) + ->and(strpos($html, 'id="bumdes-notifications"'))->toBeLessThan(strpos($html, 'id="bumdes-profile-summary"')); + expect($html)->toContain('Nama harus diisi.', 'Provinsi wajib dipilih.', 'alert-danger alert-dismissible'); + } + $data = $controller('create')->getData(); + expect($data['kabupatenOptions']->pluck('id')->all())->toBe([1]) + ->and($data['kecamatanOptions']->pluck('id')->all())->toBe([1, 2]) + ->and($data['desaOptions']->pluck('id')->all())->toBe([1]); +}); + +test('bumdes creation saves profile status staff and active or inactive board members', function () { + $request = Request::create('/bumdes', 'POST', [...$this->profileInput, + 'redirect_fragment' => 'section-kepengurusan', + 'status_keaktifan' => 'tidak_aktif_vakum', 'alasan_tidak_aktif' => 'Perbaikan kantor', + 'pengurus_inti' => [ + ['jabatan' => 'sekretaris', 'nama' => 'Budi', 'aktif' => '1'], + ['jabatan' => 'bendahara', 'nama' => 'Siti', 'aktif' => '0'], + ['jabatan' => 'penasihat_kepala_desa', 'nama' => '', 'aktif' => '0'], + ], + 'memiliki_staf' => '1', 'jumlah_staf' => 2, 'unit_usaha_staf' => 'Toko', + ]); + $this->app->instance('request', $request); + $response = ($this->bumdesAction)('store', $request); + $data = KelembagaanAktorBumdes::firstOrFail(); + expect($response->getTargetUrl())->toBe(route('bumdes.edit', $data->pengurus_bumdes_id).'#section-kepengurusan') + ->and($data->status_keaktifan?->value)->toBe('tidak_aktif_vakum') + ->and($data->alasan_tidak_aktif)->toBe('Perbaikan kantor') + ->and($data->jumlah_staf)->toBe(2) + ->and(count($data->pengurus_inti))->toBe(2) + ->and($data->pengurus_inti[0]['aktif'])->toBe('1') + ->and($data->pengurus_inti[1]['aktif'])->toBe('0'); +}); + +test('profile update redirects the upper save to the next card and ignores unknown targets', function (?string $fragment, string $expectedFragment) { + $request = Request::create('/bumdes/1', 'PUT', [...$this->profileInput, 'redirect_fragment' => $fragment]); + $this->app->instance('request', $request); + $response = ($this->bumdesAction)('update', $request, '1'); + + expect($response->getTargetUrl())->toBe(route('bumdes.edit', 1).$expectedFragment) + ->and(session('success'))->toBe('Data Profil berhasil disimpan.'); +})->with([ + ['section-kepengurusan', '#section-kepengurusan'], + [null, ''], + ['https://example.test', ''], +]); + +test('invalid moved profile fields prevent saving respondent changes', function () { + $request = Request::create('/bumdes/1', 'PUT', [...$this->profileInput, + 'status_keaktifan' => 'invalid', 'jumlah_staf' => -1, + 'pengurus_inti' => [['jabatan' => 'sekretaris', 'nama' => 'Budi', 'aktif' => 'invalid']], + ]); + $this->app->instance('request', $request); + try { + ($this->bumdesAction)('update', $request, '1'); + $this->fail('Expected profile validation errors.'); + } catch (ValidationException $exception) { + expect($exception->errors())->toHaveKeys(['status_keaktifan', 'jumlah_staf', 'pengurus_inti.0.aktif']); + } + expect(DB::table('bumdes_pengurus')->where('id', 1)->value('nama'))->toBe('Alpha') + ->and(KelembagaanAktorBumdes::count())->toBe(0); +}); + +test('capital addition details are optional when the answer is yes', function (?array $rows, int $expectedRows) { + $request = Request::create('/bumdes/1/operasional', 'POST', [ + 'pernah_penambahan_modal' => '1', 'penambahan_modal' => $rows, + ]); + $response = ($this->bumdesAction)('operasional.store', $request, '1'); + expect($response->getTargetUrl())->toBe(route('bumdes.operasional.index', 1)); + expect(KelembagaanAktorBumdes::firstOrFail()->penambahan_modal)->toHaveCount($expectedRows); +})->with([ + [null, 0], [[], 0], [[['tahun' => '', 'jumlah' => '']], 0], + [[['tahun' => 2025, 'jumlah' => '']], 1], + [[['tahun' => '', 'jumlah' => '1.000.000']], 1], +]); + +test('capital addition fields follow the selected answer without becoming required', function (bool $active) { + KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1, 'pernah_penambahan_modal' => $active]); + ($this->bumdesList)(); + $html = ($this->bumdesAction)('operasional.index', null, '1')->render(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($html); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + $inputs = $xpath->query('//div[@id="penambahan-modal-wrapper"]//input'); + expect($inputs->length)->toBe(6); + foreach ($inputs as $input) { + expect($input->hasAttribute('disabled'))->toBe(! $active) + ->and($input->hasAttribute('required'))->toBeFalse(); + } + expect($xpath->query('//span[contains(@class, "penambahan-modal-required")]')->length)->toBe(0); +})->with([true, false]); + +test('capital additions are ignored when the answer is no', function () { + $request = Request::create('/bumdes/1/operasional', 'POST', [ + 'pernah_penambahan_modal' => '0', 'penambahan_modal' => [['tahun' => '', 'jumlah' => '']], + ]); + $this->app->instance('request', $request); + ($this->bumdesAction)('operasional.store', $request, '1'); + expect(KelembagaanAktorBumdes::firstOrFail()->penambahan_modal)->toBeNull(); +}); + +test('bumdes deletion records the actor and soft deletes the dependent section data', function () { + $child = KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1]); + $response = ($this->bumdesAction)('destroy', null, '1'); + + expect($response->getTargetUrl())->toBe(route('bumdes.index')); + $parent = PengurusBumdes::withTrashed()->findOrFail(1); + $child = KelembagaanAktorBumdes::withTrashed()->findOrFail($child->id); + expect($parent->trashed())->toBeTrue()->and((int) $parent->deleted_by)->toBe(7) + ->and($child->trashed())->toBeTrue()->and((int) $child->deleted_by)->toBe(7) + ->and(DB::table('master_desa_kelurahan')->where('id', 1)->value('deleted_at'))->toBeNull(); + foreach ([$parent, $child] as $model) { + $activity = DB::table('activity_log')->where('subject_type', $model::class) + ->where('subject_id', $model->id)->where('event', 'deleted')->first(); + expect($activity)->not->toBeNull(); + expect((int) $activity->causer_id)->toBe(7); + } +}); + +test('enumerators cannot read change or delete another enumerators bumdes', function (string $action) { + auth()->user()->role = 'enumerator'; + try { + ($this->bumdesAction)($action, Request::create('/bumdes/2', 'POST', $this->profileInput), '2'); + $this->fail('Expected an ownership restriction.'); + } catch (HttpException $exception) { + expect($exception->getStatusCode())->toBe(404); + } + expect(DB::table('bumdes_pengurus')->where('id', 2)->value('nama'))->toBe('Beta') + ->and(KelembagaanAktorBumdes::count())->toBe(0); +})->with(['edit', 'update', 'destroy', 'kelembagaan.index', 'kelembagaan.store', 'operasional.index', 'operasional.store', 'rantai-nilai.index', 'rantai-nilai.store']); + +test('enumerators retain access to their own bumdes through the web routes', function () { + auth()->user()->role = 'enumerator'; + $this->get(route('bumdes.index'))->assertOk(); + $this->get(route('bumdes.edit', 1))->assertOk(); + $this->get(route('bumdes.kelembagaan-aktor.index', 1))->assertOk(); + $this->get(route('bumdes.operasional.index', 1))->assertOk(); + $this->get(route('bumdes.rantai-nilai.index', 1))->assertOk(); + $this->get(route('bumdes.edit', 2))->assertNotFound(); +}); + +test('workshop facilitators and coordinators do not gain bumdes survey access', function (string $role) { + auth()->user()->role = $role; + $this->get(route('bumdes.index'))->assertForbidden(); + $this->put(route('bumdes.update', 1), $this->profileInput)->assertForbidden(); + expect(DB::table('bumdes_pengurus')->where('id', 1)->value('nama'))->toBe('Alpha'); +})->with(['fasilitator', 'koordinator']); + +test('incomplete profiles keep secondary tabs locked on the server', function () { + DB::table('bumdes_pengurus')->where('id', 1)->update(['alamat' => null]); + try { + ($this->bumdesAction)('operasional.index', null, '1'); + $this->fail('Expected an incomplete-profile restriction.'); + } catch (HttpException $exception) { + expect($exception->getStatusCode())->toBe(403); + } +}); + +test('profile creation rolls back if the dependent section cannot be saved', function () { + Event::listen('eloquent.creating: '.KelembagaanAktorBumdes::class, function () { + throw new RuntimeException('Failure during section persistence'); + }); + $request = Request::create('/bumdes', 'POST', $this->profileInput); + expect(fn () => ($this->bumdesAction)('store', $request))->toThrow(RuntimeException::class); + expect(DB::table('bumdes_pengurus')->count())->toBe(4)->and(KelembagaanAktorBumdes::count())->toBe(0); +}); + +test('bumdes resource routes support the complete web workflow with model binding', function () { + $this->get(route('bumdes.index'))->assertOk(); + $this->get(route('bumdes.create'))->assertOk(); + $this->post(route('bumdes.store'), [...$this->profileInput, 'redirect_fragment' => 'section-kepengurusan']) + ->assertRedirect(); + $pengurus = PengurusBumdes::latest('id')->firstOrFail(); + $this->get(route('bumdes.show', $pengurus))->assertRedirect(route('bumdes.edit', $pengurus)); + $this->get(route('bumdes.edit', $pengurus))->assertOk(); + $this->post(route('bumdes.kelembagaan-aktor.store', $pengurus), [ + 'status_badan_hukum' => 'sudah_berbadan_hukum', 'nomor_dasar_hukum' => 'SK-02', 'tahun_dasar_hukum' => 2025, + 'unit_usaha' => ['simpan_pinjam'], + ])->assertRedirect(route('bumdes.kelembagaan-aktor.index', $pengurus)); + $this->get(route('bumdes.kelembagaan-aktor.index', $pengurus))->assertOk(); + $this->post(route('bumdes.operasional.store', $pengurus), ['modal_awal' => '1.000.000']) + ->assertRedirect(route('bumdes.operasional.index', $pengurus)); + $this->get(route('bumdes.operasional.index', $pengurus))->assertOk(); + $this->put(route('bumdes.update', $pengurus), [...$this->profileInput, 'nama' => 'Nama Baru']) + ->assertRedirect(route('bumdes.edit', $pengurus)); + expect($pengurus->refresh()->nama)->toBe('Nama Baru'); + $this->delete(route('bumdes.destroy', $pengurus))->assertRedirect(route('bumdes.index')); + $this->get(route('bumdes.edit', $pengurus))->assertNotFound(); + expect(KelembagaanAktorBumdes::withTrashed()->where('pengurus_bumdes_id', $pengurus->id)->firstOrFail()->trashed())->toBeTrue(); +}); + +test('web request validation keeps invalid optional capital values above the form', function () { + $url = route('bumdes.operasional.index', 1); + $this->from($url)->post(route('bumdes.operasional.store', 1), [ + 'pernah_penambahan_modal' => '1', 'penambahan_modal' => [['tahun' => 'invalid', 'jumlah' => ['invalid']]], + ])->assertRedirect($url)->assertSessionHasErrors(['penambahan_modal.0.tahun', 'penambahan_modal.0.jumlah']); + $this->get($url)->assertOk()->assertSee('Tahun penambahan modal pada baris 1 harus berupa angka.') + ->assertSee('data-has-errors="1"', false); +}); + +test('productive assets use per-row availability ownership and condition selects with legacy defaults', function () { + KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'status_kepemilikan_aset' => 'milik_bumdes_sendiri', + 'aset_produktif' => [['aset' => 'bangunan_kantor', 'dipilih' => true, 'jumlah' => '1', 'kondisi' => 'Baik']], + ]); + ($this->bumdesList)(); + $html = ($this->bumdesAction)('operasional.index', null, '1')->render(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($html); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + expect($html)->toContain('Aset Produktif Milik BUMDes', 'Ada/Tidak', 'Status Kepemilikan'); + expect($xpath->query('//div[@id="bumdes-aset-produktif"]//input[@type="radio"]')->length)->toBe(12); + foreach (['bangunan_kantor' => true, 'gudang_penyimpanan' => false] as $asset => $active) { + $selects = $xpath->query('//tr[@data-bumdes-aset="'.$asset.'"]//select'); + expect($selects->length)->toBe(2); + foreach ($selects as $select) { + expect($select->hasAttribute('disabled'))->toBe(! $active) + ->and($select->hasAttribute('required'))->toBe($active); + } + } + expect($xpath->query('//tr[@data-bumdes-aset="bangunan_kantor"]//select[contains(@name, "status_kepemilikan_aset")]/option[@selected]')->item(0)->getAttribute('value'))->toBe('milik_bumdes_sendiri'); + expect($xpath->query('//tr[@data-bumdes-aset="bangunan_kantor"]//select[contains(@name, "kondisi")]/option[@selected]')->item(0)->getAttribute('value'))->toBe('berfungsi'); + expect($xpath->query('//input[@name="status_kepemilikan_aset"]')->length)->toBe(0); + expect($xpath->query('//select[starts-with(@name, "infrastruktur_pascapanen") and contains(@name, "kondisi")]')->length)->toBe(6); +}); + +test('productive assets save different ownership and condition for each available asset', function () { + $request = Request::create('/bumdes/1/operasional', 'POST', ['aset_produktif' => [ + ['aset' => 'bangunan_kantor', 'dipilih' => '1', 'jumlah' => '2', 'status_kepemilikan_aset' => 'milik_bumdes_sendiri', 'kondisi' => 'berfungsi'], + ['aset' => 'gudang_penyimpanan', 'dipilih' => '1', 'jumlah' => '1', 'status_kepemilikan_aset' => 'sewa_pinjam_pihak_lain', 'kondisi' => 'perlu_perbaikan_evaluasi'], + ['aset' => 'lahan_usaha', 'dipilih' => '0', 'jumlah' => ['invalid'], 'status_kepemilikan_aset' => 'invalid', 'kondisi' => ['invalid']], + ]]); + ($this->bumdesAction)('operasional.store', $request, '1'); + $assets = KelembagaanAktorBumdes::firstOrFail()->aset_produktif; + expect($assets)->toHaveCount(2) + ->and($assets[0]['status_kepemilikan_aset'])->toBe('milik_bumdes_sendiri') + ->and($assets[1]['status_kepemilikan_aset'])->toBe('sewa_pinjam_pihak_lain') + ->and($assets[0]['kondisi'])->toBe('berfungsi') + ->and($assets[1]['kondisi'])->toBe('perlu_perbaikan_evaluasi') + ->and($assets[0]['jumlah'])->toBe('2'); +}); + +test('available productive assets validate their required details and choice values', function (array $details, string $error) { + $request = Request::create('/bumdes/1/operasional', 'POST', ['aset_produktif' => [ + ['aset' => 'bangunan_kantor', 'dipilih' => '1', ...$details], + ]]); + try { + ($this->bumdesAction)('operasional.store', $request, '1'); + $this->fail('Expected productive asset validation.'); + } catch (ValidationException $exception) { + expect($exception->errors())->toHaveKey('aset_produktif.0.'.$error); + } + expect(KelembagaanAktorBumdes::count())->toBe(0); +})->with([ + [['kondisi' => 'berfungsi'], 'status_kepemilikan_aset'], + [['status_kepemilikan_aset' => 'milik_bumdes_sendiri'], 'kondisi'], + [['status_kepemilikan_aset' => 'invalid', 'kondisi' => 'berfungsi'], 'status_kepemilikan_aset'], + [['status_kepemilikan_aset' => 'milik_bumdes_sendiri', 'kondisi' => 'invalid'], 'kondisi'], +]); + +test('legacy productive asset conditions and global ownership remain available without data loss', function () { + $data = KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'status_kepemilikan_aset' => 'milik_desa_dipinjamkan_dikelola', + 'aset_produktif' => [['aset' => 'bangunan_kantor', 'dipilih' => true, 'kondisi' => 'Perlu penggantian mesin']], + ]); + $request = Request::create('/bumdes/1/operasional', 'POST', ['aset_produktif' => [ + ['aset' => 'bangunan_kantor', 'dipilih' => '1', 'kondisi' => 'Perlu penggantian mesin'], + ]]); + ($this->bumdesAction)('operasional.store', $request, '1'); + $data->refresh(); + expect($data->aset_produktif[0]['kondisi'])->toBe('Perlu penggantian mesin') + ->and($data->aset_produktif[0]['status_kepemilikan_aset'])->toBe('milik_desa_dipinjamkan_dikelola') + ->and($data->status_kepemilikan_aset?->value)->toBe('milik_desa_dipinjamkan_dikelola'); + $request = Request::create('/bumdes/1/operasional', 'POST', ['aset_produktif' => [ + ['aset' => 'bangunan_kantor', 'dipilih' => '0'], + ]]); + ($this->bumdesAction)('operasional.store', $request, '1'); + expect($data->refresh()->aset_produktif)->toBe([]); +}); + +test('postharvest infrastructure uses availability radios and gated numeric fields like productive assets', function () { + KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, + 'infrastruktur_pascapanen' => [['infrastruktur' => 'timbangan', 'dipilih' => true, 'jumlah' => '2', 'kondisi' => 'baik_berfungsi']], + ]); + ($this->bumdesList)(); + $html = ($this->bumdesAction)('operasional.index', null, '1')->render(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($html); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + expect($xpath->query('//div[@id="bumdes-infrastruktur-pascapanen"]//input[@type="radio"]')->length)->toBe(12) + ->and($xpath->query('//div[@id="bumdes-infrastruktur-pascapanen"]//input[@type="checkbox"]')->length)->toBe(0); + foreach (['timbangan' => true, 'lainnya' => false] as $key => $active) { + $row = '//tr[@data-bumdes-infrastruktur="'.$key.'"]'; + $quantity = $xpath->query($row.'//input[contains(@name, "[jumlah]")]')->item(0); + $condition = $xpath->query($row.'//select')->item(0); + expect($quantity->getAttribute('type'))->toBe('number') + ->and($quantity->getAttribute('min'))->toBe('0') + ->and($quantity->hasAttribute('disabled'))->toBe(! $active) + ->and($quantity->hasAttribute('required'))->toBeFalse() + ->and($condition->hasAttribute('disabled'))->toBe(! $active) + ->and($condition->hasAttribute('required'))->toBe($active); + } + expect($xpath->query('//tr[@data-bumdes-infrastruktur="timbangan"]//input[contains(@name, "[jumlah]")]')->item(0)->getAttribute('value'))->toBe('2') + ->and($xpath->query('//tr[@data-bumdes-infrastruktur="timbangan"]//option[@selected]')->item(0)->getAttribute('value'))->toBe('baik_berfungsi') + ->and($xpath->query('//div[@id="bumdes-aset-produktif"]//input[@type="number" and @min="0"]')->length)->toBe(6); +}); + +test('postharvest infrastructure saves available rows and removes unavailable stale details', function () { + $url = route('bumdes.operasional.store', 1); + $this->post($url, [ + 'infrastruktur_pascapanen' => [ + ['infrastruktur' => 'timbangan', 'dipilih' => '1', 'jumlah' => '0', 'kondisi' => 'baik_berfungsi'], + ['infrastruktur' => 'lainnya', 'dipilih' => '1', 'jumlah' => '3', 'kondisi' => 'rusak_tidak_terpakai'], + ['infrastruktur' => 'alat_sortasi', 'dipilih' => '0', 'jumlah' => ['invalid'], 'kondisi' => ['invalid']], + ], + 'infrastruktur_pascapanen_lainnya' => 'Meja pengering', + ])->assertSessionHasNoErrors()->assertRedirect(route('bumdes.operasional.index', 1)); + $data = KelembagaanAktorBumdes::firstOrFail(); + expect($data->infrastruktur_pascapanen)->toHaveCount(2) + ->and($data->infrastruktur_pascapanen[0]['jumlah'])->toBe('0') + ->and($data->infrastruktur_pascapanen[1]['kondisi'])->toBe('rusak_tidak_terpakai') + ->and($data->infrastruktur_pascapanen_lainnya)->toBe('Meja pengering'); + $this->post($url, [ + 'infrastruktur_pascapanen' => [['infrastruktur' => 'lainnya', 'dipilih' => '0', 'jumlah' => '-1', 'kondisi' => 'invalid']], + 'infrastruktur_pascapanen_lainnya' => 'Meja pengering', + ])->assertSessionHasNoErrors(); + expect($data->refresh()->infrastruktur_pascapanen)->toBe([]) + ->and($data->infrastruktur_pascapanen_lainnya)->toBeNull(); +}); + +test('available infrastructure validates required details and condition choices', function (array $row, string $error) { + $this->post(route('bumdes.operasional.store', 1), ['infrastruktur_pascapanen' => [$row]]) + ->assertSessionHasErrors($error); + expect(KelembagaanAktorBumdes::count())->toBe(0); +})->with([ + [['infrastruktur' => 'timbangan', 'dipilih' => '1'], 'infrastruktur_pascapanen.0.kondisi'], + [['infrastruktur' => 'timbangan', 'dipilih' => '1', 'kondisi' => 'invalid'], 'infrastruktur_pascapanen.0.kondisi'], + [['infrastruktur' => 'lainnya', 'dipilih' => '1', 'kondisi' => 'baik_berfungsi'], 'infrastruktur_pascapanen_lainnya'], +]); + +test('inventory quantities reject negative fractional and nonnumeric values', function (string $field, string $quantity) { + $row = $field === 'aset_produktif' + ? ['aset' => 'bangunan_kantor', 'status_kepemilikan_aset' => 'milik_bumdes_sendiri', 'kondisi' => 'berfungsi'] + : ['infrastruktur' => 'timbangan', 'kondisi' => 'baik_berfungsi']; + $this->post(route('bumdes.operasional.store', 1), [$field => [[...$row, 'dipilih' => '1', 'jumlah' => $quantity]]]) + ->assertSessionHasErrors($field.'.0.jumlah'); + expect(KelembagaanAktorBumdes::count())->toBe(0); +})->with(['aset_produktif', 'infrastruktur_pascapanen'])->with(['-1', '1.5', 'dua']); + +test('saving each bumdes tab preserves the other tabs data and redirects to the saved tab', function () { + $controller = $this->bumdesAction; + $save = function (string $method, array $input) use ($controller) { + $request = Request::create('/bumdes/1', 'POST', $input); + $request->setLaravelSession(app('session')->driver()); + $this->app->instance('request', $request); + + return $controller($method, $request, '1'); + }; + $save('update', [...$this->profileInput, + 'status_keaktifan' => 'tidak_aktif_vakum', 'alasan_tidak_aktif' => 'Kekurangan modal', + 'memiliki_staf' => '1', 'jumlah_staf' => 3, 'unit_usaha_staf' => 'Toko', + ]); + $response = $save('operasional.store', [ + 'status_keaktifan' => 'tidak_aktif_vakum', 'alasan_tidak_aktif' => 'Kekurangan modal', + 'memiliki_staf' => '1', 'jumlah_staf' => 3, 'unit_usaha_staf' => 'Toko', + 'modal_awal' => '10.000.000', 'tahun_modal_awal' => 2024, + 'pernah_penambahan_modal' => '1', 'penambahan_modal' => [['tahun' => 2025, 'jumlah' => '1.000.000']], + ]); + expect($response->getTargetUrl())->toBe(route('bumdes.operasional.index', 1)); + $response = $save('kelembagaan.store', [ + 'status_badan_hukum' => 'sudah_berbadan_hukum', 'nomor_dasar_hukum' => 'SK-01', 'tahun_dasar_hukum' => 2024, + 'unit_usaha' => ['simpan_pinjam', 'jasa_lainnya'], 'unit_usaha_jasa_lainnya' => 'Pengiriman', + 'status_unit_komoditas' => 'ya', 'nama_unit_komoditas' => 'Kopi', + 'status_ad_art' => 'ada_lengkap', 'tahun_ad_art' => 2025, + 'status_keaktifan' => 'aktif_penuh', + ]); + expect($response->getTargetUrl())->toBe(route('bumdes.kelembagaan-aktor.index', 1)); + $data = KelembagaanAktorBumdes::firstOrFail(); + expect($data->status_keaktifan?->value)->toBe('tidak_aktif_vakum') + ->and($data->alasan_tidak_aktif)->toBe('Kekurangan modal') + ->and($data->jumlah_staf)->toBe(3) + ->and($data->modal_awal)->toBe('10000000.00') + ->and($data->penambahan_modal[0]['jumlah'])->toBe('1000000'); + $save('operasional.store', ['status_keaktifan' => 'aktif_penuh', 'status_badan_hukum' => 'tidak_tahu']); + expect(KelembagaanAktorBumdes::firstOrFail()->status_keaktifan?->value)->toBe('tidak_aktif_vakum'); + $save('update', [...$this->profileInput, 'status_keaktifan' => 'aktif_penuh', 'status_badan_hukum' => 'tidak_tahu']); + $data->refresh(); + expect($data->status_badan_hukum?->value)->toBe('sudah_berbadan_hukum') + ->and($data->nomor_dasar_hukum)->toBe('SK-01') + ->and($data->unit_usaha->map(fn ($value) => $value->value)->all())->toBe(['simpan_pinjam', 'jasa_lainnya']) + ->and($data->nama_unit_komoditas)->toBe('Kopi') + ->and($data->tahun_ad_art)->toBe(2025) + ->and($data->alasan_tidak_aktif)->toBeNull(); + $save('kelembagaan.store', ['unit_usaha' => [], 'status_ad_art' => 'tidak_ada', 'status_unit_komoditas' => 'direncanakan']); + $data->refresh(); + expect($data->unit_usaha->map(fn ($value) => $value->value)->all())->toBe([]) + ->and($data->unit_usaha_jasa_lainnya)->toBeNull() + ->and($data->nama_unit_komoditas)->toBeNull() + ->and($data->tahun_ad_art)->toBeNull() + ->and($data->status_keaktifan?->value)->toBe('aktif_penuh'); +}); + +test('ecosystem support is included in the operational form without question numbers or a separate tab', function () { + $data = KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'ada_alokasi_desa' => false, + 'dukungan_desa' => ['pendampingan_teknis', 'lainnya'], 'dukungan_desa_lainnya' => 'Pelatihan pemasaran', + 'keterlibatan_koordinasi_desa' => 'kadang', + 'kemitraan_eksternal' => ['ngo', 'lainnya'], 'kemitraan_eksternal_lainnya' => 'Perguruan tinggi', + ]); + $response = $this->get(route('bumdes.operasional.index', 1))->assertOk(); + $response->assertSee('Dukungan Ekosistem')->assertSee('Alokasi APBDes/Dana Desa Tahun Berjalan') + ->assertSee('Dukungan Lain dari Pemerintah Desa') + ->assertSee('Keterlibatan dalam Rapat Desa Terkait Komoditas') + ->assertSee('Kemitraan Eksternal'); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($response->getContent()); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + expect($xpath->query('//input[@id="alokasi-desa-tidak"]')->item(0)->hasAttribute('checked'))->toBeTrue() + ->and($xpath->query('//input[@id="jumlah-alokasi-desa"]')->item(0)->hasAttribute('disabled'))->toBeTrue() + ->and($xpath->query('//input[@name="dukungan_desa[]"]')->length)->toBe(5) + ->and($xpath->query('//input[@name="kemitraan_eksternal[]"]')->length)->toBe(6) + ->and($xpath->query('//input[@name="keterlibatan_koordinasi_desa"]')->length)->toBe(3); + expect($xpath->query('//a[contains(@class, "nav-link") and contains(., "Dukungan Ekosistem")]')->length)->toBe(0) + ->and($xpath->query('//form[@id="operasionalBumdesForm"]//input[@name="modal_awal"]')->length)->toBe(1) + ->and($xpath->query('//form[@id="operasionalBumdesForm"]//input[@name="jumlah_alokasi_desa"]')->length)->toBe(1) + ->and($xpath->query('//form[@id="operasionalBumdesForm"]//button[@type="submit"]')->length)->toBe(7); + foreach ($xpath->query('//form[@id="operasionalBumdesForm"]//legend') as $legend) { + expect(trim($legend->textContent))->not->toMatch('/^\d+\./'); + } + foreach (['dukungan-desa-lainnya-detail', 'kemitraan-eksternal-lainnya-detail'] as $id) { + $input = $xpath->query('//input[@id="'.$id.'"]')->item(0); + expect($input->hasAttribute('disabled'))->toBeFalse()->and($input->hasAttribute('required'))->toBeTrue(); + } + expect($data->dukungan_desa->first())->toBe(DukunganDesaBumdes::PENDAMPINGAN_TEKNIS) + ->and($data->keterlibatan_koordinasi_desa)->toBe(KeterlibatanKoordinasiBumdes::KADANG); +}); + +test('operational form saves capital assets and ecosystem answers together without overwriting other tabs', function () { + $data = KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'status_badan_hukum' => 'tidak_tahu', + 'status_keaktifan' => 'aktif_penuh', 'modal_awal' => 2000000, + ]); + $url = route('bumdes.operasional.index', 1); + $this->post(route('bumdes.operasional.store', 1), [ + 'ada_alokasi_desa' => '1', 'jumlah_alokasi_desa' => '10.000.000', + 'dukungan_desa' => ['pendampingan_teknis', 'regulasi_perdes', 'lainnya'], 'dukungan_desa_lainnya' => 'Pelatihan pemasaran', + 'keterlibatan_koordinasi_desa' => 'selalu', + 'kemitraan_eksternal' => ['ngo', 'swasta_offtaker', 'lainnya'], 'kemitraan_eksternal_lainnya' => 'Perguruan tinggi', + 'modal_awal' => '999', 'nama' => 'Nama tidak boleh berubah', + 'aset_produktif' => [['aset' => 'bangunan_kantor', 'dipilih' => '1', 'jumlah' => 2, 'kondisi' => 'berfungsi', 'status_kepemilikan_aset' => 'milik_bumdes_sendiri']], + ])->assertRedirect($url)->assertSessionHasNoErrors()->assertSessionHas('success', 'Data Operasional berhasil disimpan.'); + $data->refresh(); + expect($data->ada_alokasi_desa)->toBeTrue() + ->and($data->jumlah_alokasi_desa)->toBe('10000000.00') + ->and($data->dukungan_desa->map->value->all())->toBe(['pendampingan_teknis', 'regulasi_perdes', 'lainnya']) + ->and($data->dukungan_desa_lainnya)->toBe('Pelatihan pemasaran') + ->and($data->kemitraan_eksternal->map->value->all())->toBe(['ngo', 'swasta_offtaker', 'lainnya']) + ->and($data->kemitraan_eksternal_lainnya)->toBe('Perguruan tinggi') + ->and($data->keterlibatan_koordinasi_desa->value)->toBe('selalu') + ->and($data->modal_awal)->toBe('999.00') + ->and($data->aset_produktif[0]['jumlah'])->toBe(2) + ->and($data->status_badan_hukum->value)->toBe('tidak_tahu') + ->and($data->status_keaktifan->value)->toBe('aktif_penuh') + ->and($data->created_by)->toBe(7) + ->and($data->updated_by)->toBe(7) + ->and(PengurusBumdes::findOrFail(1)->nama)->toBe('Alpha'); + $this->get($url)->assertOk()->assertSee('value="10.000.000"', false)->assertSee('Pelatihan pemasaran'); + $this->put(route('bumdes.update', 1), $this->profileInput)->assertSessionHasNoErrors(); + $this->post(route('bumdes.kelembagaan-aktor.store', 1), ['status_badan_hukum' => 'tidak_tahu'])->assertSessionHasNoErrors(); + expect($data->refresh()->jumlah_alokasi_desa)->toBe('10000000.00') + ->and($data->dukungan_desa_lainnya)->toBe('Pelatihan pemasaran') + ->and($data->kemitraan_eksternal_lainnya)->toBe('Perguruan tinggi'); +}); + +test('ecosystem support clears disabled answers and allows unanswered questions', function () { + $data = KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'ada_alokasi_desa' => true, 'jumlah_alokasi_desa' => 1000, + 'dukungan_desa' => ['lainnya'], 'dukungan_desa_lainnya' => 'Pelatihan', + 'kemitraan_eksternal' => ['lainnya'], 'kemitraan_eksternal_lainnya' => 'Universitas', + ]); + $url = route('bumdes.operasional.store', 1); + $this->post($url, [ + 'ada_alokasi_desa' => '0', 'jumlah_alokasi_desa' => ['invalid'], + 'dukungan_desa' => ['tidak_ada'], 'dukungan_desa_lainnya' => ['invalid'], + 'kemitraan_eksternal' => ['belum_ada'], 'kemitraan_eksternal_lainnya' => ['invalid'], + ])->assertSessionHasNoErrors(); + expect($data->refresh()->ada_alokasi_desa)->toBeFalse() + ->and($data->jumlah_alokasi_desa)->toBeNull() + ->and($data->dukungan_desa_lainnya)->toBeNull() + ->and($data->kemitraan_eksternal_lainnya)->toBeNull(); + $this->post($url, [])->assertSessionHasNoErrors(); + expect($data->refresh()->ada_alokasi_desa)->toBeNull() + ->and($data->dukungan_desa->all())->toBe([]) + ->and($data->kemitraan_eksternal->all())->toBe([]); +}); + +test('ecosystem support validates conditional details choices and exclusive answers', function (array $input, string $field) { + $url = route('bumdes.operasional.index', 1); + $this->from($url)->post(route('bumdes.operasional.store', 1), $input) + ->assertRedirect($url)->assertSessionHasErrors($field)->assertSessionHasInput(array_key_first($input)); + $response = $this->get($url)->assertOk()->assertSee('data-has-errors="1"', false); + expect(strpos($response->getContent(), 'id="bumdes-notifications"'))->toBeLessThan(strpos($response->getContent(), 'id="operasionalBumdesForm"')); + expect(KelembagaanAktorBumdes::count())->toBe(0); +})->with([ + [['ada_alokasi_desa' => '1'], 'jumlah_alokasi_desa'], + [['ada_alokasi_desa' => '1', 'jumlah_alokasi_desa' => '-1'], 'jumlah_alokasi_desa'], + [['ada_alokasi_desa' => '1', 'jumlah_alokasi_desa' => '1.5'], 'jumlah_alokasi_desa'], + [['ada_alokasi_desa' => '1', 'jumlah_alokasi_desa' => '10abc'], 'jumlah_alokasi_desa'], + [['ada_alokasi_desa' => '1', 'jumlah_alokasi_desa' => '10000000000000000'], 'jumlah_alokasi_desa'], + [['ada_alokasi_desa' => 'invalid'], 'ada_alokasi_desa'], + [['ada_alokasi_desa' => ['invalid']], 'ada_alokasi_desa'], + [['ada_alokasi_desa' => '1', 'jumlah_alokasi_desa' => ['invalid']], 'jumlah_alokasi_desa'], + [['dukungan_desa' => ['lainnya']], 'dukungan_desa_lainnya'], + [['kemitraan_eksternal' => ['lainnya']], 'kemitraan_eksternal_lainnya'], + [['dukungan_desa' => ['lainnya'], 'dukungan_desa_lainnya' => ['invalid']], 'dukungan_desa_lainnya'], + [['kemitraan_eksternal' => ['lainnya'], 'kemitraan_eksternal_lainnya' => ['invalid']], 'kemitraan_eksternal_lainnya'], + [['dukungan_desa' => ['tidak_ada', 'pendampingan_teknis']], 'dukungan_desa'], + [['kemitraan_eksternal' => ['belum_ada', 'ngo']], 'kemitraan_eksternal'], + [['dukungan_desa' => ['invalid']], 'dukungan_desa.0'], + [['kemitraan_eksternal' => ['invalid']], 'kemitraan_eksternal.0'], + [['dukungan_desa' => 'pendampingan_teknis'], 'dukungan_desa'], + [['kemitraan_eksternal' => ['ngo', 'ngo']], 'kemitraan_eksternal.0'], + [['keterlibatan_koordinasi_desa' => 'invalid'], 'keterlibatan_koordinasi_desa'], +]); + +test('invalid ecosystem answers prevent partial updates to operational data', function () { + $data = KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'modal_awal' => 2000000, + 'dukungan_desa' => ['pendampingan_teknis'], + ]); + $this->from(route('bumdes.operasional.index', 1))->post(route('bumdes.operasional.store', 1), [ + 'modal_awal' => '5000000', 'ada_alokasi_desa' => '1', + 'dukungan_desa' => ['regulasi_perdes'], + ])->assertSessionHasErrors('jumlah_alokasi_desa'); + expect($data->refresh()->modal_awal)->toBe('2000000.00') + ->and($data->dukungan_desa->map->value->all())->toBe(['pendampingan_teknis']); + $this->get(route('bumdes.operasional.index', 1))->assertOk()->assertSee('value="5.000.000"', false); +}); + +test('ecosystem support stays locked for incomplete profiles', function () { + DB::table('bumdes_pengurus')->where('id', 1)->update(['alamat' => null]); + $this->get(route('bumdes.operasional.index', 1))->assertForbidden(); + $this->post(route('bumdes.operasional.store', 1), [])->assertForbidden(); +}); + +test('ecosystem allocation accepts zero without treating it as an empty answer', function () { + $this->post(route('bumdes.operasional.store', 1), ['ada_alokasi_desa' => '1', 'jumlah_alokasi_desa' => '0']) + ->assertSessionHasNoErrors(); + expect(KelembagaanAktorBumdes::firstOrFail()->jumlah_alokasi_desa)->toBe('0.00'); +}); + +test('ecosystem support migration can roll back without losing existing tab data', function () { + $data = KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1, 'modal_awal' => 2000000]); + $migration = require database_path('migrations/2026_11_04_000001_add_dukungan_ekosistem_to_bumdes_kelembagaan_aktor_table.php'); + $migration->down(); + expect(Schema::hasColumn('bumdes_kelembagaan_aktor', 'ada_alokasi_desa'))->toBeFalse() + ->and(DB::table('bumdes_kelembagaan_aktor')->where('id', $data->id)->value('modal_awal'))->toEqual(2000000); + $migration->up(); + expect(Schema::hasColumn('bumdes_kelembagaan_aktor', 'ada_alokasi_desa'))->toBeTrue() + ->and(DB::table('bumdes_kelembagaan_aktor')->where('id', $data->id)->value('modal_awal'))->toEqual(2000000); +}); + +test('operational management questions are grouped into separate cards with save and back buttons', function () { + $response = $this->get(route('bumdes.operasional.index', 1))->assertOk(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($response->getContent()); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + $cards = $xpath->query('//form[@id="operasionalBumdesForm"]/div[contains(@class, "card")]'); + expect($cards->length)->toBe(7); + foreach ($cards as $card) { + expect($xpath->query('.//button[@type="submit"]', $card)->length)->toBe(1) + ->and($xpath->query('.//a[normalize-space(.)="Kembali"]', $card)->length)->toBe(1); + } + foreach (['kapasitas-keuangan', 'jangkauan-layanan', 'layanan-petani', 'rantai-nilai', 'hambatan-usaha'] as $section) { + expect($xpath->query('//div[@id="section-'.$section.'"]')->length)->toBe(1); + } + foreach (['sifat_audit[]', 'pelaksana_audit', 'tahun_audit', 'tahun_laporan_musdes', 'nominal_kontribusi_pades', 'desa_lain_dilayani', 'jumlah_petani_desa_lain', 'nama_offtaker', 'tahun_mulai_offtaker', 'alasan_berhenti_offtaker'] as $field) { + foreach ($xpath->query('//*[@name="'.$field.'"]') as $input) { + expect($input->hasAttribute('disabled'))->toBeTrue(); + } + } + foreach (['jumlah_petani_desa_sendiri', 'jumlah_petani_desa_lain'] as $field) { + $input = $xpath->query('//input[@name="'.$field.'"]')->item(0); + expect($input->getAttribute('type'))->toBe('number')->and($input->getAttribute('min'))->toBe('0'); + } + foreach ($xpath->query('//form[@id="operasionalBumdesForm"]//legend') as $legend) { + expect(trim($legend->textContent))->not->toMatch('/^\d+[.)]/'); + } +}); + +test('operational management answers are stored with enums and restored without changing other tabs', function () { + $data = KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1, 'status_badan_hukum' => 'tidak_tahu', 'status_keaktifan' => 'aktif_penuh']); + $input = [ + 'modal_awal' => '1.000.000', + 'status_pembukuan' => 'rutin_tertib', 'format_pembukuan' => 'digital_excel', + 'pernah_audit' => '1', 'sifat_audit' => ['internal', 'eksternal'], 'pelaksana_audit' => 'Inspektorat', 'tahun_audit' => 2025, + 'pertanggungjawaban_musdes' => 'tahunan', 'tahun_laporan_musdes' => 2025, + 'kondisi_keuangan' => 'untung_surplus', 'kontribusi_pades' => 'tidak_rutin', 'nominal_kontribusi_pades' => '2.500.000', + 'cakupan_layanan' => 'desa_sendiri_dan_lain', 'desa_lain_dilayani' => 'Desa B, Desa C', + 'jumlah_petani_desa_sendiri' => 25, 'jumlah_petani_desa_lain' => 0, 'pencatatan_petani' => 'rapi', + 'layanan_petani' => ['simpan_pinjam', 'pelatihan', 'lainnya'], 'layanan_petani_lainnya' => 'Pengiriman hasil panen', + 'peran_rantai_nilai' => 'mengolah_sendiri', 'pengalaman_offtaker' => 'berjalan', 'nama_offtaker' => 'Mitra Kopi', 'tahun_mulai_offtaker' => 2024, + 'hambatan_usaha' => ['modal', 'akses_pasar', 'lainnya'], 'hambatan_usaha_lainnya' => 'Kendala logistik', + ]; + $this->post(route('bumdes.operasional.store', 1), $input) + ->assertRedirect(route('bumdes.operasional.index', 1))->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Data Operasional berhasil disimpan.'); + $data->refresh(); + expect($data->status_pembukuan)->toBe(StatusPembukuanBumdes::RUTIN_TERTIB) + ->and($data->sifat_audit->map->value->all())->toBe(['internal', 'eksternal']) + ->and($data->pelaksana_audit)->toBe('Inspektorat') + ->and($data->tahun_audit)->toBe(2025) + ->and($data->tahun_laporan_musdes)->toBe(2025) + ->and($data->nominal_kontribusi_pades)->toBe('2500000.00') + ->and($data->jumlah_petani_desa_lain)->toBe(0) + ->and($data->desa_lain_dilayani)->toBe('Desa B, Desa C') + ->and($data->layanan_petani->map->value->all())->toBe(['simpan_pinjam', 'pelatihan', 'lainnya']) + ->and($data->layanan_petani_lainnya)->toBe('Pengiriman hasil panen') + ->and($data->nama_offtaker)->toBe('Mitra Kopi') + ->and($data->tahun_mulai_offtaker)->toBe(2024) + ->and($data->hambatan_usaha->map->value->all())->toBe(['modal', 'akses_pasar', 'lainnya']) + ->and($data->hambatan_usaha_lainnya)->toBe('Kendala logistik') + ->and($data->status_badan_hukum->value)->toBe('tidak_tahu') + ->and($data->status_keaktifan->value)->toBe('aktif_penuh'); + $this->get(route('bumdes.operasional.index', 1))->assertOk()->assertSee('Inspektorat')->assertSee('value="2.500.000"', false)->assertSee('Mitra Kopi'); + $this->put(route('bumdes.update', 1), $this->profileInput)->assertSessionHasNoErrors(); + $this->post(route('bumdes.kelembagaan-aktor.store', 1), ['status_badan_hukum' => 'tidak_tahu'])->assertSessionHasNoErrors(); + expect($data->refresh()->nama_offtaker)->toBe('Mitra Kopi')->and($data->tahun_audit)->toBe(2025); +}); + +test('operational management skips disabled details and clears prior branch data', function () { + $data = KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'pernah_audit' => true, 'sifat_audit' => ['internal'], 'pelaksana_audit' => 'Pengawas', 'tahun_audit' => 2025, + 'pertanggungjawaban_musdes' => 'tahunan', 'tahun_laporan_musdes' => 2025, + 'kontribusi_pades' => 'rutin', 'nominal_kontribusi_pades' => 1000000, + 'cakupan_layanan' => 'desa_sendiri_dan_lain', 'desa_lain_dilayani' => 'Desa B', 'jumlah_petani_desa_lain' => 12, + 'pengalaman_offtaker' => 'berjalan', 'nama_offtaker' => 'Mitra', 'tahun_mulai_offtaker' => 2024, + 'layanan_petani' => ['lainnya'], 'layanan_petani_lainnya' => 'Pengiriman', + 'hambatan_usaha' => ['lainnya'], 'hambatan_usaha_lainnya' => 'Logistik', + ]); + $this->post(route('bumdes.operasional.store', 1), [ + 'pernah_audit' => '0', 'sifat_audit' => 'invalid', 'pelaksana_audit' => ['invalid'], 'tahun_audit' => 'invalid', + 'pertanggungjawaban_musdes' => 'belum_pernah', 'tahun_laporan_musdes' => 'invalid', + 'kontribusi_pades' => 'belum_pernah', 'nominal_kontribusi_pades' => ['invalid'], + 'cakupan_layanan' => 'desa_sendiri', 'desa_lain_dilayani' => ['invalid'], 'jumlah_petani_desa_lain' => 'invalid', + 'pengalaman_offtaker' => 'belum_pernah', 'nama_offtaker' => ['invalid'], 'tahun_mulai_offtaker' => 'invalid', 'alasan_berhenti_offtaker' => ['invalid'], + 'layanan_petani' => ['belum_ada'], 'layanan_petani_lainnya' => ['invalid'], + 'hambatan_usaha' => ['modal'], 'hambatan_usaha_lainnya' => ['invalid'], + ])->assertSessionHasNoErrors(); + $data->refresh(); + expect($data->sifat_audit->all())->toBe([]); + foreach (['pelaksana_audit', 'tahun_audit', 'tahun_laporan_musdes', 'nominal_kontribusi_pades', 'desa_lain_dilayani', 'jumlah_petani_desa_lain', 'nama_offtaker', 'tahun_mulai_offtaker', 'alasan_berhenti_offtaker', 'layanan_petani_lainnya', 'hambatan_usaha_lainnya'] as $field) { + expect($data->$field)->toBeNull(); + } + $this->post(route('bumdes.operasional.store', 1), ['pengalaman_offtaker' => 'berhenti', 'alasan_berhenti_offtaker' => 'Harga tidak sesuai'])->assertSessionHasNoErrors(); + expect($data->refresh()->alasan_berhenti_offtaker)->toBe('Harga tidak sesuai'); +}); + +test('operational management rejects missing dependent answers and invalid values without partial saves', function (array $input, array $errors) { + $data = KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1, 'modal_awal' => 2000000]); + $url = route('bumdes.operasional.index', 1); + $this->from($url)->post(route('bumdes.operasional.store', 1), [...$input, 'modal_awal' => '5000000']) + ->assertRedirect($url)->assertSessionHasErrors($errors); + expect($data->refresh()->modal_awal)->toBe('2000000.00'); + $this->get($url)->assertOk()->assertSee('data-has-errors="1"', false); +})->with([ + [['pernah_audit' => '1'], ['sifat_audit', 'pelaksana_audit', 'tahun_audit']], + [['pernah_audit' => '1', 'sifat_audit' => ['invalid'], 'pelaksana_audit' => 'Pengawas', 'tahun_audit' => 2025], ['sifat_audit.0']], + [['pertanggungjawaban_musdes' => 'tahunan'], ['tahun_laporan_musdes']], + [['pertanggungjawaban_musdes' => 'tahunan', 'tahun_laporan_musdes' => 1899], ['tahun_laporan_musdes']], + [['kontribusi_pades' => 'rutin'], ['nominal_kontribusi_pades']], + [['kontribusi_pades' => 'tidak_rutin', 'nominal_kontribusi_pades' => '-1'], ['nominal_kontribusi_pades']], + [['kontribusi_pades' => 'rutin', 'nominal_kontribusi_pades' => '1.5'], ['nominal_kontribusi_pades']], + [['kontribusi_pades' => 'rutin', 'nominal_kontribusi_pades' => ['invalid']], ['nominal_kontribusi_pades']], + [['cakupan_layanan' => 'desa_sendiri_dan_lain'], ['desa_lain_dilayani', 'jumlah_petani_desa_lain']], + [['jumlah_petani_desa_sendiri' => -1], ['jumlah_petani_desa_sendiri']], + [['jumlah_petani_desa_sendiri' => '1.5'], ['jumlah_petani_desa_sendiri']], + [['pengalaman_offtaker' => 'berjalan'], ['nama_offtaker', 'tahun_mulai_offtaker']], + [['pengalaman_offtaker' => 'berhenti'], ['alasan_berhenti_offtaker']], + [['layanan_petani' => ['lainnya']], ['layanan_petani_lainnya']], + [['layanan_petani' => ['belum_ada', 'pelatihan']], ['layanan_petani']], + [['hambatan_usaha' => ['lainnya']], ['hambatan_usaha_lainnya']], + [['hambatan_usaha' => ['modal', 'modal']], ['hambatan_usaha.0']], + [['layanan_petani' => ['lainnya'], 'layanan_petani_lainnya' => ['invalid']], ['layanan_petani_lainnya']], +]); + +test('operational fixed choices reject unknown values', function (string $field) { + $this->post(route('bumdes.operasional.store', 1), [$field => 'invalid'])->assertSessionHasErrors($field); + expect(KelembagaanAktorBumdes::count())->toBe(0); +})->with(['status_pembukuan', 'format_pembukuan', 'pernah_audit', 'pertanggungjawaban_musdes', 'kondisi_keuangan', 'kontribusi_pades', 'cakupan_layanan', 'pencatatan_petani', 'peran_rantai_nilai', 'pengalaman_offtaker', 'layanan_petani', 'hambatan_usaha']); + +test('operational historical years cannot be in the future', function () { + $future = now()->year + 1; + $this->post(route('bumdes.operasional.store', 1), [ + 'pernah_audit' => '1', 'sifat_audit' => ['internal'], 'pelaksana_audit' => 'Pengawas', 'tahun_audit' => $future, + 'pertanggungjawaban_musdes' => 'tahunan', 'tahun_laporan_musdes' => $future, + 'pengalaman_offtaker' => 'berjalan', 'nama_offtaker' => 'Mitra', 'tahun_mulai_offtaker' => $future, + ])->assertSessionHasErrors(['tahun_audit', 'tahun_laporan_musdes', 'tahun_mulai_offtaker']); + expect(KelembagaanAktorBumdes::count())->toBe(0); +}); + +test('management migration rolls back only its new columns', function () { + $data = KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1, 'modal_awal' => 2000000, 'ada_alokasi_desa' => false]); + $migration = require database_path('migrations/2026_11_04_000002_add_pengelolaan_layanan_to_bumdes_kelembagaan_aktor_table.php'); + $migration->down(); + expect(Schema::hasColumn('bumdes_kelembagaan_aktor', 'status_pembukuan'))->toBeFalse() + ->and(Schema::hasColumn('bumdes_kelembagaan_aktor', 'hambatan_usaha_lainnya'))->toBeFalse() + ->and(DB::table('bumdes_kelembagaan_aktor')->where('id', $data->id)->value('modal_awal'))->toEqual(2000000); + $migration->up(); + expect(Schema::hasColumn('bumdes_kelembagaan_aktor', 'status_pembukuan'))->toBeTrue(); +}); + +test('value chain tab renders separate cards quantities and the product by harvest price matrix', function () { + $response = $this->get(route('bumdes.rantai-nilai.index', 1))->assertOk(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($response->getContent()); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + expect($xpath->query('//form[@id="rantaiNilaiBumdesForm"]/div[contains(@class, "card")]')->length)->toBe(8) + ->and($xpath->query('//form[@id="rantaiNilaiBumdesForm"]//button[@type="submit"]')->length)->toBe(8) + ->and($xpath->query('//form[@id="rantaiNilaiBumdesForm"]//a[normalize-space(.)="Kembali"]')->length)->toBe(8) + ->and($xpath->query('//fieldset[@data-rantai-section and @disabled]')->length)->toBe(4) + ->and($xpath->query('//tr[@data-rantai-price-product]')->length)->toBe(4) + ->and($xpath->query('//input[starts-with(@name, "harga_beli_petani[")]')->length)->toBe(12) + ->and($xpath->query('//input[starts-with(@name, "volume_") and @type="number" and @step="0.001" and @min="0"]')->length)->toBe(6); + foreach ($xpath->query('//form[@id="rantaiNilaiBumdesForm"]//legend') as $legend) { + expect(trim($legend->textContent))->not->toMatch('/^\d+[.)]/'); + } + $response->assertSee('Saluran Pembelian/Sumber Pasokan')->assertSee('Saluran Penjualan Utama') + ->assertSee('Sifat Hubungan dengan Petani Pemasok')->assertSee('Harga Beli BUMDes dari Petani'); +}); + +test('value chain save buttons identify the next box without a standalone page heading', function () { + $response = $this->get(route('bumdes.rantai-nilai.index', 1))->assertOk(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($response->getContent()); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + expect($xpath->query('//div[contains(@class, "bumdes-page")]/h4')->length)->toBe(0); + foreach (['pembelian' => 'penjualan', 'penjualan' => 'hubungan-pemasok', 'hubungan-pemasok' => 'harga-beli', 'harga-beli' => 'sortasi', 'sortasi' => 'pengeringan', 'pengeringan' => 'fermentasi', 'fermentasi' => 'premium-harga'] as $current => $next) { + $button = $xpath->query('//div[@id="section-rantai-'.$current.'"]//button[@type="submit"]')->item(0); + expect($button->getAttribute('name'))->toBe('redirect_fragment') + ->and($button->getAttribute('value'))->toBe('section-rantai-'.$next); + } + expect($xpath->query('//div[@id="section-rantai-premium-harga"]//button[@type="submit"]')->item(0)->hasAttribute('name'))->toBeFalse(); + $response->assertDontSee('Memetakan peran BUMDes dalam rantai nilai komoditas, dari pembelian hasil panen hingga akses pasar.'); +}); + +test('value chain saves advance to the next box only when the bumdes trades', function (string $fragment, string $trading) { + $url = route('bumdes.rantai-nilai.index', 1); + $this->post(route('bumdes.rantai-nilai.store', 1), ['melakukan_jual_beli' => $trading, 'melakukan_pengolahan_pascapanen' => '0', 'redirect_fragment' => $fragment]) + ->assertSessionHasNoErrors()->assertRedirect($url.($trading === '1' ? '#'.$fragment : '')) + ->assertSessionHas('success', 'Data Rantai Nilai & Akses Pasar berhasil disimpan.'); + expect(KelembagaanAktorBumdes::firstOrFail()->melakukan_jual_beli)->toBe($trading === '1'); +})->with(['section-rantai-penjualan', 'section-rantai-hubungan-pemasok', 'section-rantai-harga-beli'])->with(['1', '0']); + +test('value chain validation errors stay at the notification instead of advancing', function () { + $url = route('bumdes.rantai-nilai.index', 1); + $this->from($url)->post(route('bumdes.rantai-nilai.store', 1), [ + 'melakukan_jual_beli' => '1', 'redirect_fragment' => 'section-rantai-penjualan', 'sumber_pasokan' => ['lainnya'], + ])->assertRedirect($url)->assertSessionHasErrors('sumber_pasokan_lainnya'); + expect(KelembagaanAktorBumdes::count())->toBe(0); + $this->get($url)->assertOk()->assertSee('data-has-errors="1"', false); +}); + +test('value chain save redirects accept only known box identifiers', function () { + $this->post(route('bumdes.rantai-nilai.store', 1), ['melakukan_jual_beli' => '1', 'redirect_fragment' => 'unknown-box']) + ->assertSessionHasErrors('redirect_fragment'); + expect(KelembagaanAktorBumdes::count())->toBe(0); +}); + +test('value chain answers store multiple choices volumes and per-product prices without changing other tabs', function () { + $data = KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1, 'status_badan_hukum' => 'tidak_tahu', 'modal_awal' => 2000000]); + $input = [ + 'melakukan_jual_beli' => '1', 'melakukan_pengolahan_pascapanen' => '0', + 'sumber_pasokan' => ['anggota_kelompok', 'lainnya'], 'sumber_pasokan_lainnya' => 'Kebun desa', + 'bentuk_produk_dibeli' => ['buah_segar', 'biji_basah'], + 'volume_pembelian' => ['panen_terakhir' => '1234.125', 'tahun_lalu' => '0', 'dua_tahun_lalu' => '25.5'], + 'saluran_penjualan' => ['offtaker_eksportir', 'lainnya'], 'saluran_penjualan_lainnya' => 'Hotel setempat', + 'bentuk_produk_dijual' => ['biji_kering', 'terfermentasi'], 'volume_penjualan' => ['panen_terakhir' => '250.75', 'tahun_lalu' => '0'], + 'hubungan_petani_pemasok' => ['bebas', 'semi_kontrak', 'lainnya'], 'hubungan_petani_pemasok_lainnya' => 'Kemitraan kebun desa', + 'mekanisme_pembayaran_petani' => 'lainnya', 'mekanisme_pembayaran_lainnya' => 'Transfer saat serah terima', + 'perjanjian_petani' => 'tertulis', + 'harga_beli_petani' => [ + 'buah_segar' => ['panen_terakhir' => '12.500', 'tahun_lalu' => '10.000', 'dua_tahun_lalu' => '0'], + 'biji_basah' => ['panen_terakhir' => '0'], + 'biji_kering' => ['panen_terakhir' => ['invalid']], + ], + 'penetapan_harga_beli' => 'lainnya', 'penetapan_harga_beli_lainnya' => 'Musyawarah dengan pemasok', + 'modal_awal' => '999', + ]; + $url = route('bumdes.rantai-nilai.index', 1); + $this->post(route('bumdes.rantai-nilai.store', 1), $input)->assertRedirect($url)->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Data Rantai Nilai & Akses Pasar berhasil disimpan.'); + $data->refresh(); + expect($data->melakukan_jual_beli)->toBeTrue() + ->and($data->sumber_pasokan->map->value->all())->toBe(['anggota_kelompok', 'lainnya']) + ->and($data->bentuk_produk_dibeli->map->value->all())->toBe(['buah_segar', 'biji_basah']) + ->and($data->volume_pembelian)->toBe(['panen_terakhir' => '1234.125', 'tahun_lalu' => '0', 'dua_tahun_lalu' => '25.5']) + ->and($data->saluran_penjualan->map->value->all())->toBe(['offtaker_eksportir', 'lainnya']) + ->and($data->volume_penjualan)->toBe(['panen_terakhir' => '250.75', 'tahun_lalu' => '0']) + ->and($data->harga_beli_petani)->toBe(['buah_segar' => ['panen_terakhir' => '12500', 'tahun_lalu' => '10000', 'dua_tahun_lalu' => '0'], 'biji_basah' => ['panen_terakhir' => '0']]) + ->and($data->hubungan_petani_pemasok->map->value->all())->toBe(['bebas', 'semi_kontrak', 'lainnya']) + ->and($data->mekanisme_pembayaran_lainnya)->toBe('Transfer saat serah terima') + ->and($data->penetapan_harga_beli_lainnya)->toBe('Musyawarah dengan pemasok') + ->and($data->modal_awal)->toBe('2000000.00'); + $this->get($url)->assertOk()->assertSee('value="12.500"', false)->assertSee('Kebun desa')->assertSee('Hotel setempat'); + $this->put(route('bumdes.update', 1), $this->profileInput)->assertSessionHasNoErrors(); + $this->post(route('bumdes.operasional.store', 1), ['modal_awal' => '3000000'])->assertSessionHasNoErrors(); + $this->post(route('bumdes.kelembagaan-aktor.store', 1), ['status_badan_hukum' => 'tidak_tahu'])->assertSessionHasNoErrors(); + expect($data->refresh()->sumber_pasokan_lainnya)->toBe('Kebun desa') + ->and($data->harga_beli_petani['buah_segar']['panen_terakhir'])->toBe('12500'); +}); + +test('not trading skips and clears the whole value chain questionnaire while preserving operational answers', function () { + $data = KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'modal_awal' => 2000000, 'melakukan_jual_beli' => true, + 'sumber_pasokan' => ['lainnya'], 'sumber_pasokan_lainnya' => 'Kebun desa', + 'bentuk_produk_dibeli' => ['buah_segar'], 'harga_beli_petani' => ['buah_segar' => ['panen_terakhir' => '12500']], + ]); + $this->post(route('bumdes.rantai-nilai.store', 1), [ + 'melakukan_jual_beli' => '0', 'melakukan_pengolahan_pascapanen' => '0', 'sumber_pasokan' => 'invalid', 'sumber_pasokan_lainnya' => ['invalid'], + 'volume_pembelian' => ['invalid'], 'bentuk_produk_dibeli' => 'invalid', 'harga_beli_petani' => 'invalid', + 'penetapan_harga_beli' => 'invalid', + ])->assertSessionHasNoErrors(); + expect($data->refresh()->melakukan_jual_beli)->toBeFalse() + ->and($data->sumber_pasokan->all())->toBe([]) + ->and($data->sumber_pasokan_lainnya)->toBeNull() + ->and($data->harga_beli_petani)->toBe([]) + ->and($data->modal_awal)->toBe('2000000.00'); + $this->get(route('bumdes.rantai-nilai.index', 1))->assertOk(); +}); + +test('not yet selling clears product and sales volumes and ignores their invalid details', function () { + $this->post(route('bumdes.rantai-nilai.store', 1), [ + 'melakukan_jual_beli' => '1', 'melakukan_pengolahan_pascapanen' => '0', 'saluran_penjualan' => ['belum_menjual'], + 'bentuk_produk_dijual' => 'invalid', 'volume_penjualan' => ['panen_terakhir' => ['invalid']], + ])->assertSessionHasNoErrors(); + $data = KelembagaanAktorBumdes::firstOrFail(); + expect($data->bentuk_produk_dijual->all())->toBe([])->and($data->volume_penjualan)->toBe([]); +}); + +test('value chain validates applicability other choices quantities and price matrix data', function (array $input, array $errors) { + $url = route('bumdes.rantai-nilai.index', 1); + $this->from($url)->post(route('bumdes.rantai-nilai.store', 1), $input)->assertRedirect($url)->assertSessionHasErrors($errors); + expect(KelembagaanAktorBumdes::count())->toBe(0); + $this->get($url)->assertOk()->assertSee('data-has-errors="1"', false); +})->with([ + [[], ['melakukan_jual_beli']], + [['melakukan_jual_beli' => 'invalid'], ['melakukan_jual_beli']], + [['melakukan_jual_beli' => ['invalid']], ['melakukan_jual_beli']], + [['melakukan_jual_beli' => '1', 'sumber_pasokan' => ['lainnya']], ['sumber_pasokan_lainnya']], + [['melakukan_jual_beli' => '1', 'saluran_penjualan' => ['lainnya']], ['saluran_penjualan_lainnya']], + [['melakukan_jual_beli' => '1', 'hubungan_petani_pemasok' => ['lainnya']], ['hubungan_petani_pemasok_lainnya']], + [['melakukan_jual_beli' => '1', 'mekanisme_pembayaran_petani' => 'lainnya'], ['mekanisme_pembayaran_lainnya']], + [['melakukan_jual_beli' => '1', 'penetapan_harga_beli' => 'lainnya'], ['penetapan_harga_beli_lainnya']], + [['melakukan_jual_beli' => '1', 'sumber_pasokan' => ['lainnya'], 'sumber_pasokan_lainnya' => ['invalid']], ['sumber_pasokan_lainnya']], + [['melakukan_jual_beli' => '1', 'saluran_penjualan' => ['belum_menjual', 'pasar_lokal']], ['saluran_penjualan']], + [['melakukan_jual_beli' => '1', 'sumber_pasokan' => ['anggota_kelompok', 'anggota_kelompok']], ['sumber_pasokan.0']], + [['melakukan_jual_beli' => '1', 'hubungan_petani_pemasok' => ['invalid']], ['hubungan_petani_pemasok.0']], + [['melakukan_jual_beli' => '1', 'perjanjian_petani' => 'invalid'], ['perjanjian_petani']], + [['melakukan_jual_beli' => '1', 'bentuk_produk_dibeli' => ['invalid']], ['bentuk_produk_dibeli.0']], + [['melakukan_jual_beli' => '1', 'volume_pembelian' => ['panen_terakhir' => '-1']], ['volume_pembelian.panen_terakhir']], + [['melakukan_jual_beli' => '1', 'volume_pembelian' => ['panen_terakhir' => '1.2345']], ['volume_pembelian.panen_terakhir']], + [['melakukan_jual_beli' => '1', 'volume_penjualan' => ['tahun_lalu' => 'abc']], ['volume_penjualan.tahun_lalu']], + [['melakukan_jual_beli' => '1', 'volume_pembelian' => ['invalid' => 1]], ['volume_pembelian']], + [['melakukan_jual_beli' => '1', 'bentuk_produk_dibeli' => ['buah_segar'], 'harga_beli_petani' => ['buah_segar' => ['panen_terakhir' => '-1']]], ['harga_beli_petani.buah_segar.panen_terakhir']], + [['melakukan_jual_beli' => '1', 'bentuk_produk_dibeli' => ['buah_segar'], 'harga_beli_petani' => ['buah_segar' => ['panen_terakhir' => '1.5']]], ['harga_beli_petani.buah_segar.panen_terakhir']], + [['melakukan_jual_beli' => '1', 'bentuk_produk_dibeli' => ['buah_segar'], 'harga_beli_petani' => ['buah_segar' => ['panen_terakhir' => ['invalid']]]], ['harga_beli_petani.buah_segar.panen_terakhir']], + [['melakukan_jual_beli' => '1', 'harga_beli_petani' => ['invalid' => []]], ['harga_beli_petani']], + [['melakukan_jual_beli' => '1', 'bentuk_produk_dibeli' => ['buah_segar'], 'harga_beli_petani' => ['buah_segar' => ['invalid' => '1000']]], ['harga_beli_petani.buah_segar']], +]); + +test('value chain skips inactive other descriptions and unselected product prices', function () { + $this->post(route('bumdes.rantai-nilai.store', 1), [ + 'melakukan_jual_beli' => '1', 'melakukan_pengolahan_pascapanen' => '0', 'bentuk_produk_dibeli' => ['buah_segar'], + 'harga_beli_petani' => ['buah_segar' => ['panen_terakhir' => '0'], 'biji_basah' => 'invalid'], + 'sumber_pasokan' => ['anggota_kelompok'], 'sumber_pasokan_lainnya' => ['invalid'], + 'mekanisme_pembayaran_petani' => 'tunai', 'mekanisme_pembayaran_lainnya' => ['invalid'], + 'penetapan_harga_beli' => 'pasar_setempat', 'penetapan_harga_beli_lainnya' => ['invalid'], + ])->assertSessionHasNoErrors(); + $data = KelembagaanAktorBumdes::firstOrFail(); + expect($data->harga_beli_petani)->toBe(['buah_segar' => ['panen_terakhir' => '0']]) + ->and($data->sumber_pasokan_lainnya)->toBeNull()->and($data->mekanisme_pembayaran_lainnya)->toBeNull(); +}); + +test('value chain is locked for incomplete profiles and workshop roles', function () { + DB::table('bumdes_pengurus')->where('id', 1)->update(['alamat' => null]); + $this->get(route('bumdes.rantai-nilai.index', 1))->assertForbidden(); + $this->post(route('bumdes.rantai-nilai.store', 1), ['melakukan_jual_beli' => '0'])->assertForbidden(); + DB::table('bumdes_pengurus')->where('id', 1)->update(['alamat' => 'Desa A']); + auth()->user()->role = 'fasilitator'; + $this->get(route('bumdes.rantai-nilai.index', 1))->assertForbidden(); +}); + +test('value chain migration rolls back without changing earlier operational data', function () { + $data = KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1, 'modal_awal' => 2000000, 'status_pembukuan' => 'rutin_tertib']); + $migration = require database_path('migrations/2026_11_04_000003_add_rantai_nilai_to_bumdes_kelembagaan_aktor_table.php'); + $migration->down(); + expect(Schema::hasColumn('bumdes_kelembagaan_aktor', 'sumber_pasokan'))->toBeFalse() + ->and(DB::table('bumdes_kelembagaan_aktor')->where('id', $data->id)->value('status_pembukuan'))->toBe('rutin_tertib'); + $migration->up(); + expect(Schema::hasColumn('bumdes_kelembagaan_aktor', 'sumber_pasokan'))->toBeTrue(); +}); + +test('postharvest opportunity cards show all choices and start with inactive follow-up fields', function () { + $response = $this->get(route('bumdes.rantai-nilai.index', 1))->assertOk(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($response->getContent()); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + expect($xpath->query('//div[@data-postharvest-card]')->length)->toBe(4) + ->and($xpath->query('//fieldset[@data-postharvest-section and @disabled]')->length)->toBe(4) + ->and($xpath->query('//input[@name="status_sortasi"]')->length)->toBe(3) + ->and($xpath->query('//input[@name="status_pengeringan"]')->length)->toBe(3) + ->and($xpath->query('//input[@name="status_fermentasi"]')->length)->toBe(4) + ->and($xpath->query('//input[@name="model_fermentasi[]"]')->length)->toBe(4); + foreach (['kriteria_sortasi', 'cara_tempat_pengeringan', 'proses_fermentasi'] as $field) { + $input = $xpath->query('//textarea[@name="'.$field.'"]')->item(0); + expect($input->hasAttribute('disabled'))->toBeTrue()->and($input->hasAttribute('required'))->toBeFalse(); + } + $response->assertSee('Peluang Nilai Tambah')->assertSee('Sortasi & Pemilihan Kualitas', false) + ->assertSee('Praktik Pengeringan')->assertSee('Praktik Fermentasi')->assertSee('Besaran Premium Harga'); +}); + +test('postharvest activities save independently of trading and retain existing operational answers', function (string $trading) { + $data = KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1, 'modal_awal' => 2000000, 'status_badan_hukum' => 'tidak_tahu']); + $input = [ + 'melakukan_jual_beli' => $trading, 'melakukan_pengolahan_pascapanen' => '1', + 'status_sortasi' => 'rutin_standar', 'kriteria_sortasi' => 'Ukuran seragam dan bebas cacat', + 'status_pengeringan' => 'kadang', 'cara_tempat_pengeringan' => 'Dijemur di atas para-para beratap', + 'status_fermentasi' => 'pernah_mencoba', 'model_fermentasi' => ['individual', 'unit_bumdes', 'campuran'], + 'proses_fermentasi' => 'Fermentasi terkontrol dalam kotak kayu', 'premium_harga_pascapanen' => '2.500', + 'redirect_fragment' => 'section-rantai-pengeringan', + ]; + $url = route('bumdes.rantai-nilai.index', 1); + $this->post(route('bumdes.rantai-nilai.store', 1), $input)->assertSessionHasNoErrors()->assertRedirect($url.'#section-rantai-pengeringan'); + $data->refresh(); + expect($data->melakukan_pengolahan_pascapanen)->toBeTrue() + ->and($data->status_sortasi)->toBe(StatusSortasiBumdes::RUTIN_STANDAR) + ->and($data->kriteria_sortasi)->toBe('Ukuran seragam dan bebas cacat') + ->and($data->status_pengeringan->value)->toBe('kadang') + ->and($data->status_fermentasi->value)->toBe('pernah_mencoba') + ->and($data->model_fermentasi->map->value->all())->toBe(['individual', 'unit_bumdes', 'campuran']) + ->and($data->proses_fermentasi)->toBe('Fermentasi terkontrol dalam kotak kayu') + ->and($data->premium_harga_pascapanen)->toBe('2500.00') + ->and($data->modal_awal)->toBe('2000000.00') + ->and($data->status_badan_hukum->value)->toBe('tidak_tahu'); + $this->get($url)->assertOk()->assertSee('value="2.500"', false)->assertSee('Ukuran seragam dan bebas cacat'); + $this->put(route('bumdes.update', 1), $this->profileInput)->assertSessionHasNoErrors(); + $this->post(route('bumdes.operasional.store', 1), ['modal_awal' => '3000000'])->assertSessionHasNoErrors(); + expect($data->refresh()->premium_harga_pascapanen)->toBe('2500.00')->and($data->kriteria_sortasi)->toBe('Ukuran seragam dan bebas cacat'); +})->with(['1', '0']); + +test('inactive postharvest follow-ups ignore malformed values and clear old details', function () { + $data = KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'melakukan_jual_beli' => true, 'melakukan_pengolahan_pascapanen' => true, + 'status_sortasi' => 'rutin_standar', 'kriteria_sortasi' => 'Ukuran', 'status_pengeringan' => 'rutin', 'cara_tempat_pengeringan' => 'Jemur', + 'status_fermentasi' => 'rutin', 'model_fermentasi' => ['unit_bumdes'], 'proses_fermentasi' => 'Kotak kayu', + ]); + $this->post(route('bumdes.rantai-nilai.store', 1), [ + 'melakukan_jual_beli' => '1', 'melakukan_pengolahan_pascapanen' => '1', + 'status_sortasi' => 'belum_pernah', 'kriteria_sortasi' => ['invalid'], + 'status_pengeringan' => 'tidak_pernah', 'cara_tempat_pengeringan' => ['invalid'], + 'status_fermentasi' => 'direncanakan', 'model_fermentasi' => 'invalid', 'proses_fermentasi' => ['invalid'], + 'premium_harga_pascapanen' => '0', + ])->assertSessionHasNoErrors(); + expect($data->refresh()->kriteria_sortasi)->toBeNull()->and($data->cara_tempat_pengeringan)->toBeNull() + ->and($data->model_fermentasi->all())->toBe([])->and($data->proses_fermentasi)->toBeNull() + ->and($data->premium_harga_pascapanen)->toBe('0.00'); +}); + +test('no postharvest processing clears its questionnaire without clearing buying and selling', function () { + $data = KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'melakukan_pengolahan_pascapanen' => true, 'status_sortasi' => 'rutin_standar', + 'kriteria_sortasi' => 'Ukuran', 'model_fermentasi' => ['unit_bumdes'], 'premium_harga_pascapanen' => 2500, + ]); + $this->post(route('bumdes.rantai-nilai.store', 1), [ + 'melakukan_jual_beli' => '1', 'sumber_pasokan' => ['anggota_kelompok'], + 'melakukan_pengolahan_pascapanen' => '0', 'status_sortasi' => 'invalid', 'kriteria_sortasi' => ['invalid'], + 'model_fermentasi' => 'invalid', 'premium_harga_pascapanen' => ['invalid'], 'redirect_fragment' => 'section-rantai-pengeringan', + ])->assertSessionHasNoErrors()->assertRedirect(route('bumdes.rantai-nilai.index', 1)); + expect($data->refresh()->melakukan_pengolahan_pascapanen)->toBeFalse() + ->and($data->status_sortasi)->toBeNull()->and($data->kriteria_sortasi)->toBeNull() + ->and($data->model_fermentasi->all())->toBe([])->and($data->premium_harga_pascapanen)->toBeNull() + ->and($data->sumber_pasokan->map->value->all())->toBe(['anggota_kelompok']); +}); + +test('postharvest details validate active answers before any data is saved', function (array $input, array $errors) { + $url = route('bumdes.rantai-nilai.index', 1); + $this->from($url)->post(route('bumdes.rantai-nilai.store', 1), ['melakukan_jual_beli' => '0', 'melakukan_pengolahan_pascapanen' => '1', ...$input]) + ->assertRedirect($url)->assertSessionHasErrors($errors); + expect(KelembagaanAktorBumdes::count())->toBe(0); + $this->get($url)->assertOk()->assertSee('data-has-errors="1"', false); +})->with([ + [['melakukan_pengolahan_pascapanen' => ['invalid']], ['melakukan_pengolahan_pascapanen']], + [['status_sortasi' => 'rutin_standar'], ['kriteria_sortasi']], + [['status_sortasi' => 'tidak_konsisten'], ['kriteria_sortasi']], + [['status_sortasi' => 'invalid'], ['status_sortasi']], + [['status_pengeringan' => 'rutin'], ['cara_tempat_pengeringan']], + [['status_pengeringan' => 'kadang'], ['cara_tempat_pengeringan']], + [['status_pengeringan' => 'invalid'], ['status_pengeringan']], + [['status_fermentasi' => 'rutin'], ['model_fermentasi', 'proses_fermentasi']], + [['status_fermentasi' => 'pernah_mencoba'], ['model_fermentasi', 'proses_fermentasi']], + [['status_fermentasi' => 'invalid'], ['status_fermentasi']], + [['status_fermentasi' => 'rutin', 'model_fermentasi' => ['invalid'], 'proses_fermentasi' => 'Kotak kayu'], ['model_fermentasi.0']], + [['status_fermentasi' => 'rutin', 'model_fermentasi' => ['unit_bumdes', 'unit_bumdes'], 'proses_fermentasi' => 'Kotak kayu'], ['model_fermentasi.0']], + [['status_sortasi' => 'rutin_standar', 'kriteria_sortasi' => ['invalid']], ['kriteria_sortasi']], + [['premium_harga_pascapanen' => '-1'], ['premium_harga_pascapanen']], + [['premium_harga_pascapanen' => '1.5'], ['premium_harga_pascapanen']], + [['premium_harga_pascapanen' => '10000000000000000'], ['premium_harga_pascapanen']], + [['premium_harga_pascapanen' => ['invalid']], ['premium_harga_pascapanen']], +]); + +test('postharvest save targets follow processing eligibility', function (string $fragment, string $processing) { + $url = route('bumdes.rantai-nilai.index', 1); + $this->post(route('bumdes.rantai-nilai.store', 1), [ + 'melakukan_jual_beli' => '0', 'melakukan_pengolahan_pascapanen' => $processing, 'redirect_fragment' => $fragment, + ])->assertSessionHasNoErrors()->assertRedirect($url.($processing === '1' ? '#'.$fragment : '')); +})->with(['section-rantai-pengeringan', 'section-rantai-fermentasi', 'section-rantai-premium-harga'])->with(['1', '0']); + +test('postharvest migration rolls back only the newly added opportunity fields', function () { + $data = KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1, 'melakukan_jual_beli' => true, 'sumber_pasokan' => ['campuran']]); + $migration = require database_path('migrations/2026_11_05_000001_add_peluang_nilai_tambah_to_bumdes_kelembagaan_aktor_table.php'); + $migration->down(); + expect(Schema::hasColumn('bumdes_kelembagaan_aktor', 'status_sortasi'))->toBeFalse() + ->and(DB::table('bumdes_kelembagaan_aktor')->where('id', $data->id)->value('sumber_pasokan'))->toBe('["campuran"]'); + $migration->up(); + expect(Schema::hasColumn('bumdes_kelembagaan_aktor', 'status_sortasi'))->toBeTrue(); +}); + +test('postharvest applicability requires an explicit answer and displays its error above the form', function () { + $url = route('bumdes.rantai-nilai.index', 1); + $this->from($url)->post(route('bumdes.rantai-nilai.store', 1), ['melakukan_jual_beli' => '1']) + ->assertRedirect($url)->assertSessionHasErrors(['melakukan_pengolahan_pascapanen' => 'Kegiatan pengolahan pascapanen wajib dipilih.']); + expect(KelembagaanAktorBumdes::count())->toBe(0); + $response = $this->get($url)->assertOk()->assertSee('Kegiatan pengolahan pascapanen wajib dipilih.') + ->assertSee('data-has-errors="1"', false); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($response->getContent()); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + expect($xpath->query('//input[@name="melakukan_pengolahan_pascapanen" and @required]')->length)->toBe(2) + ->and($xpath->query('//legend[contains(., "Kegiatan Pengolahan Pascapanen")]/span[contains(@class, "text-danger")]')->length)->toBe(1); +}); + +test('postharvest applicability accepts either yes or no as a required answer', function (string $answer) { + $this->post(route('bumdes.rantai-nilai.store', 1), ['melakukan_jual_beli' => '0', 'melakukan_pengolahan_pascapanen' => $answer]) + ->assertSessionHasNoErrors(); + expect(KelembagaanAktorBumdes::firstOrFail()->melakukan_pengolahan_pascapanen)->toBe($answer === '1'); +})->with(['0', '1']); + +test('reported bumdes value chain URL resolves the parent model for both display and save', function () { + $url = '/bumdes/2/rantai-nilai'; + $this->get($url)->assertOk()->assertSee('rantaiNilaiBumdesForm', false); + $this->post($url, ['melakukan_jual_beli' => '0', 'melakukan_pengolahan_pascapanen' => '0']) + ->assertRedirect(route('bumdes.rantai-nilai.index', 2))->assertSessionHasNoErrors(); + $data = KelembagaanAktorBumdes::where('pengurus_bumdes_id', 2)->firstOrFail(); + expect($data->melakukan_jual_beli)->toBeFalse()->and($data->melakukan_pengolahan_pascapanen)->toBeFalse(); +}); + +test('shared bumdes profile card presents institutional data across all profile tabs', function () { + KelembagaanAktorBumdes::create([ + 'pengurus_bumdes_id' => 1, 'status_keaktifan' => 'aktif_penuh', 'status_badan_hukum' => 'proses_pendaftaran', + 'unit_usaha' => ['simpan_pinjam', 'perdagangan_toko_desa', 'jasa_lainnya'], 'unit_usaha_jasa_lainnya' => 'Pengiriman hasil panen', + 'status_unit_komoditas' => 'ya', 'nama_unit_komoditas' => 'Unit Pengolahan Kopi', + 'peran_rantai_nilai' => 'mengolah_sendiri', 'cakupan_layanan' => 'desa_sendiri_dan_lain', + 'jumlah_petani_desa_sendiri' => 120, 'jumlah_petani_desa_lain' => 35, 'desa_lain_dilayani' => 'Desa B, Desa C', + ]); + DB::table('bumdes_pengurus')->where('id', 1)->update(['nomor_telepon' => '081234567890']); + foreach (['bumdes.edit', 'bumdes.kelembagaan-aktor.index', 'bumdes.operasional.index', 'bumdes.rantai-nilai.index'] as $route) { + $response = $this->get(route($route, 1))->assertOk(); + $dom = new DOMDocument; + $previous = libxml_use_internal_errors(true); + $dom->loadHTML($response->getContent()); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + $xpath = new DOMXPath($dom); + $card = $xpath->query('//section[@id="bumdes-profile-summary"]')->item(0); + expect($card)->not->toBeNull(); + expect($card->textContent)->toContain('Desa A', 'Aktif beroperasi penuh', 'Proses pendaftaran', 'Alpha', '081234567890', 'Pengiriman hasil panen') + ->not->toContain('Jenis Kelamin', 'Alamat Domisili', 'Petani Terlayani', 'Jangkauan Layanan', 'Peran Rantai Nilai', 'Unit Komoditas', 'Unit Pengolahan Kopi', 'Desa B, Desa C'); + expect($xpath->query('.//*[@data-bumdes-farmer-total]', $card)->length)->toBe(0); + expect($xpath->query('.//button[@data-bs-target="#bumdes-summary-extra-units"]', $card)->length)->toBe(1); + expect($xpath->query('//a[contains(@class, "nav-link") and @href="'.route('bumdes.rantai-nilai.index', 1).'"]')->length)->toBe(1); + } +}); + +test('bumdes summary uses the latest profile or tab update and handles a new unsaved profile', function () { + KelembagaanAktorBumdes::create(['pengurus_bumdes_id' => 1]); + DB::table('bumdes_pengurus')->where('id', 1)->update(['updated_at' => '2026-01-01 08:00:00']); + DB::table('bumdes_kelembagaan_aktor')->where('pengurus_bumdes_id', 1)->update(['updated_at' => '2026-02-02 09:00:00']); + $summary = ($this->bumdesAction)('edit', null, '1')->getData()['ringkasanProfil']; + expect($summary['diperbarui']->toDateTimeString())->toBe('2026-02-02 09:00:00') + ->and($summary['keaktifan']['label'])->toBe('Belum terisi') + ->and($summary['unit_usaha'])->toBe([]); + DB::table('bumdes_pengurus')->where('id', 1)->update(['updated_at' => '2026-03-03 10:00:00']); + expect(($this->bumdesAction)('edit', null, '1')->getData()['ringkasanProfil']['diperbarui']->toDateTimeString())->toBe('2026-03-03 10:00:00'); + $view = ($this->bumdesAction)('create'); + $summary = $view->getData()['ringkasanProfil']; + expect($summary['judul'])->toBe('Profil BUMDes Baru')->and($summary['unit_usaha'])->toBe([]) + ->and($summary['responden'])->toBe('Belum terisi')->and($summary['diperbarui'])->toBeNull(); + expect($view->render())->toContain('Belum tersimpan'); +});