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