524 lines
17 KiB
PHP
524 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Web\Dashboard;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
use App\Models\User;
|
|
use App\Models\Pertanian\Petani;
|
|
use App\Models\Pertanian\Komoditas;
|
|
use App\Models\Pertanian\Lahan;
|
|
use App\Models\Pertanian\Tanaman;
|
|
use App\Models\Pertanian\SurveyPelatihan;
|
|
use App\Models\Pertanian\SurveySertifikasi;
|
|
use App\Enums\AksesPasar\PraktikFermentasi;
|
|
use App\Enums\AksesPasar\SaluranTerdahulu;
|
|
use App\Enums\AksesPasar\SkorPengalaman;
|
|
|
|
use BackedEnum;
|
|
|
|
class SurveyPetaniController extends Controller
|
|
{
|
|
private const GAP_ENUM_CLASS = 'App\\Enums\\PendidikanPelatihan\\PelatihanGap';
|
|
public function index()
|
|
{
|
|
$colors = $this->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',
|
|
],
|
|
];
|
|
}
|
|
}
|