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') +
+ + {{-- Ringkasan utama --}} +
+
+
+
+
+
+
+ Total Petani +
+ +
+ {{ number_format($totalPetani, 0, ',', '.') }} + + + petani + +
+
+ +
+ + + +
+
+ + + Seluruh petani yang sudah masuk ke sistem + +
+
+
+ +
+
+
+
+
+
+ Total Luas Lahan +
+ +
+ {{ number_format($totalLuasLahan, 2, ',', '.') }} + + + ha + +
+
+ +
+ + + +
+
+ + + Akumulasi luas seluruh lahan petani + +
+
+
+ +
+
+
+
+
+
+ Total Tanaman +
+ +
+ {{ number_format($totalJumlahTanaman, 0, ',', '.') }} + + + tanaman + +
+
+ +
+ + + +
+
+ + + Akumulasi jumlah tanaman seluruh lahan + +
+
+
+
+ +
+ {{-- Kolom utama --}} +
+ {{-- Progres enumerator --}} +
+
+
+ + Progres per Enumerator +
+ + + {{ now()->format('M Y') }} + +
+ +
+ @php + $maxDone = $enumerators->max('done') ?: 1; + @endphp + + @if ($enumerators->isNotEmpty()) +
+ @foreach ($enumerators as $e) + @php + $pct = round(($e['done'] / $maxDone) * 100); + @endphp + +
+
+ {{ strtoupper(substr($e['name'], 0, 2)) }} +
+ +
+
+ + {{ $e['name'] }} + + + + {{ number_format($e['done'], 0, ',', '.') }} survei + +
+ +
+
+
+
+
+
+
+ @endforeach +
+ @else +
+ Belum ada data enumerator. +
+ @endif +
+
+ + {{-- Progres data petani --}} +
+
+
+ + Progres Data Petani +
+ + + {{ $progressPetani->count() }} petani + +
+ +
+ + + + + + + + + + + + + + + + + + + + + @forelse ($progressPetani as $petani) + @php + $barColor = match (true) { + $petani['percent'] === 100 => '#28a745', + $petani['percent'] >= 50 => '#007bff', + $petani['percent'] >= 20 => '#fd7e14', + default => '#dc3545', + }; + + $initial = collect(explode(' ', trim($petani['name']))) + ->filter() + ->take(2) + ->map(fn ($word) => strtoupper(substr($word, 0, 1))) + ->implode(''); + @endphp + + + + + @foreach ($petani['status'] as $status) + + @endforeach + + + + + + @empty + + + + @endforelse + +
+ Petani + + Lahan + + Praktik Budidaya + + Sosial Ekonomi + + Akses Pasar + + Progres + + Status +
+
+
+ {{ $initial ?: 'PT' }} +
+ + + {{ $petani['name'] }} + +
+
+ @if ($status['done']) + + + + @else + + + + @endif + +
+
+
+
+ + + {{ $petani['percent'] }}% + +
+
+ @if ($petani['percent'] === 100) + + Selesai + + @elseif ($petani['percent'] > 0) + + Sedang diisi + + @else + + Draft + + @endif +
+ Belum ada data petani. +
+
+
+
+ + {{-- Kolom samping --}} +
+ {{-- Distribusi komoditas petani --}} +
+
+
+ + Distribusi Komoditas +
+
+ +
+ @if ($komoditas->isNotEmpty()) + @php + $donutOff = 0; + @endphp + +
+ + + + @foreach ($komoditas as $k) + + + @php + $donutOff += $k['arc']; + @endphp + @endforeach + + + {{ $totalPetani }} + + + + petani + + +
+ + @foreach ($komoditas as $k) +
+
+ + + {{ $k['label'] }} + + + + {{ number_format($k['total'], 0, ',', '.') }} + + + {{ $k['pct'] }}% + + +
+ @endforeach + @else +
+ Belum ada data distribusi komoditas. +
+ @endif +
+
+ + {{-- Jumlah tanaman per komoditas --}} +
+
+
+ + Tanaman per Komoditas +
+ + + {{ number_format($totalJumlahTanaman, 0, ',', '.') }} + +
+ +
+ @if ($tanamanPerKomoditas->isNotEmpty()) + @foreach ($tanamanPerKomoditas as $tanaman) +
+ + {{ $tanaman['label'] }} + + +
+
+
+ + + {{ number_format($tanaman['total'], 0, ',', '.') }} + +
+ @endforeach + @else +
+ Belum ada data jumlah tanaman. +
+ @endif +
+
+ + {{-- Pelatihan GAP --}} +
+
+
+ + Pelatihan GAP +
+ + + {{ number_format($totalPetaniPelatihan, 0, ',', '.') }} petani + +
+ +
+ @if ($pelatihanGap->isNotEmpty()) +
+ @foreach ($pelatihanGap as $pelatihan) +
+
+ +
+ +
+
+ + {{ $pelatihan['label'] }} + + + + {{ $pelatihan['total'] }} + +
+
+
+ @endforeach +
+ +
+ + + Satu petani dapat tercatat pada lebih dari satu jenis pelatihan. + + @else +
+ Belum ada data pelatihan. +
+ @endif +
+
+ + {{-- Sertifikasi --}} +
+
+
+
+
+ + + +
+ +
+
+ Petani Bersertifikasi +
+ +
+ + {{ number_format( + $totalPetaniBersertifikat, + 0, + ',', + '.' + ) }} + + + + petani + +
+
+
+ + + {{ number_format( + $totalPetaniSertifikatAktif, + 0, + ',', + '.' + ) }} + aktif + +
+ +
+ + + Petani unik yang memiliki minimal satu riwayat sertifikasi. + +
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/layout/sidebar.blade.php b/resources/views/layout/sidebar.blade.php index db7bfd7..f3ec5cc 100644 --- a/resources/views/layout/sidebar.blade.php +++ b/resources/views/layout/sidebar.blade.php @@ -51,9 +51,9 @@ @if(in_array($userRole, ['super_admin', 'admin', 'fasilitator']) && env('APP_NAME') == 'AgroMonev') diff --git a/routes/web.php b/routes/web.php index c07257e..58e3be9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -16,7 +16,7 @@ Route::get('/', function() { $userRole = auth()->check() ? (auth()->user()->role instanceof \App\Enums\Role ? auth()->user()->role->value : auth()->user()->role) : null; if ($userRole == 'fasilitator') { - return redirect()->route('dashboard.survey'); + return redirect()->route('dashboard.survey-petani'); } return redirect()->route('dashboard.produktivitas'); })->name('home'); @@ -24,6 +24,8 @@ Route::get('/', function() { Route::middleware('auth') ->group(function () { + Route::get('/dashboard/survey-petani', [App\Http\Controllers\Web\Dashboard\SurveyPetaniController::class, 'index'])->name('dashboard.survey-petani'); + Route::get('/tekad', function () { return view('dashboard.dashboard'); })->name('dashboard.survey');