This commit is contained in:
2026-07-14 10:49:08 +07:00
8 changed files with 1244 additions and 4 deletions
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\PendidikanPelatihan;
enum PelatihanGAP: string
{
case PEMANGKASAN = 'Pemangkasan atau perawatan tanaman';
case PEMUPUKAN = 'Pemupukan atau nutrisi tanah';
case PENGENDALIAN = 'Pengendalian hama dan penyakit';
case PRAKTIK = 'Praktik Berkebun yang Baik (GAP)';
case PENGOLAHAN = 'Pengolahan/pascapanen';
case PENYERBUKAN = 'Penyerbukan manual (Vanili)';
public function label(): string
{
return $this->value;
}
public static function options(): array
{
return collect(self::cases())->map(fn ($case) => [
'value' => $case->value,
'label' => $case->label(),
])->toArray();
}
}
@@ -0,0 +1,369 @@
<?php
namespace App\Http\Controllers\Web\Dashboard;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
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\SurveyPelatihan;
use App\Models\Pertanian\SurveySertifikasi;
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')
->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',
],
];
}
}
+7
View File
@@ -5,8 +5,10 @@ namespace App\Models\Pertanian;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Models\Master\JenisKomoditas; use App\Models\Master\JenisKomoditas;
use App\Models\Master\Varietas; use App\Models\Master\Varietas;
use App\Models\Pertanian\Petani;
class Komoditas extends Model class Komoditas extends Model
{ {
@@ -42,4 +44,9 @@ class Komoditas extends Model
{ {
return $this->hasMany(Varietas::class, 'komoditas_id'); return $this->hasMany(Varietas::class, 'komoditas_id');
} }
public function petani(): HasMany
{
return $this->hasMany(Petani::class, 'komoditas_id');
}
} }
+7
View File
@@ -4,7 +4,9 @@ namespace App\Models\Pertanian;
use App\Enums\Pendidikan; use App\Enums\Pendidikan;
use App\Enums\Pernikahan; use App\Enums\Pernikahan;
use App\Models\User;
use App\Models\Wilayah\DesaKelurahan; use App\Models\Wilayah\DesaKelurahan;
use App\Models\Pertanian\Lahan;
use App\Models\Pertanian\Komoditas; use App\Models\Pertanian\Komoditas;
use App\Models\Pertanian\PraktikBudidaya; use App\Models\Pertanian\PraktikBudidaya;
use App\Models\Pertanian\SurveyPanen; use App\Models\Pertanian\SurveyPanen;
@@ -391,4 +393,9 @@ class Petani extends Model
return 'Belum tersegmentasi, data belum lengkap'; return 'Belum tersegmentasi, data belum lengkap';
} }
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
} }
+8
View File
@@ -9,8 +9,11 @@ use App\Enums\Role;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
use App\Models\Pertanian\Petani;
class User extends Authenticatable class User extends Authenticatable
{ {
use HasFactory, Notifiable, HasApiTokens, SoftDeletes; use HasFactory, Notifiable, HasApiTokens, SoftDeletes;
@@ -44,4 +47,9 @@ class User extends Authenticatable
{ {
return $this->hasOne(\App\Models\Profile::class); return $this->hasOne(\App\Models\Profile::class);
} }
public function petaniDibuat(): HasMany
{
return $this->hasMany(Petani::class, 'created_by');
}
} }
@@ -0,0 +1,821 @@
@extends('layout.main')
@push('style')
<style>
.tk-stat {
border-left: 4px solid transparent;
transition: transform .2s ease, box-shadow .2s ease;
}
.tk-stat:hover {
transform: translateY(-2px);
box-shadow: 0 .25rem 1rem rgba(34, 48, 62, .08);
}
.tk-stat.green { border-left-color: #28a745; }
.tk-stat.blue { border-left-color: #007bff; }
.tk-stat.amber { border-left-color: #fd7e14; }
.tk-stat-lbl {
font-size: 10px;
text-transform: uppercase;
letter-spacing: .7px;
font-weight: 600;
}
.tk-stat-val {
font-size: 20px;
font-weight: 800;
line-height: 1;
}
.tk-stat-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}
.tk-pb {
display: flex;
align-items: center;
gap: 6px;
}
.tk-pbt {
flex: 1;
height: 6px;
background: rgba(128, 128, 128, .15);
border-radius: 3px;
overflow: hidden;
}
.tk-pbf {
height: 100%;
border-radius: 3px;
}
.tk-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.tk-bar:last-child {
margin-bottom: 0;
}
.tk-bar-lbl {
width: 105px;
font-size: 11px;
flex-shrink: 0;
}
.tk-bar-trk {
flex: 1;
height: 7px;
background: rgba(128, 128, 128, .15);
border-radius: 4px;
overflow: hidden;
}
.tk-bar-fil {
height: 100%;
border-radius: 4px;
}
.tk-bar-val {
font-size: 11px;
min-width: 58px;
}
.tk-erow {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 0;
border-bottom: 1px solid rgba(128, 128, 128, .1);
}
.tk-erow:last-child {
border-bottom: none;
}
.tk-av {
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
flex-shrink: 0;
}
.tk-leg {
display: flex;
align-items: center;
gap: 7px;
margin-bottom: 7px;
font-size: 12px;
}
.tk-leg:last-child {
margin-bottom: 0;
}
.tk-leg-dot {
width: 9px;
height: 9px;
border-radius: 50%;
flex-shrink: 0;
}
.tk-leg-val {
margin-left: auto;
font-weight: 700;
font-size: 12px;
}
.tk-divider {
height: 1px;
background: rgba(128, 128, 128, .1);
margin: 12px 0;
}
.tk-donut-num {
fill: var(--bs-body-color);
font-size: 15px;
font-weight: 800;
}
.tk-donut-sub {
fill: var(--bs-secondary-color, #6c757d);
font-size: 9px;
}
.enumerator-scroll {
max-height: 244px;
overflow-y: auto;
overflow-x: hidden;
padding-right: 5px;
}
.progress-petani-scroll {
max-height: 575px;
overflow-y: auto;
overflow-x: auto;
}
.progress-petani-scroll thead th {
position: sticky;
top: 0;
z-index: 3;
background: var(--bs-table-bg, #f8f9fa);
white-space: nowrap;
}
.tk-training-scroll {
max-height: 330px;
overflow-y: auto;
overflow-x: hidden;
padding-right: 5px;
}
.enumerator-scroll::-webkit-scrollbar,
.progress-petani-scroll::-webkit-scrollbar,
.tk-training-scroll::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.enumerator-scroll::-webkit-scrollbar-thumb,
.progress-petani-scroll::-webkit-scrollbar-thumb,
.tk-training-scroll::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, .28);
border-radius: 10px;
}
.enumerator-scroll::-webkit-scrollbar-track,
.progress-petani-scroll::-webkit-scrollbar-track,
.tk-training-scroll::-webkit-scrollbar-track {
background: transparent;
}
</style>
@endpush
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
{{-- Ringkasan utama --}}
<div class="row g-3 mb-3">
<div class="col-md-4">
<div class="card tk-stat green h-100">
<div class="card-body">
<div class="tk-stat-top">
<div>
<div class="tk-stat-lbl text-muted mb-2">
Total Petani
</div>
<div class="tk-stat-val">
{{ number_format($totalPetani, 0, ',', '.') }}
<span class="text-muted fw-normal" style="font-size: 12px">
petani
</span>
</div>
</div>
<div class="avatar">
<span class="avatar-initial rounded bg-label-success">
<i class="ti tabler-users"></i>
</span>
</div>
</div>
<small class="text-muted">
Seluruh petani yang sudah masuk ke sistem
</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card tk-stat blue h-100">
<div class="card-body">
<div class="tk-stat-top">
<div>
<div class="tk-stat-lbl text-muted mb-2">
Total Luas Lahan
</div>
<div class="tk-stat-val">
{{ number_format($totalLuasLahan, 2, ',', '.') }}
<span class="text-muted fw-normal" style="font-size: 12px">
ha
</span>
</div>
</div>
<div class="avatar">
<span class="avatar-initial rounded bg-label-primary">
<i class="ti tabler-map-2"></i>
</span>
</div>
</div>
<small class="text-muted">
Akumulasi luas seluruh lahan petani
</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card tk-stat amber h-100">
<div class="card-body">
<div class="tk-stat-top">
<div>
<div class="tk-stat-lbl text-muted mb-2">
Total Tanaman
</div>
<div class="tk-stat-val">
{{ number_format($totalJumlahTanaman, 0, ',', '.') }}
<span class="text-muted fw-normal" style="font-size: 12px">
tanaman
</span>
</div>
</div>
<div class="avatar">
<span class="avatar-initial rounded bg-label-warning">
<i class="ti tabler-trees"></i>
</span>
</div>
</div>
<small class="text-muted">
Akumulasi jumlah tanaman seluruh lahan
</small>
</div>
</div>
</div>
</div>
<div class="row g-3">
{{-- Kolom utama --}}
<div class="col-xl-8">
{{-- Progres enumerator --}}
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">
<i class="ti tabler-chart-bar me-2 text-success"></i>
Progres per Enumerator
</h6>
<span class="badge bg-label-secondary text-secondary">
{{ now()->format('M Y') }}
</span>
</div>
<div class="card-body">
@php
$maxDone = $enumerators->max('done') ?: 1;
@endphp
@if ($enumerators->isNotEmpty())
<div class="enumerator-scroll">
@foreach ($enumerators as $e)
@php
$pct = round(($e['done'] / $maxDone) * 100);
@endphp
<div class="tk-erow">
<div
class="tk-av rounded"
style="
width: 36px;
height: 36px;
background: {{ $e['bg'] }};
color: {{ $e['tx'] }};
"
>
{{ strtoupper(substr($e['name'], 0, 2)) }}
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between mb-1">
<span class="fw-semibold" style="font-size: 13px">
{{ $e['name'] }}
</span>
<small class="text-muted">
{{ number_format($e['done'], 0, ',', '.') }} survei
</small>
</div>
<div class="tk-pb">
<div class="tk-pbt">
<div
class="tk-pbf"
style="
width: {{ $pct }}%;
background: {{ $e['color'] }};
"
></div>
</div>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center text-muted py-3">
Belum ada data enumerator.
</div>
@endif
</div>
</div>
{{-- Progres data petani --}}
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">
<i class="ti tabler-list-check me-2 text-success"></i>
Progres Data Petani
</h6>
<span class="badge bg-label-secondary text-secondary">
{{ $progressPetani->count() }} petani
</span>
</div>
<div class="table-responsive progress-petani-scroll">
<table class="table table-hover mb-0" style="font-size: 12.5px">
<thead class="table-light">
<tr>
<th style="font-size:10px;letter-spacing:.7px;text-transform:uppercase">
Petani
</th>
<th class="text-center"
style="font-size:10px;letter-spacing:.7px;text-transform:uppercase">
Lahan
</th>
<th class="text-center"
style="font-size:10px;letter-spacing:.7px;text-transform:uppercase">
Praktik Budidaya
</th>
<th class="text-center"
style="font-size:10px;letter-spacing:.7px;text-transform:uppercase">
Sosial Ekonomi
</th>
<th class="text-center"
style="font-size:10px;letter-spacing:.7px;text-transform:uppercase">
Akses Pasar
</th>
<th style="
min-width: 140px;
font-size: 10px;
letter-spacing: .7px;
text-transform: uppercase;
">
Progres
</th>
<th style="font-size:10px;letter-spacing:.7px;text-transform:uppercase">
Status
</th>
</tr>
</thead>
<tbody>
@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
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<div
class="tk-av rounded"
style="
width: 28px;
height: 28px;
background: rgba(40,167,69,.15);
color: #28a745;
"
>
{{ $initial ?: 'PT' }}
</div>
<span class="fw-semibold">
{{ $petani['name'] }}
</span>
</div>
</td>
@foreach ($petani['status'] as $status)
<td class="text-center">
@if ($status['done'])
<span
class="badge rounded-pill bg-label-success text-success"
title="{{ $status['label'] }} sudah diisi"
>
<i class="ti tabler-check"></i>
</span>
@else
<span
class="badge rounded-pill bg-label-secondary text-secondary"
title="{{ $status['label'] }} belum diisi"
>
<i class="ti tabler-minus"></i>
</span>
@endif
</td>
@endforeach
<td style="min-width: 140px">
<div class="tk-pb">
<div class="tk-pbt">
<div
class="tk-pbf"
style="
width: {{ $petani['percent'] }}%;
background: {{ $barColor }};
"
></div>
</div>
<small
class="text-muted"
style="min-width: 30px; text-align: right"
>
{{ $petani['percent'] }}%
</small>
</div>
</td>
<td>
@if ($petani['percent'] === 100)
<span class="badge bg-label-success text-success">
Selesai
</span>
@elseif ($petani['percent'] > 0)
<span class="badge bg-label-primary text-primary">
Sedang diisi
</span>
@else
<span class="badge bg-label-warning text-warning">
Draft
</span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center text-muted py-4">
Belum ada data petani.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
{{-- Kolom samping --}}
<div class="col-xl-4">
{{-- Distribusi komoditas petani --}}
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0 fw-bold">
<i class="ti tabler-plant me-2 text-success"></i>
Distribusi Komoditas
</h6>
</div>
<div class="card-body">
@if ($komoditas->isNotEmpty())
@php
$donutOff = 0;
@endphp
<div class="d-flex justify-content-center py-2">
<svg
width="120"
height="120"
viewBox="0 0 120 120"
role="img"
aria-label="Distribusi komoditas petani"
>
<circle
cx="60"
cy="60"
r="46"
fill="none"
stroke="rgba(128,128,128,.15)"
stroke-width="16"
/>
@foreach ($komoditas as $k)
<circle
cx="60"
cy="60"
r="46"
fill="none"
stroke="{{ $k['color'] }}"
stroke-width="16"
stroke-dasharray="
{{ $k['arc'] }}
{{ $circumference - $k['arc'] }}
"
stroke-dashoffset="{{ -$donutOff }}"
transform="rotate(-90 60 60)"
/>
@php
$donutOff += $k['arc'];
@endphp
@endforeach
<text
x="60"
y="56"
text-anchor="middle"
class="tk-donut-num"
>
{{ $totalPetani }}
</text>
<text
x="60"
y="70"
text-anchor="middle"
class="tk-donut-sub"
>
petani
</text>
</svg>
</div>
@foreach ($komoditas as $k)
<div class="tk-leg">
<div
class="tk-leg-dot"
style="background: {{ $k['color'] }}"
></div>
<span class="text-muted">
{{ $k['label'] }}
</span>
<span class="tk-leg-val">
{{ number_format($k['total'], 0, ',', '.') }}
<span
class="text-muted fw-normal"
style="font-size: 10px"
>
{{ $k['pct'] }}%
</span>
</span>
</div>
@endforeach
@else
<div class="text-center text-muted py-4">
Belum ada data distribusi komoditas.
</div>
@endif
</div>
</div>
{{-- Jumlah tanaman per komoditas --}}
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">
<i class="ti tabler-trees me-2 text-primary"></i>
Tanaman per Komoditas
</h6>
<span class="badge bg-label-primary text-primary">
{{ number_format($totalJumlahTanaman, 0, ',', '.') }}
</span>
</div>
<div class="card-body">
@if ($tanamanPerKomoditas->isNotEmpty())
@foreach ($tanamanPerKomoditas as $tanaman)
<div class="tk-bar">
<span
class="tk-bar-lbl text-muted text-truncate"
title="{{ $tanaman['label'] }}"
>
{{ $tanaman['label'] }}
</span>
<div class="tk-bar-trk">
<div
class="tk-bar-fil"
style="
width: {{ $tanaman['pct'] }}%;
background: {{ $tanaman['color'] }};
"
></div>
</div>
<span class="tk-bar-val fw-semibold text-end">
{{ number_format($tanaman['total'], 0, ',', '.') }}
</span>
</div>
@endforeach
@else
<div class="text-center text-muted py-4">
Belum ada data jumlah tanaman.
</div>
@endif
</div>
</div>
{{-- Pelatihan GAP --}}
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">
<i class="ti tabler-school me-2 text-warning"></i>
Pelatihan GAP
</h6>
<span class="badge bg-label-warning text-warning">
{{ number_format($totalPetaniPelatihan, 0, ',', '.') }} petani
</span>
</div>
<div class="card-body">
@if ($pelatihanGap->isNotEmpty())
<div class="tk-training-scroll">
@foreach ($pelatihanGap as $pelatihan)
<div class="tk-erow">
<div
class="tk-av rounded"
style="
width: 34px;
height: 34px;
background: {{ $pelatihan['bg'] }};
color: {{ $pelatihan['tx'] }};
"
>
<i class="ti tabler-certificate"></i>
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-center gap-2">
<span
class="fw-semibold"
style="font-size: 12px"
>
{{ $pelatihan['label'] }}
</span>
<span
class="badge flex-shrink-0"
style="
background: {{ $pelatihan['bg'] }};
color: {{ $pelatihan['tx'] }};
"
>
{{ $pelatihan['total'] }}
</span>
</div>
</div>
</div>
@endforeach
</div>
<div class="tk-divider"></div>
<small class="text-muted">
Satu petani dapat tercatat pada lebih dari satu jenis pelatihan.
</small>
@else
<div class="text-center text-muted py-4">
Belum ada data pelatihan.
</div>
@endif
</div>
</div>
{{-- Sertifikasi --}}
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between gap-3">
<div class="d-flex align-items-center gap-3">
<div class="avatar">
<span class="avatar-initial rounded bg-label-success">
<i class="ti tabler-rosette-discount-check"></i>
</span>
</div>
<div>
<div class="tk-stat-lbl text-muted mb-1">
Petani Bersertifikasi
</div>
<div class="d-flex align-items-baseline gap-1">
<span class="tk-stat-val">
{{ number_format(
$totalPetaniBersertifikat,
0,
',',
'.'
) }}
</span>
<span class="text-muted" style="font-size: 12px">
petani
</span>
</div>
</div>
</div>
<span class="badge bg-label-success text-success">
{{ number_format(
$totalPetaniSertifikatAktif,
0,
',',
'.'
) }}
aktif
</span>
</div>
<div class="tk-divider"></div>
<small class="text-muted">
Petani unik yang memiliki minimal satu riwayat sertifikasi.
</small>
</div>
</div>
</div>
</div>
</div>
@endsection
+3 -3
View File
@@ -51,9 +51,9 @@
@if(in_array($userRole, ['super_admin', 'admin', 'fasilitator']) && env('APP_NAME') == 'AgroMonev') @if(in_array($userRole, ['super_admin', 'admin', 'fasilitator']) && env('APP_NAME') == 'AgroMonev')
<ul class="menu-sub"> <ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('dashboard.survey') ? 'active' : '' }}"> <li class="menu-item {{ request()->routeIs('dashboard.survey-petani') ? 'active' : '' }}">
<a href="{{ route('dashboard.survey') }}" class="menu-link"> <a href="{{ route('dashboard.survey-petani') }}" class="menu-link">
<div data-i18n="Survey">Survey</div> <div data-i18n="Survey Petani">Survey Petani</div>
</a> </a>
</li> </li>
</ul> </ul>
+3 -1
View File
@@ -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; $userRole = auth()->check() ? (auth()->user()->role instanceof \App\Enums\Role ? auth()->user()->role->value : auth()->user()->role) : null;
if ($userRole == 'fasilitator') { if ($userRole == 'fasilitator') {
return redirect()->route('dashboard.survey'); return redirect()->route('dashboard.survey-petani');
} }
return redirect()->route('dashboard.produktivitas'); return redirect()->route('dashboard.produktivitas');
})->name('home'); })->name('home');
@@ -24,6 +24,8 @@ Route::get('/', function() {
Route::middleware('auth') Route::middleware('auth')
->group(function () { ->group(function () {
Route::get('/dashboard/survey-petani', [App\Http\Controllers\Web\Dashboard\SurveyPetaniController::class, 'index'])->name('dashboard.survey-petani');
Route::get('/tekad', function () { Route::get('/tekad', function () {
return view('dashboard.dashboard'); return view('dashboard.dashboard');
})->name('dashboard.survey'); })->name('dashboard.survey');