colors(); // Progres per enumerator $enumerators = User::query() ->where('role', 'fasilitator') ->withCount([ 'petaniDibuat as done', ]) ->orderByDesc('done') ->get() ->map(function ($enumerator) use ($colors) { $color = $colors[array_rand($colors)]; return [ 'name' => $enumerator->name, 'done' => (int) $enumerator->done, 'color' => $color['color'], 'bg' => $color['bg'], 'tx' => $color['tx'], ]; }) ->values(); // Tabel dan nama model $petaniTable = (new Petani())->getTable(); $komoditasTable = (new Komoditas())->getTable(); $lahanTable = (new Lahan())->getTable(); // Distribusi petani berdasarkan komoditas $komoditasCollection = DB::table("{$petaniTable} as p") ->leftJoin("{$komoditasTable} as k", 'k.id', '=', 'p.komoditas_id') ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as nama") ->selectRaw('COUNT(p.id) as petani_count') ->where('k.is_utama', true) ->groupBy('k.id', 'k.nama') ->orderByDesc('petani_count') ->get(); // $totalPetani = (int) $komoditasCollection->sum('petani_count'); $totalPetani = Petani::count(); $circumference = 2 * pi() * 46; $komoditas = $komoditasCollection ->map(function ($item, $index) use ( $colors, $totalPetani, $circumference ) { $color = $colors[$index % count($colors)]; $ratio = $totalPetani > 0 ? (int) $item->petani_count / $totalPetani : 0; return [ 'label' => $item->nama, 'total' => (int) $item->petani_count, 'pct' => round($ratio * 100, 1), 'arc' => $ratio * $circumference, 'color' => $color['color'], ]; }) ->values(); // Progres pengisian data per petani $progressPetani = Petani::query() ->select(['id', 'nama', 'komoditas_id']) ->with([ 'komoditas:id,nama', 'lahans', 'praktikBudidaya', 'sosialEkonomi', 'aksesPasar', ]) ->orderBy('nama') ->get() ->map(function ($petani) { $status = [ 'lahan' => $this->moduleStatus( 'Lahan', $this->hasCompleteLahan($petani) ), 'praktik_budidaya' => $this->moduleStatus( 'Praktik Budidaya', $this->hasCompletePraktikBudidaya($petani->praktikBudidaya) ), 'sosial_ekonomi' => $this->moduleStatus( 'Sosial Ekonomi', $this->hasCompleteSosialEkonomi($petani->sosialEkonomi) ), 'akses_pasar' => $this->moduleStatus( 'Akses Pasar', $this->hasCompleteAksesPasar($petani->aksesPasar) ), ]; $totalModul = count($status); $modulSelesai = collect($status) ->where('done', true) ->count(); return [ 'id' => $petani->id, 'name' => $petani->nama, 'komoditas' => $petani->komoditas?->nama ?? 'Belum ditentukan', 'status' => $status, 'done' => $modulSelesai, 'total' => $totalModul, 'percent' => $totalModul > 0 ? (int) round(($modulSelesai / $totalModul) * 100) : 0, ]; }) ->values(); // Ringkasan lahan // $totalLuasLahan = ((float) Lahan::query() // ->sum('luas_lahan')) / 10000; $totalLuasLahan = Lahan::has('petani')->sum('luas_lahan'); // Jumlah tanaman berdasarkan komoditas petani $tanamanTable = (new Tanaman())->getTable(); $tanamanPerKomoditasCollection = DB::table("{$tanamanTable} as t") ->join("{$lahanTable} as l", 'l.id', '=', 't.lahan_id') ->join("{$petaniTable} as p", 'p.id', '=', 'l.petani_id') ->leftJoin("{$komoditasTable} as k", 'k.id', '=', 't.komoditas_id') ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") ->selectRaw('COUNT(t.id) as total') ->where('k.is_utama', true) ->whereNull('t.deleted_at') ->whereNull('l.deleted_at') ->whereNull('p.deleted_at') ->groupBy('k.id', 'k.nama') ->orderByDesc('total') ->get(); $totalJumlahTanaman = (int) $tanamanPerKomoditasCollection->sum('total'); $tanamanPerKomoditas = $tanamanPerKomoditasCollection ->map(function ($item, $index) use ($colors, $totalJumlahTanaman) { $color = $colors[$index % count($colors)]; $total = (int) $item->total; return [ 'label' => $item->label, 'total' => $total, 'pct' => $totalJumlahTanaman > 0 ? round(($total / $totalJumlahTanaman) * 100, 1) : 0, 'color' => $color['color'], 'bg' => $color['bg'], 'tx' => $color['tx'], ]; }) ->values(); // Distribusi peserta pelatihan GAP $gapOptions = collect($this->pelatihanGapOptions()); $knownGapOptions = $gapOptions->reject(function ($label, $value) { return $this->normalizeText((string) $value) === 'lainnya' || $this->normalizeText((string) $label) === 'lainnya'; }); $gapLookup = []; foreach ($knownGapOptions as $value => $label) { $gapLookup[$this->normalizeText((string) $value)] = (string) $label; $gapLookup[$this->normalizeText((string) $label)] = (string) $label; } $pelatihanPairs = SurveyPelatihan::query() ->whereNotNull('petani_id') ->whereNotNull('jenis_pelatihan') ->where('jenis_pelatihan', '!=', '') ->get([ 'petani_id', 'jenis_pelatihan', ]) ->map(function ($row) use ($gapLookup) { $normalized = $this->normalizeText((string) $row->jenis_pelatihan); return [ 'petani_id' => $row->petani_id, 'label' => $gapLookup[$normalized] ?? 'Lainnya', ]; }) ->unique(fn ($item) => $item['petani_id'] . '|' . $item['label']) ->values(); $pelatihanCounts = $pelatihanPairs->countBy('label'); $pelatihanLabels = $knownGapOptions ->values() ->unique() ->push('Lainnya') ->values(); $pelatihanGap = $pelatihanLabels ->map(function ($label, $index) use ($pelatihanCounts, $colors) { $color = $colors[$index % count($colors)]; return [ 'label' => $label, 'total' => (int) $pelatihanCounts->get($label, 0), 'color' => $color['color'], 'bg' => $color['bg'], 'tx' => $color['tx'], ]; }) ->values(); $totalPetaniPelatihan = $pelatihanPairs ->pluck('petani_id') ->unique() ->count(); // Sertifikasi $totalPetaniBersertifikat = SurveySertifikasi::query() ->whereNotNull('petani_id') ->distinct() ->count('petani_id'); $totalPetaniSertifikatAktif = SurveySertifikasi::query() ->whereNotNull('petani_id') ->where('is_aktif', true) ->distinct() ->count('petani_id'); return view('dashboard.survey-petani', [ 'enumerators' => $enumerators, 'komoditas' => $komoditas, 'totalPetani' => $totalPetani, 'circumference' => $circumference, 'progressPetani' => $progressPetani, 'totalLuasLahan' => $totalLuasLahan, 'totalJumlahTanaman' => $totalJumlahTanaman, 'tanamanPerKomoditas' => $tanamanPerKomoditas, 'pelatihanGap' => $pelatihanGap, 'totalPetaniPelatihan' => $totalPetaniPelatihan, 'totalPetaniBersertifikat' => $totalPetaniBersertifikat, 'totalPetaniSertifikatAktif' => $totalPetaniSertifikatAktif, ]); } private function moduleStatus(string $label, bool $done): array { return [ 'label' => $label, 'done' => $done, ]; } private function hasCompleteLahan(Petani $petani): bool { return $petani->lahans->contains(function ($lahan) { return $this->allFieldsFilled($lahan, [ 'luas_lahan', 'komoditas_id', 'jenis_komoditas_id', 'umur_tanaman', 'kondisi_tanaman', ]); }); } private function hasCompletePraktikBudidaya(?Model $praktikBudidaya): bool { return $this->allFieldsFilled($praktikBudidaya, [ 'frekuensi_pemangkasan_utama', 'pemberian_pupuk', 'metode_penanganan_opt', 'frekuensi_sanitasi_lahan', 'penanganan_gulma', 'kondisi_pengelolaan_air', ]); } private function hasCompleteSosialEkonomi(?Model $sosialEkonomi): bool { return $this->allFieldsFilled($sosialEkonomi, [ 'skor_periode_paceklik', 'skor_kemampuan_menabung', 'skor_kecukupan_penghasilan', 'skor_kejelasan_status_lahan', 'kemauan_mencoba_praktik_baru', 'pemahaman_mutu_harga', 'panutan_petani_sukses', 'kemauan_ikut_program', 'orientasi_rencana_jangka_panjang', 'keaktifan_pertemuan_kelompok', 'peran_dalam_kelompok', 'manfaat_kelompok_tani', 'status_rekening_tabungan', 'pengalaman_kredit_kebun', 'kondisi_aset_produktif', 'keterlibatan_program_keuangan_formal', 'keterikatan_hutang_panen', 'praktik_ijon', 'kebebasan_memilih_pembeli', ]); } private function hasCompleteAksesPasar(?Model $aksesPasar): bool { if (! $this->allFieldsFilled($aksesPasar, [ 'skor_pengalaman', 'praktik_fermentasi', 'skor_kesadaran_premium', 'tingkat_kepercayaan_pembeli_baru', ])) { return false; } $skorPengalaman = (int) $aksesPasar->getAttribute('skor_pengalaman'); if ( $skorPengalaman !== SkorPengalaman::BELUM_PERNAH->value && ! $this->valueIsFilled($aksesPasar->getAttribute('saluran_terdahulu')) ) { return false; } if ( in_array($skorPengalaman, [ SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value, SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value, ], true) && ! $this->valueIsFilled($aksesPasar->getAttribute('alasan_berhenti')) ) { return false; } if ( in_array( SaluranTerdahulu::LAINNYA->value, $aksesPasar->getAttribute('saluran_terdahulu') ?? [], true ) && ! $this->valueIsFilled($aksesPasar->getAttribute('saluran_terdahulu_lainnya')) ) { return false; } if ( $aksesPasar->getAttribute('praktik_fermentasi') !== PraktikFermentasi::BELUM_PERNAH->value && ! $this->allFieldsFilled($aksesPasar, [ 'proses_fermentasi', 'model_fermentasi_memungkinkan', ]) ) { return false; } return true; } private function allFieldsFilled(?Model $model, array $fields): bool { if (! $model) { return false; } foreach ($fields as $field) { if (! $this->valueIsFilled($model->getAttribute($field))) { return false; } } return true; } private function valueIsFilled(mixed $value): bool { if ($value instanceof BackedEnum) { $value = $value->value; } if (is_array($value)) { return collect($value) ->contains(fn ($item) => $this->valueIsFilled($item)); } return ! is_null($value) && $value !== ''; } private function pelatihanGapOptions(): array { $enumClass = self::GAP_ENUM_CLASS; if (enum_exists($enumClass)) { if (method_exists($enumClass, 'options')) { return collect($enumClass::options()) ->mapWithKeys(function ($option, $key) { if (is_array($option)) { $value = (string) ($option['value'] ?? $key); $label = (string) ( $option['label'] ?? $option['name'] ?? $option['value'] ?? $key ); return [$value => $label]; } return [(string) $key => (string) $option]; }) ->all(); } return collect($enumClass::cases()) ->mapWithKeys(function ($case) { $value = $case instanceof BackedEnum ? (string) $case->value : (string) $case->name; $label = method_exists($case, 'label') ? (string) $case->label() : Str::headline($case->name); return [$value => $label]; }) ->all(); } return [ 'pemangkasan_perawatan_tanaman' => 'Pemangkasan/perawatan tanaman', 'pemupukan_nutrisi_tanah' => 'Pemupukan/nutrisi tanah', 'pengendalian_hama_penyakit' => 'Pengendalian hama & penyakit', 'praktik_berkebun_baik_gap' => 'Praktik Berkebun yang Baik (GAP)', 'pengolahan_pascapanen' => 'Pengolahan/pascapanen', 'penyerbukan_manual_vanili' => 'Penyerbukan manual (vanili)', 'lainnya' => 'Lainnya', ]; } private function normalizeText(string $value): string { return Str::lower(Str::squish($value)); } private function colors(): array { return [ [ 'color' => '#6FD68F', 'bg' => 'rgba(111,214,143,.24)', 'tx' => '#6FD68F', ], [ 'color' => '#5FC7D6', 'bg' => 'rgba(95,199,214,.24)', 'tx' => '#5FC7D6', ], [ 'color' => '#FFB46F', 'bg' => 'rgba(255,180,111,.26)', 'tx' => '#FFB46F', ], [ 'color' => '#F47C86', 'bg' => 'rgba(244,124,134,.26)', 'tx' => '#F47C86', ], [ 'color' => '#9D8EF0', 'bg' => 'rgba(157,142,240,.26)', 'tx' => '#9D8EF0', ], [ 'color' => '#65D8BD', 'bg' => 'rgba(101,216,189,.26)', 'tx' => '#65D8BD', ], ]; } }