Compare commits
11
Commits
df2b5a1312
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
013cb7b728 | ||
|
|
4ab745cfe5 | ||
|
|
c66eed56e2 | ||
|
|
8be0906a2e | ||
|
|
fcd79b7cfe | ||
|
|
bea9be8a39 | ||
|
|
5220b10de7 | ||
|
|
24725e3df1 | ||
|
|
60d32a88e9 | ||
|
|
4370d3c3d0 | ||
|
|
7699430301 |
@@ -3,6 +3,8 @@
|
||||
namespace App\Http\Controllers\Web\Dashboard;
|
||||
|
||||
use App\Enums\AksesPasar\PraktikFermentasi;
|
||||
use App\Enums\AksesPasar\SkorKesadaranPremium;
|
||||
use App\Enums\AksesPasar\SkorPengalaman;
|
||||
use App\Enums\KeparahanSerangan;
|
||||
use App\Enums\KondisiTanaman;
|
||||
use App\Enums\PenyakitTanaman;
|
||||
@@ -114,70 +116,196 @@ class SummaryController extends Controller
|
||||
'kecamatan_id',
|
||||
'desaKelurahan_id',
|
||||
'komoditas_id',
|
||||
'segmen',
|
||||
]),
|
||||
] + $filterData + $this->marketDashboardData($request));
|
||||
}
|
||||
|
||||
private function marketDashboardData(Request $request): array
|
||||
{
|
||||
if (! $request->filled('komoditas_id')) {
|
||||
return [
|
||||
'selectedKomoditas' => null,
|
||||
'priceCard' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$selectedKomoditas = Komoditas::query()
|
||||
->whereKey($request->komoditas_id)
|
||||
->first(['id', 'nama']);
|
||||
|
||||
if (! $selectedKomoditas) {
|
||||
return [
|
||||
'selectedKomoditas' => null,
|
||||
'priceCard' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$productCaption = $this->marketProductCaption($selectedKomoditas->nama);
|
||||
$productLabel = 'Buah segar hasil petik';
|
||||
$products = [
|
||||
['name' => 'Kakao', 'caption' => 'Harga Biji Kering', 'product' => 'Biji kering/Biji Kupas', 'variant' => 'success'],
|
||||
['name' => 'Vanili', 'caption' => 'Harga Buah Segar', 'product' => 'Buah segar hasil petik', 'variant' => 'primary'],
|
||||
['name' => 'Kopi', 'caption' => 'Harga Ceri Merah', 'product' => 'Buah segar hasil petik', 'variant' => 'warning'],
|
||||
['name' => 'Kemiri', 'caption' => 'Harga Gelondong', 'product' => 'Buah segar hasil petik', 'variant' => 'info'],
|
||||
];
|
||||
$commodities = Komoditas::query()
|
||||
->whereIn('nama', array_column($products, 'name'))
|
||||
->pluck('id', 'nama');
|
||||
|
||||
$petaniIds = $this->filteredPetaniIdsQuery($request);
|
||||
$priceQuery = DB::table('harga_jual_panen')
|
||||
$prices = DB::table('harga_jual_panen')
|
||||
->join('petani', 'petani.id', '=', 'harga_jual_panen.petani_id')
|
||||
->whereIn('harga_jual_panen.petani_id', $petaniIds)
|
||||
->whereIn('petani.komoditas_id', $commodities->values())
|
||||
->where('harga_jual_panen.harga_panen_terakhir', '>', 0)
|
||||
->where('harga_jual_panen.bentuk_produk', $productLabel);
|
||||
|
||||
$averagePrice = (float) (clone $priceQuery)->avg('harga_jual_panen.harga_panen_terakhir');
|
||||
$totalRecords = (int) (clone $priceQuery)->count();
|
||||
->whereIn('harga_jual_panen.bentuk_produk', array_unique(array_column($products, 'product')))
|
||||
->select('petani.komoditas_id', 'harga_jual_panen.bentuk_produk')
|
||||
->selectRaw('AVG(harga_jual_panen.harga_panen_terakhir) as average_price, COUNT(*) as records')
|
||||
->groupBy('petani.komoditas_id', 'harga_jual_panen.bentuk_produk')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'selectedKomoditas' => $selectedKomoditas,
|
||||
'priceCard' => [
|
||||
'value' => $averagePrice > 0
|
||||
? 'Rp '.number_format($averagePrice, 0, ',', '.')
|
||||
: 'Rp 0',
|
||||
'label' => 'Harga Jual',
|
||||
'caption' => $productCaption,
|
||||
'description' => 'Harga rata-rata buah segar hasil petik di tingkat petani',
|
||||
'records' => $totalRecords,
|
||||
'unit' => '/kg',
|
||||
'variant' => 'success',
|
||||
'icon' => 'tabler-cash',
|
||||
],
|
||||
'priceCards' => collect($products)->map(function (array $product) use ($commodities, $prices) {
|
||||
$commodityId = $commodities->get($product['name']);
|
||||
$price = $commodityId === null ? null : $prices->first(fn ($row) =>
|
||||
(int) $row->komoditas_id === (int) $commodityId
|
||||
&& $row->bentuk_produk === $product['product']
|
||||
);
|
||||
|
||||
return [
|
||||
'value' => 'Rp '.number_format((float) ($price?->average_price ?? 0), 0, ',', '.'),
|
||||
'label' => $product['name'].' - '.$product['caption'],
|
||||
'description' => 'Harga rata-rata panen terakhir di tingkat petani',
|
||||
'records' => (int) ($price?->records ?? 0),
|
||||
'unit' => '/kg',
|
||||
'variant' => $product['variant'],
|
||||
'icon' => 'tabler-cash',
|
||||
];
|
||||
})->all(),
|
||||
'marketCharts' => $this->marketChannelCharts($request),
|
||||
'priceTrend' => $this->marketPriceTrend($request),
|
||||
'premiumAwareness' => $this->marketPremiumAwareness($request),
|
||||
];
|
||||
}
|
||||
|
||||
private function marketProductCaption(string $komoditasName): string
|
||||
private function marketPriceTrend(Request $request): array
|
||||
{
|
||||
$normalized = str($komoditasName)->lower()->trim()->toString();
|
||||
$products = ['Buah segar hasil petik', 'Biji basah', 'Biji kering/Biji Kupas', 'Terfermentasi (Rp/kg)'];
|
||||
$periods = [
|
||||
'harga_dua_tahun_lalu' => 'Dua Tahun Lalu',
|
||||
'harga_tahun_lalu' => 'Tahun Lalu',
|
||||
'harga_panen_terakhir' => 'Panen Terakhir',
|
||||
];
|
||||
$commodities = Komoditas::query()
|
||||
->when($request->filled('komoditas_id'), fn ($query) => $query->where('id', $request->komoditas_id))
|
||||
->orderBy('nama')
|
||||
->get(['id', 'nama']);
|
||||
|
||||
return match (true) {
|
||||
str_contains($normalized, 'kakao') => 'Harga Buah Kakao',
|
||||
str_contains($normalized, 'vanili') => 'Harga Curing',
|
||||
str_contains($normalized, 'kopi') => 'Harga Ceri Merah',
|
||||
str_contains($normalized, 'kemiri') => 'Harga Gelondong',
|
||||
default => 'Harga Buah Segar',
|
||||
};
|
||||
$query = DB::table('harga_jual_panen')
|
||||
->join('petani', 'petani.id', '=', 'harga_jual_panen.petani_id')
|
||||
->whereIn('harga_jual_panen.petani_id', $this->filteredPetaniIdsQuery($request))
|
||||
->whereIn('petani.komoditas_id', $commodities->pluck('id'))
|
||||
->whereIn('harga_jual_panen.bentuk_produk', $products)
|
||||
->select('petani.komoditas_id', 'harga_jual_panen.bentuk_produk')
|
||||
->groupBy('petani.komoditas_id', 'harga_jual_panen.bentuk_produk');
|
||||
|
||||
foreach (array_keys($periods) as $field) {
|
||||
$query->selectRaw("AVG(CASE WHEN harga_jual_panen.{$field} > 0 THEN harga_jual_panen.{$field} END) as {$field}");
|
||||
}
|
||||
|
||||
$prices = $query->get()->groupBy('komoditas_id');
|
||||
|
||||
return [
|
||||
'periods' => array_values($periods),
|
||||
'commodities' => $commodities->map(function ($commodity) use ($products, $periods, $prices) {
|
||||
$rows = $prices->get($commodity->id, collect())->keyBy('bentuk_produk');
|
||||
|
||||
return [
|
||||
'id' => $commodity->id,
|
||||
'name' => $commodity->nama,
|
||||
'series' => collect($products)->map(function ($product) use ($periods, $rows) {
|
||||
$row = $rows->get($product);
|
||||
|
||||
return [
|
||||
'name' => $product === 'Terfermentasi (Rp/kg)' ? 'Terfermentasi' : $product,
|
||||
'data' => array_map(fn ($field) => isset($row->{$field}) ? round((float) $row->{$field}, 2) : null, array_keys($periods)),
|
||||
];
|
||||
})->all(),
|
||||
];
|
||||
})->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function marketChannelCharts(Request $request): array
|
||||
{
|
||||
$rows = DB::table('akses_pasar')
|
||||
->whereNull('deleted_at')
|
||||
->whereIn('petani_id', $this->filteredPetaniIdsQuery($request))
|
||||
->orderBy('id')
|
||||
->get(['petani_id', 'tujuan_penjualan', 'sifat_hubungan_bisnis', 'frekuensi_penjualan', 'skor_pengalaman'])
|
||||
->unique('petani_id');
|
||||
|
||||
$channels = ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Langsung ke pembeli/eksportir', 'Lainnya'];
|
||||
$relationships = [
|
||||
'Bebas, bisa pindah pembeli kapan saja' => 'Bebas pindah pembeli',
|
||||
'Langganan tetap, tapi tidak terikat' => 'Langganan tetap',
|
||||
'Berbasis hutang, harus jual ke sana karena ada pinjaman' => 'Berbasis hutang',
|
||||
'Titip jual,dibayar setelah produk terjual' => 'Titip jual',
|
||||
'Ada komitmen volume, sudah ada kesepakatan jumlah' => 'Komitmen volume',
|
||||
'Semi-kontrak, ada kesepakatan harga tertentu' => 'Semi-kontrak',
|
||||
];
|
||||
$timing = ['Langsung setelah panen', 'Ditumpuk dulu beberapa waktu', 'Saat butuh uang mendesak'];
|
||||
$experience = collect(SkorPengalaman::options())
|
||||
->mapWithKeys(fn ($option) => [$option['value'] => $option['label']])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'saluranPenjualan' => $this->marketResponseChart($rows, 'tujuan_penjualan', array_combine($channels, $channels), true),
|
||||
'hubunganPembeli' => $this->marketResponseChart($rows, 'sifat_hubungan_bisnis', $relationships),
|
||||
'waktuPenjualan' => $this->marketResponseChart($rows, 'frekuensi_penjualan', array_combine($timing, $timing)),
|
||||
'aksesSaluranLain' => $this->marketResponseChart($rows, 'skor_pengalaman', $experience),
|
||||
];
|
||||
}
|
||||
|
||||
private function marketPremiumAwareness(Request $request): array
|
||||
{
|
||||
$rows = DB::table('akses_pasar')
|
||||
->whereNull('deleted_at')
|
||||
->whereIn('petani_id', $this->filteredPetaniIdsQuery($request))
|
||||
->orderBy('id')
|
||||
->get(['petani_id', 'skor_kesadaran_premium'])
|
||||
->unique('petani_id');
|
||||
$options = collect(SkorKesadaranPremium::options())
|
||||
->mapWithKeys(fn ($option) => [$option['value'] => $option['label']])
|
||||
->all();
|
||||
$chart = $this->marketResponseChart($rows, 'skor_kesadaran_premium', $options);
|
||||
$chart['fullLabels'] = [
|
||||
'Belum pernah mendengar sama sekali',
|
||||
'Pernah mendengar tapi belum paham persyaratannya',
|
||||
'Pernah mendengar dan sedikit memahami persyaratannya',
|
||||
'Sudah sangat memahami persyaratannya, tapi belum pernah mengakses pasar premium',
|
||||
'Sudah mengakses dan mendapat harga premium secara konsisten',
|
||||
];
|
||||
|
||||
return $chart;
|
||||
}
|
||||
|
||||
private function marketResponseChart($rows, string $field, array $options, bool $multiple = false): array
|
||||
{
|
||||
$counts = array_fill_keys(array_keys($options), 0);
|
||||
$respondents = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$values = $multiple ? $this->arrayValues($row->{$field}) : [$row->{$field}];
|
||||
$answers = [];
|
||||
|
||||
foreach ($values as $value) {
|
||||
if (is_string($value) || is_int($value)) {
|
||||
$value = is_string($value) ? trim($value) : $value;
|
||||
if (array_key_exists($value, $options)) {
|
||||
$answers[$value] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($answers === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$respondents++;
|
||||
foreach (array_keys($answers) as $answer) {
|
||||
$counts[$answer]++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'labels' => array_values($options),
|
||||
'fullLabels' => $field === 'skor_pengalaman' ? array_values($options) : array_keys($options),
|
||||
'series' => array_values($counts),
|
||||
'respondents' => $respondents,
|
||||
'percentages' => array_map(fn ($count) => $respondents > 0 ? round($count / $respondents * 100, 1) : 0, array_values($counts)),
|
||||
];
|
||||
}
|
||||
|
||||
private function productionDashboardData(Request $request): array
|
||||
@@ -223,7 +351,6 @@ class SummaryController extends Controller
|
||||
$healthSourceName = $tanaman->isNotEmpty() ? 'tanaman' : 'lahan';
|
||||
|
||||
$landMetrics = $this->karakteristikLandMetrics($request);
|
||||
$totalPetani = $landMetrics['total_petani'];
|
||||
$totalLuasLahan = $landMetrics['total_luas_lahan'];
|
||||
$avgLuasPerPetani = $landMetrics['ratarata_luas_lahan'];
|
||||
$actualProductivity = $this->actualProductivityByCommodityChart($request);
|
||||
@@ -233,14 +360,6 @@ class SummaryController extends Controller
|
||||
$aksesPasar = $this->filteredAksesPasar($request);
|
||||
|
||||
$kpiCards = [
|
||||
[
|
||||
'value' => number_format($totalPetani, 0, ',', '.'),
|
||||
'unit' => 'orang',
|
||||
'label' => 'Jumlah Petani',
|
||||
'description' => 'Petani sesuai filter yang dipilih',
|
||||
'icon' => 'tabler-users',
|
||||
'variant' => 'primary',
|
||||
],
|
||||
[
|
||||
'value' => number_format($totalLuasLahan, 2, ',', '.'),
|
||||
'unit' => 'ha',
|
||||
@@ -294,7 +413,7 @@ class SummaryController extends Controller
|
||||
: $value,
|
||||
),
|
||||
'praktikSortasiPanen' => $this->enumOptionChart($surveyPanen, 'praktik_sortasi', PraktikSortasi::class),
|
||||
'praktikFermentasiPascapanen' => $this->fermentationPracticeChart($aksesPasar),
|
||||
'praktikFermentasiPascapanen' => $this->fermentationPracticeByCommodityChart($aksesPasar),
|
||||
'produktivitasAktualPerKomoditas' => $actualProductivity,
|
||||
],
|
||||
];
|
||||
@@ -312,7 +431,6 @@ class SummaryController extends Controller
|
||||
'desa' => (string) $request->input('desaKelurahan_id', ''),
|
||||
],
|
||||
'levels' => $levels,
|
||||
'cultivationLevels' => $this->productionCultivationMapLevelData($request, $levels),
|
||||
'information' => $this->productionMapInformationData($request),
|
||||
];
|
||||
}
|
||||
@@ -511,6 +629,7 @@ class SummaryController extends Controller
|
||||
'map_lahan.path',
|
||||
'map_lahan.luas_lahan',
|
||||
'map_lahan.jumlah_tanaman',
|
||||
'map_lahan.petani_id',
|
||||
'map_lahan.umur_tanaman',
|
||||
'map_lahan.serangan_hama',
|
||||
'map_lahan.penyakit_tanaman',
|
||||
@@ -537,58 +656,10 @@ class SummaryController extends Controller
|
||||
])
|
||||
->groupBy('lahan_id');
|
||||
|
||||
$geoLahan = $mapLahan
|
||||
->filter(fn ($lahan) => $this->hasMapPolygon($lahan->path))
|
||||
->map(function ($lahan) use ($tanamanByLahan) {
|
||||
$tanamanRows = $tanamanByLahan->get($lahan->id, collect());
|
||||
$komoditas = $this->productionCommodityDetails($lahan, $tanamanRows);
|
||||
$opt = $this->productionOptDetails($lahan, $tanamanRows);
|
||||
|
||||
return [
|
||||
'id' => $lahan->id,
|
||||
'parent_id' => $lahan->desa_kelurahan_id,
|
||||
'polygon' => $lahan->path,
|
||||
'nama_petani' => $lahan->petani_nama ?? $lahan->profile_nama ?? 'N/A',
|
||||
'luas' => (float) $lahan->luas_lahan,
|
||||
'komoditas' => $komoditas,
|
||||
'opt' => $opt,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'provinsi' => array_values($geoProvinsi),
|
||||
'kabupaten' => array_values($geoKabupaten),
|
||||
'kecamatan' => array_values($geoKecamatan),
|
||||
'desa' => array_values($geoDesa),
|
||||
'lahan' => $geoLahan,
|
||||
];
|
||||
}
|
||||
|
||||
private function productionCultivationMapLevelData(Request $request, array $baseLevels): array
|
||||
{
|
||||
$mapLahan = (clone $this->filteredLahanQuery($request))
|
||||
->whereNotNull('map_lahan.path')
|
||||
->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id')
|
||||
->leftJoin('petani', 'petani.id', '=', 'map_lahan.petani_id')
|
||||
->tap(fn ($query) => $this->whereActiveMapOwner($query))
|
||||
->orderByDesc('map_lahan.updated_at')
|
||||
->limit(500)
|
||||
->get([
|
||||
'map_lahan.id',
|
||||
'map_lahan.desa_kelurahan_id',
|
||||
'map_lahan.path',
|
||||
'map_lahan.luas_lahan',
|
||||
'map_lahan.petani_id',
|
||||
'profiles.nama as profile_nama',
|
||||
'petani.nama as petani_nama',
|
||||
]);
|
||||
$petaniIds = $mapLahan->pluck('petani_id')->filter()->unique()->values();
|
||||
$practiceByPetani = DB::table('praktik_budidaya')
|
||||
->whereNull('praktik_budidaya.deleted_at')
|
||||
->when($petaniIds->isNotEmpty(), fn ($query) => $query->whereIn('praktik_budidaya.petani_id', $petaniIds))
|
||||
->when($petaniIds->isEmpty(), fn ($query) => $query->whereRaw('1 = 0'))
|
||||
->whereIn('praktik_budidaya.petani_id', $petaniIds)
|
||||
->orderByDesc('praktik_budidaya.updated_at')
|
||||
->get([
|
||||
'praktik_budidaya.petani_id',
|
||||
@@ -601,7 +672,10 @@ class SummaryController extends Controller
|
||||
|
||||
$geoLahan = $mapLahan
|
||||
->filter(fn ($lahan) => $this->hasMapPolygon($lahan->path))
|
||||
->map(function ($lahan) use ($practiceByPetani) {
|
||||
->map(function ($lahan) use ($tanamanByLahan, $practiceByPetani) {
|
||||
$tanamanRows = $tanamanByLahan->get($lahan->id, collect());
|
||||
$komoditas = $this->productionCommodityDetails($lahan, $tanamanRows);
|
||||
$opt = $this->productionOptDetails($lahan, $tanamanRows);
|
||||
$practice = $practiceByPetani->get($lahan->petani_id);
|
||||
|
||||
return [
|
||||
@@ -610,6 +684,8 @@ class SummaryController extends Controller
|
||||
'polygon' => $lahan->path,
|
||||
'nama_petani' => $lahan->petani_nama ?? $lahan->profile_nama ?? 'N/A',
|
||||
'luas' => (float) $lahan->luas_lahan,
|
||||
'komoditas' => $komoditas,
|
||||
'opt' => $opt,
|
||||
'pemangkasan' => $this->enumLabel(FrekuensiPemangkasanUtama::class, $practice->frekuensi_pemangkasan_utama ?? null, 'Belum ada data'),
|
||||
'pemupukan' => $this->enumLabel(PemberianPupuk::class, $practice->pemberian_pupuk ?? null, 'Belum ada data'),
|
||||
'praktik_baik' => $this->arrayValues($practice->kebiasaan_konservasi_tanah ?? null),
|
||||
@@ -619,10 +695,10 @@ class SummaryController extends Controller
|
||||
->all();
|
||||
|
||||
return [
|
||||
'provinsi' => $baseLevels['provinsi'] ?? [],
|
||||
'kabupaten' => $baseLevels['kabupaten'] ?? [],
|
||||
'kecamatan' => $baseLevels['kecamatan'] ?? [],
|
||||
'desa' => $baseLevels['desa'] ?? [],
|
||||
'provinsi' => array_values($geoProvinsi),
|
||||
'kabupaten' => array_values($geoKabupaten),
|
||||
'kecamatan' => array_values($geoKecamatan),
|
||||
'desa' => array_values($geoDesa),
|
||||
'lahan' => $geoLahan,
|
||||
];
|
||||
}
|
||||
@@ -826,7 +902,12 @@ class SummaryController extends Controller
|
||||
return DB::table('akses_pasar')
|
||||
->whereNull('akses_pasar.deleted_at')
|
||||
->whereIn('akses_pasar.petani_id', $petaniIds)
|
||||
->get(['praktik_fermentasi']);
|
||||
->leftJoin('petani as akses_petani', 'akses_petani.id', '=', 'akses_pasar.petani_id')
|
||||
->leftJoin('master_komoditas as akses_komoditas', 'akses_komoditas.id', '=', 'akses_petani.komoditas_id')
|
||||
->get([
|
||||
'akses_pasar.praktik_fermentasi',
|
||||
'akses_komoditas.nama as komoditas_nama',
|
||||
]);
|
||||
}
|
||||
|
||||
private function filteredSurveyProduktivitas(Request $request)
|
||||
@@ -1254,10 +1335,10 @@ class SummaryController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
private function fermentationPracticeChart($rows): array
|
||||
private function fermentationPracticeByCommodityChart($rows): array
|
||||
{
|
||||
$labels = ['Ya, rutin', 'Pernah mencoba', 'Belum pernah'];
|
||||
$counts = collect($labels)->mapWithKeys(fn ($label) => [$label => 0]);
|
||||
$matrix = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$label = $this->fermentationPracticeLabel($row->praktik_fermentasi ?? null);
|
||||
@@ -1266,12 +1347,21 @@ class SummaryController extends Controller
|
||||
continue;
|
||||
}
|
||||
|
||||
$counts[$label] = (int) $counts[$label] + 1;
|
||||
$commodity = $row->komoditas_nama ?: 'Belum ditentukan';
|
||||
$matrix[$commodity] ??= array_fill_keys($labels, 0);
|
||||
$matrix[$commodity][$label]++;
|
||||
}
|
||||
|
||||
ksort($matrix, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
return [
|
||||
'labels' => $labels,
|
||||
'series' => collect($labels)->map(fn ($label) => (int) ($counts[$label] ?? 0))->values()->all(),
|
||||
'labels' => array_keys($matrix),
|
||||
'series' => collect($labels)
|
||||
->map(fn ($label) => [
|
||||
'name' => $label,
|
||||
'data' => collect($matrix)->map(fn ($counts) => (int) $counts[$label])->values()->all(),
|
||||
])
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,8 @@ class AksesPasarController extends Controller
|
||||
'skorPengalamanBerhentiValues' => [
|
||||
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
|
||||
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
|
||||
]
|
||||
],
|
||||
'sumberInfoLainnyaValue' => 'Lainnya',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -128,6 +129,17 @@ class AksesPasarController extends Controller
|
||||
|
||||
return $request->validate([
|
||||
'petani_id' => ['required', 'exists:petani,id'],
|
||||
|
||||
'tujuan_penjualan' => ['nullable', 'array'],
|
||||
'tujuan_penjualan.*' => ['string'],
|
||||
'tujuan_penjualan_lainnya' => [
|
||||
Rule::requiredIf(fn () => in_array('Lainnya', $request->input('tujuan_penjualan', []), true)),
|
||||
'nullable', 'string', 'max:255'
|
||||
],
|
||||
'sifat_hubungan_bisnis' => ['nullable', 'string', 'max:255'],
|
||||
'bentuk_penjualan' => ['nullable', 'string', 'max:255'],
|
||||
'frekuensi_penjualan' => ['nullable', 'string', 'max:255'],
|
||||
|
||||
'skor_pengalaman' => ['nullable', 'integer', Rule::in(SkorPengalaman::values())],
|
||||
'alasan_berhenti' => [
|
||||
Rule::requiredIf(fn () => in_array((int)$request->input('skor_pengalaman'), [
|
||||
@@ -143,6 +155,14 @@ class AksesPasarController extends Controller
|
||||
Rule::requiredIf(fn () => in_array(SaluranTerdahulu::LAINNYA->value, $request->input('saluran_terdahulu', []), true)),
|
||||
'nullable', 'string', 'max:255'
|
||||
],
|
||||
|
||||
'tahu_harga_sebelumnya' => ['nullable', 'string', 'max:255'],
|
||||
'sumber_info_harga' => ['nullable', 'array'],
|
||||
'sumber_info_harga.*' => ['string'],
|
||||
'sumber_info_harga_lainnya' => [
|
||||
Rule::requiredIf(fn () => in_array('Lainnya', $request->input('sumber_info_harga', []), true)),
|
||||
'nullable', 'string', 'max:255'
|
||||
],
|
||||
|
||||
'praktik_fermentasi' => ['nullable', 'string', Rule::in(PraktikFermentasi::values())],
|
||||
'proses_fermentasi' => [
|
||||
@@ -168,6 +188,7 @@ class AksesPasarController extends Controller
|
||||
], [
|
||||
'petani_id.required' => 'Petani harus dipilih.',
|
||||
'petani_id.exists' => 'Petani tidak ditemukan.',
|
||||
'tujuan_penjualan_lainnya.required' => 'Keterangan saluran penjualan lainnya harus diisi.',
|
||||
'skor_pengalaman.integer' => 'Skor pengalaman harus berupa angka.',
|
||||
'skor_pengalaman.in' => 'Skor pengalaman tidak valid.',
|
||||
'alasan_berhenti.required' => 'Alasan berhenti harus diisi.',
|
||||
@@ -204,6 +225,10 @@ class AksesPasarController extends Controller
|
||||
{
|
||||
$payload = collect($validated)->except(['harga_jual'])->all();
|
||||
|
||||
if (!in_array('Lainnya', $payload['tujuan_penjualan'] ?? [], true)) {
|
||||
$payload['tujuan_penjualan_lainnya'] = null;
|
||||
}
|
||||
|
||||
if (!in_array((int)($payload['skor_pengalaman'] ?? 0), [
|
||||
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
|
||||
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
|
||||
@@ -215,6 +240,10 @@ class AksesPasarController extends Controller
|
||||
$payload['saluran_terdahulu_lainnya'] = null;
|
||||
}
|
||||
|
||||
if (!in_array('Lainnya', $payload['sumber_info_harga'] ?? [], true)) {
|
||||
$payload['sumber_info_harga_lainnya'] = null;
|
||||
}
|
||||
|
||||
if (($payload['praktik_fermentasi'] ?? null) === PraktikFermentasi::BELUM_PERNAH->value) {
|
||||
$payload['proses_fermentasi'] = null;
|
||||
$payload['model_fermentasi_memungkinkan'] = null;
|
||||
@@ -223,7 +252,6 @@ class AksesPasarController extends Controller
|
||||
return $payload;
|
||||
}
|
||||
|
||||
|
||||
private function syncHargaJual($petaniId, array $hargaJualData, $createdBy, $updatedBy = null): void
|
||||
{
|
||||
HargaJualPanen::where('petani_id', $petaniId)->delete();
|
||||
@@ -279,4 +307,4 @@ class AksesPasarController extends Controller
|
||||
|
||||
return is_null($digits) ? '' : number_format((int) $digits, 0, ',', '.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,20 +49,43 @@ class ProfilPetaniController extends Controller
|
||||
$skorPetani = SkorPetani::where('petani_id', $id)->first();
|
||||
$enumerator = Profile::where('user_id', $petani->created_by)->first();
|
||||
$komoditasUtama = Komoditas::where('id', $petani->komoditas_id)->first();
|
||||
$lahan = Lahan::where('petani_id', $id )->get();
|
||||
$lahan = Lahan::query()
|
||||
->select(['id', 'petani_id', 'nama', 'path', 'luas_lahan', 'komoditas_id', 'desa_kelurahan_id'])
|
||||
->with([
|
||||
'komoditas:id,nama',
|
||||
'desaKelurahan:id,nama,path,kecamatan_id',
|
||||
'desaKelurahan.kecamatan:id,nama,path',
|
||||
])
|
||||
->where('petani_id', $id)
|
||||
->get();
|
||||
|
||||
$desaKelurahan = DesaKelurahan::with('kecamatan')
|
||||
->where('id', $petani->desa_kelurahan_id)
|
||||
->first();
|
||||
|
||||
$ktpMasked = $petani->ktp ? Str::mask($petani->ktp, '*', 4, -4) : '-';
|
||||
$profiling = $skorPetani ? $this->buildProfiling($skorPetani) : null;
|
||||
$assessmentFields = [
|
||||
'skor_kesiapan_adopsi',
|
||||
'skor_d2_sistem_praktik_budidaya',
|
||||
'skor_d4_sosial_ekonomi',
|
||||
'skor_d6_rantai_nilai_akses_pasar',
|
||||
'skor_komp_1_d4_k3_motivasi_kapasitas_adopsi',
|
||||
'skor_komp_2_d4_k2_ekonomi_rumah_tangga',
|
||||
'skor_komp_3_d2_praktik_budidaya_gap',
|
||||
'skor_komp_4_d4_k4_keterlibatan_kelembagaan',
|
||||
'skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan',
|
||||
'skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar',
|
||||
];
|
||||
$hasAssessment = $skorPetani !== null && collect($assessmentFields)->every(
|
||||
fn (string $field) => $skorPetani->{$field} !== null
|
||||
);
|
||||
$profiling = $hasAssessment ? $this->buildProfiling($skorPetani) : null;
|
||||
|
||||
$tanaman = Tanaman::where('petani_id', $petani->id)
|
||||
->where('komoditas_id', $petani->komoditas_id)
|
||||
->first();
|
||||
|
||||
if ($tanaman) {
|
||||
if ($tanaman?->updated_at && $tanaman->umur_tanaman !== null) {
|
||||
$tahunInputTanaman = $tanaman->updated_at->year;
|
||||
$umurTanaman = (int) $tanaman->umur_tanaman;
|
||||
$tahunSekarang = now()->year;
|
||||
@@ -162,8 +185,8 @@ class ProfilPetaniController extends Controller
|
||||
|
||||
$d4SosialEkonomi = null;
|
||||
|
||||
$terdaftarKelompok = $sosialEkonomi->terdaftar_kelompok_tani;
|
||||
$peranKelompok = $sosialEkonomi->peran_dalam_kelompok;
|
||||
$terdaftarKelompok = $sosialEkonomi?->terdaftar_kelompok_tani;
|
||||
$peranKelompok = $sosialEkonomi?->peran_dalam_kelompok;
|
||||
|
||||
if ($sosialEkonomi) {
|
||||
|
||||
@@ -207,8 +230,8 @@ class ProfilPetaniController extends Controller
|
||||
'icon' => 'tabler-minus',
|
||||
],
|
||||
default => [
|
||||
'label' => Str::headline($status),
|
||||
'short' => Str::headline($status),
|
||||
'label' => filled($status) ? Str::headline($status) : 'Belum diisi',
|
||||
'short' => filled($status) ? Str::headline($status) : 'Belum diisi',
|
||||
'color' => 'secondary',
|
||||
'icon' => 'tabler-help-circle',
|
||||
],
|
||||
@@ -434,12 +457,61 @@ class ProfilPetaniController extends Controller
|
||||
],
|
||||
];
|
||||
|
||||
// Report missing profile data without treating an unanswered field as zero.
|
||||
$missingSections = [];
|
||||
$identityFields = [
|
||||
'nama' => 'Nama', 'ktp' => 'NIK', 'tanggal_lahir' => 'Tanggal lahir',
|
||||
'jenis_kelamin' => 'Jenis kelamin', 'pendidikan_terakhir' => 'Pendidikan terakhir',
|
||||
'status_perkawinan' => 'Status perkawinan',
|
||||
];
|
||||
foreach ($identityFields as $field => $label) {
|
||||
if (blank($petani->{$field})) {
|
||||
$missingSections[] = $label;
|
||||
}
|
||||
}
|
||||
if (!$desaKelurahan?->nama || !$desaKelurahan?->kecamatan?->nama) {
|
||||
$missingSections[] = 'Wilayah desa/kecamatan';
|
||||
}
|
||||
if (!$komoditasUtama?->nama) {
|
||||
$missingSections[] = 'Komoditas utama';
|
||||
}
|
||||
if ($lahan->isEmpty()) {
|
||||
$missingSections[] = 'Data lahan';
|
||||
}
|
||||
if ($tahunTanam === null) {
|
||||
$missingSections[] = 'Umur tanaman';
|
||||
}
|
||||
if (!$praktikBudidaya || $indikatorOnFarm->contains(fn ($item) => $item['skor'] === null)) {
|
||||
$missingSections[] = 'Praktik budidaya';
|
||||
}
|
||||
if ($surveyPanen?->hasil_komoditas_utama_permusim === null) {
|
||||
$missingSections[] = 'Survei panen';
|
||||
}
|
||||
$socialFields = [
|
||||
'jumlah_tanggungan', 'skor_kecukupan_penghasilan', 'skor_kemampuan_menabung',
|
||||
'status_kepemilikan_lahan', 'terdaftar_kelompok_tani', 'status_rekening_tabungan',
|
||||
'pengalaman_kredit_kebun', 'keterikatan_hutang_panen', 'praktik_ijon',
|
||||
'kebebasan_memilih_pembeli',
|
||||
];
|
||||
if (!$sosialEkonomi || collect($socialFields)->contains(fn ($field) => blank($sosialEkonomi->{$field}))) {
|
||||
$missingSections[] = 'Sosial ekonomi';
|
||||
}
|
||||
if (!$aksesPasar || blank($aksesPasar->saluran_terdahulu)) {
|
||||
$missingSections[] = 'Akses pasar';
|
||||
}
|
||||
if (!$hasAssessment) {
|
||||
$missingSections[] = 'Penilaian kesiapan adopsi belum lengkap';
|
||||
}
|
||||
|
||||
$data = [
|
||||
'missingSections' => $missingSections,
|
||||
'hasAssessment' => $hasAssessment,
|
||||
'petani' => $petani,
|
||||
'skorPetani' => $skorPetani,
|
||||
'enumerator' => $enumerator,
|
||||
'komoditasUtama' => $komoditasUtama,
|
||||
'lahan' => $lahan,
|
||||
'farmerLandMap' => $this->farmerLandMapData($lahan),
|
||||
'desaKelurahan' => $desaKelurahan,
|
||||
'ktpMasked' => $ktpMasked,
|
||||
'profiling' => $profiling,
|
||||
@@ -447,7 +519,6 @@ class ProfilPetaniController extends Controller
|
||||
'umurTanaman' => $umurTanaman,
|
||||
'surveyPanen' => $surveyPanen,
|
||||
'praktikBudidaya' => $praktikBudidaya,
|
||||
'praktikBudidaya' => $praktikBudidaya,
|
||||
'indikatorOnFarm' => $indikatorOnFarm,
|
||||
'detailOnFarm' => $detailOnFarm,
|
||||
'sosialEkonomi' => $sosialEkonomi,
|
||||
@@ -464,6 +535,35 @@ class ProfilPetaniController extends Controller
|
||||
return view('produktivitas.profil-petani.index', $data);
|
||||
}
|
||||
|
||||
private function farmerLandMapData(\Illuminate\Support\Collection $lands): array
|
||||
{
|
||||
$regions = ['desa' => [], 'kecamatan' => []];
|
||||
$plots = [];
|
||||
|
||||
foreach ($lands as $land) {
|
||||
$plot = [
|
||||
'id' => $land->id,
|
||||
'nama' => $land->nama,
|
||||
'path' => $land->path,
|
||||
'luas' => $land->luas_lahan,
|
||||
'komoditas' => $land->komoditas?->nama,
|
||||
];
|
||||
foreach (['desa' => $land->desaKelurahan, 'kecamatan' => $land->desaKelurahan?->kecamatan] as $level => $region) {
|
||||
$plot[$level . '_id'] = $region?->id;
|
||||
if ($region && !isset($regions[$level][$region->id])) {
|
||||
$regions[$level][$region->id] = [
|
||||
'id' => $region->id,
|
||||
'nama' => $region->nama,
|
||||
'path' => $region->path,
|
||||
];
|
||||
}
|
||||
}
|
||||
$plots[] = $plot;
|
||||
}
|
||||
|
||||
return ['lands' => $plots, 'regions' => $regions];
|
||||
}
|
||||
|
||||
private function getScoreColor(?int $score): string
|
||||
{
|
||||
if ($score === null) {
|
||||
|
||||
@@ -22,10 +22,18 @@ class AksesPasar extends Model
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'petani_id',
|
||||
'tujuan_penjualan',
|
||||
'tujuan_penjualan_lainnya',
|
||||
'sifat_hubungan_bisnis',
|
||||
'bentuk_penjualan',
|
||||
'frekuensi_penjualan',
|
||||
'skor_pengalaman',
|
||||
'saluran_terdahulu',
|
||||
'saluran_terdahulu_lainnya',
|
||||
'alasan_berhenti',
|
||||
'tahu_harga_sebelumnya',
|
||||
'sumber_info_harga',
|
||||
'sumber_info_harga_lainnya',
|
||||
'praktik_fermentasi',
|
||||
'proses_fermentasi',
|
||||
'model_fermentasi_memungkinkan',
|
||||
@@ -44,7 +52,9 @@ class AksesPasar extends Model
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tujuan_penjualan' => 'array',
|
||||
'saluran_terdahulu' => 'array',
|
||||
'sumber_info_harga' => 'array',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
|
||||
@@ -61,7 +61,7 @@ return new class extends Migration
|
||||
$table->integer('harga_tahun_lalu')->nullable();
|
||||
$table->integer('harga_dua_tahun_lalu')->nullable();
|
||||
|
||||
$table ->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table ->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table ->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table ->foreignId('deleted_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('akses_pasar', function (Blueprint $table) {
|
||||
$table->text('tujuan_penjualan')->nullable();
|
||||
$table->string('tujuan_penjualan_lainnya')->nullable();
|
||||
$table->text('sifat_hubungan_bisnis')->nullable();
|
||||
$table->text('bentuk_penjualan')->nullable();
|
||||
$table->string('frekuensi_penjualan')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('akses_pasar', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'tujuan_penjualan',
|
||||
'tujuan_penjualan_lainnya',
|
||||
'sifat_hubungan_bisnis',
|
||||
'bentuk_penjualan',
|
||||
'frekuensi_penjualan'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('akses_pasar', function (Blueprint $table) {
|
||||
$table->string('tahu_harga_sebelumnya')->nullable();
|
||||
$table->text('sumber_info_harga')->nullable();
|
||||
$table->string('sumber_info_harga_lainnya')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('akses_pasar', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'tahu_harga_sebelumnya',
|
||||
'sumber_info_harga',
|
||||
'sumber_info_harga_lainnya',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -81,6 +81,29 @@ window.config = {
|
||||
enableMenuLocalStorage: true // Enable menu state with local storage support
|
||||
};
|
||||
|
||||
// Choose the label color with the higher contrast against each chart slice.
|
||||
window.config.chartTextColor = function (color) {
|
||||
let hex = String(color || '').trim().replace(/^#/, '');
|
||||
if (/^[0-9a-f]{3}$/i.test(hex)) {
|
||||
hex = hex.split('').map(value => value + value).join('');
|
||||
}
|
||||
if (!/^[0-9a-f]{6}$/i.test(hex)) return '#212529';
|
||||
|
||||
const luminance = function (value) {
|
||||
const channels = value.match(/../g).map(channel => {
|
||||
const srgb = parseInt(channel, 16) / 255;
|
||||
return srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
||||
};
|
||||
const background = luminance(hex);
|
||||
const dark = luminance('212529');
|
||||
const darkContrast = (Math.max(background, dark) + 0.05) / (Math.min(background, dark) + 0.05);
|
||||
const lightContrast = 1.05 / (background + 0.05);
|
||||
|
||||
return darkContrast >= lightContrast ? '#212529' : '#ffffff';
|
||||
};
|
||||
|
||||
window.assetsPath = document.documentElement.getAttribute('data-assets-path');
|
||||
window.templateName = document.documentElement.getAttribute('data-template');
|
||||
|
||||
|
||||
@@ -2011,7 +2011,11 @@
|
||||
series: data.series,
|
||||
colors: colors,
|
||||
stroke: { width: 0 },
|
||||
dataLabels: { enabled: false },
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
style: { colors: colors.map(config.chartTextColor) },
|
||||
dropShadow: { enabled: false },
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
@@ -2051,7 +2055,7 @@
|
||||
<div
|
||||
style="
|
||||
background: ${color};
|
||||
color: #fff;
|
||||
color: ${config.chartTextColor(color)};
|
||||
padding: 8px 12px;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -1713,6 +1713,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const totalPetani = lakiLaki + perempuan;
|
||||
|
||||
if (genderChartEl) {
|
||||
const genderColors = [pastelSemantic.info || '#5FC7D6', pastelSemantic.pink || '#F47C86'];
|
||||
const genderChartConfig = {
|
||||
chart: {
|
||||
height: 365,
|
||||
@@ -1732,7 +1733,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
},
|
||||
labels: ['Laki-laki', 'Perempuan'],
|
||||
series: [lakiLaki, perempuan],
|
||||
colors: [pastelSemantic.info || '#5FC7D6', pastelSemantic.pink || '#F47C86'],
|
||||
colors: genderColors,
|
||||
stroke: { width: 0 },
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
@@ -1748,15 +1749,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
fontSize: '13px',
|
||||
fontFamily: fontFamily,
|
||||
fontWeight: 600,
|
||||
colors: ['#fff']
|
||||
colors: genderColors.map(config.chartTextColor)
|
||||
},
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
top: 1,
|
||||
left: 1,
|
||||
blur: 2,
|
||||
opacity: 0.45
|
||||
}
|
||||
dropShadow: { enabled: false }
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
@@ -2120,6 +2115,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const farmerGroupFilters = ['terdaftar', 'tidak_terdaftar', 'belum_terisi'];
|
||||
const totalFarmerGroup = farmerGroupData.reduce((total, value) => total + value, 0);
|
||||
|
||||
const farmerGroupColors = [pastelSemantic.success || '#6FD68F', pastelSemantic.warning || '#FFB46F', pastelSemantic.secondary || '#9EA6B0'];
|
||||
const farmerGroupChartConfig = {
|
||||
chart: {
|
||||
height: 365,
|
||||
@@ -2139,7 +2135,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
},
|
||||
labels: ['Terdaftar', 'Tidak Terdaftar', 'Belum Terisi'],
|
||||
series: farmerGroupData,
|
||||
colors: [pastelSemantic.success || '#6FD68F', pastelSemantic.warning || '#FFB46F', pastelSemantic.secondary || '#9EA6B0'],
|
||||
colors: farmerGroupColors,
|
||||
stroke: { width: 0 },
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
@@ -2155,15 +2151,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
fontSize: '13px',
|
||||
fontFamily: fontFamily,
|
||||
fontWeight: 600,
|
||||
colors: ['#fff']
|
||||
colors: farmerGroupColors.map(config.chartTextColor)
|
||||
},
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
top: 1,
|
||||
left: 1,
|
||||
blur: 2,
|
||||
opacity: 0.45
|
||||
}
|
||||
dropShadow: { enabled: false }
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
@@ -2219,6 +2209,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const commoditySeries = @json($data['komoditas_utama']['series'] ?? []);
|
||||
const totalCommodityFarmers = commoditySeries.reduce((total, value) => total + Number(value), 0);
|
||||
|
||||
const commodityColors = [
|
||||
pastelSemantic.primary || '#9D8EF0',
|
||||
pastelSemantic.warning || '#FFB46F',
|
||||
pastelSemantic.success || '#6FD68F',
|
||||
pastelSemantic.info || '#5FC7D6',
|
||||
pastelSemantic.danger || '#F47C86',
|
||||
pastelSemantic.secondary || '#9EA6B0'
|
||||
];
|
||||
const commodityChartConfig = {
|
||||
chart: {
|
||||
height: 365,
|
||||
@@ -2237,14 +2235,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
},
|
||||
labels: commodityLabels,
|
||||
series: commoditySeries.map(Number),
|
||||
colors: [
|
||||
pastelSemantic.primary || '#9D8EF0',
|
||||
pastelSemantic.warning || '#FFB46F',
|
||||
pastelSemantic.success || '#6FD68F',
|
||||
pastelSemantic.info || '#5FC7D6',
|
||||
pastelSemantic.danger || '#F47C86',
|
||||
pastelSemantic.secondary || '#9EA6B0'
|
||||
],
|
||||
colors: commodityColors,
|
||||
stroke: { width: 0 },
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
@@ -2260,15 +2251,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
fontSize: '13px',
|
||||
fontFamily: fontFamily,
|
||||
fontWeight: 600,
|
||||
colors: ['#fff']
|
||||
colors: commodityColors.map(config.chartTextColor)
|
||||
},
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
top: 1,
|
||||
left: 1,
|
||||
blur: 2,
|
||||
opacity: 0.45
|
||||
}
|
||||
dropShadow: { enabled: false }
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
@@ -2293,7 +2278,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const percentage = totalCommodityFarmers > 0 ? ((value / totalCommodityFarmers) * 100).toFixed(1) : '0.0';
|
||||
const color = w.globals.colors[seriesIndex];
|
||||
return `
|
||||
<div style="background:${color}; color:${headingColor}; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
|
||||
<div style="background:${color}; color:${config.chartTextColor(color)}; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
|
||||
${label}: <strong>${value} petani (${percentage}%)</strong>
|
||||
</div>
|
||||
`;
|
||||
@@ -2433,6 +2418,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const landOwnershipStatusSeries = (@json($data['status_kepemilikan_lahan']['series'] ?? [])).map(Number);
|
||||
const totalLandOwnershipStatusFarmers = landOwnershipStatusSeries.reduce((total, value) => total + value, 0);
|
||||
|
||||
const landOwnershipStatusColors = [
|
||||
pastelSemantic.success || '#6FD68F',
|
||||
pastelSemantic.warning || '#FFB46F',
|
||||
pastelSemantic.info || '#5FC7D6',
|
||||
pastelSemantic.secondary || '#9EA6B0'
|
||||
];
|
||||
const landOwnershipStatusChartConfig = {
|
||||
chart: {
|
||||
height: 365,
|
||||
@@ -2452,12 +2443,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
},
|
||||
labels: landOwnershipStatusLabels,
|
||||
series: landOwnershipStatusSeries,
|
||||
colors: [
|
||||
pastelSemantic.success || '#6FD68F',
|
||||
pastelSemantic.warning || '#FFB46F',
|
||||
pastelSemantic.info || '#5FC7D6',
|
||||
pastelSemantic.secondary || '#9EA6B0'
|
||||
],
|
||||
colors: landOwnershipStatusColors,
|
||||
stroke: { width: 0 },
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
@@ -2473,15 +2459,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
fontSize: '13px',
|
||||
fontFamily: fontFamily,
|
||||
fontWeight: 600,
|
||||
colors: ['#fff']
|
||||
colors: landOwnershipStatusColors.map(config.chartTextColor)
|
||||
},
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
top: 1,
|
||||
left: 1,
|
||||
blur: 2,
|
||||
opacity: 0.45
|
||||
}
|
||||
dropShadow: { enabled: false }
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
@@ -2918,6 +2898,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
style: { colors: segmentPalette.map(config.chartTextColor) },
|
||||
dropShadow: { enabled: false },
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
@push('style')
|
||||
<style>
|
||||
.pasar-filter-card .filter-item {
|
||||
min-width: 210px;
|
||||
min-width: 190px;
|
||||
}
|
||||
|
||||
.pasar-kpi-value {
|
||||
color: var(--bs-heading-color);
|
||||
font-size: 1.85rem;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
@@ -25,14 +25,103 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.pasar-empty-state {
|
||||
min-height: 220px;
|
||||
.chart-group-header-toggle {
|
||||
cursor: pointer;
|
||||
background-color: rgba(var(--bs-primary-rgb, 115, 103, 240), 0.08);
|
||||
border-inline-start: 4px solid var(--bs-primary, #9D8EF0);
|
||||
box-shadow: none;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.chart-group-header-toggle:hover {
|
||||
background-color: rgba(var(--bs-primary-rgb, 115, 103, 240), 0.13);
|
||||
}
|
||||
|
||||
.chart-group-header-toggle .card-body {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.chart-group-header-toggle h5 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.chart-group-header-toggle:focus-visible {
|
||||
outline: 2px solid var(--bs-primary);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.chart-group-icon-toggle {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chart-group-icon-toggle .icon-base {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.chart-group-panel.show,
|
||||
.chart-group-panel.collapsing {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.pasar-chart-card .card-title h5 {
|
||||
color: var(--bs-heading-color);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.pasar-chart-card .card-title small,
|
||||
.chart-group-header-toggle p {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.pasar-chart {
|
||||
min-height: 330px;
|
||||
}
|
||||
|
||||
.pasar-chart-empty {
|
||||
min-height: 330px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.8125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pasar-chart-card .apexcharts-tooltip {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
overflow: visible !important;
|
||||
white-space: normal;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.pasar-chart-tooltip {
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, .15);
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.pasar-trend-tooltip-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.pasar-filter-card .filter-item {
|
||||
width: 100%;
|
||||
@@ -47,35 +136,6 @@
|
||||
<form id="form-filter" class="col-12" method="GET" action="{{ route('dashboard.pasar') }}">
|
||||
<div class="card card-border-shadow-primary pasar-filter-card">
|
||||
<div class="card-body py-3">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<div class="d-flex align-items-center me-2">
|
||||
<i class="icon-base ti tabler-adjustments-horizontal me-2"></i>
|
||||
<span class="fw-medium">Komoditas <span class="text-danger">*</span></span>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<select id="filter-komoditas" class="form-select form-select-sm" name="komoditas_id">
|
||||
<option value="">Pilih Komoditas</option>
|
||||
@foreach ($komoditas as $item)
|
||||
<option value="{{ $item->id }}" {{ old('komoditas_id', $komoditas_id) == $item->id ? 'selected' : '' }}>
|
||||
{{ $item->nama }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="icon-base ti tabler-filter me-1"></i>
|
||||
Filter
|
||||
</button>
|
||||
<button type="button" id="btn-reset-dashboard-filter" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="icon-base ti tabler-refresh me-1"></i>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<div class="d-flex align-items-center me-2">
|
||||
<i class="icon-base ti tabler-map-2 me-2"></i>
|
||||
@@ -111,39 +171,57 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<div class="d-flex align-items-center me-2">
|
||||
<i class="icon-base ti tabler-adjustments-horizontal me-2"></i>
|
||||
<span class="fw-medium">Data</span>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<select id="filter-komoditas" class="form-select form-select-sm" name="komoditas_id">
|
||||
<option value="">Semua Komoditas</option>
|
||||
@foreach ($komoditas as $item)
|
||||
<option value="{{ $item->id }}" {{ old('komoditas_id', $komoditas_id) == $item->id ? 'selected' : '' }}>
|
||||
{{ $item->nama }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<select id="filter-segmen" class="form-select form-select-sm" name="segmen">
|
||||
<option value="">Semua Segmen</option>
|
||||
@foreach ($segments as $segment)
|
||||
<option value="{{ $segment['name'] }}" {{ old('segmen', $segmen) == $segment['name'] ? 'selected' : '' }}>
|
||||
{{ $segment['name'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="icon-base ti tabler-filter me-1"></i>
|
||||
Filter
|
||||
</button>
|
||||
<button type="button" id="btn-reset-dashboard-filter" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="icon-base ti tabler-refresh me-1"></i>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if (! $selectedKomoditas)
|
||||
<div class="alert alert-primary d-flex align-items-start gap-3" role="alert">
|
||||
<i class="icon-base ti tabler-info-circle mt-1"></i>
|
||||
<div>
|
||||
<h6 class="alert-heading mb-1">Pilih komoditas terlebih dahulu</h6>
|
||||
<div>Data dashboard pasar baru akan ditampilkan setelah filter komoditas wajib diisi.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body pasar-empty-state">
|
||||
<div>
|
||||
<div class="avatar avatar-lg mx-auto mb-3">
|
||||
<span class="avatar-initial rounded bg-label-primary">
|
||||
<i class="icon-base ti tabler-building-store icon-28px"></i>
|
||||
</span>
|
||||
</div>
|
||||
<h5 class="mb-1">Belum ada data pasar yang ditampilkan</h5>
|
||||
<p class="text-body-secondary mb-0">Gunakan filter komoditas untuk melihat harga jual rata-rata.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-xl-4 col-md-6">
|
||||
<div class="row g-4 mb-4">
|
||||
@foreach ($priceCards as $priceCard)
|
||||
<div class="col-12 col-md-6 col-xl-3">
|
||||
<div class="card card-border-shadow-{{ $priceCard['variant'] }} h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-4">
|
||||
<div class="d-flex flex-column align-items-start gap-3">
|
||||
<div class="avatar avatar-lg">
|
||||
<span class="avatar-initial rounded bg-label-{{ $priceCard['variant'] }}">
|
||||
<i class="icon-base ti {{ $priceCard['icon'] }} icon-28px"></i>
|
||||
@@ -161,15 +239,125 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-top d-flex align-items-center justify-content-between gap-3">
|
||||
<span class="badge bg-label-success">{{ $priceCard['caption'] }}</span>
|
||||
<div class="mt-4 pt-3 border-top d-flex align-items-center justify-content-between gap-2 flex-wrap">
|
||||
<span class="badge bg-label-{{ $priceCard['variant'] }}">{{ $priceCard['records'] > 0 ? 'Rata-rata harga' : 'Belum ada data' }}</span>
|
||||
<small class="text-body-secondary">{{ number_format($priceCard['records'], 0, ',', '.') }} data harga</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
|
||||
data-bs-toggle="collapse" data-bs-target="#saluran-harga-charts"
|
||||
aria-expanded="false" aria-controls="saluran-harga-charts" aria-label="Tampilkan chart">
|
||||
<div class="card-body d-flex justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<h5>Saluran & Harga</h5>
|
||||
<p class="mb-0 text-body-secondary">Saluran penjualan, hubungan dengan pembeli, waktu penjualan, akses ke saluran alternatif, dan tren harga.</p>
|
||||
</div>
|
||||
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
|
||||
<i class="icon-base ti tabler-chevron-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@php
|
||||
$chartDefinitions = [
|
||||
'saluranPenjualan' => ['title' => 'Saluran Penjualan Utama', 'description' => 'Kepada siapa petani biasanya menjual hasil panen?', 'variant' => 'success'],
|
||||
'hubunganPembeli' => ['title' => 'Sifat Hubungan dengan Pembeli', 'description' => 'Hubungan jual petani dengan pembeli utama saat ini.', 'variant' => 'primary'],
|
||||
'waktuPenjualan' => ['title' => 'Waktu Penjualan Hasil Panen', 'description' => 'Kapan petani biasanya menjual hasil panen?', 'variant' => 'warning'],
|
||||
'aksesSaluranLain' => ['title' => 'Pengalaman & Akses ke Saluran Lain', 'description' => 'Pengalaman menjual ke tempat lain dan akses yang masih dimiliki.', 'variant' => 'info'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="collapse chart-group-panel" id="saluran-harga-charts">
|
||||
<div class="row g-4 mb-4">
|
||||
@foreach ($chartDefinitions as $key => $definition)
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card pasar-chart-card h-100">
|
||||
<div class="card-header d-flex align-items-start justify-content-between gap-2">
|
||||
<div class="card-title mb-0">
|
||||
<h5 class="m-0 me-2" id="title-{{ $key }}">{{ $definition['title'] }}</h5>
|
||||
<small class="text-body-secondary">{{ $definition['description'] }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div id="chart-{{ $key }}" class="pasar-chart" role="img" aria-labelledby="title-{{ $key }}" aria-describedby="summary-{{ $key }}"></div>
|
||||
<span id="summary-{{ $key }}" class="visually-hidden">
|
||||
@foreach ($marketCharts[$key]['labels'] as $index => $label)
|
||||
{{ $label }}: {{ $marketCharts[$key]['series'][$index] }} responden ({{ $marketCharts[$key]['percentages'][$index] }}%).
|
||||
@endforeach
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@forelse ($priceTrend['commodities'] as $commodity)
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card pasar-chart-card h-100">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">
|
||||
<h5 class="m-0 me-2" id="title-tren-harga-{{ $commodity['id'] }}">Tren Harga 3 Musim — {{ $commodity['name'] }}</h5>
|
||||
<small class="text-body-secondary">Rata-rata harga jual per bentuk produk selama tiga periode panen (Rp/kg).</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="chart-tren-harga-{{ $commodity['id'] }}" class="pasar-chart" role="img" aria-labelledby="title-tren-harga-{{ $commodity['id'] }}" aria-describedby="summary-tren-harga-{{ $commodity['id'] }}"></div>
|
||||
<span id="summary-tren-harga-{{ $commodity['id'] }}" class="visually-hidden"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-12">
|
||||
<div class="card pasar-chart-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0"><h5 class="m-0">Tren Harga 3 Musim</h5></div>
|
||||
</div>
|
||||
<div class="card-body pasar-chart-empty">Belum ada komoditas untuk filter yang dipilih.</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
|
||||
data-bs-toggle="collapse" data-bs-target="#kesiapan-premium-charts"
|
||||
aria-expanded="false" aria-controls="kesiapan-premium-charts" aria-label="Tampilkan chart">
|
||||
<div class="card-body d-flex justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<h5>Kesiapan Pasar Premium</h5>
|
||||
<p class="mb-0 text-body-secondary">Kesadaran petani tentang pasar premium, persyaratan masuk, dan pengalaman mengaksesnya.</p>
|
||||
</div>
|
||||
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
|
||||
<i class="icon-base ti tabler-chevron-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse chart-group-panel" id="kesiapan-premium-charts">
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card pasar-chart-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">
|
||||
<h5 class="m-0 me-2" id="title-kesadaranPremium">Kesadaran Pasar Premium</h5>
|
||||
<small class="text-body-secondary">Pengetahuan tentang pasar premium dan persyaratan yang dibutuhkan untuk masuk ke pasar tersebut.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="chart-kesadaranPremium" class="pasar-chart" role="img" aria-labelledby="title-kesadaranPremium" aria-describedby="summary-kesadaranPremium"></div>
|
||||
<span id="summary-kesadaranPremium" class="visually-hidden">
|
||||
@foreach ($premiumAwareness['fullLabels'] as $index => $label)
|
||||
{{ $label }}: {{ $premiumAwareness['series'][$index] }} responden ({{ $premiumAwareness['percentages'][$index] }}%).
|
||||
@endforeach
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -177,6 +365,230 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const wilayah = @json($wilayah);
|
||||
const marketCharts = @json($marketCharts);
|
||||
const chartDefinitions = @json($chartDefinitions);
|
||||
const priceTrend = @json($priceTrend);
|
||||
const premiumAwareness = @json($premiumAwareness);
|
||||
|
||||
function escapeHtml(value) {
|
||||
const element = document.createElement('span');
|
||||
element.textContent = String(value);
|
||||
return element.innerHTML;
|
||||
}
|
||||
|
||||
function formatNumber(value, digits = 0) {
|
||||
return Number(value).toLocaleString('id-ID', { maximumFractionDigits: digits });
|
||||
}
|
||||
|
||||
const compactLabels = {
|
||||
saluranPenjualan: ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Pembeli langsung/eksportir', 'Lainnya'],
|
||||
waktuPenjualan: ['Langsung setelah panen', 'Ditumpuk beberapa waktu', 'Saat butuh uang mendesak'],
|
||||
aksesSaluranLain: ['Tidak pernah', 'Berhenti, tanpa akses', 'Berhenti, bisa kembali', 'Sesekali masih menjual', 'Akses alternatif aktif'],
|
||||
kesadaranPremium: ['Belum pernah mendengar', 'Belum paham persyaratan', 'Sedikit paham persyaratan', 'Paham, belum mengakses', 'Akses premium konsisten'],
|
||||
};
|
||||
const mobileLabels = {
|
||||
saluranPenjualan: ['Tengkulak/lokal', 'Koperasi', 'BUMDes', 'Pembeli/eksportir', 'Lainnya'],
|
||||
hubunganPembeli: ['Bebas pindah', 'Langganan tetap', 'Berbasis hutang', 'Titip jual', 'Komitmen volume', 'Semi-kontrak'],
|
||||
waktuPenjualan: ['Setelah panen', 'Ditumpuk dulu', 'Uang mendesak'],
|
||||
aksesSaluranLain: ['Tidak pernah', 'Tanpa akses', 'Bisa kembali', 'Sesekali jual', 'Akses aktif'],
|
||||
kesadaranPremium: ['Belum mendengar', 'Belum paham syarat', 'Sedikit paham', 'Paham, belum akses', 'Akses konsisten'],
|
||||
};
|
||||
|
||||
function hasTrendData(commodity) {
|
||||
return commodity && commodity.series.some(function (series) {
|
||||
return series.data.some(function (value) { return value !== null && Number(value) > 0; });
|
||||
});
|
||||
}
|
||||
|
||||
function formatPrice(value) {
|
||||
return value === null || value === undefined ? 'Belum ada data' : 'Rp ' + formatNumber(value) + '/kg';
|
||||
}
|
||||
|
||||
function renderPriceTrend(commodity) {
|
||||
const element = document.getElementById('chart-tren-harga-' + commodity.id);
|
||||
const summary = document.getElementById('summary-tren-harga-' + commodity.id);
|
||||
|
||||
if (!hasTrendData(commodity) || typeof ApexCharts === 'undefined') {
|
||||
element.classList.add('pasar-chart-empty');
|
||||
element.textContent = !hasTrendData(commodity)
|
||||
? 'Belum ada data harga untuk komoditas dan filter yang dipilih.'
|
||||
: 'Chart belum dapat dimuat. Silakan muat ulang halaman.';
|
||||
return;
|
||||
}
|
||||
|
||||
summary.textContent = commodity.name + '. ' + commodity.series.map(function (series) {
|
||||
return series.name + ': ' + series.data.map(function (value, index) {
|
||||
return priceTrend.periods[index] + ' ' + formatPrice(value);
|
||||
}).join(', ');
|
||||
}).join('. ');
|
||||
|
||||
const colors = config.chartColors.series.slice(0, 4);
|
||||
const labelStyle = { colors: config.colors.bodyColor, fontSize: '12px', fontFamily: config.fontFamily };
|
||||
new ApexCharts(element, {
|
||||
chart: { type: 'line', height: 360, fontFamily: config.fontFamily, foreColor: config.colors.bodyColor, toolbar: { show: false }, zoom: { enabled: false } },
|
||||
series: commodity.series,
|
||||
colors: colors,
|
||||
stroke: { width: 3, curve: 'straight' },
|
||||
markers: { size: 5, strokeWidth: 2, hover: { sizeOffset: 2 } },
|
||||
dataLabels: { enabled: false },
|
||||
xaxis: {
|
||||
categories: priceTrend.periods,
|
||||
tickPlacement: 'between',
|
||||
labels: { rotate: 0, style: labelStyle },
|
||||
axisBorder: { color: config.chartColors.grid },
|
||||
axisTicks: { color: config.chartColors.grid },
|
||||
},
|
||||
yaxis: {
|
||||
min: 0,
|
||||
title: { text: 'Harga (Rp/kg)', style: { color: config.colors.bodyColor, fontFamily: config.fontFamily, fontWeight: 400 } },
|
||||
labels: { style: labelStyle, formatter: function (value) { return formatNumber(value); } },
|
||||
},
|
||||
grid: { borderColor: config.chartColors.grid, strokeDashArray: 4, padding: { left: 8, right: 20 } },
|
||||
legend: { position: 'bottom', fontSize: '13px', fontFamily: config.fontFamily, labels: { colors: config.colors.bodyColor }, itemMargin: { horizontal: 12, vertical: 6 } },
|
||||
tooltip: {
|
||||
shared: true,
|
||||
intersect: false,
|
||||
custom: function ({ dataPointIndex }) {
|
||||
const rows = commodity.series.map(function (series, index) {
|
||||
return `<div class="pasar-trend-tooltip-row"><span style="color:${colors[index]}">${escapeHtml(series.name)}</span><strong>${formatPrice(series.data[dataPointIndex])}</strong></div>`;
|
||||
}).join('');
|
||||
return `<div class="pasar-chart-tooltip" style="background:${config.colors.cardColor};color:${config.colors.headingColor}"><strong>${escapeHtml(commodity.name)} — ${escapeHtml(priceTrend.periods[dataPointIndex])}</strong>${rows}</div>`;
|
||||
},
|
||||
},
|
||||
responsive: [{
|
||||
breakpoint: 576,
|
||||
options: {
|
||||
chart: { height: 420 },
|
||||
legend: { fontSize: '12px', itemMargin: { horizontal: 6, vertical: 4 } },
|
||||
xaxis: {
|
||||
labels: {
|
||||
hideOverlappingLabels: false,
|
||||
style: { fontSize: '11px' },
|
||||
formatter: function (value) {
|
||||
return { 'Dua Tahun Lalu': '2 thn lalu', 'Tahun Lalu': 'Tahun lalu', 'Panen Terakhir': 'Terakhir' }[value] || value;
|
||||
},
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
title: { text: undefined },
|
||||
labels: { style: { fontSize: '11px' }, formatter: function (value) { return formatNumber(value); } },
|
||||
},
|
||||
},
|
||||
}],
|
||||
}).render();
|
||||
}
|
||||
|
||||
function renderResponseChart(key, data, definition) {
|
||||
const element = document.getElementById('chart-' + key);
|
||||
if (!data.respondents || typeof ApexCharts === 'undefined') {
|
||||
element.classList.add('pasar-chart-empty');
|
||||
element.textContent = !data.respondents
|
||||
? 'Belum ada data jawaban untuk filter yang dipilih.'
|
||||
: 'Chart belum dapat dimuat. Silakan muat ulang halaman.';
|
||||
return;
|
||||
}
|
||||
|
||||
const color = config.chartColors.semantic[definition.variant];
|
||||
const borderColor = config.chartColors.grid;
|
||||
const labelStyle = { colors: config.colors.bodyColor, fontSize: '12px', fontFamily: config.fontFamily };
|
||||
const maxCount = Math.max(...data.series);
|
||||
const tickCount = Math.min(4, maxCount);
|
||||
const axisMax = Math.ceil(maxCount * 1.25 / tickCount) * tickCount;
|
||||
|
||||
new ApexCharts(element, {
|
||||
chart: { type: 'bar', height: 330, fontFamily: config.fontFamily, foreColor: config.colors.bodyColor, toolbar: { show: false } },
|
||||
series: [{ name: 'Responden', data: data.series }],
|
||||
colors: [color],
|
||||
plotOptions: { bar: { horizontal: true, borderRadius: 5, barHeight: '62%', dataLabels: { position: 'top' } } },
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
textAnchor: 'start',
|
||||
offsetX: 6,
|
||||
style: { fontSize: '12px', fontFamily: config.fontFamily, fontWeight: 500, colors: [config.colors.headingColor] },
|
||||
formatter: function (value, options) {
|
||||
return `${formatNumber(value)} (${formatNumber(data.percentages[options.dataPointIndex], 1)}%)`;
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
categories: compactLabels[key] || data.labels,
|
||||
min: 0,
|
||||
max: axisMax,
|
||||
tickAmount: tickCount,
|
||||
labels: { style: labelStyle, formatter: function (value) { return formatNumber(value); } },
|
||||
axisBorder: { color: borderColor },
|
||||
axisTicks: { color: borderColor },
|
||||
},
|
||||
yaxis: { labels: { maxWidth: 185, style: labelStyle } },
|
||||
grid: { borderColor: borderColor, strokeDashArray: 4, padding: { right: 20 } },
|
||||
legend: { show: false },
|
||||
tooltip: {
|
||||
theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light',
|
||||
custom: function ({ dataPointIndex }) {
|
||||
return `<div class="pasar-chart-tooltip" style="background:${color};color:${config.colors.headingColor};font-family:${config.fontFamily}">${escapeHtml(data.fullLabels[dataPointIndex])}: <strong>${formatNumber(data.series[dataPointIndex])} responden (${formatNumber(data.percentages[dataPointIndex], 1)}%)</strong></div>`;
|
||||
},
|
||||
},
|
||||
responsive: [{
|
||||
breakpoint: 576,
|
||||
options: {
|
||||
xaxis: { categories: mobileLabels[key], tickAmount: 2 },
|
||||
yaxis: { labels: { maxWidth: 125, style: { fontSize: '11px' } } },
|
||||
grid: { padding: { right: 10 } },
|
||||
dataLabels: { style: { fontSize: '11px' }, formatter: function (value) { return formatNumber(value); } },
|
||||
},
|
||||
}],
|
||||
}).render();
|
||||
}
|
||||
|
||||
function renderMarketCharts() {
|
||||
Object.entries(marketCharts).forEach(function ([key, data]) {
|
||||
renderResponseChart(key, data, chartDefinitions[key]);
|
||||
});
|
||||
priceTrend.commodities.forEach(renderPriceTrend);
|
||||
}
|
||||
|
||||
function setupChartPanel(panelId, renderCharts) {
|
||||
const chartPanel = document.getElementById(panelId);
|
||||
const chartControl = document.querySelector('.chart-group-header-toggle[aria-controls="' + chartPanel.id + '"]');
|
||||
const chartIcon = chartControl.querySelector('.chart-group-icon-toggle i');
|
||||
let rendered = false;
|
||||
|
||||
function syncChartPanelState() {
|
||||
const expanded = chartPanel.classList.contains('show');
|
||||
const label = expanded ? 'Sembunyikan chart' : 'Tampilkan chart';
|
||||
chartControl.setAttribute('aria-expanded', String(expanded));
|
||||
chartControl.setAttribute('aria-label', label);
|
||||
chartControl.title = label;
|
||||
chartIcon.classList.toggle('tabler-chevron-up', expanded);
|
||||
chartIcon.classList.toggle('tabler-chevron-down', !expanded);
|
||||
}
|
||||
|
||||
chartControl.addEventListener('keydown', function (event) {
|
||||
if (event.target !== chartControl || !['Enter', ' '].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
chartControl.click();
|
||||
});
|
||||
|
||||
chartPanel.addEventListener('shown.bs.collapse', function (event) {
|
||||
if (event.target !== chartPanel) return;
|
||||
syncChartPanelState();
|
||||
if (!rendered) {
|
||||
renderCharts();
|
||||
rendered = true;
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
chartPanel.addEventListener('hidden.bs.collapse', function (event) {
|
||||
if (event.target === chartPanel) syncChartPanelState();
|
||||
});
|
||||
|
||||
syncChartPanelState();
|
||||
}
|
||||
|
||||
setupChartPanel('saluran-harga-charts', renderMarketCharts);
|
||||
setupChartPanel('kesiapan-premium-charts', function () {
|
||||
renderResponseChart('kesadaranPremium', premiumAwareness, { variant: 'primary' });
|
||||
});
|
||||
|
||||
function fillSelect($select, items, placeholder) {
|
||||
$select.empty().append(new Option(placeholder, ''));
|
||||
@@ -208,7 +620,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
$('#filter-provinsi, #filter-kabkota, #filter-kecamatan, #filter-desa, #filter-komoditas').select2({ width: '100%' });
|
||||
$('#filter-provinsi, #filter-kabkota, #filter-kecamatan, #filter-desa, #filter-komoditas, #filter-segmen').select2({ width: '100%' });
|
||||
|
||||
$('#filter-provinsi').on('change', function () {
|
||||
const provinsi = selectedProvinsi();
|
||||
|
||||
@@ -758,7 +758,7 @@
|
||||
<div class="card-body d-flex justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<h5>Peta Sebaran</h5>
|
||||
<p class="text-body-secondary mb-0">Lahan, tanaman, dan OPT berdasarkan filter wilayah.</p>
|
||||
<p class="text-body-secondary mb-0">Lahan, tanaman, OPT, dan praktik budidaya berdasarkan filter wilayah.</p>
|
||||
</div>
|
||||
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
|
||||
<i class="icon-base ti tabler-chevron-down"></i>
|
||||
@@ -773,8 +773,8 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-2">
|
||||
<div>
|
||||
<h6 class="mb-0">Peta Sebaran Lahan & Tanaman</h6>
|
||||
<small class="text-body-secondary">Lahan dan tanaman berdasarkan filter wilayah, komoditas, dan segmen</small>
|
||||
<h6 class="mb-0">Peta Sebaran Lahan, Tanaman & Praktik Budidaya</h6>
|
||||
<small class="text-body-secondary">Lahan, tanaman, OPT, pemangkasan, pemupukan, dan praktik baik berdasarkan filter wilayah, komoditas, dan segmen</small>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary active produksi-map-layer-btn" data-map-layer="satellite">Satellite</button>
|
||||
@@ -795,34 +795,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card h-100 produksi-chart-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-2">
|
||||
<div>
|
||||
<h6 class="mb-0">Peta Praktik Budidaya</h6>
|
||||
<small class="text-body-secondary">Intensitas pemangkasan, pemupukan, dan kebiasaan praktik baik</small>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary active produksi-cultivation-map-layer-btn" data-map-layer="satellite">Satellite</button>
|
||||
<button type="button" class="btn btn-outline-secondary produksi-cultivation-map-layer-btn" data-map-layer="topo">Topographic</button>
|
||||
<button type="button" class="btn btn-outline-secondary produksi-cultivation-map-layer-btn" data-map-layer="parcels">Parcels</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="produksi-budidaya-map" class="produksi-map">
|
||||
<button type="button" id="produksi-budidaya-map-back" class="btn btn-sm btn-dark shadow produksi-map-back">
|
||||
<i class="icon-base ti tabler-arrow-left me-1"></i>
|
||||
Kembali ke Atas
|
||||
</button>
|
||||
<div id="produksi-budidaya-map-empty" class="produksi-map-empty">Belum ada poligon lahan untuk filter ini.</div>
|
||||
<div class="produksi-map-information-overlay" tabindex="0" role="region" aria-label="Informasi peta praktik budidaya">
|
||||
<div class="produksi-map-information" id="produksi-budidaya-map-information"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1044,8 +1016,8 @@
|
||||
<div class="card h-100 produksi-chart-card">
|
||||
<div class="card-header d-flex align-items-start justify-content-between gap-2">
|
||||
<div class="card-title mb-0">
|
||||
<h5 class="m-0 me-2">Praktik Fermentasi atau Pengolahan Pascapanen</h5>
|
||||
<small class="text-body-secondary">Apakah sudah ada praktik fermentasi atau pengolahan pascapanen yang dilakukan</small>
|
||||
<h5 class="m-0 me-2">Praktik Fermentasi atau Pengolahan Pascapanen per Komoditas</h5>
|
||||
<small class="text-body-secondary">Jumlah petani per komoditas berdasarkan praktik fermentasi atau pengolahan pascapanen.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -1079,11 +1051,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const mapData = @json($mapData);
|
||||
const dashboardFilters = @json($dashboardFilters);
|
||||
const mapInformationUrl = @json(route('dashboard.produktivitas.map-information'));
|
||||
const initialMapInformation = mapData.information || {};
|
||||
const mapInformation = {
|
||||
production: { ...initialMapInformation },
|
||||
cultivation: { ...initialMapInformation },
|
||||
};
|
||||
const mapInformation = { ...(mapData.information || {}) };
|
||||
const wilayah = @json($wilayah);
|
||||
const headingColor = config.colors.headingColor;
|
||||
const legendColor = config.colors.bodyColor;
|
||||
@@ -1094,9 +1062,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const labelColor = legendColor;
|
||||
const borderColor = pastelChart.grid || 'rgba(128, 128, 128, .18)';
|
||||
let productionMapRendered = false;
|
||||
let cultivationMapRendered = false;
|
||||
let actualProductivityRendered = false;
|
||||
const mapInformationRequestIds = { production: 0, cultivation: 0 };
|
||||
let mapInformationRequestId = 0;
|
||||
|
||||
function hasData(data) {
|
||||
return data && Array.isArray(data.series) && data.series.some(function (value) {
|
||||
@@ -1114,7 +1081,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
function renderProductionMapInformation(elementId, information, includeOpt = true) {
|
||||
function renderProductionMapInformation(elementId, information) {
|
||||
const element = document.getElementById(elementId);
|
||||
|
||||
if (!element) return;
|
||||
@@ -1193,17 +1160,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
<span class="produksi-map-information-section-title">Jumlah Petani / Komoditas</span>
|
||||
<div class="produksi-map-information-bars">${renderBars(commodityRows, 'Belum ada data komoditas.', 'petani')}</div>
|
||||
</section>
|
||||
${includeOpt ? `
|
||||
<section>
|
||||
<span class="produksi-map-information-section-title">OPT (Organisme Pengganggu Tanaman)</span>
|
||||
<div class="produksi-map-information-bars">${renderBars(optRows, 'Belum ada data OPT.', 'lahan')}</div>
|
||||
</section>
|
||||
` : ''}
|
||||
<section>
|
||||
<span class="produksi-map-information-section-title">OPT (Organisme Pengganggu Tanaman)</span>
|
||||
<div class="produksi-map-information-bars">${renderBars(optRows, 'Belum ada data OPT.', 'lahan')}</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadProductionMapInformation(mapType, level = null, id = null) {
|
||||
const requestId = ++mapInformationRequestIds[mapType];
|
||||
async function loadProductionMapInformation(level = null, id = null) {
|
||||
const requestId = ++mapInformationRequestId;
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Object.entries(dashboardFilters || {}).forEach(function ([key, value]) {
|
||||
@@ -1225,14 +1190,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (!response.ok) return;
|
||||
|
||||
const information = await response.json();
|
||||
if (requestId !== mapInformationRequestIds[mapType]) return;
|
||||
if (requestId !== mapInformationRequestId) return;
|
||||
|
||||
Object.assign(mapInformation[mapType], information);
|
||||
if (mapType === 'production') {
|
||||
renderProductionMapInformation('produksi-map-information', mapInformation.production);
|
||||
} else {
|
||||
renderProductionMapInformation('produksi-budidaya-map-information', mapInformation.cultivation, false);
|
||||
}
|
||||
Object.assign(mapInformation, information);
|
||||
renderProductionMapInformation('produksi-map-information', mapInformation);
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat informasi wilayah pada peta.', error);
|
||||
}
|
||||
@@ -1290,13 +1251,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function produksiChartTooltip(color, label, value, unit = '', total = null, digits = 0) {
|
||||
function produksiChartTooltip(color, label, value, unit = '', total = null, digits = 0, textColor = headingColor) {
|
||||
const numericValue = Number(value || 0);
|
||||
const unitText = unit ? ` ${escapeHtml(unit)}` : '';
|
||||
const percentageText = total === null ? '' : ` (${chartPercentage(numericValue, total)}%)`;
|
||||
|
||||
return `
|
||||
<div class="produksi-apex-tooltip" style="background:${escapeHtml(color)}; color:${escapeHtml(headingColor)}; font-family:${fontFamily};">
|
||||
<div class="produksi-apex-tooltip" style="background:${escapeHtml(color)}; color:${escapeHtml(textColor)}; font-family:${fontFamily};">
|
||||
${escapeHtml(label)}: <strong>${formatNumber(numericValue, digits)}${unitText}${percentageText}</strong>
|
||||
</div>
|
||||
`;
|
||||
@@ -1425,6 +1386,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const opt = Array.isArray(item.opt) && item.opt.length
|
||||
? item.opt.map(value => `<span class="produksi-opt-chip">${escapeHtml(value)}</span>`).join('')
|
||||
: '<span class="text-body-secondary small">Belum ada data OPT</span>';
|
||||
const praktikBaik = Array.isArray(item.praktik_baik) && item.praktik_baik.length
|
||||
? item.praktik_baik.map(value => `<li>${escapeHtml(value)}</li>`).join('')
|
||||
: '<li>Belum ada data praktik baik</li>';
|
||||
|
||||
return `
|
||||
<div class="produksi-lahan-popup">
|
||||
@@ -1449,6 +1413,19 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
<span class="produksi-popup-label">OPT</span>
|
||||
<div class="produksi-opt-list">${opt}</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<span class="produksi-popup-label">Praktik Budidaya</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="produksi-popup-label">Intensitas Pemangkasan</span>
|
||||
<span class="produksi-popup-value">${escapeHtml(item.pemangkasan || 'Belum ada data')}</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="produksi-popup-label">Intensitas Pemupukan</span>
|
||||
<span class="produksi-popup-value">${escapeHtml(item.pemupukan || 'Belum ada data')}</span>
|
||||
</div>
|
||||
<span class="produksi-popup-label">Kebiasaan praktik baik</span>
|
||||
<ul class="produksi-praktik-list">${praktikBaik}</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1513,7 +1490,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
renderProductionMapLevel(previous.level, previous.parentId);
|
||||
productionMapState.informationLevel = previous.informationLevel;
|
||||
productionMapState.informationId = previous.informationId;
|
||||
loadProductionMapInformation('production', previous.informationLevel, previous.informationId);
|
||||
loadProductionMapInformation(previous.informationLevel, previous.informationId);
|
||||
|
||||
if (previous.bounds?.isValid()) {
|
||||
productionMapState.map.fitBounds(previous.bounds, { padding: [30, 30] });
|
||||
@@ -1585,7 +1562,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
layer.bindPopup(landPopup(item), {
|
||||
className: 'produksi-map-popup',
|
||||
closeButton: true,
|
||||
maxWidth: 320,
|
||||
maxWidth: 340,
|
||||
maxHeight: 360,
|
||||
});
|
||||
} else {
|
||||
layer.bindTooltip(regionTooltip(item), {
|
||||
@@ -1606,7 +1584,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
productionMapState.informationLevel = nextLevel;
|
||||
productionMapState.informationId = item.id;
|
||||
loadProductionMapInformation('production', nextLevel, item.id);
|
||||
loadProductionMapInformation(nextLevel, item.id);
|
||||
renderProductionMapLevel(nextProductionMapLevel(nextLevel), item.id, event.target.getBounds());
|
||||
});
|
||||
}
|
||||
@@ -1641,234 +1619,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function initialCultivationMapLevel() {
|
||||
const levels = mapData.cultivationLevels || {};
|
||||
const filters = mapData.filters || {};
|
||||
|
||||
if (filters.desa && (levels.desa || []).length) return 'desa';
|
||||
if (filters.kecamatan && (levels.kecamatan || []).length) return 'kecamatan';
|
||||
if (filters.kabupaten && (levels.kabupaten || []).length) return 'kabupaten';
|
||||
if ((levels.kabupaten || []).length) return 'kabupaten';
|
||||
if ((levels.kecamatan || []).length) return 'kecamatan';
|
||||
if ((levels.desa || []).length) return 'desa';
|
||||
if ((levels.provinsi || []).length) return 'provinsi';
|
||||
|
||||
return 'lahan';
|
||||
}
|
||||
|
||||
function cultivationPopup(item) {
|
||||
const praktikBaik = Array.isArray(item.praktik_baik) && item.praktik_baik.length
|
||||
? item.praktik_baik.map(value => `<li>${escapeHtml(value)}</li>`).join('')
|
||||
: '<li>Belum ada data praktik baik</li>';
|
||||
|
||||
return `
|
||||
<div class="produksi-lahan-popup">
|
||||
<div class="produksi-lahan-popup-header">
|
||||
<span class="produksi-popup-label">Pemilik Lahan</span>
|
||||
<h6>${escapeHtml(item.nama_petani || 'N/A')}</h6>
|
||||
</div>
|
||||
<div class="produksi-lahan-popup-body">
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<span class="produksi-popup-label">Luas Lahan</span>
|
||||
<span class="produksi-popup-value">${formatNumber(item.luas || 0, 2)} Ha</span>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<span class="produksi-popup-label">ID Lahan</span>
|
||||
<span class="produksi-popup-value">TR-${escapeHtml(item.id)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="produksi-popup-label">Intensitas Pemangkasan</span>
|
||||
<span class="produksi-popup-value">${escapeHtml(item.pemangkasan || 'Belum ada data')}</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="produksi-popup-label">Intensitas Pemupukan</span>
|
||||
<span class="produksi-popup-value">${escapeHtml(item.pemupukan || 'Belum ada data')}</span>
|
||||
</div>
|
||||
<span class="produksi-popup-label">Kebiasaan praktik baik</span>
|
||||
<ul class="produksi-praktik-list">${praktikBaik}</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const cultivationMapState = {
|
||||
activeLayer: 'satellite',
|
||||
backButton: null,
|
||||
empty: null,
|
||||
group: null,
|
||||
history: [],
|
||||
informationId: null,
|
||||
informationLevel: null,
|
||||
layers: {},
|
||||
map: null,
|
||||
};
|
||||
|
||||
function initCultivationMap() {
|
||||
const element = document.getElementById('produksi-budidaya-map');
|
||||
|
||||
if (!element || typeof L === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cultivationMapState.map) {
|
||||
return true;
|
||||
}
|
||||
|
||||
cultivationMapState.layers = {
|
||||
satellite: L.tileLayer('https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', { maxZoom: 22 }),
|
||||
topo: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'),
|
||||
parcels: L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'),
|
||||
};
|
||||
cultivationMapState.map = L.map('produksi-budidaya-map', {
|
||||
center: [-8.67, 121.0],
|
||||
zoom: 10,
|
||||
layers: [cultivationMapState.layers.satellite],
|
||||
zoomControl: false,
|
||||
});
|
||||
cultivationMapState.group = L.featureGroup().addTo(cultivationMapState.map);
|
||||
cultivationMapState.backButton = document.getElementById('produksi-budidaya-map-back');
|
||||
cultivationMapState.empty = document.getElementById('produksi-budidaya-map-empty');
|
||||
enableMapInformationInteraction(element);
|
||||
|
||||
L.control.zoom({ position: 'topleft' }).addTo(cultivationMapState.map);
|
||||
|
||||
cultivationMapState.backButton?.addEventListener('click', function () {
|
||||
const previous = cultivationMapState.history.pop();
|
||||
|
||||
if (previous) {
|
||||
renderCultivationMapLevel(previous.level, previous.parentId);
|
||||
cultivationMapState.informationLevel = previous.informationLevel;
|
||||
cultivationMapState.informationId = previous.informationId;
|
||||
loadProductionMapInformation('cultivation', previous.informationLevel, previous.informationId);
|
||||
|
||||
if (previous.bounds?.isValid()) {
|
||||
cultivationMapState.map.fitBounds(previous.bounds, { padding: [30, 30] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function setCultivationMapLayer(type, button) {
|
||||
if (!cultivationMapState.map || !cultivationMapState.layers[type]) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.values(cultivationMapState.layers).forEach(function (layer) {
|
||||
cultivationMapState.map.removeLayer(layer);
|
||||
});
|
||||
cultivationMapState.layers[type].addTo(cultivationMapState.map);
|
||||
cultivationMapState.activeLayer = type;
|
||||
|
||||
document.querySelectorAll('.produksi-cultivation-map-layer-btn').forEach(function (item) {
|
||||
item.classList.toggle('active', item === button);
|
||||
});
|
||||
}
|
||||
|
||||
function renderCultivationMapLevel(level = null, parentId = null, fallbackBounds = null) {
|
||||
if (!initCultivationMap()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const levels = mapData.cultivationLevels || {};
|
||||
const nextLevel = level || initialCultivationMapLevel();
|
||||
let dataToRender = levels[nextLevel] || [];
|
||||
|
||||
if (parentId !== null) {
|
||||
dataToRender = dataToRender.filter(item => String(item.parent_id) === String(parentId));
|
||||
}
|
||||
|
||||
cultivationMapState.group.clearLayers();
|
||||
|
||||
if (nextLevel === 'lahan' && parentId !== null) {
|
||||
const parentDesa = (levels.desa || []).find(item => String(item.id) === String(parentId));
|
||||
const desaLayer = parentDesa
|
||||
? makeLayerFromPolygon(parentDesa.polygon, {
|
||||
color: '#F47C86',
|
||||
dashArray: '5, 8',
|
||||
fillOpacity: 0,
|
||||
interactive: false,
|
||||
weight: 2,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (desaLayer) {
|
||||
cultivationMapState.group.addLayer(desaLayer);
|
||||
}
|
||||
}
|
||||
|
||||
dataToRender.forEach(function (item) {
|
||||
const layer = makeLayerFromPolygon(item.polygon, nextLevel === 'lahan'
|
||||
? { color: '#5FC7D6', fillColor: '#5FC7D6', fillOpacity: .58, weight: 2 }
|
||||
: { color: '#9D8EF0', fillColor: '#9D8EF0', fillOpacity: .3, weight: 2 });
|
||||
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextLevel === 'lahan') {
|
||||
layer.bindPopup(cultivationPopup(item), {
|
||||
className: 'produksi-map-popup',
|
||||
closeButton: true,
|
||||
maxWidth: 340,
|
||||
});
|
||||
} else {
|
||||
layer.bindTooltip(regionTooltip(item), {
|
||||
className: 'produksi-map-tooltip',
|
||||
direction: 'top',
|
||||
offset: [0, -10],
|
||||
sticky: true,
|
||||
});
|
||||
|
||||
layer.on('click', function (event) {
|
||||
L.DomEvent.stopPropagation(event);
|
||||
cultivationMapState.history.push({
|
||||
level: nextLevel,
|
||||
parentId,
|
||||
bounds: cultivationMapState.group.getBounds(),
|
||||
informationLevel: cultivationMapState.informationLevel,
|
||||
informationId: cultivationMapState.informationId,
|
||||
});
|
||||
cultivationMapState.informationLevel = nextLevel;
|
||||
cultivationMapState.informationId = item.id;
|
||||
loadProductionMapInformation('cultivation', nextLevel, item.id);
|
||||
renderCultivationMapLevel(nextProductionMapLevel(nextLevel), item.id, event.target.getBounds());
|
||||
});
|
||||
}
|
||||
|
||||
cultivationMapState.group.addLayer(layer);
|
||||
});
|
||||
|
||||
if (cultivationMapState.backButton) {
|
||||
cultivationMapState.backButton.style.display = cultivationMapState.history.length > 0 ? 'block' : 'none';
|
||||
}
|
||||
|
||||
const layersCount = cultivationMapState.group.getLayers().length;
|
||||
|
||||
if (cultivationMapState.empty) {
|
||||
cultivationMapState.empty.style.display = layersCount > 0 ? 'none' : 'flex';
|
||||
}
|
||||
|
||||
if (layersCount > 0) {
|
||||
const bounds = cultivationMapState.group.getBounds();
|
||||
|
||||
if (bounds.isValid()) {
|
||||
cultivationMapState.map.fitBounds(bounds, { padding: [30, 30], maxZoom: 18 });
|
||||
}
|
||||
} else if (fallbackBounds?.isValid()) {
|
||||
cultivationMapState.map.fitBounds(fallbackBounds, { padding: [30, 30], maxZoom: 18 });
|
||||
} else {
|
||||
cultivationMapState.map.setView([-8.67, 121.0], 10);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
cultivationMapState.map.invalidateSize();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function renderDonut(selector, data, options = {}) {
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) return;
|
||||
@@ -1886,6 +1636,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
stroke: { width: 0 },
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
dropShadow: { enabled: false },
|
||||
formatter: function (value) {
|
||||
return value.toFixed(0) + '%';
|
||||
},
|
||||
@@ -1893,7 +1644,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
fontSize: '13px',
|
||||
fontFamily: fontFamily,
|
||||
fontWeight: 600,
|
||||
colors: ['#fff'],
|
||||
colors: chartColors.map(config.chartTextColor),
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
@@ -1915,7 +1666,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const label = w.globals.labels[seriesIndex] || 'Data';
|
||||
const color = w.globals.colors?.[seriesIndex] || chartColors[seriesIndex % chartColors.length] || '#9D8EF0';
|
||||
|
||||
return produksiChartTooltip(color, label, value, options.unit || 'data', total);
|
||||
return produksiChartTooltip(color, label, value, options.unit || 'data', total, 0, config.chartTextColor(color));
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
@@ -1992,6 +1743,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
legend: { show: false },
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
dropShadow: { enabled: false },
|
||||
formatter: function (value) {
|
||||
return value >= 8 ? value.toFixed(0) + '%' : '';
|
||||
},
|
||||
@@ -1999,7 +1751,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
fontSize: '11px',
|
||||
fontFamily: fontFamily,
|
||||
fontWeight: 600,
|
||||
colors: ['#fff'],
|
||||
colors: colors.map(config.chartTextColor),
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
@@ -2008,7 +1760,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const label = w.globals.labels[seriesIndex] || 'Data';
|
||||
const color = w.globals.colors?.[seriesIndex] || colors[seriesIndex % colors.length] || '#9D8EF0';
|
||||
|
||||
return produksiChartTooltip(color, label, value, options.unit || 'data', total);
|
||||
return produksiChartTooltip(color, label, value, options.unit || 'data', total, 0, config.chartTextColor(color));
|
||||
},
|
||||
},
|
||||
plotOptions: { pie: { expandOnClick: true } },
|
||||
@@ -2231,7 +1983,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
renderGroupedBar('#chart-sumber-air-kebun', chartData.sumberAirKebun, { unit: 'petani', yTitle: 'Jumlah Petani', height: 330 });
|
||||
renderGroupedBar('#chart-kebiasaan-praktik-baik', chartData.kebiasaanPraktikBaik, { unit: 'petani', yTitle: 'Jumlah Petani', height: 330 });
|
||||
renderPieWithLegend('#chart-praktik-sortasi-panen', chartData.praktikSortasiPanen, { unit: 'petani' });
|
||||
renderBar('#chart-praktik-fermentasi-pascapanen', chartData.praktikFermentasiPascapanen, { height: 330, color: pastelSemantic.primary || '#9D8EF0', unit: 'petani' });
|
||||
renderGroupedBar('#chart-praktik-fermentasi-pascapanen', chartData.praktikFermentasiPascapanen, { unit: 'petani', yTitle: 'Jumlah Petani', height: 330 });
|
||||
renderActualProductivityIfVisible();
|
||||
}
|
||||
|
||||
@@ -2261,37 +2013,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
productionMapRendered = true;
|
||||
}
|
||||
|
||||
function renderCultivationMapIfOpen() {
|
||||
if (!document.getElementById('peta-sebaran-panel')?.classList.contains('show')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cultivationMapRendered) {
|
||||
renderCultivationMapLevel();
|
||||
cultivationMapRendered = true;
|
||||
return;
|
||||
}
|
||||
|
||||
cultivationMapState.map?.invalidateSize();
|
||||
cultivationMapRendered = true;
|
||||
}
|
||||
|
||||
renderProductionMapInformation('produksi-map-information', mapInformation.production);
|
||||
renderProductionMapInformation('produksi-budidaya-map-information', mapInformation.cultivation, false);
|
||||
renderProductionMapInformation('produksi-map-information', mapInformation);
|
||||
|
||||
if (typeof L === 'undefined') {
|
||||
const mapEmpty = document.getElementById('produksi-map-empty');
|
||||
const cultivationMapEmpty = document.getElementById('produksi-budidaya-map-empty');
|
||||
|
||||
if (mapEmpty) {
|
||||
mapEmpty.textContent = 'Leaflet belum dimuat.';
|
||||
mapEmpty.style.display = 'flex';
|
||||
}
|
||||
|
||||
if (cultivationMapEmpty) {
|
||||
cultivationMapEmpty.textContent = 'Leaflet belum dimuat.';
|
||||
cultivationMapEmpty.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.produksi-map-layer-btn').forEach(function (button) {
|
||||
@@ -2300,12 +2030,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.produksi-cultivation-map-layer-btn').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
setCultivationMapLayer(button.dataset.mapLayer, button);
|
||||
});
|
||||
});
|
||||
|
||||
$('#filter-provinsi, #filter-kabkota, #filter-kecamatan, #filter-desa, #filter-komoditas, #filter-segmen').select2({ width: '100%' });
|
||||
|
||||
$('#filter-provinsi').on('change', function () {
|
||||
@@ -2388,7 +2112,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
if (panel.id === 'peta-sebaran-panel' && typeof L !== 'undefined') {
|
||||
renderProductionMapIfOpen();
|
||||
renderCultivationMapIfOpen();
|
||||
}
|
||||
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
@@ -90,6 +90,6 @@
|
||||
|
||||
<script src="{{ asset('assets/vendor/js/template-customizer.js') }}"></script>
|
||||
|
||||
<script src="{{ asset('assets/js/config.js') }}"></script>
|
||||
<script src="{{ asset('assets/js/config.js') }}?v={{ filemtime(public_path('assets/js/config.js')) }}"></script>
|
||||
@stack('scripts-head')
|
||||
</head>
|
||||
@@ -234,7 +234,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
|
||||
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan Profil</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -30,11 +30,110 @@
|
||||
|
||||
<div class="card mb-6">
|
||||
<div class="card-body pt-12">
|
||||
<h5 class="pb-3 border-bottom mb-4">Pengalaman Saluran Lain</h5>
|
||||
|
||||
<h5 class="pb-4 border-bottom mb-4">Saluran Penjualan & Akses Pasar</h5>
|
||||
|
||||
<h5 class="mb-4 mt-4">Saluran Penjualan Utama</h5>
|
||||
<div class="mb-4">
|
||||
<p class="form-label d-block">Kepada siapa Bapak/Ibu biasanya menjual hasil panen? (boleh pilih lebih dari satu)</p>
|
||||
<div class="row">
|
||||
@php
|
||||
$tujuanOptions = ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Langsung ke pembeli/eksportir'];
|
||||
@endphp
|
||||
@foreach($tujuanOptions as $tujuan)
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="tujuan_penjualan[]" value="{{ $tujuan }}" id="tujuan-{{ Str::slug($tujuan) }}" {{ in_array($tujuan, (array) old('tujuan_penjualan', $aksesPasar?->tujuan_penjualan ?? []), true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="tujuan-{{ Str::slug($tujuan) }}">{{ $tujuan }}</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="tujuan_penjualan[]" value="Lainnya" id="tujuan-lainnya" {{ in_array('Lainnya', (array) old('tujuan_penjualan', $aksesPasar?->tujuan_penjualan ?? []), true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="tujuan-lainnya">Lainnya:</label>
|
||||
</div>
|
||||
<input type="text" class="form-control w-auto flex-grow-1 max-w-400" style="max-width: 400px;" name="tujuan_penjualan_lainnya" value="{{ old('tujuan_penjualan_lainnya', $aksesPasar?->tujuan_penjualan_lainnya) }}" placeholder="Sebutkan lainnya...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-4 mt-4">Sifat Hubungan Dengan Pembeli</h5>
|
||||
<div class="mb-4">
|
||||
<p class="form-label d-block">Bagaimana sifat hubungan jual Bapak/Ibu dengan pembeli utama saat ini?</p>
|
||||
<div class="row">
|
||||
@php
|
||||
$sifatHubunganOptions = [
|
||||
'Bebas, bisa pindah pembeli kapan saja',
|
||||
'Langganan tetap, tapi tidak terikat',
|
||||
'Berbasis hutang, harus jual ke sana karena ada pinjaman',
|
||||
'Titip jual,dibayar setelah produk terjual',
|
||||
'Ada komitmen volume, sudah ada kesepakatan jumlah',
|
||||
'Semi-kontrak, ada kesepakatan harga tertentu'
|
||||
];
|
||||
@endphp
|
||||
@foreach($sifatHubunganOptions as $sifat)
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="radio" name="sifat_hubungan_bisnis" value="{{ $sifat }}" id="sifat-{{ Str::slug($sifat) }}" {{ old('sifat_hubungan_bisnis', $aksesPasar?->sifat_hubungan_bisnis) === $sifat ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="sifat-{{ Str::slug($sifat) }}">{{ $sifat }}</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-4 mt-4">Bentuk Produk Yang Dijual</h5>
|
||||
<div class="mb-4">
|
||||
<p class="form-label d-block">Dalam bentuk apa produk dijual?</p>
|
||||
<div class="row">
|
||||
@php
|
||||
$bentukPenjualanOptions = [
|
||||
'Buah segar hasil petik (gelondongan)',
|
||||
'Biji/polong basah (belum dikeringkan)',
|
||||
'Biji/polong kering/biji kupas',
|
||||
'Terfermentasi'
|
||||
];
|
||||
@endphp
|
||||
@foreach($bentukPenjualanOptions as $bentuk)
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="radio" name="bentuk_penjualan" value="{{ $bentuk }}" id="bentuk-{{ Str::slug($bentuk) }}" {{ old('bentuk_penjualan', $aksesPasar?->bentuk_penjualan) === $bentuk ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="bentuk-{{ Str::slug($bentuk) }}">{{ $bentuk }}</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-4 mt-4">Frekuensi Penjualan</h5>
|
||||
<div class="mb-4">
|
||||
<p class="form-label d-block">Kapan biasanya menjual hasil panen?</p>
|
||||
<div class="d-flex flex-column">
|
||||
@php
|
||||
$frekuensiOptions = [
|
||||
'Langsung setelah panen',
|
||||
'Ditumpuk dulu beberapa waktu',
|
||||
'Saat butuh uang mendesak'
|
||||
];
|
||||
@endphp
|
||||
@foreach($frekuensiOptions as $freq)
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="radio" name="frekuensi_penjualan" value="{{ $freq }}" id="freq-{{ Str::slug($freq) }}" {{ old('frekuensi_penjualan', $aksesPasar?->frekuensi_penjualan) === $freq ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="freq-{{ Str::slug($freq) }}">{{ $freq }}</label>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="pb-4 border-bottom mb-4 mt-4">Pengalaman Saluran Lain</h5>
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-4 form-control-validation">
|
||||
<label class="form-label d-block fw-bold">Pengalaman & akses ke saluran lain</label>
|
||||
<div class="col-md-12 mb-4 form-control-validation">
|
||||
<label class="form-label d-block">Apakah pernah menjual ke tempat lain sebelumnya?</label>
|
||||
@foreach($skorPengalamanOptions as $option)
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input @error('skor_pengalaman') is-invalid @enderror" type="radio"
|
||||
@@ -46,41 +145,67 @@
|
||||
@error('skor_pengalaman')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-4 form-control-validation">
|
||||
<label class="form-label" for="alasan-berhenti">Alasan berhenti</label>
|
||||
<input type="text" class="form-control @error('alasan_berhenti') is-invalid @enderror" id="alasan-berhenti" name="alasan_berhenti" value="{{ old('alasan_berhenti', $aksesPasar?->alasan_berhenti) }}" placeholder="Opsional (jika sudah berhenti)...">
|
||||
@error('alasan_berhenti')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4 form-control-validation">
|
||||
<label class="form-label d-block mb-2" for="saluran-terdahulu">Dulu pernah jual ke?</label>
|
||||
@foreach($saluranTerdahuluOptions as $option)
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input @error('saluran_terdahulu') is-invalid @enderror"
|
||||
type="checkbox"
|
||||
id="saluran-terdahulu-{{ Str::slug($option['value']) }}"
|
||||
name="saluran_terdahulu[]"
|
||||
value="{{ $option['value'] }}"
|
||||
{{ in_array($option['value'], (array) old('saluran_terdahulu', $aksesPasar?->saluran_terdahulu ?? []), true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="saluran-terdahulu-{{ Str::slug($option['value']) }}">
|
||||
{{ $option['label'] }}
|
||||
</label>
|
||||
<div class="col-md-12" id="blok-saluran-lanjutan">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-4 form-control-validation">
|
||||
<label class="form-label" for="alasan-berhenti">Alasan berhenti</label>
|
||||
<input type="text" class="form-control @error('alasan_berhenti') is-invalid @enderror" id="alasan-berhenti" name="alasan_berhenti" value="{{ old('alasan_berhenti', $aksesPasar?->alasan_berhenti) }}" placeholder="Opsional (jika sudah berhenti)...">
|
||||
@error('alasan_berhenti')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
@endforeach
|
||||
@error('saluran_terdahulu')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-4 form-control-validation">
|
||||
<label class="form-label" for="saluran-lainnya">Lainnya</label>
|
||||
<input type="text" class="form-control @error('saluran_terdahulu_lainnya') is-invalid @enderror" id="saluran-lainnya" name="saluran_terdahulu_lainnya" value="{{ old('saluran_terdahulu_lainnya', $aksesPasar?->saluran_terdahulu_lainnya) }}" placeholder="Sebutkan saluran lainnya...">
|
||||
@error('saluran_terdahulu_lainnya')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
<div class="col-md-6 mb-4"></div>
|
||||
|
||||
<div class="col-md-12 mb-4 form-control-validation">
|
||||
<label class="form-label d-block" for="saluran-terdahulu">Jika pernah, dulu menjual ke mana? (boleh pilih lebih dari satu)</label>
|
||||
<div class="row">
|
||||
@foreach($saluranTerdahuluOptions as $option)
|
||||
@if($option['value'] !== $saluranLainnyaValue)
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input @error('saluran_terdahulu') is-invalid @enderror"
|
||||
type="checkbox"
|
||||
id="saluran-terdahulu-{{ Str::slug($option['value']) }}"
|
||||
name="saluran_terdahulu[]"
|
||||
value="{{ $option['value'] }}"
|
||||
{{ in_array($option['value'], (array) old('saluran_terdahulu', $aksesPasar?->saluran_terdahulu ?? []), true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="saluran-terdahulu-{{ Str::slug($option['value']) }}">
|
||||
{{ $option['label'] }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input @error('saluran_terdahulu') is-invalid @enderror"
|
||||
type="checkbox"
|
||||
id="saluran-terdahulu-lainnya-checkbox"
|
||||
name="saluran_terdahulu[]"
|
||||
value="{{ $saluranLainnyaValue }}"
|
||||
{{ in_array($saluranLainnyaValue, (array) old('saluran_terdahulu', $aksesPasar?->saluran_terdahulu ?? []), true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label text-nowrap" for="saluran-terdahulu-lainnya-checkbox">
|
||||
Lainnya:
|
||||
</label>
|
||||
</div>
|
||||
<input type="text" class="form-control w-auto flex-grow-1 @error('saluran_terdahulu_lainnya') is-invalid @enderror" style="max-width: 400px;" id="saluran-lainnya" name="saluran_terdahulu_lainnya" value="{{ old('saluran_terdahulu_lainnya', $aksesPasar?->saluran_terdahulu_lainnya) }}" placeholder="Sebutkan saluran lainnya...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@error('saluran_terdahulu')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||
@error('saluran_terdahulu_lainnya')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="pb-3 border-bottom mb-4 mt-4">HARGA DI SETIAP TITIK</h5>
|
||||
<h6 class="mb-2">Harga Jual Panen Terakhir</h6>
|
||||
<p class="mb-1">Harga jual komoditas tiga musim panen terakhir:</p>
|
||||
<p class="mb-4">Dalam tiga musim panen terakhir, berapa harga jual yang Bapak/Ibu terima untuk komoditas ini sesuai dengan bentuk produk yang dijual?</p>
|
||||
<h5 class="pb-4 border-bottom mb-4 mt-4">Harga Di Setiap Titik</h5>
|
||||
<h5 class="mb-4 mt-4">Harga Jual Panen Terakhir</h5>
|
||||
<p class="form-label d-block">Harga jual komoditas tiga musim panen terakhir:</p>
|
||||
<p class="form-label d-block">Dalam tiga musim panen terakhir, berapa harga jual yang Bapak/Ibu terima untuk komoditas ini sesuai dengan bentuk produk yang dijual?</p>
|
||||
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-bordered align-middle">
|
||||
@@ -97,7 +222,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" name="harga_jual[{{ $row['index'] }}][bentuk_produk]" value="{{ $row['bentuk'] }}">
|
||||
<strong>{{ $row['bentuk'] }}:</strong>
|
||||
{{ $row['bentuk'] }}:
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
@@ -126,18 +251,87 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h5 class="pb-3 border-bottom mb-4 mt-4">PELUANG NILAI TAMBAH</h5>
|
||||
|
||||
<div class="border rounded p-4 mb-4">
|
||||
<h5 class="mb-4 mt-4">Sumber Informasi Harga</h5>
|
||||
<div class="mb-4">
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="mb-1 text-primary">Detail Praktik Fermentasi</h6>
|
||||
<p class="text-muted small mb-0">Apakah sudah ada praktik fermentasi atau pengolahan pascapanen yang Ibu/Bapak lakukan?</p>
|
||||
<p class="form-label d-block">Sebelum menjual, apakah Bapak/Ibu sudah tahu harga yang akan diterima?</p>
|
||||
<div class="row">
|
||||
@php
|
||||
$tahuHargaOptions = [
|
||||
'Selalu tahu sebelum pergi jual',
|
||||
'Kadang tahu, kadang tidak',
|
||||
'Baru tahu saat sudah di tempat pembeli',
|
||||
'Tidak pernah tahu sebelumnya — langsung jual saja'
|
||||
];
|
||||
@endphp
|
||||
@foreach($tahuHargaOptions as $tahu)
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input @error('tahu_harga_sebelumnya') is-invalid @enderror" type="radio" name="tahu_harga_sebelumnya" value="{{ $tahu }}" id="tahu-{{ Str::slug($tahu) }}" {{ old('tahu_harga_sebelumnya', $aksesPasar?->tahu_harga_sebelumnya) === $tahu ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="tahu-{{ Str::slug($tahu) }}">{{ $tahu }}</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@error('tahu_harga_sebelumnya')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-0">
|
||||
<p class="form-label d-block">Dari mana biasanya Bapak/Ibu tahu harga hasil panen sebelum menjual? (boleh pilih lebih dari satu)</p>
|
||||
<div class="row">
|
||||
@php
|
||||
$sumberInfoHargaOptions = [
|
||||
'Tengkulak/pedagang yang datang langsung',
|
||||
'Sesama petani/tetangga',
|
||||
'PPL/petugas dinas',
|
||||
'HP/WhatsApp/internet',
|
||||
'Radio/TV',
|
||||
'Facebook',
|
||||
'Tiktok',
|
||||
'Instagram',
|
||||
'Tidak pernah cek — langsung jual saja'
|
||||
];
|
||||
$sumberInfoLainnyaValue = 'Lainnya';
|
||||
@endphp
|
||||
|
||||
@foreach($sumberInfoHargaOptions as $sumber)
|
||||
<div class="col-md-4">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input @error('sumber_info_harga') is-invalid @enderror" type="checkbox" name="sumber_info_harga[]" value="{{ $sumber }}" id="sumber-{{ Str::slug($sumber) }}" {{ in_array($sumber, (array) old('sumber_info_harga', $aksesPasar?->sumber_info_harga ?? []), true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="sumber-{{ Str::slug($sumber) }}">{{ $sumber }}</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input @error('sumber_info_harga') is-invalid @enderror" type="checkbox" name="sumber_info_harga[]" value="{{ $sumberInfoLainnyaValue }}" id="sumber-lainnya-checkbox" {{ in_array($sumberInfoLainnyaValue, (array) old('sumber_info_harga', $aksesPasar?->sumber_info_harga ?? []), true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label text-nowrap" for="sumber-lainnya-checkbox">Lainnya:</label>
|
||||
</div>
|
||||
<input type="text" class="form-control w-auto flex-grow-1 @error('sumber_info_harga_lainnya') is-invalid @enderror" style="max-width: 400px;" id="sumber-info-lainnya-input" name="sumber_info_harga_lainnya" value="{{ old('sumber_info_harga_lainnya', $aksesPasar?->sumber_info_harga_lainnya) }}" placeholder="Sebutkan sumber lainnya...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@error('sumber_info_harga')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||
@error('sumber_info_harga_lainnya')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h5 class="pb-4 border-bottom mb-4 mt-4">Peluang Nilai Tambah</h5>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-4 mt-4">Detail Praktik Fermentasi</h5>
|
||||
<p class="form-label d-block">Apakah sudah ada praktik fermentasi atau pengolahan pascapanen yang Ibu/Bapak lakukan?</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-4 form-control-validation d-flex flex-column">
|
||||
<div class="mb-2 d-flex align-items-end">
|
||||
<label class="form-label mb-0" for="praktik-fermentasi">Praktik fermentasi yang dilakukan</label>
|
||||
<div class="d-flex align-items-end">
|
||||
<label class="form-label" for="praktik-fermentasi">Praktik fermentasi yang dilakukan</label>
|
||||
</div>
|
||||
<div>
|
||||
@foreach($praktikFermentasiOptions as $option)
|
||||
@@ -158,8 +352,8 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-4 form-control-validation d-flex flex-column">
|
||||
<div class="mb-2 d-flex align-items-end">
|
||||
<label class="form-label mb-0" for="proses-fermentasi">Jika ya, bagaimana prosesnya?</label>
|
||||
<div class="d-flex align-items-end">
|
||||
<label class="form-label" for="proses-fermentasi">Jika ya, bagaimana prosesnya?</label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea class="form-control @error('proses_fermentasi') is-invalid @enderror" id="proses-fermentasi" name="proses_fermentasi" rows="2" placeholder="Ceritakan prosesnya di sini...">{{ old('proses_fermentasi', $aksesPasar?->proses_fermentasi) }}</textarea>
|
||||
@@ -168,8 +362,8 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-4 form-control-validation d-flex flex-column">
|
||||
<div class="mb-2 d-flex align-items-end">
|
||||
<label class="form-label mb-0" for="model-fermentasi">Menurut Bapak/Ibu, apa model yang paling memungkinkan?</label>
|
||||
<div class="d-flex align-items-end">
|
||||
<label class="form-label" for="model-fermentasi">Menurut Bapak/Ibu, apa model yang paling memungkinkan?</label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea class="form-control @error('model_fermentasi_memungkinkan') is-invalid @enderror" id="model-fermentasi" name="model_fermentasi_memungkinkan" rows="2" placeholder="Model fermentasinya...">{{ old('model_fermentasi_memungkinkan', $aksesPasar?->model_fermentasi_memungkinkan) }}</textarea>
|
||||
@@ -179,11 +373,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="pb-3 border-bottom mb-4 mt-4">AKSES PASAR PREMIUM</h5>
|
||||
<h6 class="mb-3">Kesadaran Pasar Premium*</h6>
|
||||
<h5 class="pb-4 border-bottom mb-4 mt-4">Akses Pasar Premium</h5>
|
||||
<h5 class="mb-4 mt-4">Kesadaran Pasar Premium*</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4 form-control-validation">
|
||||
<label class="form-label d-block mb-2" for="skor-premium">Pengetahuan & akses pasar premium</label>
|
||||
<label class="form-label d-block" for="skor-premium">Pengetahuan & akses pasar premium</label>
|
||||
@foreach($skorKesadaranPremiumOptions as $option)
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input @error('skor_kesadaran_premium') is-invalid @enderror"
|
||||
@@ -201,16 +395,16 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4 form-control-validation">
|
||||
<label class="form-label mb-2" for="sumber-info">Sumber informasi utama</label>
|
||||
<label class="form-label" for="sumber-info">Sumber informasi utama</label>
|
||||
<input type="text" class="form-control @error('sumber_info_premium') is-invalid @enderror" id="sumber-info" name="sumber_info_premium" value="{{ old('sumber_info_premium', $aksesPasar?->sumber_info_premium) }}">
|
||||
@error('sumber_info_premium')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="pb-3 border-bottom mb-4 mt-4">KEPERCAYAAN & PERSEPSI RISIKO</h5>
|
||||
<h5 class="pb-4 border-bottom mb-4 mt-4">Kepercayaan & Persepsi Risiko</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4 form-control-validation">
|
||||
<label class="form-label d-block mb-2" for="tingkat-kepercayaan">Seberapa percaya Bapak/Ibu untuk menjual ke pembeli atau pasar baru, di luar pembeli yang biasa?</label>
|
||||
<label class="form-label d-block" for="tingkat-kepercayaan">Seberapa percaya Bapak/Ibu untuk menjual ke pembeli atau pasar baru, di luar pembeli yang biasa?</label>
|
||||
@foreach($tingkatKepercayaanOptions as $option)
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input @error('tingkat_kepercayaan_pembeli_baru') is-invalid @enderror"
|
||||
@@ -228,7 +422,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4 form-control-validation">
|
||||
<label class="form-label mb-2" for="alasan-kepercayaan">Apa yang membuat Bapak/Ibu ragu atau yakin untuk mencoba pembeli baru?</label>
|
||||
<label class="form-label" for="alasan-kepercayaan">Apa yang membuat Bapak/Ibu ragu atau yakin untuk mencoba pembeli baru?</label>
|
||||
<textarea class="form-control @error('alasan_kepercayaan_pembeli_baru') is-invalid @enderror"
|
||||
id="alasan-kepercayaan"
|
||||
name="alasan_kepercayaan_pembeli_baru"
|
||||
@@ -239,8 +433,9 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-device-floppy me-1_5 icon-sm"></i>Simpan Akses Pasar
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>Simpan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,6 +453,8 @@
|
||||
const saluranLainnyaValue = @json($saluranLainnyaValue);
|
||||
const praktikBelumPernahValue = String(@json($praktikBelumPernahValue));
|
||||
const skorPengalamanBerhentiValues = @json($skorPengalamanBerhentiValues ?? [2,3]).map(Number);
|
||||
const skorBelumPernahValue = "1";
|
||||
const sumberInfoLainnyaValue = @json($sumberInfoLainnyaValue ?? 'Lainnya');
|
||||
|
||||
function onlyDigits(value) {
|
||||
return String(value || '').replace(/\D/g, '');
|
||||
@@ -265,10 +462,25 @@
|
||||
|
||||
function formatRupiah(value) {
|
||||
const digits = onlyDigits(value);
|
||||
|
||||
return digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||
}
|
||||
|
||||
function toggleSaluranLanjutan() {
|
||||
const selectedRadio = $('input[name="skor_pengalaman"]:checked');
|
||||
const val = selectedRadio.val();
|
||||
const labelText = selectedRadio.parent().text().toLowerCase();
|
||||
|
||||
const isBelumPernah = !val || labelText.includes('belum pernah') || val === skorBelumPernahValue;
|
||||
|
||||
if (isBelumPernah) {
|
||||
$('#blok-saluran-lanjutan').addClass('d-none');
|
||||
$('#blok-saluran-lanjutan input[type="text"]').val('');
|
||||
$('#blok-saluran-lanjutan input[type="checkbox"]').prop('checked', false);
|
||||
} else {
|
||||
$('#blok-saluran-lanjutan').removeClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function syncAlasanBerhenti() {
|
||||
const val = Number($('input[name="skor_pengalaman"]:checked').val());
|
||||
const isBerhenti = skorPengalamanBerhentiValues.includes(val);
|
||||
@@ -282,6 +494,12 @@
|
||||
if (!isLainnya) $('#saluran-lainnya').val('');
|
||||
}
|
||||
|
||||
function syncSumberInfoLainnya() {
|
||||
const isLainnya = $('input[name="sumber_info_harga[]"][value="' + sumberInfoLainnyaValue + '"]').is(':checked');
|
||||
$('#sumber-info-lainnya-input').prop('disabled', !isLainnya);
|
||||
if (!isLainnya) $('#sumber-info-lainnya-input').val('');
|
||||
}
|
||||
|
||||
function syncPraktikFermentasi() {
|
||||
const isBelumPernah = $('input[name="praktik_fermentasi"]:checked').val() === praktikBelumPernahValue;
|
||||
$('#proses-fermentasi, #model-fermentasi').prop('disabled', isBelumPernah);
|
||||
@@ -290,9 +508,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$('input[name="skor_pengalaman"]').on('change', syncAlasanBerhenti);
|
||||
$('input[name="skor_pengalaman"]').on('change', function () {
|
||||
toggleSaluranLanjutan();
|
||||
syncAlasanBerhenti();
|
||||
});
|
||||
|
||||
$('input[name="saluran_terdahulu[]"]').on('change', syncSaluranLainnya);
|
||||
$('input[name="sumber_info_harga[]"]').on('change', syncSumberInfoLainnya);
|
||||
$('input[name="praktik_fermentasi"]').on('change', syncPraktikFermentasi);
|
||||
|
||||
$(document).on('input', '.rupiah-input', function () {
|
||||
const cursorAtEnd = this.selectionStart === this.value.length;
|
||||
this.value = formatRupiah(this.value);
|
||||
@@ -301,12 +525,15 @@
|
||||
this.setSelectionRange(this.value.length, this.value.length);
|
||||
}
|
||||
});
|
||||
|
||||
$('.rupiah-input').each(function () {
|
||||
this.value = formatRupiah(this.value);
|
||||
});
|
||||
|
||||
toggleSaluranLanjutan();
|
||||
syncAlasanBerhenti();
|
||||
syncSaluranLainnya();
|
||||
syncSumberInfoLainnya();
|
||||
syncPraktikFermentasi();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.js"></script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
@@ -43,13 +44,18 @@
|
||||
<div class="col-md-3">
|
||||
<div class="user-avatar-section">
|
||||
<div class=" d-flex align-items-center flex-column">
|
||||
<img id="fotoPreview"
|
||||
class="mb-3 shadow-sm rounded-3"
|
||||
<button type="button" class="border-0 bg-transparent p-0 mb-3 rounded-3"
|
||||
data-bs-toggle="modal" data-bs-target="#fotoProfilModal"
|
||||
aria-label="Lihat foto profil petani" title="Lihat foto profil petani">
|
||||
<img id="fotoPreview"
|
||||
class="shadow-sm rounded-3"
|
||||
src="{{ $petani->foto ? route('petani.show-image', ['id' => $petani->id, 'field' => 'foto']) : asset('assets/img/avatars/3.jpg') }}"
|
||||
alt="Foto profil petani"
|
||||
data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
data-bs-original-title="Klik untuk mengganti foto profil petani"
|
||||
style="width: 120px !important; height: 120px !important; cursor:pointer; object-fit:cover; display:block; margin: 0 auto;" />
|
||||
</button>
|
||||
<button type="button" id="btnGantiFoto" class="btn btn-sm btn-outline-primary mb-3">
|
||||
Ganti Foto
|
||||
</button>
|
||||
|
||||
<button type="button" id="btnHapusFoto" class="btn btn-sm btn-outline-danger mb-4" style="display: {{ $petani->foto ? 'block' : 'none' }};">
|
||||
Hapus Foto
|
||||
@@ -113,7 +119,7 @@
|
||||
<select class="form-control @error('pendidikan_terakhir') is-invalid @enderror" id="edit-petani-pendidikan-terakhir" name="pendidikan_terakhir">
|
||||
<option value="">Pilih Pendidikan Terakhir</option>
|
||||
@foreach($pendidikan as $value)
|
||||
<option value="{{ $value['value'] }}" {{ old('pendidikan_terakhir', $petani->pendidikan_terakhir->value) == $value['value'] ? 'selected' : '' }}>{{ $value['label'] }}</option>
|
||||
<option value="{{ $value['value'] }}" {{ old('pendidikan_terakhir', $petani->pendidikan_terakhir?->value) == $value['value'] ? 'selected' : '' }}>{{ $value['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('pendidikan_terakhir')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
@@ -123,7 +129,7 @@
|
||||
<select class="form-control @error('status_perkawinan') is-invalid @enderror" id="edit-petani-status-pernikahan" name="status_perkawinan">
|
||||
<option value="">Pilih Status Pernikahan</option>
|
||||
@foreach($pernikahan as $value)
|
||||
<option value="{{ $value['value'] }}" {{ old('status_perkawinan', $petani->status_perkawinan->value) == $value['value'] ? 'selected' : '' }}>{{ $value['label'] }}</option>
|
||||
<option value="{{ $value['value'] }}" {{ old('status_perkawinan', $petani->status_perkawinan?->value) == $value['value'] ? 'selected' : '' }}>{{ $value['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('status_perkawinan')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
@@ -159,7 +165,7 @@
|
||||
<select class="form-control @error('provinsi_id') is-invalid @enderror" id="edit-petani-provinsi" name="provinsi_id">
|
||||
<option value="">Pilih Provinsi</option>
|
||||
@foreach($provinsi as $p)
|
||||
<option value="{{ $p->id }}" {{ old('provinsi_id', $petani->desaKelurahan->kecamatan->kabupatenKota->provinsi_id) == $p->id ? 'selected' : '' }}>{{ $p->nama }}</option>
|
||||
<option value="{{ $p->id }}" {{ old('provinsi_id', $petani->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi_id) == $p->id ? 'selected' : '' }}>{{ $p->nama }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('provinsi_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
@@ -168,7 +174,9 @@
|
||||
<label class="form-label" for="edit-petani-kabupaten">Kabupaten/Kota<span class="text-danger">*</span></label>
|
||||
<select class="form-control @error('kabupaten_id') is-invalid @enderror" id="edit-petani-kabupaten" name="kabupaten_id" >
|
||||
<option value="">Pilih Kabupaten/Kota</option>
|
||||
@if($petani->desaKelurahan?->kecamatan?->kabupatenKota)
|
||||
<option value="{{ $petani->desaKelurahan->kecamatan->kabupatenKota->id }}" selected>{{ $petani->desaKelurahan->kecamatan->kabupatenKota->nama }}</option>
|
||||
@endif
|
||||
</select>
|
||||
@error('kabupaten_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
@@ -176,7 +184,9 @@
|
||||
<label class="form-label" for="edit-petani-kecamatan">Kecamatan<span class="text-danger">*</span></label>
|
||||
<select class="form-control @error('kecamatan_id') is-invalid @enderror" id="edit-petani-kecamatan" name="kecamatan_id" >
|
||||
<option value="">Pilih Kecamatan</option>
|
||||
@if($petani->desaKelurahan?->kecamatan)
|
||||
<option value="{{ $petani->desaKelurahan->kecamatan->id }}" selected>{{ $petani->desaKelurahan->kecamatan->nama }}</option>
|
||||
@endif
|
||||
</select>
|
||||
@error('kecamatan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
@@ -184,7 +194,9 @@
|
||||
<label class="form-label" for="edit-petani-desa">Desa/Kelurahan<span class="text-danger">*</span></label>
|
||||
<select class="form-control @error('desa_kelurahan_id') is-invalid @enderror" id="edit-petani-desa" name="desa_kelurahan_id" >
|
||||
<option value="">Pilih Desa/Kelurahan</option>
|
||||
@if($petani->desaKelurahan)
|
||||
<option value="{{ $petani->desaKelurahan->id }}" selected>{{ $petani->desaKelurahan->nama }}</option>
|
||||
@endif
|
||||
</select>
|
||||
@error('desa_kelurahan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
@@ -242,7 +254,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
|
||||
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan Profil</button>
|
||||
</div>
|
||||
|
||||
@@ -253,6 +265,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="fotoProfilModal" tabindex="-1" aria-labelledby="fotoProfilModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="fotoProfilModalLabel">Foto Profil Petani</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Tutup"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img id="fotoProfilBesar" class="img-fluid rounded-3" alt="Foto profil petani" style="max-height: 75vh; object-fit: contain;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="cropModal" tabindex="-1" aria-labelledby="cropModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
@@ -344,7 +370,11 @@
|
||||
|
||||
let croppieInstance = null;
|
||||
|
||||
$('#fotoPreview').on('click', function () {
|
||||
$('#fotoProfilModal').on('show.bs.modal', function () {
|
||||
$('#fotoProfilBesar').attr('src', $('#fotoPreview').attr('src'));
|
||||
});
|
||||
|
||||
$('#btnGantiFoto').on('click', function () {
|
||||
$('#foto').val('');
|
||||
$('#foto').click();
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<hr class="my-2"> <h6 class="mb-1" style="font-size: 0.85rem;">Domisili</h6>
|
||||
<p class="mb-0 text-muted" style="font-size: 0.75rem;">Alamat: {{ isset($petani) ? $petani->alamat : 'Alamat' }}</p>
|
||||
<p class="mb-0 text-muted" style="font-size: 0.75rem; line-height: 1.3;">{{ isset($petani) ? $petani->desaKelurahan->nama : 'Desa/Kelurahan' }}, {{ $petani ? $petani->desaKelurahan->kecamatan->nama : 'Kecamatan' }}, {{ $petani ? $petani->desaKelurahan->kecamatan->kabupatenKota->nama : 'Kabupaten/Kota' }}, {{ $petani ? $petani->desaKelurahan->kecamatan->kabupatenKota->provinsi->nama : 'Provinsi' }}</p>
|
||||
<p class="mb-0 text-muted" style="font-size: 0.75rem; line-height: 1.3;">{{ isset($petani) ? $petani->desaKelurahan?->nama ?? 'Belum diisi' : 'Desa/Kelurahan' }}, {{ $petani ? $petani->desaKelurahan?->kecamatan?->nama ?? 'Belum diisi' : 'Kecamatan' }}, {{ $petani ? $petani->desaKelurahan?->kecamatan?->kabupatenKota?->nama ?? 'Belum diisi' : 'Kabupaten/Kota' }}, {{ $petani ? $petani->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi?->nama ?? 'Belum diisi' : 'Provinsi' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,7 +116,7 @@
|
||||
</h5>
|
||||
<p class="mb-0" style="font-size: 0.75rem;">
|
||||
@if(!is_null($skorLahanTanaman))
|
||||
<span class="me-1">{{ number_format($totalTanaman, 0, ',', '.') }} tanaman {{ $petani?->komoditas->nama }}</span>
|
||||
<span class="me-1">{{ number_format($totalTanaman, 0, ',', '.') }} tanaman {{ $petani?->komoditas?->nama ?? 'Belum diisi' }}</span>
|
||||
@else
|
||||
<span class="me-1">Belum terisi</span>
|
||||
@endif
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
@push('style')
|
||||
<style>
|
||||
#data-petani {
|
||||
scroll-margin-top: 7rem;
|
||||
}
|
||||
|
||||
.petani-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.indicator-filter-row {
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
@@ -84,8 +92,8 @@
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid flex-grow-1 container-p-y">
|
||||
<div class="d-flex flex-row gap-3 mb-4">
|
||||
<div class="w-25">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<x-summary-card
|
||||
title="Petani"
|
||||
count="{{ number_format($totalPetani ?? 0, 0, ',', '.') }}"
|
||||
@@ -94,7 +102,7 @@
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-25">
|
||||
<div class="col-12 col-md-4">
|
||||
<x-summary-card
|
||||
title="Lahan & Tanaman"
|
||||
count="{{ number_format($totalLuasLahan ?? 0, 2, ',', '.') }} ha"
|
||||
@@ -103,7 +111,7 @@
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-25">
|
||||
<div class="col-12 col-md-4">
|
||||
<x-summary-card
|
||||
title="Segmentasi"
|
||||
count="{{ number_format((int) ($skor->firstWhere('segmen', 'Penggerak')->total ?? 0), 0, ',', '.') }} Penggerak"
|
||||
@@ -265,7 +273,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="data-petani">
|
||||
<div class="card-header border-bottom">
|
||||
<h5 class="card-title mb-0">Data Petani</h5>
|
||||
</div>
|
||||
@@ -315,10 +323,10 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($petani as $index => $item)
|
||||
<tr>
|
||||
<tr class="petani-row">
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>
|
||||
<span class="text-heading d-block">{{ $item->nama }}</span>
|
||||
<a href="{{ route('petani.edit', $item->id) }}" class="text-heading d-block">{{ $item->nama }}</a>
|
||||
<small class="text-muted">
|
||||
{{ $item->jenis_kelamin ? 'Laki-laki' : 'Perempuan' }}
|
||||
@if(! is_null($item->umur))
|
||||
@@ -423,6 +431,14 @@
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.petani-row', function (event) {
|
||||
if ($(event.target).closest('a, button, input, select, textarea, label').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = this.querySelector('.edit-record').href;
|
||||
});
|
||||
|
||||
$(document).on('click', '.delete-record', function () {
|
||||
const id = $(this).data('id');
|
||||
const name = $(this).data('nama');
|
||||
|
||||
@@ -517,7 +517,7 @@
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 text-end">
|
||||
<a href="{{ route('petani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
|
||||
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan Lahan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -406,8 +406,8 @@
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 text-end">
|
||||
<a href="{{ route('petani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
|
||||
<button id="delete-lahan"type="button" class="btn btn-danger data-submit wave-effect wave-light"><i class="ti tabler-trash me-2"></i> Hapus Lahan</button>
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
|
||||
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan Lahan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-primary">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
|
||||
@@ -167,10 +167,13 @@
|
||||
Tambah Pelatihan
|
||||
</button>
|
||||
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan Riwayat Pelatihan
|
||||
</button>
|
||||
<div class="d-flex flex-wrap justify-content-end gap-2">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan Riwayat Pelatihan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -347,10 +350,13 @@
|
||||
Tambah Sertifikasi
|
||||
</button>
|
||||
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan Sertifikasi Keahlian
|
||||
</button>
|
||||
<div class="d-flex flex-wrap justify-content-end gap-2">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan Sertifikasi Keahlian
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -499,6 +499,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
Simpan
|
||||
</button>
|
||||
|
||||
@@ -530,6 +530,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -1039,6 +1040,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -1352,6 +1354,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -1542,6 +1545,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -1678,9 +1682,12 @@
|
||||
Tambah Komoditas
|
||||
</button>
|
||||
|
||||
<button type="submit" class="btn btn-success">
|
||||
Simpan Produktivitas
|
||||
</button>
|
||||
<div class="d-flex flex-wrap justify-content-end gap-2">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
Simpan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1725,6 +1732,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
|
||||
@@ -399,6 +399,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -567,6 +568,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -696,6 +698,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -915,6 +918,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -1043,6 +1047,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
|
||||
@@ -398,6 +398,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -566,6 +567,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -694,6 +696,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -912,6 +915,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
@@ -1039,6 +1043,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('petani.index') }}" class="btn btn-label-secondary me-2">Kembali</a>
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-label-secondary me-2">Kembali</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\AksesPasar\PraktikFermentasi;
|
||||
use App\Http\Controllers\Web\Dashboard\SummaryController;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
beforeEach(function () {
|
||||
Schema::create('master_komoditas', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nama');
|
||||
});
|
||||
foreach ([
|
||||
'master_kabupaten_kota' => 'provinsi_id',
|
||||
'master_kecamatan' => 'kabupaten_kota_id',
|
||||
'master_desa_kelurahan' => 'kecamatan_id',
|
||||
] as $name => $parent) {
|
||||
Schema::create($name, function (Blueprint $table) use ($parent) {
|
||||
$table->id();
|
||||
$table->integer($parent);
|
||||
});
|
||||
}
|
||||
Schema::create('petani', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('komoditas_id')->nullable();
|
||||
$table->integer('desa_kelurahan_id');
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('akses_pasar', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('petani_id');
|
||||
$table->string('praktik_fermentasi')->nullable();
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('petani_skor', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('petani_id');
|
||||
$table->float('skor_kesiapan_adopsi');
|
||||
$table->softDeletes();
|
||||
});
|
||||
DB::table('master_komoditas')->insert([
|
||||
['id' => 1, 'nama' => 'Kakao'], ['id' => 2, 'nama' => 'Kopi'], ['id' => 3, 'nama' => 'Vanili'],
|
||||
]);
|
||||
DB::table('master_kabupaten_kota')->insert(['id' => 1, 'provinsi_id' => 1]);
|
||||
DB::table('master_kecamatan')->insert(['id' => 1, 'kabupaten_kota_id' => 1]);
|
||||
DB::table('master_desa_kelurahan')->insert([
|
||||
['id' => 1, 'kecamatan_id' => 1], ['id' => 2, 'kecamatan_id' => 1],
|
||||
]);
|
||||
|
||||
foreach ([
|
||||
[1, 1, 1, PraktikFermentasi::RUTIN_SENDIRI->value],
|
||||
[2, 1, 1, PraktikFermentasi::RUTIN_KELOMPOK->value],
|
||||
[3, 2, 2, PraktikFermentasi::PERNAH_SENDIRI->value],
|
||||
[4, 2, 2, PraktikFermentasi::PERNAH_KELOMPOK->value],
|
||||
[5, 3, 2, PraktikFermentasi::BELUM_PERNAH->value],
|
||||
[6, 3, 2, null],
|
||||
[7, 1, 1, ''],
|
||||
[8, 2, 2, PraktikFermentasi::RUTIN_SENDIRI->value],
|
||||
[9, null, 2, PraktikFermentasi::BELUM_PERNAH->value],
|
||||
[10, 1, 1, PraktikFermentasi::RUTIN_SENDIRI->value],
|
||||
[11, 1, 1, 'Tidak dikenal'],
|
||||
] as [$id, $commodity, $village, $practice]) {
|
||||
DB::table('petani')->insert([
|
||||
'id' => $id, 'komoditas_id' => $commodity, 'desa_kelurahan_id' => $village,
|
||||
'deleted_at' => $id === 10 ? '2026-01-01' : null,
|
||||
]);
|
||||
DB::table('akses_pasar')->insert([
|
||||
'petani_id' => $id, 'praktik_fermentasi' => $practice,
|
||||
'deleted_at' => $id === 8 ? '2026-01-01' : null,
|
||||
]);
|
||||
}
|
||||
DB::table('petani_skor')->insert([
|
||||
['petani_id' => 1, 'skor_kesiapan_adopsi' => 4.1, 'deleted_at' => null],
|
||||
['petani_id' => 2, 'skor_kesiapan_adopsi' => 2.6, 'deleted_at' => null],
|
||||
['petani_id' => 3, 'skor_kesiapan_adopsi' => 4.4, 'deleted_at' => '2026-01-01'],
|
||||
]);
|
||||
|
||||
$controller = app(SummaryController::class);
|
||||
$query = new ReflectionMethod($controller, 'filteredAksesPasar');
|
||||
$chart = new ReflectionMethod($controller, 'fermentationPracticeByCommodityChart');
|
||||
$this->fermentationChart = fn (array $filters = []) => $chart->invoke(
|
||||
$controller, $query->invoke($controller, new Request($filters)),
|
||||
);
|
||||
});
|
||||
|
||||
test('fermentation groups individual and collective responses by commodity', function () {
|
||||
expect(($this->fermentationChart)())->toBe([
|
||||
'labels' => ['Belum ditentukan', 'Kakao', 'Kopi', 'Vanili'],
|
||||
'series' => [
|
||||
['name' => 'Ya, rutin', 'data' => [0, 2, 0, 0]],
|
||||
['name' => 'Pernah mencoba', 'data' => [0, 0, 2, 0]],
|
||||
['name' => 'Belum pernah', 'data' => [1, 0, 0, 1]],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('fermentation respects region and commodity filters', function () {
|
||||
$chart = ($this->fermentationChart)(['desaKelurahan_id' => 2, 'komoditas_id' => 2]);
|
||||
expect($chart['labels'])->toBe(['Kopi']);
|
||||
expect(array_column($chart['series'], 'data'))->toBe([[0], [2], [0]]);
|
||||
expect(($this->fermentationChart)(['desaKelurahan_id' => 1])['labels'])->toBe(['Kakao']);
|
||||
});
|
||||
|
||||
test('fermentation respects segment filters and excludes deleted scores', function () {
|
||||
$chart = ($this->fermentationChart)(['segmen' => 'Pemerhati']);
|
||||
expect($chart['labels'])->toBe(['Kakao']);
|
||||
expect(array_column($chart['series'], 'data'))->toBe([[1], [0], [0]]);
|
||||
expect(($this->fermentationChart)(['segmen' => 'Penggerak'])['labels'])->toBe(['Kakao']);
|
||||
});
|
||||
|
||||
test('fermentation returns empty grouped series when no matching responses exist', function () {
|
||||
$chart = ($this->fermentationChart)(['provinsi_id' => 99]);
|
||||
expect($chart['labels'])->toBe([]);
|
||||
expect(array_column($chart['series'], 'data'))->toBe([[], [], []]);
|
||||
});
|
||||
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Web\Dashboard\SummaryController;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
beforeEach(function () {
|
||||
Schema::create('master_komoditas', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nama');
|
||||
$table->softDeletes();
|
||||
});
|
||||
foreach ([
|
||||
'master_kabupaten_kota' => 'provinsi_id',
|
||||
'master_kecamatan' => 'kabupaten_kota_id',
|
||||
'master_desa_kelurahan' => 'kecamatan_id',
|
||||
] as $name => $parent) {
|
||||
Schema::create($name, function (Blueprint $table) use ($parent) {
|
||||
$table->id();
|
||||
$table->integer($parent);
|
||||
});
|
||||
}
|
||||
Schema::create('petani', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('komoditas_id');
|
||||
$table->integer('desa_kelurahan_id');
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('harga_jual_panen', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('petani_id');
|
||||
$table->string('bentuk_produk');
|
||||
$table->integer('harga_panen_terakhir')->nullable();
|
||||
$table->integer('harga_tahun_lalu')->nullable();
|
||||
$table->integer('harga_dua_tahun_lalu')->nullable();
|
||||
});
|
||||
Schema::create('petani_skor', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('petani_id');
|
||||
$table->float('skor_kesiapan_adopsi');
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('akses_pasar', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('petani_id');
|
||||
$table->text('tujuan_penjualan')->nullable();
|
||||
$table->text('sifat_hubungan_bisnis')->nullable();
|
||||
$table->string('frekuensi_penjualan')->nullable();
|
||||
$table->integer('skor_pengalaman')->nullable();
|
||||
$table->integer('skor_kesadaran_premium')->nullable();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
DB::table('master_komoditas')->insert([
|
||||
['id' => 1, 'nama' => 'Kakao'], ['id' => 2, 'nama' => 'Vanili'],
|
||||
['id' => 3, 'nama' => 'Kopi'], ['id' => 4, 'nama' => 'Kemiri'],
|
||||
]);
|
||||
DB::table('master_kabupaten_kota')->insert(['id' => 1, 'provinsi_id' => 1]);
|
||||
DB::table('master_kecamatan')->insert(['id' => 1, 'kabupaten_kota_id' => 1]);
|
||||
DB::table('master_desa_kelurahan')->insert([
|
||||
['id' => 1, 'kecamatan_id' => 1], ['id' => 2, 'kecamatan_id' => 1],
|
||||
]);
|
||||
DB::table('petani')->insert([
|
||||
['id' => 1, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
|
||||
['id' => 2, 'komoditas_id' => 1, 'desa_kelurahan_id' => 2, 'deleted_at' => null],
|
||||
['id' => 3, 'komoditas_id' => 2, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
|
||||
['id' => 4, 'komoditas_id' => 3, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
|
||||
['id' => 5, 'komoditas_id' => 4, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
|
||||
['id' => 6, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => '2026-01-01'],
|
||||
]);
|
||||
foreach ([
|
||||
[1, 'Biji kering/Biji Kupas', 10000], [2, 'Biji kering/Biji Kupas', 20000],
|
||||
[1, 'Biji kering/Biji Kupas', 0], [1, 'Biji kering/Biji Kupas', null],
|
||||
[1, 'Biji kering/Biji Kupas', -1000], [1, 'Biji basah', 90000],
|
||||
[1, 'Buah segar hasil petik', 5000],
|
||||
[3, 'Buah segar hasil petik', 30000], [3, 'Biji kering/Biji Kupas', 500000],
|
||||
[3, 'Terfermentasi (Rp/kg)', 900000], [4, 'Buah segar hasil petik', 12000],
|
||||
[5, 'Buah segar hasil petik', 8000], [5, 'Biji kering/Biji Kupas', 40000],
|
||||
[6, 'Biji kering/Biji Kupas', 1000000],
|
||||
] as [$farmer, $product, $price]) {
|
||||
DB::table('harga_jual_panen')->insert([
|
||||
'petani_id' => $farmer, 'bentuk_produk' => $product, 'harga_panen_terakhir' => $price,
|
||||
]);
|
||||
}
|
||||
DB::table('petani_skor')->insert([
|
||||
['petani_id' => 1, 'skor_kesiapan_adopsi' => 4.2, 'deleted_at' => null],
|
||||
['petani_id' => 2, 'skor_kesiapan_adopsi' => 2.5, 'deleted_at' => null],
|
||||
['petani_id' => 3, 'skor_kesiapan_adopsi' => 4.5, 'deleted_at' => '2026-01-01'],
|
||||
]);
|
||||
|
||||
$controller = app(SummaryController::class);
|
||||
$method = new ReflectionMethod($controller, 'marketDashboardData');
|
||||
$this->marketCards = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['priceCards'];
|
||||
$this->marketCharts = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['marketCharts'];
|
||||
$this->priceTrend = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['priceTrend'];
|
||||
$this->premiumAwareness = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['premiumAwareness'];
|
||||
});
|
||||
|
||||
test('market shows all four prices using the appropriate product and positive active farmer prices', function () {
|
||||
$cards = ($this->marketCards)();
|
||||
|
||||
expect(array_column($cards, 'label'))->toBe([
|
||||
'Kakao - Harga Biji Kering', 'Vanili - Harga Buah Segar',
|
||||
'Kopi - Harga Ceri Merah', 'Kemiri - Harga Gelondong',
|
||||
]);
|
||||
expect(array_column($cards, 'value'))->toBe(['Rp 15.000', 'Rp 30.000', 'Rp 12.000', 'Rp 8.000']);
|
||||
expect(array_column($cards, 'records'))->toBe([2, 1, 1, 1]);
|
||||
});
|
||||
|
||||
test('market prices respect each region level and commodity selection', function () {
|
||||
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
|
||||
expect(($this->marketCards)([$region => 1])[0]['value'])
|
||||
->toBe($region === 'desaKelurahan_id' ? 'Rp 10.000' : 'Rp 15.000');
|
||||
expect(array_column(($this->marketCards)([$region => 99]), 'records'))->toBe([0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
$cards = ($this->marketCards)(['komoditas_id' => 2]);
|
||||
expect(array_column($cards, 'records'))->toBe([0, 1, 0, 0]);
|
||||
expect(array_column($cards, 'value'))->toBe(['Rp 0', 'Rp 30.000', 'Rp 0', 'Rp 0']);
|
||||
});
|
||||
|
||||
test('market prices respect segment selection and ignore deleted scores', function () {
|
||||
$cards = ($this->marketCards)(['segmen' => 'Penggerak']);
|
||||
expect(array_column($cards, 'records'))->toBe([1, 0, 0, 0]);
|
||||
expect($cards[0]['value'])->toBe('Rp 10.000');
|
||||
expect(($this->marketCards)(['segmen' => 'Pemerhati'])[0]['value'])->toBe('Rp 20.000');
|
||||
});
|
||||
|
||||
test('market keeps four empty cards when no prices or active commodities are available', function () {
|
||||
DB::table('master_komoditas')->update(['deleted_at' => '2026-01-01']);
|
||||
|
||||
$cards = ($this->marketCards)();
|
||||
expect(array_column($cards, 'records'))->toBe([0, 0, 0, 0]);
|
||||
expect(array_column($cards, 'value'))->toBe(['Rp 0', 'Rp 0', 'Rp 0', 'Rp 0']);
|
||||
});
|
||||
|
||||
test('market charts count multiple channels once per respondent and use answered question denominators', function () {
|
||||
DB::table('akses_pasar')->insert([
|
||||
[
|
||||
'petani_id' => 1,
|
||||
'tujuan_penjualan' => json_encode(['Koperasi', 'BUMDes', 'Koperasi', 'Lainnya']),
|
||||
'sifat_hubungan_bisnis' => 'Bebas, bisa pindah pembeli kapan saja',
|
||||
'frekuensi_penjualan' => 'Langsung setelah panen',
|
||||
'skor_pengalaman' => 5,
|
||||
],
|
||||
[
|
||||
'petani_id' => 2,
|
||||
'tujuan_penjualan' => json_encode(['Koperasi']),
|
||||
'sifat_hubungan_bisnis' => 'Titip jual,dibayar setelah produk terjual',
|
||||
'frekuensi_penjualan' => null,
|
||||
'skor_pengalaman' => 1,
|
||||
],
|
||||
[
|
||||
'petani_id' => 3,
|
||||
'tujuan_penjualan' => '[]',
|
||||
'sifat_hubungan_bisnis' => '',
|
||||
'frekuensi_penjualan' => 'Saat butuh uang mendesak',
|
||||
'skor_pengalaman' => 3,
|
||||
],
|
||||
]);
|
||||
|
||||
$charts = ($this->marketCharts)();
|
||||
expect($charts['saluranPenjualan']['series'])->toBe([0, 2, 1, 0, 1]);
|
||||
expect($charts['saluranPenjualan']['respondents'])->toBe(2);
|
||||
expect($charts['saluranPenjualan']['percentages'])->toBe([0.0, 100.0, 50.0, 0.0, 50.0]);
|
||||
expect($charts['hubunganPembeli']['series'])->toBe([1, 0, 0, 1, 0, 0]);
|
||||
expect($charts['hubunganPembeli']['respondents'])->toBe(2);
|
||||
expect($charts['waktuPenjualan']['series'])->toBe([1, 0, 1]);
|
||||
expect($charts['waktuPenjualan']['respondents'])->toBe(2);
|
||||
expect($charts['aksesSaluranLain']['series'])->toBe([1, 0, 1, 0, 1]);
|
||||
expect($charts['aksesSaluranLain']['percentages'])->toBe([33.3, 0.0, 33.3, 0.0, 33.3]);
|
||||
});
|
||||
|
||||
test('market charts respect all dashboard filters and exclude deleted farmers and responses', function () {
|
||||
foreach ([1, 2, 3, 6] as $farmer) {
|
||||
DB::table('akses_pasar')->insert([
|
||||
'petani_id' => $farmer,
|
||||
'tujuan_penjualan' => json_encode(['Koperasi']),
|
||||
'sifat_hubungan_bisnis' => 'Langganan tetap, tapi tidak terikat',
|
||||
'frekuensi_penjualan' => 'Ditumpuk dulu beberapa waktu',
|
||||
'skor_pengalaman' => 4,
|
||||
]);
|
||||
}
|
||||
DB::table('akses_pasar')->insert([
|
||||
'petani_id' => 4, 'tujuan_penjualan' => json_encode(['BUMDes']), 'deleted_at' => '2026-01-01',
|
||||
]);
|
||||
|
||||
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
|
||||
foreach (($this->marketCharts)([$region => 1]) as $chart) {
|
||||
expect($chart['respondents'])->toBe($region === 'desaKelurahan_id' ? 2 : 3);
|
||||
}
|
||||
foreach (($this->marketCharts)([$region => 99]) as $chart) {
|
||||
expect($chart['respondents'])->toBe(0);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ([['komoditas_id' => 1], ['komoditas_id' => 2], ['segmen' => 'Penggerak'], ['segmen' => 'Pemerhati']] as $filters) {
|
||||
foreach (($this->marketCharts)($filters) as $chart) {
|
||||
expect($chart['respondents'])->toBe($filters === ['komoditas_id' => 1] ? 2 : 1);
|
||||
}
|
||||
}
|
||||
foreach (($this->marketCharts)(['komoditas_id' => 2, 'segmen' => 'Penggerak']) as $chart) {
|
||||
expect($chart['respondents'])->toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('market charts keep ordered zero categories when answers are missing or invalid', function () {
|
||||
DB::table('akses_pasar')->insert([
|
||||
'petani_id' => 1, 'tujuan_penjualan' => '[null, {}, "Unknown"]',
|
||||
'sifat_hubungan_bisnis' => 'Unknown', 'frekuensi_penjualan' => '', 'skor_pengalaman' => 9,
|
||||
]);
|
||||
|
||||
$charts = ($this->marketCharts)();
|
||||
expect(array_map(fn ($chart) => count($chart['series']), $charts))->toBe([
|
||||
'saluranPenjualan' => 5, 'hubunganPembeli' => 6, 'waktuPenjualan' => 3, 'aksesSaluranLain' => 5,
|
||||
]);
|
||||
foreach ($charts as $chart) {
|
||||
expect($chart['respondents'])->toBe(0);
|
||||
expect(array_sum($chart['series']))->toBe(0);
|
||||
expect(array_sum($chart['percentages']))->toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('price trend averages each product and commodity chronologically without treating missing prices as zero', function () {
|
||||
DB::table('harga_jual_panen')->delete();
|
||||
foreach ([
|
||||
[1, 'Buah segar hasil petik', 10000, 8000, 6000],
|
||||
[2, 'Buah segar hasil petik', 20000, 12000, null],
|
||||
[3, 'Buah segar hasil petik', 30000, 20000, 10000],
|
||||
[1, 'Biji basah', 0, 7000, 4000],
|
||||
[1, 'Biji kering/Biji Kupas', -1000, null, 0],
|
||||
[1, 'Terfermentasi (Rp/kg)', null, 20000, 10000],
|
||||
[6, 'Buah segar hasil petik', 1000000, 1000000, 1000000],
|
||||
] as [$farmer, $product, $latest, $lastYear, $twoYearsAgo]) {
|
||||
DB::table('harga_jual_panen')->insert([
|
||||
'petani_id' => $farmer, 'bentuk_produk' => $product,
|
||||
'harga_panen_terakhir' => $latest, 'harga_tahun_lalu' => $lastYear, 'harga_dua_tahun_lalu' => $twoYearsAgo,
|
||||
]);
|
||||
}
|
||||
|
||||
$trend = ($this->priceTrend)();
|
||||
$commodities = array_column($trend['commodities'], null, 'id');
|
||||
expect($trend['periods'])->toBe(['Dua Tahun Lalu', 'Tahun Lalu', 'Panen Terakhir']);
|
||||
expect(array_column($commodities[1]['series'], 'name'))->toBe([
|
||||
'Buah segar hasil petik', 'Biji basah', 'Biji kering/Biji Kupas', 'Terfermentasi',
|
||||
]);
|
||||
expect(array_column($commodities[1]['series'], 'data'))->toBe([
|
||||
[6000.0, 10000.0, 15000.0], [4000.0, 7000.0, null], [null, null, null], [10000.0, 20000.0, null],
|
||||
]);
|
||||
expect($commodities[2]['series'][0]['data'])->toBe([10000.0, 20000.0, 30000.0]);
|
||||
});
|
||||
|
||||
test('price trend respects dashboard region commodity and segment filters', function () {
|
||||
// Historical prices must follow the same filters as the latest harvest.
|
||||
DB::table('harga_jual_panen')->where('harga_panen_terakhir', '>', 0)
|
||||
->update(['harga_tahun_lalu' => DB::raw('harga_panen_terakhir / 2')]);
|
||||
|
||||
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
|
||||
$commodities = array_column(($this->priceTrend)([$region => 1])['commodities'], null, 'id');
|
||||
$price = $region === 'desaKelurahan_id' ? 10000.0 : 15000.0;
|
||||
expect($commodities[1]['series'][2]['data'])->toBe([null, $price / 2, $price]);
|
||||
foreach (($this->priceTrend)([$region => 99])['commodities'] as $commodity) {
|
||||
expect(array_column($commodity['series'], 'data'))->toBe(array_fill(0, 4, [null, null, null]));
|
||||
}
|
||||
}
|
||||
|
||||
$trend = ($this->priceTrend)(['komoditas_id' => 2]);
|
||||
expect(array_column($trend['commodities'], 'id'))->toBe([2]);
|
||||
expect($trend['commodities'][0]['series'][0]['data'])->toBe([null, 15000.0, 30000.0]);
|
||||
|
||||
foreach (['Penggerak' => 10000.0, 'Pemerhati' => 20000.0] as $segment => $price) {
|
||||
$commodities = array_column(($this->priceTrend)(['segmen' => $segment])['commodities'], null, 'id');
|
||||
expect($commodities[1]['series'][2]['data'])->toBe([null, $price / 2, $price]);
|
||||
expect($commodities[2]['series'][0]['data'])->toBe([null, null, null]);
|
||||
}
|
||||
});
|
||||
|
||||
test('price trend retains missing points and handles empty or deleted commodity selections', function () {
|
||||
DB::table('harga_jual_panen')->delete();
|
||||
$trend = ($this->priceTrend)();
|
||||
expect($trend['commodities'])->toHaveCount(4);
|
||||
foreach ($trend['commodities'] as $commodity) {
|
||||
expect(array_column($commodity['series'], 'data'))->toBe(array_fill(0, 4, [null, null, null]));
|
||||
}
|
||||
|
||||
expect(($this->priceTrend)(['komoditas_id' => 99])['commodities'])->toBe([]);
|
||||
DB::table('master_komoditas')->where('id', 1)->update(['deleted_at' => '2026-01-01']);
|
||||
expect(($this->priceTrend)(['komoditas_id' => 1])['commodities'])->toBe([]);
|
||||
});
|
||||
|
||||
test('premium awareness counts each active farmer once and preserves the five ordered levels', function () {
|
||||
DB::table('akses_pasar')->insert([
|
||||
'petani_id' => 4, 'skor_kesadaran_premium' => 4, 'deleted_at' => '2026-01-01',
|
||||
]);
|
||||
foreach ([1 => 1, 2 => 3, 3 => 3, 4 => 5, 5 => null, 6 => 5] as $farmer => $score) {
|
||||
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
|
||||
}
|
||||
DB::table('akses_pasar')->insert(['petani_id' => 1, 'skor_kesadaran_premium' => 2]);
|
||||
|
||||
$chart = ($this->premiumAwareness)();
|
||||
expect($chart['series'])->toBe([1, 0, 2, 0, 1]);
|
||||
expect($chart['percentages'])->toBe([25.0, 0.0, 50.0, 0.0, 25.0]);
|
||||
expect($chart['respondents'])->toBe(4);
|
||||
expect($chart['fullLabels'][0])->toBe('Belum pernah mendengar sama sekali');
|
||||
expect($chart['fullLabels'][4])->toBe('Sudah mengakses dan mendapat harga premium secara konsisten');
|
||||
});
|
||||
|
||||
test('premium awareness respects all dashboard region commodity and segment filters', function () {
|
||||
foreach ([1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 5] as $farmer => $score) {
|
||||
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
|
||||
}
|
||||
|
||||
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
|
||||
$chart = ($this->premiumAwareness)([$region => 1]);
|
||||
expect($chart['respondents'])->toBe($region === 'desaKelurahan_id' ? 4 : 5);
|
||||
expect(($this->premiumAwareness)([$region => 99])['respondents'])->toBe(0);
|
||||
}
|
||||
expect(($this->premiumAwareness)(['komoditas_id' => 1])['series'])->toBe([1, 1, 0, 0, 0]);
|
||||
expect(($this->premiumAwareness)(['komoditas_id' => 2])['series'])->toBe([0, 0, 1, 0, 0]);
|
||||
expect(($this->premiumAwareness)(['segmen' => 'Penggerak'])['series'])->toBe([1, 0, 0, 0, 0]);
|
||||
expect(($this->premiumAwareness)(['segmen' => 'Pemerhati'])['series'])->toBe([0, 1, 0, 0, 0]);
|
||||
expect(($this->premiumAwareness)(['komoditas_id' => 2, 'segmen' => 'Penggerak'])['respondents'])->toBe(0);
|
||||
});
|
||||
|
||||
test('premium awareness retains empty categories and excludes missing or invalid scores', function () {
|
||||
$emptyChart = ($this->premiumAwareness)();
|
||||
foreach ([1 => 0, 2 => 6, 3 => null, 4 => -1, 5 => null] as $farmer => $score) {
|
||||
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
|
||||
}
|
||||
$chart = ($this->premiumAwareness)();
|
||||
expect($chart)->toBe($emptyChart);
|
||||
expect($chart['series'])->toBe([0, 0, 0, 0, 0]);
|
||||
expect($chart['respondents'])->toBe(0);
|
||||
expect($chart['labels'])->toHaveCount(5);
|
||||
expect(array_sum($chart['percentages']))->toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\PraktikBudidaya\FrekuensiPemangkasanUtama;
|
||||
use App\Enums\PraktikBudidaya\PemberianPupuk;
|
||||
use App\Http\Controllers\Web\Dashboard\SummaryController;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
beforeEach(function () {
|
||||
foreach ([
|
||||
'master_provinsi' => null,
|
||||
'master_kabupaten_kota' => 'provinsi_id',
|
||||
'master_kecamatan' => 'kabupaten_kota_id',
|
||||
'master_desa_kelurahan' => 'kecamatan_id',
|
||||
] as $name => $parent) {
|
||||
Schema::create($name, function (Blueprint $table) use ($parent) {
|
||||
$table->id();
|
||||
$table->string('nama');
|
||||
$table->text('path')->nullable();
|
||||
if ($parent) {
|
||||
$table->integer($parent);
|
||||
}
|
||||
$table->softDeletes();
|
||||
});
|
||||
DB::table($name)->insert(['id' => 1, 'nama' => 'Wilayah'] + ($parent ? [$parent => 1] : []));
|
||||
}
|
||||
Schema::create('master_komoditas', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nama');
|
||||
});
|
||||
foreach (['petani', 'profiles'] as $name) {
|
||||
Schema::create($name, function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nama');
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
foreach (['map_lahan', 'map_tanaman'] as $name) {
|
||||
Schema::create($name, function (Blueprint $table) use ($name) {
|
||||
$table->id();
|
||||
$table->integer('komoditas_id');
|
||||
$table->integer('umur_tanaman')->nullable();
|
||||
foreach (['serangan_hama', 'penyakit_tanaman', 'jenis_hama', 'jenis_penyakit'] as $column) {
|
||||
$table->text($column)->nullable();
|
||||
}
|
||||
if ($name === 'map_lahan') {
|
||||
$table->integer('petani_id')->nullable();
|
||||
$table->integer('profile_id')->nullable();
|
||||
$table->integer('desa_kelurahan_id');
|
||||
$table->text('path');
|
||||
$table->float('luas_lahan');
|
||||
$table->integer('jumlah_tanaman');
|
||||
} else {
|
||||
$table->integer('lahan_id');
|
||||
}
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
Schema::create('praktik_budidaya', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('petani_id');
|
||||
$table->integer('frekuensi_pemangkasan_utama');
|
||||
$table->integer('pemberian_pupuk');
|
||||
$table->text('kebiasaan_konservasi_tanah');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
DB::table('master_komoditas')->insert(['id' => 1, 'nama' => 'Kakao']);
|
||||
DB::table('petani')->insert(['id' => 1, 'nama' => 'Petani A']);
|
||||
DB::table('profiles')->insert(['id' => 1, 'nama' => 'Pemilik B']);
|
||||
$land = [
|
||||
'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'luas_lahan' => 1.5,
|
||||
'jumlah_tanaman' => 50, 'umur_tanaman' => 8,
|
||||
'path' => '[[[-8.1,121.1],[-8.2,121.1],[-8.1,121.2]]]',
|
||||
'jenis_hama' => '["Penggerek buah"]',
|
||||
];
|
||||
DB::table('map_lahan')->insert($land + ['id' => 1, 'petani_id' => 1]);
|
||||
DB::table('map_lahan')->insert($land + ['id' => 2, 'profile_id' => 1]);
|
||||
DB::table('map_lahan')->insert($land + ['id' => 3, 'petani_id' => 1, 'deleted_at' => '2026-01-01']);
|
||||
DB::table('map_tanaman')->insert([
|
||||
'lahan_id' => 1, 'komoditas_id' => 1, 'umur_tanaman' => 4,
|
||||
'jenis_penyakit' => '["Busuk buah"]',
|
||||
]);
|
||||
foreach ([
|
||||
['2026-01-01', 1, null],
|
||||
['2026-02-01', 4, null],
|
||||
['2026-03-01', 5, '2026-03-02'],
|
||||
] as [$updatedAt, $value, $deletedAt]) {
|
||||
DB::table('praktik_budidaya')->insert([
|
||||
'petani_id' => 1, 'frekuensi_pemangkasan_utama' => $value, 'pemberian_pupuk' => $value,
|
||||
'kebiasaan_konservasi_tanah' => '["Menumpuk daun kering/sisa tanaman di sekitar pohon"]',
|
||||
'updated_at' => $updatedAt, 'deleted_at' => $deletedAt,
|
||||
]);
|
||||
}
|
||||
$controller = app(SummaryController::class);
|
||||
$method = new ReflectionMethod($controller, 'productionMapLevelData');
|
||||
$this->mapLevels = fn (array $filters = []) => $method->invoke($controller, new Request($filters));
|
||||
});
|
||||
|
||||
test('land map combines plants and OPT with the latest active cultivation practice', function () {
|
||||
$lands = collect(($this->mapLevels)()['lahan'])->keyBy('id');
|
||||
|
||||
expect($lands->keys()->all())->toBe([1, 2]);
|
||||
expect($lands[1])->toMatchArray([
|
||||
'nama_petani' => 'Petani A',
|
||||
'luas' => 1.5,
|
||||
'komoditas' => [['nama' => 'Kakao', 'jumlah' => 1, 'umur' => 4.0]],
|
||||
'opt' => ['Busuk Buah', 'Penggerek Buah'],
|
||||
'pemangkasan' => FrekuensiPemangkasanUtama::DUA_KALI_TAHUN->label(),
|
||||
'pemupukan' => PemberianPupuk::JENIS_DOSIS_SESUAI_WAKTU_CUKUP_TEPAT->label(),
|
||||
'praktik_baik' => ['Menumpuk daun kering/sisa tanaman di sekitar pohon'],
|
||||
]);
|
||||
expect($lands[2])->toMatchArray([
|
||||
'nama_petani' => 'Pemilik B',
|
||||
'komoditas' => [['nama' => 'Kakao', 'jumlah' => 50, 'umur' => 8.0]],
|
||||
'pemangkasan' => 'Belum ada data',
|
||||
'pemupukan' => 'Belum ada data',
|
||||
'praktik_baik' => [],
|
||||
]);
|
||||
});
|
||||
|
||||
test('combined map respects region and commodity filters', function () {
|
||||
expect(($this->mapLevels)(['desaKelurahan_id' => 1, 'komoditas_id' => 1])['lahan'])->toHaveCount(2);
|
||||
expect(($this->mapLevels)(['desaKelurahan_id' => 99])['lahan'])->toBe([]);
|
||||
expect(($this->mapLevels)(['komoditas_id' => 99])['lahan'])->toBe([]);
|
||||
});
|
||||
Reference in New Issue
Block a user