diff --git a/app/Enums/PendidikanPelatihan/PelatihanGAP.php b/app/Enums/PendidikanPelatihan/PelatihanGAP.php new file mode 100644 index 0000000..3b13a3d --- /dev/null +++ b/app/Enums/PendidikanPelatihan/PelatihanGAP.php @@ -0,0 +1,26 @@ +value; + } + + public static function options(): array + { + return collect(self::cases())->map(fn ($case) => [ + 'value' => $case->value, + 'label' => $case->label(), + ])->toArray(); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Web/Dashboard/SurveyPetaniController.php b/app/Http/Controllers/Web/Dashboard/SurveyPetaniController.php new file mode 100644 index 0000000..eaf892c --- /dev/null +++ b/app/Http/Controllers/Web/Dashboard/SurveyPetaniController.php @@ -0,0 +1,369 @@ +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') + ->groupBy('k.id', 'k.nama') + ->orderByDesc('petani_count') + ->get(); + + $totalPetani = (int) $komoditasCollection->sum('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', + ]) + ->withExists([ + 'lahans as has_lahan', + 'praktikBudidaya as has_praktik_budidaya', + 'sosialEkonomi as has_sosial_ekonomi', + 'aksesPasar as has_akses_pasar', + ]) + ->orderBy('nama') + ->get() + ->map(function ($petani) { + $status = [ + 'lahan' => [ + 'label' => 'Lahan', + 'done' => (bool) $petani->has_lahan, + ], + 'praktik_budidaya' => [ + 'label' => 'Praktik Budidaya', + 'done' => (bool) $petani->has_praktik_budidaya, + ], + 'sosial_ekonomi' => [ + 'label' => 'Sosial Ekonomi', + 'done' => (bool) $petani->has_sosial_ekonomi, + ], + 'akses_pasar' => [ + 'label' => 'Akses Pasar', + 'done' => (bool) $petani->has_akses_pasar, + ], + ]; + + $totalModul = count($status); + + $modulSelesai = collect($status) + ->where('done', true) + ->count(); + + return [ + 'id' => $petani->id, + 'name' => $petani->nama, + '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'); + + $totalJumlahTanaman = (int) Lahan::query() + ->sum('jumlah_tanaman'); + + // Jumlah tanaman berdasarkan komoditas petani + + $tanamanPerKomoditasCollection = DB::table("{$lahanTable} as l") + ->join("{$petaniTable} as p", 'p.id', '=', 'l.petani_id') + ->leftJoin("{$komoditasTable} as k", 'k.id', '=', 'p.komoditas_id') + ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") + ->selectRaw('COALESCE(SUM(l.jumlah_tanaman), 0) as total') + ->groupBy('k.id', 'k.nama') + ->orderByDesc('total') + ->get(); + + $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 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' => '#28a745', + 'bg' => 'rgba(40,167,69,.15)', + 'tx' => '#28a745', + ], + [ + 'color' => '#007bff', + 'bg' => 'rgba(0,123,255,.15)', + 'tx' => '#007bff', + ], + [ + 'color' => '#fd7e14', + 'bg' => 'rgba(253,126,20,.15)', + 'tx' => '#fd7e14', + ], + [ + 'color' => '#dc3545', + 'bg' => 'rgba(220,53,69,.15)', + 'tx' => '#dc3545', + ], + [ + 'color' => '#6f42c1', + 'bg' => 'rgba(111,66,193,.15)', + 'tx' => '#6f42c1', + ], + [ + 'color' => '#20c997', + 'bg' => 'rgba(32,201,151,.15)', + 'tx' => '#20c997', + ], + ]; + } +} diff --git a/app/Models/Pertanian/Komoditas.php b/app/Models/Pertanian/Komoditas.php index d40315b..fc044b0 100644 --- a/app/Models/Pertanian/Komoditas.php +++ b/app/Models/Pertanian/Komoditas.php @@ -5,8 +5,10 @@ namespace App\Models\Pertanian; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\HasMany; use App\Models\Master\JenisKomoditas; use App\Models\Master\Varietas; +use App\Models\Pertanian\Petani; class Komoditas extends Model { @@ -42,4 +44,9 @@ class Komoditas extends Model { return $this->hasMany(Varietas::class, 'komoditas_id'); } + + public function petani(): HasMany + { + return $this->hasMany(Petani::class, 'komoditas_id'); + } } diff --git a/app/Models/Pertanian/Petani.php b/app/Models/Pertanian/Petani.php index 6026e95..82db184 100644 --- a/app/Models/Pertanian/Petani.php +++ b/app/Models/Pertanian/Petani.php @@ -4,7 +4,9 @@ namespace App\Models\Pertanian; use App\Enums\Pendidikan; use App\Enums\Pernikahan; +use App\Models\User; use App\Models\Wilayah\DesaKelurahan; +use App\Models\Pertanian\Lahan; use App\Models\Pertanian\Komoditas; use App\Models\Pertanian\PraktikBudidaya; use App\Models\Pertanian\SurveyPanen; @@ -391,4 +393,9 @@ class Petani extends Model return 'Belum tersegmentasi, data belum lengkap'; } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 0fb0634..d34f3ed 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,8 +9,11 @@ use App\Enums\Role; use Illuminate\Notifications\Notifiable; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Database\Eloquent\Relations\HasMany; use Laravel\Sanctum\HasApiTokens; +use App\Models\Pertanian\Petani; + class User extends Authenticatable { use HasFactory, Notifiable, HasApiTokens, SoftDeletes; @@ -44,4 +47,9 @@ class User extends Authenticatable { return $this->hasOne(\App\Models\Profile::class); } + + public function petaniDibuat(): HasMany + { + return $this->hasMany(Petani::class, 'created_by'); + } } diff --git a/resources/views/dashboard/survey-petani.blade.php b/resources/views/dashboard/survey-petani.blade.php new file mode 100644 index 0000000..681e1b2 --- /dev/null +++ b/resources/views/dashboard/survey-petani.blade.php @@ -0,0 +1,821 @@ +@extends('layout.main') + +@push('style') + +@endpush + +@section('content') +
| + Petani + | + ++ Lahan + | + ++ Praktik Budidaya + | + ++ Sosial Ekonomi + | + ++ Akses Pasar + | + ++ Progres + | + ++ Status + | +
|---|---|---|---|---|---|---|
|
+
+
+
+ {{ $initial ?: 'PT' }}
+
+
+
+ {{ $petani['name'] }}
+
+ |
+
+ @foreach ($petani['status'] as $status)
+ + @if ($status['done']) + + + + @else + + + + @endif + | + @endforeach + +
+
+
+
+
+
+
+
+ {{ $petani['percent'] }}%
+
+ |
+
+ + @if ($petani['percent'] === 100) + + Selesai + + @elseif ($petani['percent'] > 0) + + Sedang diisi + + @else + + Draft + + @endif + | +|||
| + Belum ada data petani. + | +||||||