Files
database-pertani-web/resources/views/dashboard/karakteristik-petani.blade.php
T

3743 lines
154 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('layout.main')
@push('style')
<link rel="stylesheet" href="{{ asset('assets/vendor/libs/apex-charts/apex-charts.css') }}" />
<style>
.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 p {
font-size: 0.8125rem;
line-height: 1.4;
}
.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;
}
.readiness-score-list { display: grid; gap: 12px; }
.readiness-score-row { display: flex; align-items: baseline; gap: 10px; font-size: 0.8125rem; line-height: 1.5; }
.readiness-score-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.readiness-score-name { flex: 1; min-width: 0; }
.readiness-score-value { flex: 0 0 4ch; text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--bs-heading-color); font-size: 0.875rem; font-weight: 600; }
#farmerPagination .page-link {
padding: 0.4809rem;
font-size: 0.9375rem;
line-height: 1.375;
border-radius: 0.375rem;
}
#farmerPagination .pagination {
flex-wrap: wrap;
}
@media (max-width: 575.98px) {
#farmerPagination .page-link {
font-size: 0.8125rem;
min-block-size: calc(1.7501875rem + 2px);
min-inline-size: calc(1.7509515rem + 2px);
padding-block: 0.211rem;
padding-inline: 0.1635rem;
}
}
.summary-card .card-body {
padding: 22px 22px;
}
.summary-card .avatar-lg {
width: 48px;
height: 48px;
flex-shrink: 0;
}
.summary-card .avatar-lg .avatar-initial {
width: 48px;
height: 48px;
}
.summary-card h4 {
font-size: 1.6rem;
line-height: 1.2;
}
.summary-card h5 {
font-size: 1.05rem;
}
.demografi-kpi-value {
overflow-wrap: anywhere;
}
#genderChart,
#commodityChart,
#farmerGroupChart,
#landOwnershipStatusChart {
min-height: 365px;
}
#genderChart .apexcharts-canvas,
#genderChart svg,
#commodityChart .apexcharts-canvas,
#commodityChart svg,
#farmerGroupChart .apexcharts-canvas,
#farmerGroupChart svg,
#landOwnershipStatusChart .apexcharts-canvas,
#landOwnershipStatusChart svg {
max-width: 100% !important;
}
#genderChart .apexcharts-tooltip,
#ageGroupChart .apexcharts-tooltip,
#educationChart .apexcharts-tooltip,
#dependentChart .apexcharts-tooltip,
#farmerGroupChart .apexcharts-tooltip,
#commodityChart .apexcharts-tooltip,
#landOwnershipStatusChart .apexcharts-tooltip,
#landAreaChart .apexcharts-tooltip,
#incomeChart .apexcharts-tooltip {
background: transparent !important;
border: none !important;
box-shadow: none !important;
overflow: visible !important;
}
</style>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<style>
#farmer-map-shell:fullscreen {
width: 100%;
height: 100%;
background: var(--bs-body-bg);
}
#farmer-map-shell:fullscreen #map-sebaran-petani {
height: 100% !important;
}
.clickable-row {
cursor: pointer;
}
.clickable-row:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.segment-distribution {
align-items: center;
display: grid;
gap: 1rem;
grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);
max-width: 100%;
overflow: hidden;
}
.segment-chart-shell {
min-width: 0;
width: 100%;
}
#chart-segmentasi-petani,
#chart-segmentasi-petani .apexcharts-canvas,
#chart-segmentasi-petani svg,
.segment-commodity-chart,
.segment-commodity-chart .apexcharts-canvas,
.segment-commodity-chart svg {
max-width: 100% !important;
}
.segment-table-shell {
min-width: 0;
overflow-x: auto;
width: 100%;
}
.segment-table {
table-layout: fixed;
width: 100%;
}
.segment-table th:first-child,
.segment-table td:first-child {
min-width: 0;
width: 52%;
}
.segment-table th:nth-child(2),
.segment-table td:nth-child(2),
.segment-table th:nth-child(3),
.segment-table td:nth-child(3) {
white-space: nowrap;
width: 24%;
}
.segment-table td {
vertical-align: middle;
}
.segment-table td:first-child .d-flex {
min-width: 0;
}
.segment-table td:first-child span {
display: inline-block;
min-width: 0;
overflow-wrap: anywhere;
word-break: normal;
}
.segment-commodity-chart {
min-height: 260px;
overflow: hidden;
width: 100%;
}
@media (max-width: 1399.98px) {
.segment-distribution {
grid-template-columns: 1fr;
}
.segment-chart-shell {
margin-left: auto;
margin-right: auto;
max-width: 260px;
}
}
@media (max-width: 575.98px) {
.segment-table {
min-width: 320px;
}
}
</style>
<style>
.profile-farming-shell {
max-width: none;
width: 100%;
}
.profile-farming-tabs {
gap: .2rem;
margin-bottom: 0 !important;
overflow-x: auto;
scrollbar-width: none;
width: 100%;
}
.profile-farming-tabs::-webkit-scrollbar {
display: none;
}
.profile-farming-tabs .nav-item {
flex: 1 1 0;
min-width: 0;
}
.profile-farming-tabs .nav-link {
align-items: center;
display: flex;
justify-content: center;
min-height: 38px;
padding-left: .75rem;
padding-right: .75rem;
white-space: nowrap;
width: 100%;
}
.profile-farming-tab-content {
background: transparent;
border: 0;
border-radius: 0;
box-shadow: none;
padding: 0;
}
@media (max-width: 767.98px) {
.profile-farming-tabs .nav-item {
flex-basis: auto;
}
}
</style>
@endpush
@section('content')
@php
$activeTab = $activeTab ?? 'demografi-petani';
@endphp
<div class="container-fluid flex-grow-1 container-p-y pb-0 profile-farming-shell">
@php($showTabFilterInput = true)
<div class="row g-6 py-4">
<form id="form-filter" class="col-12" method="GET" action="{{ $indeksFilterAction ?? route('dashboard.indeks-petani') }}">
@if(isset($showTabFilterInput) && $showTabFilterInput)
<input type="hidden" name="tab" id="profile-filter-tab" value="{{ $activeTab ?? 'demografi-petani' }}">
@elseif(isset($activeTab) && $activeTab === 'indeks-petani')
<input type="hidden" name="tab" value="indeks-petani">
@endif
@csrf
<div class="col-12">
<div class="card card-border-shadow-primary profile-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-map-2 me-2"></i>
<span class="fw-medium">Wilayah</span>
</div>
<div class="filter-item">
<select id="filter-provinsi" class="form-select form-select-sm" name="provinsi_id" data-default="{{ $provinsi_id }}">
<option value="">Semua Provinsi</option>
@foreach($provinsi as $p)
<option value="{{ $p->provinsi_id }}" {{ old('provinsi_id', $provinsi_id) == $p->provinsi_id ? 'selected' : '' }}>
{{ $p->provinsi }}
</option>
@endforeach
</select>
</div>
<div class="filter-item">
<select id="filter-kabkota" class="form-select form-select-sm" name="kabupatenKota_id" data-default="{{ $kabupatenKota_id }}" disabled>
<option value="">Semua Kabupaten/Kota</option>
</select>
</div>
<div class="filter-item">
<select id="filter-kecamatan" class="form-select form-select-sm" name="kecamatan_id" data-default="{{ $kecamatan_id }}" disabled>
<option value="">Semua Kecamatan</option>
</select>
</div>
<div class="filter-item">
<select id="filter-desa" class="form-select form-select-sm" name="desaKelurahan_id" data-default="{{ $desaKelurahan_id }}" disabled>
<option value="">Semua Desa/Kelurahan</option>
</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>
</div>
</form>
</div>
<div class="nav-align-top">
<ul class="nav nav-pills flex-column flex-md-row mb-6 row-gap-2 profile-farming-tabs" role="tablist">
<li class="nav-item">
<button
type="button"
class="nav-link {{ $activeTab === 'demografi-petani' ? 'active' : '' }}"
role="tab"
data-bs-toggle="tab"
data-bs-target="#profile-tab-demografi-petani"
aria-controls="profile-tab-demografi-petani"
aria-selected="{{ $activeTab === 'demografi-petani' ? 'true' : 'false' }}"
>
<i class="icon-base ti tabler-users icon-sm me-1_5"></i>
Demografi Petani
</button>
</li>
<li class="nav-item">
<button
type="button"
class="nav-link {{ $activeTab === 'indeks-petani' ? 'active' : '' }}"
role="tab"
data-bs-toggle="tab"
data-bs-target="#profile-tab-indeks-petani"
aria-controls="profile-tab-indeks-petani"
aria-selected="{{ $activeTab === 'indeks-petani' ? 'true' : 'false' }}"
>
<i class="icon-base ti tabler-chart-radar icon-sm me-1_5"></i>
Indeks Petani
</button>
</li>
</ul>
</div>
</div>
<div class="tab-content profile-farming-tab-content">
<div
class="tab-pane fade {{ $activeTab === 'demografi-petani' ? 'show active' : '' }}"
id="profile-tab-demografi-petani"
role="tabpanel"
>
<div class="container-fluid flex-grow-1 container-p-y px-0">
<div class="row g-6">
<div class="col-xl-4 col-lg-6 col-sm-6">
<div class="card card-border-shadow-primary h-100">
<div class="card-body">
<div class="d-flex align-items-center gap-4">
<div class="avatar avatar-lg">
<span class="avatar-initial rounded bg-label-primary">
<i class="icon-base ti tabler-shovel-pitchforks icon-28px"></i>
</span>
</div>
<div>
<div class="d-flex align-items-baseline gap-2">
<h4 class="mb-0 demografi-kpi-value">
{{ $data['total_petani'] }}
</h4>
<small class="text-body-secondary">
petani
</small>
</div>
<p class="mb-0 mt-1">
Total Petani
</p>
<small class="text-body-secondary">
Petani yang telah terdata
</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6 col-sm-6">
<div class="card card-border-shadow-secondary h-100">
<div class="card-body">
<div class="d-flex align-items-center gap-4">
<div class="avatar avatar-lg">
<span class="avatar-initial rounded bg-label-secondary">
<i class="icon-base ti tabler-plant-2 icon-28px"></i>
</span>
</div>
<div>
<div class="d-flex align-items-baseline gap-2">
<h4 class="mb-0 demografi-kpi-value">
{{ number_format($data['total_komoditas'] ?? 0, 0, ',', '.') }}
</h4>
<small class="text-body-secondary">
komoditas
</small>
</div>
<p class="mb-0 mt-1">
Komoditas Utama
</p>
<small class="text-body-secondary">
Komoditas utama yang dikelola
</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6 col-sm-6">
<div class="card card-border-shadow-warning h-100">
<div class="card-body">
<div class="d-flex align-items-center gap-4">
<div class="avatar avatar-lg">
<span class="avatar-initial rounded bg-label-warning">
<i class="icon-base ti tabler-map-2 icon-28px"></i>
</span>
</div>
<div>
<div class="d-flex align-items-baseline gap-2">
<h4 class="mb-0 demografi-kpi-value">
{{ number_format($data['desa_sasaran']['total'] ?? 0, 0, ',', '.') }}
</h4>
<small class="text-body-secondary">
desa
</small>
</div>
<p class="mb-0 mt-1">
Desa Sasaran
</p>
<small class="text-body-secondary">
Asal desa/kelurahan petani
</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6 col-sm-6">
<div class="card card-border-shadow-info h-100">
<div class="card-body">
<div class="d-flex align-items-center gap-4">
<div class="avatar avatar-lg">
<span class="avatar-initial rounded bg-label-info">
<i class="icon-base ti tabler-wallet icon-28px"></i>
</span>
</div>
<div>
<div class="d-flex align-items-baseline gap-2">
<h4 class="mb-0 demografi-kpi-value">
Rp {{ number_format($data['ratarata_penghasilan'] ?? 0, 0, ',', '.') }}
</h4>
</div>
<p class="mb-0 mt-1">
Rata-rata Penghasilan Petani
</p>
<small class="text-body-secondary">
Dari pendapatan komoditas tercatat
</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6 col-sm-6">
<div class="card card-border-shadow-success h-100">
<div class="card-body">
<div class="d-flex align-items-center gap-4">
<div class="avatar avatar-lg">
<span class="avatar-initial rounded bg-label-success">
<i class="icon-base ti tabler-chart-area-line icon-28px"></i>
</span>
</div>
<div>
<div class="d-flex align-items-baseline gap-2">
<h4 class="mb-0 demografi-kpi-value">
{{ number_format($data['total_luas_lahan'], 2) }}
</h4>
<small class="text-body-secondary">
ha
</small>
</div>
<p class="mb-0 mt-1">
Total Luas Lahan
</p>
<small class="text-body-secondary">
Seluruh lahan petani
</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6 col-sm-6">
<div class="card card-border-shadow-danger h-100">
<div class="card-body">
<div class="d-flex align-items-center gap-4">
<div class="avatar avatar-lg">
<span class="avatar-initial rounded bg-label-danger">
<i class="icon-base ti tabler-ruler-measure icon-28px"></i>
</span>
</div>
<div>
<div class="d-flex align-items-baseline gap-2">
<h4 class="mb-0 demografi-kpi-value">
{{ number_format($data['ratarata_luas_lahan'], 2) }}
</h4>
<small class="text-body-secondary">
ha
</small>
</div>
<p class="mb-0 mt-1">
Rata-rata Luas Lahan
</p>
<small class="text-body-secondary">
Rata-rata per petani
</small>
</div>
</div>
</div>
</div>
</div>
<!--/ Card Border Shadow -->
<!-- Vehicles overview -->
<div class="col-12">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#demografi-charts"
aria-expanded="false" aria-controls="demografi-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Profil Demografi</h5>
<p class="text-body-secondary mb-0">Komposisi petani berdasarkan gender, kelompok umur, dan pendidikan.</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="demografi-charts">
<div class="row g-6 pt-4">
<div class="col-12 col-md-6 col-xl-4">
<div class="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">Gender</h5>
<small class="text-body-secondary">Distribusi petani berdasarkan gender</small>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih." aria-label="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="genderChart"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4">
<div class="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">Kelompok Umur</h5>
<small class="text-body-secondary">
Distribusi petani berdasarkan kelompok umur
</small>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih." aria-label="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="ageGroupChart"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4">
<div class="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">Tingkat Pendidikan</h5>
<small class="text-body-secondary">
Distribusi petani berdasarkan pendidikan terakhir
</small>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih." aria-label="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="educationChart"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#rumah-tangga-charts"
aria-expanded="false" aria-controls="rumah-tangga-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Sosial Ekonomi</h5>
<p class="text-body-secondary mb-0">Tanggungan keluarga, penghasilan usaha tani, dan keanggotaan kelompok tani.</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="rumah-tangga-charts">
<div class="row g-6 pt-4">
<div class="col-12 col-md-6 col-xl-4">
<div class="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">Jumlah Tanggungan</h5>
<small class="text-body-secondary">
Distribusi petani berdasarkan jumlah tanggungan
</small>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih." aria-label="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="dependentChart"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4">
<div class="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">
Rentang Penghasilan per Bulan
</h5>
<small class="text-body-secondary">
Distribusi petani berdasarkan penghasilan dari usaha tani per musim
</small>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih." aria-label="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="incomeChart"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4">
<div class="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">Status Keanggotaan Kelompok Tani</h5>
<small class="text-body-secondary">
Petani yang terdaftar di kelompok tani
</small>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih." aria-label="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="farmerGroupChart"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#usaha-tani-charts"
aria-expanded="false" aria-controls="usaha-tani-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Profil Lahan</h5>
<p class="text-body-secondary mb-0">Karakteristik lahan usaha tani berdasarkan komoditas utama, luas lahan garapan, dan status kepemilikan lahan.</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="usaha-tani-charts">
<div class="row g-6 pt-4">
<div class="col-12 col-md-6 col-xl-4">
<div class="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">
Komoditas Utama yang Dikelola
</h5>
<small class="text-body-secondary">
Jumlah petani berdasarkan komoditas utama
</small>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih." aria-label="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="commodityChart"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4">
<div class="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">
Luas Lahan yang Dimiliki
</h5>
<small class="text-body-secondary">
Distribusi petani berdasarkan luas lahan garapan
</small>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih." aria-label="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="landAreaChart"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4">
<div class="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">
Status Kepemilikan Lahan
</h5>
<small class="text-body-secondary">
Distribusi petani berdasarkan status lahan
</small>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih." aria-label="Klik bagian grafik untuk memfilter Daftar Petani di bawah sesuai kategori yang dipilih.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="landOwnershipStatusChart"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xxl-12 col-lg-12">
<div class="card">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-3">
<div>
<div class="d-flex align-items-center gap-2">
<h5 class="card-title mb-0">
Daftar Petani
</h5>
<span
class="badge bg-label-primary"
id="farmerTotalBadge"
>
{{ $data['total_petani'] }} petani
</span>
</div>
<small class="text-body-secondary">
Menampilkan data petani sesuai data yang tersedia
</small>
</div>
<div class="d-flex align-items-center gap-2">
<div
class="input-group input-group-merge"
style="width: 220px;"
>
<span class="input-group-text">
<i class="icon-base ti tabler-search"></i>
</span>
<input
type="text"
id="farmerSearch"
class="form-control"
placeholder="Cari petani..."
>
</div>
<button type="button" id="clearFarmerTableFilter" class="btn btn-label-secondary d-none text-nowrap"><i class="icon-base ti tabler-filter-off me-1" aria-hidden="true"></i>Bersihkan Filter</button>
<button type="button" class="btn btn-label-secondary btn-icon" data-bs-toggle="collapse" data-bs-target="#demografi-table-filter-panel" aria-expanded="false" aria-controls="demografi-table-filter-panel" aria-label="Filter tabel" title="Filter tabel"><i class="icon-base ti tabler-filter"></i></button>
</div>
</div>
<div class="collapse" id="demografi-table-filter-panel"><div class="card-body border-top">
<div class="row g-3">
@foreach ($tableFilters as $key => [$label, $options])
<div class="col-12 col-sm-6 col-lg-4">
<label class="form-label" for="farmer-filter-{{ $key }}">{{ $label }}</label>
<select id="farmer-filter-{{ $key }}" class="form-select form-select-sm farmer-table-filter" data-filter="{{ $key }}">
<option value="">Semua</option>
@foreach ($options as $value => $text)
<option value="{{ $value }}">{{ $text }}</option>
@endforeach
</select>
</div>
@endforeach
</div>
<small class="text-body-secondary d-block mt-3">Filter tabel mengikuti filter dashboard yang telah diterapkan.</small>
<div class="d-flex justify-content-end mt-3"><button type="button" id="applyFarmerFilter" class="btn btn-primary btn-sm">Terapkan Filter</button></div>
</div></div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>No.</th>
<th>Nama Petani</th>
<th>Gender</th>
<th>Umur</th>
<th>Komoditas</th>
<th>Luas Lahan</th>
<th>Pendidikan</th>
<th>Rentang Pendapatan</th>
</tr>
</thead>
<tbody id="farmerTableBody">
<tr><td colspan="8" class="text-center text-body-secondary py-5">Memuat data petani...</td></tr>
</tbody>
</table>
</div>
<div class="card-footer d-flex flex-wrap justify-content-between align-items-center gap-3">
<small class="text-body-secondary" id="farmerTableInfo">Memuat data petani...</small>
<div id="farmerPagination"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="tab-pane fade {{ $activeTab === 'indeks-petani' ? 'show active' : '' }}"
id="profile-tab-indeks-petani"
role="tabpanel"
>
@php($showIndeksFilter = false)
<div class="container-fluid flex-grow-1 container-p-y px-0">
<div class="row g-6">
<div class="col-lg-3 col-sm-6">
<div class="card card-border-shadow-primary h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<div class="avatar me-4">
<span class="avatar-initial rounded bg-label-primary">
<i class="icon-base ti tabler-user-check icon-28px"></i>
</span>
</div>
<h4 class="mb-0" id="count-petani">0</h4>
</div>
<p class="mb-1">Total Petani</p>
<p class="mb-0">
<span class="text-heading fw-medium me-2" id="count-komoditas">0</span>
<small class="text-body-secondary">komoditas utama</small>
</p>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="card card-border-shadow-warning h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<div class="avatar me-4">
<span class="avatar-initial rounded bg-label-warning">
<i class="icon-base ti tabler-trees icon-28px"></i>
</span>
</div>
<h4 class="mb-0" id="sum-lahan">0</h4>
<small class="text-body-warning">ha</small>
</div>
<p class="mb-1">Total Luas Lahan</p>
<p class="mb-0">
<small class="text-body-warning">rata-rata</small>
<span class="text-heading fw-medium" id="avg-lahan"></span>
<small class="text-body-warning">ha/petani</small>
</p>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="card card-border-shadow-info h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<div class="avatar me-4">
<span class="avatar-initial rounded bg-label-info">
<i class="icon-base ti tabler-star icon-28px"></i>
</span>
</div>
<h4 class="mb-0" id="avg-skor"></h4>
</div>
<p class="mb-1">Rata-rata Indeks Kesiapan Adopsi</p>
<p class="mb-0">
<small class="text-body-secondary">Kategori Indeks:</small>
<span class="text-heading fw-medium me-2" id="avg-segmen"></span>
</p>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="card card-border-shadow-success h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<div class="avatar me-4">
<span class="avatar-initial rounded bg-label-success">
<i class="icon-base ti tabler-map-2 icon-28px"></i>
</span>
</div>
<h4 class="mb-0" id="count-desa">0</h4>
</div>
<p class="mb-1">Jumlah Desa Sasaran</p>
<p class="mb-0">
<span class="text-heading fw-medium me-2" id="count-kecamatan">0</span>
<small class="text-body-secondary">kecamatan sasaran</small>
</p>
</div>
</div>
</div>
</div>
<div class="py-4">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#indeks-segmentasi-charts"
aria-expanded="false" aria-controls="indeks-segmentasi-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Indeks Petani</h5>
<p class="text-body-secondary mb-0">Komposisi petani menurut tingkat kesiapan adopsi, secara keseluruhan dan pada setiap komoditas.</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="indeks-segmentasi-charts">
<div class="row g-6 py-4">
<div class="col-lg-6 col-sm-12">
<div class="card card-border-shadow-primary h-100">
<div class="card-header d-flex justify-content-between align-items-start gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Distribusi Indeks Petani</h5>
<p class="card-subtitle">Jumlah dan proporsi petani pada setiap segmen kesiapan adopsi.</p>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik segmen pada grafik untuk memfilter Daftar Petani di bawah." aria-label="Klik segmen pada grafik untuk memfilter Daftar Petani di bawah.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div class="segment-distribution">
<div class="segment-chart-shell">
<div id="chart-segmentasi-petani"></div>
</div>
<div class="segment-table-shell table-responsive">
<table class="table card-table table-border-top-0 table-border-bottom-0 segment-table mb-0">
<thead>
<tr>
<th>Segmen</th>
<th class="text-end">Petani</th>
<th class="text-end">%</th>
</tr>
</thead>
<tbody id="table-segmentasi-petani"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6 col-sm-12">
<div class="card card-border-shadow-secondary h-100">
<div class="card-header d-flex justify-content-between align-items-start gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Distribusi Indeks Petani</h5>
<p class="card-subtitle">Perbandingan komposisi segmen kesiapan adopsi petani antar-komoditas.</p>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik bagian batang untuk memfilter Daftar Petani di bawah berdasarkan komoditas dan segmen." aria-label="Klik bagian batang untuk memfilter Daftar Petani di bawah berdasarkan komoditas dan segmen.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div class="segment-commodity-chart" id="chart-segmentasi-petani-komoditas"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="py-4">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#indeks-kesiapan-charts"
aria-expanded="false" aria-controls="indeks-kesiapan-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Dimensi dan Komponen Kesiapan Adopsi</h5>
<p class="text-body-secondary mb-0">Perbandingan skor dimensi dan komponen kesiapan adopsi petani.</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="indeks-kesiapan-charts">
<div class="row g-6 py-4">
<div class="col-lg-6 col-sm-12">
<div class="card card-border-shadow-info h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Dimensi Kesiapan Adopsi</h5>
<p class="card-subtitle">Rata-rata skor budidaya, sosial ekonomi rumah tangga, serta rantai nilai dan akses pasar.</p>
</div>
</div>
<div class="card-body">
<div id="chart-dimensi"></div>
<p class="small text-body-secondary text-center mb-0">Skala skor 0–5</p>
<div id="chart-dimensi-scores" class="readiness-score-list mt-3" aria-live="polite"><p class="text-body-secondary small mb-0">Memuat informasi skor…</p></div>
</div>
</div>
</div>
<div class="col-lg-6 col-sm-12">
<div class="card card-border-shadow-warning h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Komponen Kesiapan Adopsi</h5>
<p class="card-subtitle">Rata-rata skor enam komponen yang membentuk kesiapan adopsi petani.</p>
</div>
</div>
<div class="card-body">
<div id="chart-komponen"></div>
<p class="small text-body-secondary text-center mb-0">Skala skor 0–5</p>
<div id="chart-komponen-scores" class="readiness-score-list mt-3" aria-live="polite"><p class="text-body-secondary small mb-0">Memuat informasi skor…</p></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="py-4">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#indeks-wilayah-charts"
aria-expanded="false" aria-controls="indeks-wilayah-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Sebaran wilayah petani</h5>
<p class="text-body-secondary mb-0">Peta dan komposisi segmen kesiapan adopsi petani per desa/kelurahan.</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="indeks-wilayah-charts">
<div class="row g-6 py-4">
<div class="col-lg-12 col-sm-12">
<div class="card card-border-shadow-danger h-100">
<div class="card-header d-flex justify-content-between align-items-start gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Peta Sebaran</h5>
<p class="card-subtitle">Warna wilayah menunjukkan kategori rata-rata indeks kesiapan adopsi petani di setiap desa/kelurahan.</p>
</div>
<button type="button" class="btn btn-icon btn-sm btn-label-secondary chart-filter-help flex-shrink-0"
data-bs-toggle="tooltip" data-bs-trigger="hover focus click" data-bs-placement="top"
title="Klik wilayah desa/kelurahan untuk melihat rincian dan memfilter Daftar Petani di bawah." aria-label="Klik wilayah desa/kelurahan untuk melihat rincian dan memfilter Daftar Petani di bawah.">
<i class="icon-base ti tabler-help-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div id="farmer-map-shell" class="position-relative">
<button type="button" id="farmer-map-fullscreen" class="btn btn-light btn-sm shadow-sm position-absolute top-0 end-0 m-3" style="z-index: 1000;" aria-label="Buka peta layar penuh" title="Buka peta layar penuh">
<i class="icon-base ti tabler-maximize"></i>
</button>
<div id="map-sebaran-petani" style="height: 430px;"></div>
</div>
<span class="small text-muted">Klik wilayah desa/kelurahan untuk melihat rincian dan menyaring daftar petani.</span>
</div>
</div>
</div>
<div class="col-lg-12 col-sm-12">
<div class="card card-border-shadow-success h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Tabel Sebaran</h5>
<p class="card-subtitle">Jumlah dan persentase petani pada setiap segmen kesiapan adopsi serta kategori belum lengkap, dikelompokkan menurut desa/kelurahan.</p>
</div>
</div>
<div class="card-body">
<table id="table-desa" class="table card-table table-border-top-0 table-border-bottom-0">
<thead>
<tr>
<th class="pe-0">Desa/kelurahan</th>
<th class="text-end pe-0">Total Petani</th>
<th class="text-end text-danger pe-0">Pemula</th>
<th class="text-end text-info pe-0">Perintis</th>
<th class="text-end text-warning pe-0">Pemerhati</th>
<th class="text-end text-success pe-0">Penggerak</th>
<th class="text-end text-secondary pe-0">Belum lengkap</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-6 py-4">
<div class="col-lg-12 col-sm-12">
<div class="card card-border-shadow-primary h-100">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-3">
<div>
<div class="d-flex align-items-center gap-2">
<h5 class="card-title mb-0">Daftar Petani</h5>
<span class="badge bg-label-primary" id="indexFarmerTotalBadge">0 petani</span>
</div>
<small class="text-body-secondary">Rincian wilayah, komoditas, luas lahan, skor kesiapan adopsi, dan segmen setiap petani.</small>
</div>
<div class="d-flex align-items-center gap-2">
<div class="input-group input-group-merge" style="width: 220px;">
<span class="input-group-text"><i class="icon-base ti tabler-search"></i></span>
<input type="search" id="indexFarmerSearch" class="form-control" placeholder="Cari petani..." aria-label="Cari petani pada tabel indeks">
</div>
<button type="button" id="btn-reset-filter" class="btn btn-label-secondary d-none text-nowrap"><i class="icon-base ti tabler-filter-off me-1" aria-hidden="true"></i>Bersihkan Filter</button>
<button type="button" class="btn btn-label-secondary btn-icon" data-bs-toggle="collapse" data-bs-target="#index-table-filter-panel" aria-expanded="false" aria-controls="index-table-filter-panel" aria-label="Filter tabel" title="Filter tabel"><i class="icon-base ti tabler-filter"></i></button>
</div>
</div>
<div class="collapse" id="index-table-filter-panel"><div class="card-body border-top">
<div class="row g-3">
<div class="col-12 col-md-4">
<label class="form-label" for="index-table-desa">Desa/Kelurahan</label>
<select id="index-table-desa" class="form-select form-select-sm index-table-filter" data-column="1"><option value="">Semua Desa/Kelurahan</option></select>
</div>
<div class="col-12 col-md-4">
<label class="form-label" for="index-table-komoditas">Komoditas</label>
<select id="index-table-komoditas" class="form-select form-select-sm index-table-filter" data-column="2"><option value="">Semua Komoditas</option></select>
</div>
<div class="col-12 col-md-4">
<label class="form-label" for="index-table-segmen">Segmen</label>
<select id="index-table-segmen" class="form-select form-select-sm index-table-filter" data-column="5"><option value="">Semua Segmen</option></select>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-3"><button type="button" id="applyIndexTableFilter" class="btn btn-primary btn-sm">Terapkan Filter</button></div></div>
<div class="card-body">
<table id="table-petani" class="table table-hover">
<thead>
<tr>
<th class="pe-0">Nama Petani</th>
<th class="pe-0">Desa/Kelurahan</th>
<th class="pe-0">Komoditas</th>
<th class="text-end pe-0">Luas Lahan (ha)</th>
<th class="text-end pe-0">Indeks Kesiapan Adopsi</th>
<th class="pe-0">Segmen</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
'use strict';
const dashboardFilters = @json($dashboardFilters);
const dashboardFilterParams = new URLSearchParams();
Object.entries(dashboardFilters).forEach(([key, value]) => {
if (value !== null && value !== '') {
dashboardFilterParams.set(key, value);
}
});
document.addEventListener('DOMContentLoaded', function () {
const headingColor = config.colors.headingColor;
const legendColor = config.colors.bodyColor;
const fontFamily = config.fontFamily;
const pastelChart = config.chartColors || {};
const pastelSeries = pastelChart.series || ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'];
const pastelSemantic = pastelChart.semantic || {};
const pastelText = pastelChart.text || {};
function hexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '');
if (!/^[0-9a-fA-F]{6}$/.test(value)) {
return hex;
}
const red = parseInt(value.slice(0, 2), 16);
const green = parseInt(value.slice(2, 4), 16);
const blue = parseInt(value.slice(4, 6), 16);
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function softBadgeStyle(backgroundColor, textColor) {
const borderColor = hexToRgba(backgroundColor, 0.36);
return [
`background-color:${hexToRgba(backgroundColor, 0.22)} !important`,
`border:1px solid ${borderColor}`,
`color:${textColor} !important`,
'font-weight:600'
].join(';');
}
const farmerDataUrl = @json(route('dashboard.demografi-petani.data'));
const farmerProfileUrl = @json(url('/dashboard/profil/petani'));
let activeFilters = {
gender: null,
umur: null,
pendidikan: null,
jumlah_tanggungan: null,
kelompok_tani: null,
komoditas: null,
luas_lahan: null,
status_kepemilikan_lahan: null,
penghasilan: null
};
const farmerSearchInput = document.querySelector('#farmerSearch');
const farmerFilterSelects = document.querySelectorAll('.farmer-table-filter');
const farmerClearButton = document.querySelector('#clearFarmerTableFilter');
let searchTimer = null;
let farmerTableRequest = null;
function escapeHtml(value) {
if (value === null || value === undefined) {
return '';
}
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function formatLuas(value) {
const number = Number(value || 0);
if (number <= 0) {
return '-';
}
return number.toLocaleString('id-ID', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' ha';
}
function updateFarmerClearFilter() {
const hasFilters = Object.values(activeFilters).some((value) => value !== null && value !== undefined && value !== '');
const hasSearch = Boolean(farmerSearchInput?.value.trim());
farmerClearButton.classList.toggle('d-none', !hasFilters && !hasSearch);
}
async function loadFarmerTable(page = 1) {
updateFarmerClearFilter();
farmerTableRequest?.abort();
const request = new AbortController();
farmerTableRequest = request;
const searchInput = farmerSearchInput;
const search = searchInput ? searchInput.value.trim() : '';
const params = new URLSearchParams();
params.set('page', page);
dashboardFilterParams.forEach((value, key) => params.set(key, value));
Object.entries(activeFilters).forEach(([key, value]) => {
if (value !== null && value !== undefined && value !== '') {
params.set(key, value);
}
});
if (search) {
params.set('search', search);
}
const tableBody = document.querySelector('#farmerTableBody');
if (tableBody) {
tableBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-5">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
<span class="text-body-secondary">Memuat data petani...</span>
</td>
</tr>
`;
}
try {
const response = await fetch(`${farmerDataUrl}?${params.toString()}`, {
method: 'GET',
signal: request.signal,
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP Error ${response.status}`);
}
const result = await response.json();
if (!request.signal.aborted) renderFarmerTable(result);
} catch (error) {
if (error.name === 'AbortError') return;
console.error('Gagal memuat tabel petani:', error);
if (tableBody) {
tableBody.innerHTML = '<tr><td colspan="8" class="text-center text-danger py-5">Data gagal dimuat. Silakan coba kembali.</td></tr>';
}
}
}
function renderFarmerTable(result) {
const tbody = document.querySelector('#farmerTableBody');
if (!tbody) {
return;
}
const rows = result.data || [];
if (rows.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-5 text-body-secondary">
Tidak ada data petani yang sesuai dengan filter.
</td>
</tr>
`;
updateFarmerPagination(result.pagination);
return;
}
tbody.innerHTML = rows.map((petani, index) => {
const rowNumber = ((result.pagination.current_page - 1) * result.pagination.per_page) + index + 1;
const genderBadge = Number(petani.jenis_kelamin) === 1
? `<span class="badge" style="${softBadgeStyle(pastelSemantic.info || '#5FC7D6', pastelText.info || '#00BAD1')}">Laki-laki</span>`
: `<span class="badge" style="${softBadgeStyle(pastelSemantic.danger || '#F47C86', pastelText.danger || '#FF4C51')}">Perempuan</span>`;
const umur = (petani.umur !== null && petani.umur !== undefined) ? `${petani.umur} tahun` : '-';
return `
<tr class="farmer-row" data-id="${petani.id}" style="cursor: pointer;">
<td><span class="text-heading fw-medium">${rowNumber}</span></td>
<td><span class="text-heading fw-medium">${escapeHtml(petani.nama)}</span></td>
<td>${genderBadge}</td>
<td>${escapeHtml(umur)}</td>
<td>${escapeHtml(petani.komoditas || '-')}</td>
<td>${formatLuas(petani.luas_lahan)}</td>
<td>${escapeHtml(petani.pendidikan || '-')}</td>
<td>${escapeHtml(petani.rentang_penghasilan || '-')}</td>
</tr>
`;
}).join('');
updateFarmerPagination(result.pagination);
}
function updateFarmerPagination(pagination) {
if (!pagination) {
return;
}
const totalBadge = document.querySelector('#farmerTotalBadge');
if (totalBadge) {
totalBadge.textContent = `${pagination.total} petani`;
}
const tableInfo = document.querySelector('#farmerTableInfo');
if (tableInfo) {
tableInfo.textContent = `Menampilkan ${pagination.from ?? 0} - ${pagination.to ?? 0} dari ${pagination.total ?? 0} petani`;
}
const paginationEl = document.querySelector('#farmerPagination');
if (!paginationEl) {
return;
}
if (pagination.last_page <= 1) {
paginationEl.innerHTML = '';
return;
}
let html = `<nav><ul class="pagination mb-0">`;
html += `
<li class="page-item ${pagination.current_page === 1 ? 'disabled' : ''}">
<button type="button" class="page-link farmer-page" data-page="${pagination.current_page - 1}">
<i class="icon-base ti tabler-chevron-left"></i>
</button>
</li>
`;
html = html.replace('<ul class="pagination mb-0">', '<ul class="pagination mb-0">' +
'<li class="page-item ' + (pagination.current_page === 1 ? 'disabled' : '') + '">' +
'<button type="button" class="page-link farmer-page" data-page="1" aria-label="Halaman pertama"><i class="icon-base ti tabler-chevrons-left"></i></button></li>');
const currentPage = pagination.current_page;
const lastPage = pagination.last_page;
const startPage = Math.max(1, currentPage - 2);
const endPage = Math.min(lastPage, currentPage + 2);
if (startPage > 1) {
html += `
<li class="page-item">
<button type="button" class="page-link farmer-page" data-page="1">1</button>
</li>
`;
if (startPage > 2) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
}
for (let page = startPage; page <= endPage; page++) {
html += `
<li class="page-item ${page === currentPage ? 'active' : ''}">
<button type="button" class="page-link farmer-page" data-page="${page}">${page}</button>
</li>
`;
}
if (endPage < lastPage) {
if (endPage < lastPage - 1) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
html += `
<li class="page-item">
<button type="button" class="page-link farmer-page" data-page="${lastPage}">${lastPage}</button>
</li>
`;
}
html += `
<li class="page-item ${currentPage === lastPage ? 'disabled' : ''}">
<button type="button" class="page-link farmer-page" data-page="${currentPage + 1}">
<i class="icon-base ti tabler-chevron-right"></i>
</button>
</li>
`;
html += '<li class="page-item ' + (currentPage === lastPage ? 'disabled' : '') + '">' +
'<button type="button" class="page-link farmer-page" data-page="' + lastPage + '" aria-label="Ke halaman terakhir" title="Ke halaman terakhir"><i class="icon-base ti tabler-chevrons-right"></i></button></li>';
html += `</ul></nav>`;
paginationEl.innerHTML = html;
}
function applyFarmerFilter(filter, value) {
if (activeFilters[filter] !== null && String(activeFilters[filter]) === String(value)) {
activeFilters[filter] = null;
} else {
activeFilters[filter] = value;
}
farmerFilterSelects.forEach((select) => {
select.value = activeFilters[select.dataset.filter] ?? '';
});
loadFarmerTable(1);
}
function resetFarmerFilter() {
clearTimeout(searchTimer);
Object.keys(activeFilters).forEach((key) => { activeFilters[key] = null; });
const searchInput = farmerSearchInput;
if (searchInput) {
searchInput.value = '';
}
farmerFilterSelects.forEach((select) => {
select.value = activeFilters[select.dataset.filter] ?? '';
});
loadFarmerTable(1);
}
document.querySelector('#applyFarmerFilter').addEventListener('click', function () {
farmerFilterSelects.forEach((select) => {
activeFilters[select.dataset.filter] = select.value || null;
});
loadFarmerTable(1);
});
document.addEventListener('input', function (event) {
if (event.target.id !== 'farmerSearch') {
return;
}
updateFarmerClearFilter();
clearTimeout(searchTimer);
searchTimer = setTimeout(function () {
loadFarmerTable(1);
}, 350);
});
document.addEventListener('click', function (event) {
const button = event.target.closest('.farmer-page');
if (!button) {
return;
}
if (button.closest('.page-item.disabled')) {
return;
}
const page = Number(button.dataset.page);
if (!page || page < 1) {
return;
}
loadFarmerTable(page);
});
document.addEventListener('click', function (event) {
const button = event.target.closest('#clearFarmerTableFilter');
if (!button) {
return;
}
resetFarmerFilter();
});
const genderChartEl = document.querySelector('#genderChart');
const lakiLaki = Number(@json($data['petani_lakilaki'] ?? 0));
const perempuan = Number(@json($data['petani_perempuan'] ?? 0));
const totalPetani = lakiLaki + perempuan;
if (genderChartEl) {
const genderChartConfig = {
chart: {
height: 365,
parentHeightOffset: 0,
type: 'pie',
toolbar: { show: false },
events: {
dataPointSelection: function (event, chartContext, options) {
const index = options.dataPointIndex;
if (index < 0) {
return;
}
const values = [1, 0];
applyFarmerFilter('gender', values[index]);
}
}
},
labels: ['Laki-laki', 'Perempuan'],
series: [lakiLaki, perempuan],
colors: [pastelSemantic.info || '#5FC7D6', pastelSemantic.pink || '#F47C86'],
stroke: { width: 0 },
dataLabels: {
enabled: true,
formatter: function (value, options) {
const count = options.w.globals.series[options.seriesIndex] || 0;
if (count <= 0) {
return '';
}
return `${Number(value).toFixed(1)}%`;
},
style: {
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 600,
colors: ['#fff']
},
dropShadow: {
enabled: true,
top: 1,
left: 1,
blur: 2,
opacity: 0.45
}
},
legend: {
show: true,
position: 'bottom',
offsetY: 10,
markers: { size: 4, strokeWidth: 0 },
itemMargin: { horizontal: 15, vertical: 5 },
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 400,
labels: { colors: legendColor, useSeriesColors: false },
formatter: function (seriesName, options) {
const value = options.w.globals.series[options.seriesIndex];
const percentage = totalPetani > 0 ? ((value / totalPetani) * 100).toFixed(1) : '0.0';
return `${seriesName}: ${value} (${percentage}%)`;
}
},
tooltip: {
theme: 'dark',
y: {
formatter: function (value) {
const percentage = totalPetani > 0 ? ((value / totalPetani) * 100).toFixed(1) : '0.0';
return `${value} petani (${percentage}%)`;
}
}
},
grid: { padding: { top: 15 } },
plotOptions: {
pie: {
dataLabels: {
offset: -14,
minAngleToShowLabel: 8
}
}
},
responsive: [
{
breakpoint: 1025,
options: {
chart: { height: 380 }
}
}
]
};
const genderChart = new ApexCharts(genderChartEl, genderChartConfig);
genderChart.render();
}
const ageGroupChartEl = document.querySelector('#ageGroupChart');
if (ageGroupChartEl) {
const ageFilterValues = ['kurang_30', '30_39', '40_49', '50_59', '60_keatas'];
const ageGroupData = [
Number(@json($data['kelompok_umur']['kurang_30'] ?? 0)),
Number(@json($data['kelompok_umur']['30_39'] ?? 0)),
Number(@json($data['kelompok_umur']['40_49'] ?? 0)),
Number(@json($data['kelompok_umur']['50_59'] ?? 0)),
Number(@json($data['kelompok_umur']['60_keatas'] ?? 0))
];
const totalAgeGroup = ageGroupData.reduce((total, value) => total + value, 0);
const ageGroupChartConfig = {
chart: {
height: 320,
type: 'bar',
parentHeightOffset: 0,
toolbar: { show: false },
events: {
dataPointSelection: function (event, chartContext, options) {
const index = options.dataPointIndex;
if (index < 0) {
return;
}
applyFarmerFilter('umur', ageFilterValues[index]);
}
}
},
series: [
{
name: 'Petani',
data: ageGroupData
}
],
colors: [pastelSemantic.primary || '#9D8EF0'],
plotOptions: {
bar: {
borderRadius: 4,
columnWidth: '45%',
dataLabels: { position: 'top' }
}
},
dataLabels: {
enabled: true,
offsetY: -20,
style: {
fontSize: '12px',
fontFamily: fontFamily,
fontWeight: 500,
colors: [headingColor]
},
formatter: function (value) {
const percentage = totalAgeGroup > 0 ? (Number(value) / totalAgeGroup) * 100 : 0;
return `${value} (${percentage.toFixed(1)}%)`;
}
},
xaxis: {
categories: ['< 30', '30 - 39', '40 - 49', '50 - 59', '≥ 60'],
labels: {
style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily },
formatter: function (value) {
return value + ' tahun';
}
},
axisBorder: { show: false },
axisTicks: { show: false }
},
yaxis: {
min: 0,
labels: {
style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily },
formatter: function (value) {
return Math.floor(value);
}
},
title: {
text: 'Petani',
style: { color: legendColor, fontSize: '12px', fontFamily: fontFamily, fontWeight: 400 }
}
},
grid: {
borderColor: config.colors.borderColor,
strokeDashArray: 5,
xaxis: { lines: { show: false } },
padding: { top: 34 }
},
legend: { show: false },
tooltip: {
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
const value = series[seriesIndex][dataPointIndex];
const label = w.globals.labels[dataPointIndex];
const percentage = totalAgeGroup > 0 ? (Number(value) / totalAgeGroup) * 100 : 0;
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; 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);">
${label} tahun: <strong>${value} petani (${percentage.toFixed(1)}%)</strong>
</div>
`;
}
}
};
new ApexCharts(ageGroupChartEl, ageGroupChartConfig).render();
}
const educationChartEl = document.querySelector('#educationChart');
if (educationChartEl) {
const educationData = [
Number(@json($data['pendidikan']['tidak_sekolah'] ?? 0)),
Number(@json($data['pendidikan']['sd'] ?? 0)),
Number(@json($data['pendidikan']['smp'] ?? 0)),
Number(@json($data['pendidikan']['sma'] ?? 0)),
Number(@json($data['pendidikan']['diploma'] ?? 0)),
Number(@json($data['pendidikan']['s1_keatas'] ?? 0))
];
const educationFilters = ['tidak_sekolah', 'sd', 'smp', 'sma', 'diploma', 's1_keatas'];
const totalPendidikan = educationData.reduce((total, value) => total + value, 0);
const educationChartConfig = {
chart: {
height: 330,
type: 'bar',
parentHeightOffset: 0,
toolbar: { show: false },
events: {
dataPointSelection: function (event, chartContext, options) {
const index = options.dataPointIndex;
if (index < 0) {
return;
}
applyFarmerFilter('pendidikan', educationFilters[index]);
}
}
},
series: [{ name: 'Petani', data: educationData }],
colors: [pastelSemantic.success || '#6FD68F'],
plotOptions: {
bar: {
horizontal: true,
borderRadius: 4,
barHeight: '55%',
dataLabels: { position: 'top' }
}
},
dataLabels: {
enabled: true,
textAnchor: 'start',
offsetX: 6,
style: {
fontSize: '12px',
fontFamily: fontFamily,
fontWeight: 500,
colors: [headingColor]
},
formatter: function (value) {
const percentage = totalPendidikan > 0 ? ((value / totalPendidikan) * 100).toFixed(1) : '0.0';
return `${value} (${percentage}%)`;
}
},
xaxis: {
categories: ['Tidak Sekolah', 'SD / Sederajat', 'SMP / Sederajat', 'SMA / Sederajat', 'Diploma', 'S1 ke atas'],
labels: {
style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily },
formatter: function (value) {
return Math.floor(value);
}
},
axisBorder: { show: false },
axisTicks: { show: false },
title: {
text: 'Petani',
style: { color: legendColor, fontSize: '12px', fontFamily: fontFamily, fontWeight: 400 }
}
},
yaxis: {
labels: { style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily } }
},
grid: {
borderColor: config.colors.borderColor,
strokeDashArray: 5,
xaxis: { lines: { show: true } },
yaxis: { lines: { show: false } },
padding: { right: 55 }
},
legend: { show: false },
tooltip: {
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
const value = series[seriesIndex][dataPointIndex];
const label = w.globals.labels[dataPointIndex];
const percentage = totalPendidikan > 0 ? ((value / totalPendidikan) * 100).toFixed(1) : '0.0';
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; 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);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
}
}
};
new ApexCharts(educationChartEl, educationChartConfig).render();
}
const dependentChartEl = document.querySelector('#dependentChart');
if (dependentChartEl) {
const dependentData = [
Number(@json($data['jumlah_tanggungan']['0'] ?? 0)),
Number(@json($data['jumlah_tanggungan']['1_2'] ?? 0)),
Number(@json($data['jumlah_tanggungan']['3_4'] ?? 0)),
Number(@json($data['jumlah_tanggungan']['5_keatas'] ?? 0))
];
const dependentFilters = ['0', '1_2', '3_4', '5_keatas'];
const totalDependentsFarmers = dependentData.reduce((total, value) => total + value, 0);
const dependentChartConfig = {
chart: {
height: 330,
type: 'bar',
parentHeightOffset: 0,
toolbar: { show: false },
events: {
dataPointSelection: function (event, chartContext, options) {
const index = options.dataPointIndex;
if (index < 0) {
return;
}
applyFarmerFilter('jumlah_tanggungan', dependentFilters[index]);
}
}
},
series: [{ name: 'Petani', data: dependentData }],
colors: [pastelSemantic.danger || '#F47C86'],
plotOptions: {
bar: {
horizontal: true,
borderRadius: 4,
barHeight: '50%',
dataLabels: { position: 'top' }
}
},
dataLabels: {
enabled: true,
textAnchor: 'start',
offsetX: 6,
style: {
fontSize: '12px',
fontFamily: fontFamily,
fontWeight: 500,
colors: [headingColor]
},
formatter: function (value) {
const percentage = totalDependentsFarmers > 0 ? ((value / totalDependentsFarmers) * 100).toFixed(1) : '0.0';
return `${value} (${percentage}%)`;
}
},
xaxis: {
categories: ['0 orang', '1 - 2 orang', '3 - 4 orang', '>= 5 orang'],
labels: {
style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily },
formatter: function (value) {
return Math.floor(value);
}
},
axisBorder: { show: false },
axisTicks: { show: false },
title: {
text: 'Petani',
style: { color: legendColor, fontSize: '12px', fontFamily: fontFamily, fontWeight: 400 }
}
},
yaxis: {
labels: { style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily } }
},
grid: {
borderColor: config.colors.borderColor,
strokeDashArray: 5,
xaxis: { lines: { show: true } },
yaxis: { lines: { show: false } },
padding: { right: 55 }
},
legend: { show: false },
tooltip: {
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
const value = series[seriesIndex][dataPointIndex];
const label = w.globals.labels[dataPointIndex];
const percentage = totalDependentsFarmers > 0 ? ((value / totalDependentsFarmers) * 100).toFixed(1) : '0.0';
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; 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);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
}
}
};
new ApexCharts(dependentChartEl, dependentChartConfig).render();
}
const farmerGroupChartEl = document.querySelector('#farmerGroupChart');
if (farmerGroupChartEl) {
const farmerGroupData = [
Number(@json($data['kelompok_tani']['terdaftar'] ?? 0)),
Number(@json($data['kelompok_tani']['tidak_terdaftar'] ?? 0)),
Number(@json($data['kelompok_tani']['belum_terisi'] ?? 0))
];
const farmerGroupFilters = ['terdaftar', 'tidak_terdaftar', 'belum_terisi'];
const totalFarmerGroup = farmerGroupData.reduce((total, value) => total + value, 0);
const farmerGroupChartConfig = {
chart: {
height: 365,
type: 'pie',
parentHeightOffset: 0,
toolbar: { show: false },
events: {
dataPointSelection: function (event, chartContext, options) {
const index = options.dataPointIndex;
if (index < 0) {
return;
}
applyFarmerFilter('kelompok_tani', farmerGroupFilters[index]);
}
}
},
labels: ['Terdaftar', 'Tidak Terdaftar', 'Belum Terisi'],
series: farmerGroupData,
colors: [pastelSemantic.success || '#6FD68F', pastelSemantic.warning || '#FFB46F', pastelSemantic.secondary || '#9EA6B0'],
stroke: { width: 0 },
dataLabels: {
enabled: true,
formatter: function (value, options) {
const count = options.w.globals.series[options.seriesIndex] || 0;
if (count <= 0) {
return '';
}
return `${Number(value).toFixed(1)}%`;
},
style: {
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 600,
colors: ['#fff']
},
dropShadow: {
enabled: true,
top: 1,
left: 1,
blur: 2,
opacity: 0.45
}
},
legend: {
show: true,
position: 'bottom',
offsetY: 10,
markers: { size: 4, strokeWidth: 0 },
itemMargin: { horizontal: 15, vertical: 5 },
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 400,
labels: { colors: legendColor, useSeriesColors: false },
formatter: function (seriesName, options) {
const value = options.w.globals.series[options.seriesIndex];
const percentage = totalFarmerGroup > 0 ? ((value / totalFarmerGroup) * 100).toFixed(1) : '0.0';
return `${seriesName}: ${value} (${percentage}%)`;
}
},
tooltip: {
theme: 'dark',
y: {
formatter: function (value) {
const percentage = totalFarmerGroup > 0 ? ((value / totalFarmerGroup) * 100).toFixed(1) : '0.0';
return `${value} petani (${percentage}%)`;
}
}
},
plotOptions: {
pie: {
dataLabels: {
offset: -14,
minAngleToShowLabel: 8
}
}
},
responsive: [
{
breakpoint: 1025,
options: {
chart: { height: 380 }
}
}
]
};
new ApexCharts(farmerGroupChartEl, farmerGroupChartConfig).render();
}
const commodityChartEl = document.querySelector('#commodityChart');
if (commodityChartEl) {
const commodityIds = @json($data['komoditas_utama']['ids'] ?? []);
const commodityLabels = @json($data['komoditas_utama']['labels'] ?? []);
const commoditySeries = @json($data['komoditas_utama']['series'] ?? []);
const totalCommodityFarmers = commoditySeries.reduce((total, value) => total + Number(value), 0);
const commodityChartConfig = {
chart: {
height: 365,
type: 'pie',
parentHeightOffset: 0,
toolbar: { show: false },
events: {
dataPointSelection: function (event, chartContext, options) {
const index = options.dataPointIndex;
if (index < 0) {
return;
}
applyFarmerFilter('komoditas', commodityIds[index]);
}
}
},
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'
],
stroke: { width: 0 },
dataLabels: {
enabled: true,
formatter: function (value, options) {
const count = options.w.globals.series[options.seriesIndex] || 0;
if (count <= 0) {
return '';
}
return `${Number(value).toFixed(1)}%`;
},
style: {
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 600,
colors: ['#fff']
},
dropShadow: {
enabled: true,
top: 1,
left: 1,
blur: 2,
opacity: 0.45
}
},
legend: {
show: true,
position: 'bottom',
offsetY: 10,
markers: { size: 4, strokeWidth: 0 },
itemMargin: { horizontal: 15, vertical: 5 },
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 400,
labels: { colors: legendColor, useSeriesColors: false },
formatter: function (seriesName, options) {
const value = options.w.globals.series[options.seriesIndex];
const percentage = totalCommodityFarmers > 0 ? ((value / totalCommodityFarmers) * 100).toFixed(1) : '0.0';
return `${seriesName}: ${value} (${percentage}%)`;
}
},
tooltip: {
custom: function ({ series, seriesIndex, w }) {
const value = series[seriesIndex];
const label = w.globals.labels[seriesIndex];
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);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
}
},
plotOptions: {
pie: {
dataLabels: {
offset: -14,
minAngleToShowLabel: 8
}
}
},
responsive: [
{
breakpoint: 1400,
options: {
chart: { height: 390 },
legend: { position: 'bottom' }
}
},
{
breakpoint: 576,
options: {
chart: { height: 380 },
legend: { position: 'bottom' }
}
}
]
};
new ApexCharts(commodityChartEl, commodityChartConfig).render();
}
const landAreaChartEl = document.querySelector('#landAreaChart');
if (landAreaChartEl) {
const landAreaData = [
Number(@json($data['luas_lahan']['kurang_05'] ?? 0)),
Number(@json($data['luas_lahan']['05_1'] ?? 0)),
Number(@json($data['luas_lahan']['1_2'] ?? 0)),
Number(@json($data['luas_lahan']['lebih_2'] ?? 0))
];
const landAreaFilters = ['kurang_05', '05_1', '1_2', 'lebih_2'];
const totalLandFarmers = landAreaData.reduce((total, value) => total + value, 0);
const landAreaChartConfig = {
chart: {
height: 330,
type: 'bar',
parentHeightOffset: 0,
toolbar: { show: false },
events: {
dataPointSelection: function (event, chartContext, options) {
const index = options.dataPointIndex;
if (index < 0) {
return;
}
applyFarmerFilter('luas_lahan', landAreaFilters[index]);
}
}
},
series: [{ name: 'Petani', data: landAreaData }],
colors: [pastelSemantic.warning || '#FFB46F'],
plotOptions: {
bar: {
horizontal: true,
borderRadius: 4,
barHeight: '50%',
dataLabels: { position: 'top' }
}
},
dataLabels: {
enabled: true,
textAnchor: 'start',
offsetX: 6,
style: {
fontSize: '12px',
fontFamily: fontFamily,
fontWeight: 500,
colors: [headingColor]
},
formatter: function (value) {
const percentage = totalLandFarmers > 0 ? ((value / totalLandFarmers) * 100).toFixed(1) : '0.0';
return `${value} (${percentage}%)`;
}
},
xaxis: {
categories: ['< 0,5 ha', '0,5 - 1 ha', '1 - 2 ha', '> 2 ha'],
labels: {
style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily },
formatter: function (value) {
return Math.floor(value);
}
},
axisBorder: { show: false },
axisTicks: { show: false },
title: {
text: 'Petani',
style: { color: legendColor, fontSize: '12px', fontFamily: fontFamily, fontWeight: 400 }
}
},
yaxis: {
labels: { style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily } }
},
grid: {
borderColor: config.colors.borderColor,
strokeDashArray: 5,
xaxis: { lines: { show: true } },
yaxis: { lines: { show: false } },
padding: { right: 55 }
},
legend: { show: false },
tooltip: {
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
const value = series[seriesIndex][dataPointIndex];
const label = w.globals.labels[dataPointIndex];
const percentage = totalLandFarmers > 0 ? ((value / totalLandFarmers) * 100).toFixed(1) : '0.0';
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; 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);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
}
}
};
new ApexCharts(landAreaChartEl, landAreaChartConfig).render();
}
const landOwnershipStatusChartEl = document.querySelector('#landOwnershipStatusChart');
if (landOwnershipStatusChartEl) {
const landOwnershipStatusKeys = @json($data['status_kepemilikan_lahan']['keys'] ?? []);
const landOwnershipStatusLabels = @json($data['status_kepemilikan_lahan']['labels'] ?? []);
const landOwnershipStatusSeries = (@json($data['status_kepemilikan_lahan']['series'] ?? [])).map(Number);
const totalLandOwnershipStatusFarmers = landOwnershipStatusSeries.reduce((total, value) => total + value, 0);
const landOwnershipStatusChartConfig = {
chart: {
height: 365,
type: 'pie',
parentHeightOffset: 0,
toolbar: { show: false },
events: {
dataPointSelection: function (event, chartContext, options) {
const index = options.dataPointIndex;
if (index < 0) {
return;
}
applyFarmerFilter('status_kepemilikan_lahan', landOwnershipStatusKeys[index]);
}
}
},
labels: landOwnershipStatusLabels,
series: landOwnershipStatusSeries,
colors: [
pastelSemantic.success || '#6FD68F',
pastelSemantic.warning || '#FFB46F',
pastelSemantic.info || '#5FC7D6',
pastelSemantic.secondary || '#9EA6B0'
],
stroke: { width: 0 },
dataLabels: {
enabled: true,
formatter: function (value, options) {
const count = options.w.globals.series[options.seriesIndex] || 0;
if (count <= 0) {
return '';
}
return `${Number(value).toFixed(1)}%`;
},
style: {
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 600,
colors: ['#fff']
},
dropShadow: {
enabled: true,
top: 1,
left: 1,
blur: 2,
opacity: 0.45
}
},
legend: {
show: true,
position: 'bottom',
offsetY: 10,
markers: { size: 4, strokeWidth: 0 },
itemMargin: { horizontal: 15, vertical: 5 },
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 400,
labels: { colors: legendColor, useSeriesColors: false },
formatter: function (seriesName, options) {
const value = options.w.globals.series[options.seriesIndex];
const percentage = totalLandOwnershipStatusFarmers > 0 ? ((value / totalLandOwnershipStatusFarmers) * 100).toFixed(1) : '0.0';
return `${seriesName}: ${value} (${percentage}%)`;
}
},
tooltip: {
theme: 'dark',
y: {
formatter: function (value) {
const percentage = totalLandOwnershipStatusFarmers > 0 ? ((value / totalLandOwnershipStatusFarmers) * 100).toFixed(1) : '0.0';
return `${value} petani (${percentage}%)`;
}
}
},
plotOptions: {
pie: {
dataLabels: {
offset: -14,
minAngleToShowLabel: 8
}
}
},
responsive: [
{
breakpoint: 1025,
options: {
chart: { height: 380 }
}
}
]
};
new ApexCharts(landOwnershipStatusChartEl, landOwnershipStatusChartConfig).render();
}
const incomeChartEl = document.querySelector('#incomeChart');
if (incomeChartEl) {
const incomeData = [
Number(@json($data['penghasilan']['kurang_1'] ?? 0)),
Number(@json($data['penghasilan']['1_3'] ?? 0)),
Number(@json($data['penghasilan']['3_5'] ?? 0)),
Number(@json($data['penghasilan']['5_10'] ?? 0)),
Number(@json($data['penghasilan']['lebih_10'] ?? 0))
];
const incomeFilters = ['kurang_1', '1_3', '3_5', '5_10', 'lebih_10'];
const totalIncomeFarmers = incomeData.reduce((total, value) => total + value, 0);
const incomeChartConfig = {
chart: {
height: 330,
type: 'bar',
parentHeightOffset: 0,
toolbar: { show: false },
events: {
dataPointSelection: function (event, chartContext, options) {
const index = options.dataPointIndex;
if (index < 0) {
return;
}
applyFarmerFilter('penghasilan', incomeFilters[index]);
}
}
},
series: [{ name: 'Petani', data: incomeData }],
colors: [pastelSemantic.info || '#5FC7D6'],
plotOptions: {
bar: {
horizontal: true,
borderRadius: 4,
barHeight: '50%',
dataLabels: { position: 'top' }
}
},
dataLabels: {
enabled: true,
textAnchor: 'start',
offsetX: 6,
style: {
fontSize: '12px',
fontFamily: fontFamily,
fontWeight: 500,
colors: [headingColor]
},
formatter: function (value) {
const percentage = totalIncomeFarmers > 0 ? ((value / totalIncomeFarmers) * 100).toFixed(1) : '0.0';
return `${value} (${percentage}%)`;
}
},
xaxis: {
categories: ['< Rp1 juta', 'Rp1 - 3 juta', 'Rp3 - 5 juta', 'Rp5 - 10 juta', '> Rp10 juta'],
labels: {
style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily },
formatter: function (value) {
return Math.floor(value);
}
},
axisBorder: { show: false },
axisTicks: { show: false },
title: {
text: 'Petani',
style: { color: legendColor, fontSize: '12px', fontFamily: fontFamily, fontWeight: 400 }
}
},
yaxis: {
labels: { style: { colors: legendColor, fontSize: '12px', fontFamily: fontFamily } }
},
grid: {
borderColor: config.colors.borderColor,
strokeDashArray: 5,
xaxis: { lines: { show: true } },
yaxis: { lines: { show: false } },
padding: { right: 55 }
},
legend: { show: false },
tooltip: {
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
const value = series[seriesIndex][dataPointIndex];
const label = w.globals.labels[dataPointIndex];
const percentage = totalIncomeFarmers > 0 ? ((value / totalIncomeFarmers) * 100).toFixed(1) : '0.0';
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; 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);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
}
}
};
new ApexCharts(incomeChartEl, incomeChartConfig).render();
}
loadFarmerTable(1);
document.addEventListener(
'click',
function (event) {
const row = event.target.closest('.farmer-row');
if (!row) { return; }
const petaniId = row.dataset.id;
if (!petaniId) { return; }
window.location.href = `${farmerProfileUrl}/${petaniId}`;
}
);
});
</script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script>
const wilayah = @json($wilayah);
function fillSelect($select, data, valueField, textField, placeholder) {
$select.empty().append(
`<option value="">${placeholder}</option>`
);
data.forEach(function (item) {
$select.append(
`<option value="${item[valueField]}">${item[textField]}</option>`
);
});
$select.trigger('change.select2');
}
$('#filter-provinsi').select2().on('change', function () {
console.log('change provinsi');
let provinsiId = $(this).val();
fillSelect($('#filter-kabkota'), [], 'id', 'nama', 'Semua Kabupaten/Kota');
fillSelect($('#filter-kecamatan'), [], 'id', 'nama', 'Semua Kecamatan');
fillSelect($('#filter-desa'), [], 'id', 'nama', 'Semua Desa/Kelurahan');
if (!provinsiId) {
$('#filter-kabkota, #filter-kecamatan, #filter-desa')
.prop('disabled', true);
return;
}
let provinsi = wilayah.find(x => x.id == provinsiId);
if (!provinsi) {
return;
}
let kabupaten = provinsi.kabupaten || [];
fillSelect($('#filter-kabkota'), kabupaten, 'id', 'nama', 'Semua Kabupaten/Kota');
$('#filter-kabkota').prop('disabled', false);
$('#filter-kecamatan, #filter-desa').prop('disabled', true);
let defaultKabupaten = $('#filter-kabkota').data('default');
$('#filter-kabkota').removeData('default');
if (defaultKabupaten) {
$('#filter-kabkota').val(defaultKabupaten).trigger('change');
}
});
$('#filter-kabkota').select2().on('change', function () {
let kabupatenId = $(this).val();
fillSelect($('#filter-kecamatan'), [], 'id', 'nama', 'Semua Kecamatan');
fillSelect($('#filter-desa'), [], 'id', 'nama', 'Semua Desa/Kelurahan');
$('#filter-desa').prop('disabled', true);
if (!kabupatenId) {
$('#filter-kecamatan').prop('disabled', true);
return;
}
// Cari kabupaten di semua provinsi
let kabupaten = null;
wilayah.forEach(function (provinsi) {
let found = (provinsi.kabupaten || []).find(
x => x.id == kabupatenId
);
if (found) {
kabupaten = found;
}
});
if (!kabupaten) {
return;
}
let kecamatan = kabupaten.kecamatan || [];
fillSelect($('#filter-kecamatan'), kecamatan, 'id', 'nama', 'Semua Kecamatan');
$('#filter-kecamatan').prop('disabled', false);
let defaultKecamatan = $('#filter-kecamatan').data('default');
$('#filter-kecamatan').removeData('default');
if (defaultKecamatan) {
console.log('defaultKecamatan', defaultKecamatan);
$('#filter-kecamatan').val(defaultKecamatan).trigger('change');
}
});
$('#filter-kecamatan').select2().on('change', function () {
let kecamatanId = $(this).val();
fillSelect($('#filter-desa'), [], 'id', 'nama', 'Semua Desa/Kelurahan');
if (!kecamatanId) {
$('#filter-desa').prop('disabled', true);
return;
}
let kecamatan = null;
wilayah.forEach(function (provinsi) {
(provinsi.kabupaten || []).forEach(function (kabupaten) {
let found = (kabupaten.kecamatan || []).find(
x => x.id == kecamatanId
);
if (found) {
kecamatan = found;
}
});
});
if (!kecamatan) {
return;
}
let desa = kecamatan.desa || [];
fillSelect($('#filter-desa'), desa, 'id', 'nama', 'Semua Desa/Kelurahan');
$('#filter-desa').prop('disabled', false);
let defaultDesa = $('#filter-desa').data('default');
$('#filter-desa').removeData('default');
if (defaultDesa) {
console.log('default desa', defaultDesa);
$('#filter-desa').val(defaultDesa).trigger('change');
}
});
$('#filter-desa, #filter-segmen, #filter-komoditas').select2();
let defaultProvinsi = $('#filter-provinsi').data('default');
if (defaultProvinsi) {
$('#filter-provinsi')
.val(defaultProvinsi)
.trigger('change');
}
$('#btn-reset-dashboard-filter').on('click', function () {
const $form = $('#form-filter');
const action = $form.attr('action') || window.location.pathname;
const url = new URL(action, window.location.origin);
const activeTab = $('#profile-filter-tab').val();
if (activeTab) {
url.searchParams.set('tab', activeTab);
}
window.location.href = url.toString();
});
$('.data-table').DataTable({
"paging": false,
"lengthChange": true,
"searching": false,
"ordering": false,
"info": false,
"autoWidth": false,
"responsive": false,
"scrollX": true
});
// Chart Dimensi dan Komponen Kesiapan Adopsi
let cardColor, labelColor, fontFamily, headingColor, shadeColor, legendColor, borderColor, barBgColor;
if (isDarkStyle) {
barBgColor = '#3d4157';
shadeColor = 'dark';
} else {
barBgColor = '#efeef0';
shadeColor = '';
}
cardColor = config.colors.cardColor;
labelColor = config.colors.textMuted;
legendColor = config.colors.bodyColor;
borderColor = config.colors.borderColor;
headingColor = config.colors.headingColor;
</script>
<script>
$(document).ready(function () {
$.ajax({
url: '{{ route('dashboard.indeks-petani.summary') }}',
type: 'GET',
data: dashboardFilterParams.toString(),
dataType: 'json',
success: function (data) {
$('#count-petani').text(data.petani);
$('#count-komoditas').text(data.komoditas);
$('#sum-lahan').text(new Intl.NumberFormat('id-ID', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(data.sum_lahan));
$('#avg-lahan').text(new Intl.NumberFormat('id-ID', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(data.avg_lahan));
$('#count-desa').text(data.desa);
$('#count-kecamatan').text(data.kecamatan);
$('#avg-skor').text(new Intl.NumberFormat('id-ID', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(data.skor));
$('#avg-segmen').text(data.segmen);
}
});
})
</script>
<script>
$(document).ready(function () {
const segments = @json($segments);
const segmentPalette = segments.map(function (segment, index) {
return config.chartColors?.semantic?.[segment.color] || config.chartColors?.series?.[index % config.chartColors.series.length] || '#9EA6B0';
});
const segmentTextColors = config.chartColors?.text || {};
// Chart Segmentasi Petani
var chartSegmentasiPetani;
let distributionLoading = false;
function loadChartSegmentasiPetani() {
const container = document.querySelector('#chart-segmentasi-petani');
if (!container.clientWidth || distributionLoading || chartSegmentasiPetani) return;
distributionLoading = true;
$.ajax({
url: '{{ route("dashboard.indeks-petani.segmen") }}',
type: 'GET',
data: dashboardFilterParams.toString(),
dataType: 'json',
success: function(response) {
if (chartSegmentasiPetani) {
chartSegmentasiPetani.destroy();
}
var optionsSegmentasiPetani = {
series: response.series.map((value) => Number(value) || 0),
chart: {
type: 'donut',
width: '100%',
height: 280,
events: {
dataPointSelection: function(event, chartContext, config) {
const segmen =
chartContext.w.config.labels[
config.dataPointIndex
];
window.applyIndexFarmerFilter?.(5, segmen);
}
}
},
labels: @json(collect($segments)->pluck('name')->values()),
colors: segmentPalette,
plotOptions: {
pie: {
borderRadius: 12,
spacing: 5,
donut: {
size: '68%',
labels: {
show: true,
total: {
show: true,
label: 'Petani',
},
},
},
},
},
stroke: {
width: 0,
},
dataLabels: {
enabled: false,
},
legend: {
show: false,
},
responsive: [
{
breakpoint: 360,
options: {
chart: {
width: 220,
},
},
},
],
};
chartSegmentasiPetani = new ApexCharts(
document.querySelector('#chart-segmentasi-petani'),
optionsSegmentasiPetani
);
chartSegmentasiPetani.render().catch(function (error) {
chartSegmentasiPetani = null;
console.error('Gagal merender distribusi indeks:', error);
});
let html = '';
segments.forEach(function(segment, index) {
const total = response.series[index] ?? 0;
const persen = response.persentase[index] ?? 0;
const segmentColor = segmentTextColors[segment.color] || config.colors[segment.color] || segmentPalette[index] || '#808390';
html += `
<tr>
<td>
<div class="d-flex justify-content-start align-items-center gap-2">
<div class="flex-shrink-0">
<i class="icon-base ti ${segment.icon} icon-lg" style="color:${segmentColor};"></i>
</div>
<span style="color:${segmentColor};">
${segment.name}
</span>
</div>
</td>
<td class="text-end pe-0">
${Number(total).toLocaleString('id-ID')}
</td>
<td class="text-end pe-0">
${Number(persen).toLocaleString('id-ID', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}%
</td>
</tr>
`;
});
$('#table-segmentasi-petani').html(html);
},
error: function(xhr) {
distributionLoading = false;
console.error('Gagal mengambil data segmentasi:', xhr);
}
});
}
document.addEventListener('shown.bs.tab', loadChartSegmentasiPetani);
document.getElementById('indeks-segmentasi-charts').addEventListener('shown.bs.collapse', loadChartSegmentasiPetani);
loadChartSegmentasiPetani();
})
</script>
<script>
$(document).ready(function () {
const segmentPalette = (@json($segments)).map(function (segment, index) {
return config.chartColors?.semantic?.[segment.color] || config.chartColors?.series?.[index % config.chartColors.series.length] || '#9EA6B0';
});
var optionsSegmentasiPetaniByKomoditas = {
series: [],
colors: segmentPalette,
chart: {
type: 'bar',
height: 260,
stacked: true,
parentHeightOffset: 0,
toolbar: {
show: false
},
events: {
dataPointSelection: function(event, chartContext, config) {
const komoditas =
config.w.config.xaxis.categories[config.dataPointIndex];
const segmen = chartContext.w.config.series[config.seriesIndex]?.name;
window.applyIndexFarmerFilter?.(2, komoditas, segmen);
}
}
},
plotOptions: {
bar: {
horizontal: true,
},
},
stroke: {
width: 1,
colors: ['#fff'],
},
title: {
show: false,
},
xaxis: {
categories: [],
labels: {
trim: true,
hideOverlappingLabels: true,
},
},
yaxis: {
labels: {
maxWidth: 118,
style: {
fontSize: '12px',
},
},
},
tooltip: {
y: {
formatter: function(val) {
return val + ' petani';
},
},
},
fill: {
opacity: 1,
},
legend: {
position: 'top',
horizontalAlign: 'left',
offsetX: 0,
fontSize: '12px',
itemMargin: {
horizontal: 8,
vertical: 2,
},
},
};
let chartSegmentasiPetaniByKomoditas;
function loadChartSegmentasiKomoditas() {
$.ajax({
url: "{{ route('dashboard.indeks-petani.segmen.komoditas') }}",
type: "GET",
data: dashboardFilterParams.toString(),
dataType: "json",
beforeSend: function() {
// optional
// tampilkan loading
},
success: function(response) {
chartSegmentasiPetaniByKomoditas.updateOptions({
xaxis: { categories: response.categories },
series: response.series.map((series) => ({
...series,
data: series.data.map((value) => Number(value) || 0)
}))
});
},
error: function(xhr) {
console.error(
'Gagal mengambil data chart:',
xhr.responseText
);
}
});
}
function initializeCommodityDistribution() {
const container = document.querySelector('#chart-segmentasi-petani-komoditas');
if (!container.clientWidth || chartSegmentasiPetaniByKomoditas) return;
chartSegmentasiPetaniByKomoditas = new ApexCharts(container, optionsSegmentasiPetaniByKomoditas);
chartSegmentasiPetaniByKomoditas.render().then(loadChartSegmentasiKomoditas).catch(function (error) {
chartSegmentasiPetaniByKomoditas = null;
console.error('Gagal merender distribusi indeks per komoditas:', error);
});
}
document.addEventListener('shown.bs.tab', initializeCommodityDistribution);
document.getElementById('indeks-segmentasi-charts').addEventListener('shown.bs.collapse', initializeCommodityDistribution);
initializeCommodityDistribution();
})
</script>
<script>
function formatReadinessScore(value) {
if (value === null || value === undefined || value === '' || !Number.isFinite(Number(value))) return '—';
return Number(value).toLocaleString('id-ID', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function spaceReadinessRadarLabels(chart) {
chart.el.querySelectorAll('.apexcharts-radar-series .apexcharts-xaxis-label').forEach(function (label) {
var x = Number(label.getAttribute('x')) || 0;
var y = Number(label.getAttribute('y')) || 0;
var dx = Math.abs(x) > 10 ? (x > 0 ? 16 : -16) : 0;
var dy = Math.abs(x) <= 10 ? (y > 0 ? 20 : -20) : 0;
label.setAttribute('transform', 'translate(' + dx + ',' + dy + ')');
});
var categoryBoxes = Array.from(chart.el.querySelectorAll('.apexcharts-radar-series .apexcharts-xaxis-label'), function (label) { return label.getBoundingClientRect(); });
var scoreLabels = chart.el.querySelectorAll('.apexcharts-datalabels .apexcharts-datalabel');
scoreLabels.forEach(function (label, index) {
var angle = (2 * Math.PI * index / chart.w.config.labels.length) - Math.PI / 2;
var x = Number(label.getAttribute('x')) || 0;
var y = Number(label.getAttribute('y')) || 0;
var radius = Math.sqrt(x * x + y * y);
var distance = Math.max(18, 30 - radius);
var dx = Math.cos(angle) * distance;
var dy = Math.sin(angle) * distance;
label.setAttribute('transform', 'translate(' + dx + ',' + dy + ')');
label.setAttribute('text-anchor', Math.abs(dx) < 5 ? 'middle' : (dx > 0 ? 'start' : 'end'));
label.style.paintOrder = 'stroke';
label.style.stroke = config.colors.cardColor;
label.style.strokeWidth = '3px';
label.style.strokeLinejoin = 'round';
label.style.pointerEvents = 'none';
// ApexCharts moves the score group outside the series after rendering.
// Keep a clear gap from category labels even when a score reaches 5.
for (var attempt = 0; attempt < 7; attempt++) {
var scoreBox = label.getBoundingClientRect();
var overlapsCategory = categoryBoxes.some(function (box) {
return scoreBox.left < box.right + 10 && scoreBox.right > box.left - 10 &&
scoreBox.top < box.bottom + 10 && scoreBox.bottom > box.top - 10;
});
if (!overlapsCategory || distance <= 12) break;
distance = Math.max(12, distance - 4);
dx = Math.cos(angle) * distance;
dy = Math.sin(angle) * distance;
label.setAttribute('transform', 'translate(' + dx + ',' + dy + ')');
}
});
}
function readinessRadarOptions(title, labels, categories, pointColors) {
return {
chart: {
height: 420, type: 'radar', toolbar: { show: false }, animations: { enabled: false },
events: { mounted: spaceReadinessRadarLabels, updated: spaceReadinessRadarLabels }
},
series: [{ name: title, data: [] }],
labels: labels,
plotOptions: { radar: { polygons: { strokeColors: borderColor, connectorColors: borderColor } } },
stroke: { width: 2 },
legend: { show: false },
colors: [config.chartColors?.semantic?.primary || '#9D8EF0'],
fill: { opacity: 0.12 },
markers: {
size: 5, strokeWidth: 2, strokeColors: config.colors.cardColor, hover: { sizeOffset: 2 },
discrete: pointColors.map(function (color, index) {
return { seriesIndex: 0, dataPointIndex: index, fillColor: color, strokeColor: config.colors.cardColor, size: 6 };
})
},
dataLabels: {
enabled: true,
formatter: formatReadinessScore,
offsetX: 0, offsetY: 0,
style: { fontSize: '12px', fontFamily: fontFamily, fontWeight: 600, colors: [config.colors.headingColor] },
background: { enabled: false },
dropShadow: { enabled: false }
},
tooltip: { enabled: false },
grid: { show: false, padding: { top: 28, bottom: 24 } },
xaxis: { categories: categories, labels: { show: true, style: { colors: labels.map(function () { return legendColor; }), fontSize: '13px', fontFamily: fontFamily, fontWeight: 500 } } },
yaxis: {
show: false, min: 0, max: 5, tickAmount: 5,
labels: { show: false }
},
responsive: [{ breakpoint: 769, options: { chart: { height: 420 }, xaxis: { labels: { style: { fontSize: '11px' } } } } }]
};
}
function updateReadinessScoreLegend(chart, containerId, values, colors) {
var container = document.getElementById(containerId);
var fragment = document.createDocumentFragment();
chart.w.config.labels.forEach(function (label, index) {
var row = document.createElement('div');
row.className = 'readiness-score-row';
var dot = document.createElement('span');
dot.className = 'readiness-score-dot';
dot.style.backgroundColor = colors[index];
dot.setAttribute('aria-hidden', 'true');
var name = document.createElement('span');
name.className = 'readiness-score-name';
name.textContent = label;
var score = document.createElement('strong');
score.className = 'readiness-score-value';
score.textContent = formatReadinessScore(values[index]);
row.append(dot, name, score);
fragment.appendChild(row);
});
container.replaceChildren(fragment);
}
</script>
<script>
$(document).ready(function () {
var palette = [
config.chartColors?.semantic?.success || '#6FD68F',
config.chartColors?.semantic?.primary || '#9D8EF0',
config.chartColors?.semantic?.warning || '#FFB46F',
config.chartColors?.semantic?.info || '#5FC7D6',
config.chartColors?.semantic?.danger || '#F47C86',
config.chartColors?.semantic?.secondary || '#9EA6B0'
];
[
{ id: 'dimensi', key: 'radarDimensi', url: "{{ route('dashboard.indeks-petani.dimensi') }}", title: "Dimensi Kesiapan Adopsi", labels: ["Sistem & Praktik Budidaya","Sosial Ekonomi Rumah Tangga","Rantai Nilai & Akses Pasar"], categories: [["Sistem & Praktik","Budidaya"],["Sosial Ekonomi","Rumah Tangga"],["Rantai Nilai &","Akses Pasar"]] },
{ id: 'komponen', key: 'radarKomponen', url: "{{ route('dashboard.indeks-petani.komponen') }}", title: "Komponen Kesiapan Adopsi", labels: ["Motivasi & Kapasitas Adopsi","Kondisi Ekonomi Rumah Tangga","Praktik Budidaya & GAP","Keterlibatan Kelembagaan","Akses & Kebebasan Keuangan","Rantai Nilai & Akses Pasar"], categories: [["Motivasi & Kapasitas","Adopsi"],["Kondisi Ekonomi","Rumah Tangga"],["Praktik Budidaya","& GAP"],["Keterlibatan","Kelembagaan"],["Akses & Kebebasan","Keuangan"],["Rantai Nilai &","Akses Pasar"]] }
].forEach(function (definition) {
var pointColors = palette.slice(0, definition.labels.length);
var chart = new ApexCharts(document.getElementById('chart-' + definition.id), readinessRadarOptions(definition.title, definition.labels, definition.categories, pointColors));
chart.render().then(function () {
return $.ajax({
url: definition.url,
type: 'GET',
data: dashboardFilterParams.toString(),
dataType: 'json'
}).then(function (response) {
var values = response[definition.key];
if (!Array.isArray(values)) throw new Error('Data radar tidak valid');
updateReadinessScoreLegend(chart, 'chart-' + definition.id + '-scores', values, pointColors);
return chart.updateSeries([{ name: definition.title, data: values }]);
});
}).catch(function (error) {
document.getElementById('chart-' + definition.id + '-scores').textContent = 'Informasi skor belum dapat dimuat.';
console.error('Gagal memuat radar ' + definition.id + ':', error);
});
});
});
</script>
<script>
$(document).ready(function () {
// Map Sebaran Petani
var map = L.map('map-sebaran-petani').setView([-0.789, 113.921], 5);
const mapShell = document.getElementById('farmer-map-shell');
const fullscreenButton = document.getElementById('farmer-map-fullscreen');
if (!document.fullscreenEnabled || !mapShell.requestFullscreen) {
fullscreenButton.hidden = true;
}
fullscreenButton.addEventListener('click', async function () {
try {
if (document.fullscreenElement === mapShell) {
await document.exitFullscreen();
} else {
await mapShell.requestFullscreen();
}
} catch (error) {
console.error('Tidak dapat membuka peta layar penuh:', error);
}
});
document.addEventListener('fullscreenchange', function () {
const isFullscreen = document.fullscreenElement === mapShell;
fullscreenButton.innerHTML = '<i class="icon-base ti ' + (isFullscreen ? 'tabler-minimize' : 'tabler-maximize') + '"></i>';
const label = isFullscreen ? 'Keluar dari layar penuh' : 'Buka peta layar penuh';
fullscreenButton.setAttribute('aria-label', label);
fullscreenButton.title = label;
requestAnimationFrame(focusMapData);
});
document.getElementById('indeks-wilayah-charts').addEventListener('shown.bs.collapse', function () {
focusMapData();
if (window.jQuery?.fn?.dataTable) {
window.jQuery.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
}
});
document.addEventListener('shown.bs.tab', function (event) {
if (event.target.getAttribute('data-bs-target') === '#profile-tab-indeks-petani') {
focusMapData();
}
});
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
const warnaSegmen = {
@foreach($segments as $segment)
"{{ $segment['name'] }}": config.chartColors?.semantic?.["{{ $segment['color'] }}"] || '#9EA6B0',
@endforeach
};
const group = L.featureGroup().addTo(map);
function focusMapData() {
const container = map.getContainer();
if (!container.clientWidth || !container.clientHeight) {
return;
}
map.invalidateSize();
const bounds = group.getBounds();
if (bounds.isValid()) {
map.fitBounds(bounds, {
padding: [32, 32],
maxZoom: 14,
animate: false
});
}
}
function loadMapDesa() {
$.ajax({
url: "{{ route('dashboard.indeks-petani.map') }}",
type: "GET",
data: dashboardFilterParams.toString(),
beforeSend: function () {
// Optional
// $('#map-loading').show();
},
success: function (desaData) {
group.clearLayers();
if (!desaData || desaData.length === 0) {
return;
}
desaData.forEach(function (desa) {
if (!desa.path) {
return;
}
let coordinates;
try {
coordinates = typeof desa.path === 'string'
? JSON.parse(desa.path)
: desa.path;
} catch (error) {
console.error('Path polygon tidak valid:', desa);
return;
}
const polygon = L.polygon(coordinates, {
color: '#666',
weight: 1,
fillColor:
warnaSegmen[desa.segmentasi?.segmen] || '#9EA6B0',
fillOpacity: 0.7
});
polygon.desaId = desa.desa_kelurahan_id;
polygon.bindPopup(`
<b>Desa/Kel.: ${desa.desa_kelurahan ?? '-'}</b><br>
Kec.: ${desa.kecamatan ?? '-'}<br>
Kab.: ${desa.kabupaten ?? '-'}<br>
<hr>
Petani: ${desa.segmentasi?.jumlah_petani ?? 0}<br>
Indeks:
${Number(desa.segmentasi?.skor ?? 0).toFixed(2)}
<br>
Segmen:
<b>${desa.segmentasi?.segmen ?? '-'}</b>
<br>
Segmen Dominan:
<b>${desa.segmentasi?.segmen_dominan ?? '-'}</b>
(${desa.segmentasi?.jumlah_dominan ?? 0} petani)
`);
polygon.on('click', function () {
window.applyIndexFarmerFilter?.(1, desa.desa_kelurahan);
});
polygon.addTo(group);
});
requestAnimationFrame(focusMapData);
},
error: function (xhr) {
console.error(
'Gagal mengambil data map:',
xhr.responseText
);
},
complete: function () {
// Optional
// $('#map-loading').hide();
}
});
}
loadMapDesa();
})
</script>
<script>
$(document).ready(function () {
const segmentTextColors = config.chartColors?.text || {};
var tableDesa = $('#table-desa').DataTable({
"paging": true,
"pageLength": 5,
"lengthChange": false,
"searching": false,
"ordering": true,
"info": false,
"autoWidth": false,
"responsive": false,
"scrollX": true,
"serverSide": false,
"processing": true,
"ajax": {
"url": "{{ route('dashboard.indeks-petani.desa') }}",
"type": "GET",
"data": function (data) {
data = dashboardFilterParams.toString();
return data;
},
"dataSrc": function(json) {
return json;
},
"dataType": "json"
},
columns: [
{ data: 'desa', render: $.fn.dataTable.render.text() },
{
data: 'jumlah_petani', className: 'text-end',
render: function (data, type) {
return type === 'display' ? Number(data).toLocaleString('id-ID') : Number(data);
}
}
].concat(['pemula', 'perintis', 'pemerhati', 'penggerak', 'belum_lengkap'].map(function (segment) {
return {
data: 'segments.' + segment,
className: 'text-end text-nowrap',
render: function (data, type) {
var count = Number(data && data.count) || 0;
if (type !== 'display') return count;
var percentage = Number(data && data.percentage || 0).toLocaleString('id-ID', { maximumFractionDigits: 2 });
var segmentColor = segmentTextColors[data && data.color || 'secondary'] || '#808390';
return '<span class="fw-semibold" style="color:' + segmentColor + ';">' + count.toLocaleString('id-ID') +
'</span> <span class="text-body-secondary small">(' + percentage + '%)</span>';
}
};
}))
});
})
</script>
<script>
$(document).ready(function () {
const indexSegmentColorKeys = {
@foreach($segments as $segment)
"{{ $segment['name'] }}": "{{ $segment['color'] }}",
@endforeach
};
const indexSegmentColors = {
@foreach($segments as $segment)
"{{ $segment['name'] }}": config.chartColors?.semantic?.["{{ $segment['color'] }}"] || '#9EA6B0',
@endforeach
};
const indexSegmentTextColors = config.chartColors?.text || {};
function indexHexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '');
if (!/^[0-9a-fA-F]{6}$/.test(value)) {
return hex;
}
const red = parseInt(value.slice(0, 2), 16);
const green = parseInt(value.slice(2, 4), 16);
const blue = parseInt(value.slice(4, 6), 16);
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function indexSegmentBadgeStyle(backgroundColor, textColor) {
return [
`background-color:${indexHexToRgba(backgroundColor, 0.22)} !important`,
`border:1px solid ${indexHexToRgba(backgroundColor, 0.36)}`,
`color:${textColor} !important`,
'font-weight:600'
].join(';');
}
var tablePetani = $('#table-petani').DataTable({
dom: 'rt<"card-footer d-flex flex-wrap justify-content-between align-items-center gap-3"ip>',
pagingType: 'full_numbers',
language: {
info: 'Menampilkan _START_ - _END_ dari _TOTAL_ petani',
infoEmpty: 'Menampilkan 0 - 0 dari 0 petani',
infoFiltered: '',
emptyTable: 'Tidak ada data petani sesuai filter.',
zeroRecords: 'Tidak ada data petani sesuai filter.',
paginate: {
first: '<i class="icon-base ti tabler-chevrons-left"></i>',
previous: '<i class="icon-base ti tabler-chevron-left"></i>',
next: '<i class="icon-base ti tabler-chevron-right"></i>',
last: '<i class="icon-base ti tabler-chevrons-right"></i>'
}
},
drawCallback: function () {
const api = this.api();
const hasFilter = Boolean(api.search()) || api.columns().search().toArray().some(Boolean);
$('#btn-reset-filter').toggleClass('d-none', !hasFilter);
const info = api.page.info();
$('#indexFarmerTotalBadge').text(info.recordsDisplay + ' petani');
},
"paging": true,
"pageLength": 10,
"lengthChange": false,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false,
"responsive": false,
"scrollX": true,
"serverSide": false,
"processing": true,
"ajax": {
"url": "{{ route('dashboard.indeks-petani.data') }}",
"type": "GET",
"data": function (data) {
data = dashboardFilterParams.toString();
return data;
},
"dataSrc": function(json) {
return json;
},
"dataType": "json"
},
columns: [
{
data: 'nama',
name: 'nama',
className: 'pe-0'
},
{
data: 'desa',
name: 'desa',
className: 'pe-0'
},
{
data: 'komoditas',
name: 'komoditas',
className: 'pe-0'
},
{
data: 'luas_lahan',
name: 'luas_lahan',
className: 'text-end pe-0',
render: function(data) {
return Number(data ?? 0).toLocaleString('id-ID', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
},
{
data: 'skor',
name: 'skor',
className: 'text-end pe-0',
render: function(data) {
if (data === null || data === '') {
return '';
}
return Number(data).toLocaleString('id-ID', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
},
{
data: null,
name: 'segmen',
className: 'pe-0',
orderable: false,
render: function(data, type) {
if (type !== 'display') {
return data.segmen ?? '';
}
if (!data.skor || !data.segmen) {
return '';
}
const segmentKey = indexSegmentColorKeys[data.segmen] || data.color || 'secondary';
const segmentColor = indexSegmentColors[data.segmen] || config.chartColors?.semantic?.[segmentKey] || '#9EA6B0';
const segmentTextColor = indexSegmentTextColors[segmentKey] || '#808390';
return `
<span class="badge" style="${indexSegmentBadgeStyle(segmentColor, segmentTextColor)}">
${data.segmen}
</span>
`;
}
}
],
order: [
[0, 'asc']
],
createdRow: function(row, data) {
$(row)
.addClass('clickable-row')
.attr('data-url', data.url);
}
});
window.applyIndexFarmerFilter = function (column, value, segmen) {
if (value === null || value === undefined) return;
const escaped = $.fn.dataTable.util.escapeRegex(String(value));
const search = '^' + escaped + '$';
const nextValue = tablePetani.column(column).search() === search ? '' : String(value);
tablePetani.column(column).search(nextValue ? search : '', true, false);
$('.index-table-filter[data-column="' + column + '"]').val(nextValue);
if (segmen !== undefined) {
tablePetani.column(5).search('^' + $.fn.dataTable.util.escapeRegex(segmen) + '$', true, false);
$('#index-table-segmen').val(segmen);
}
tablePetani.draw();
};
$('#indexFarmerSearch').on('input', function () { tablePetani.search(this.value).draw(); });
tablePetani.on('xhr.dt', function (event, settings, rows) {
if (!Array.isArray(rows)) return;
const fields = { 1: 'desa', 2: 'komoditas', 5: 'segmen' };
document.querySelectorAll('.index-table-filter').forEach((select) => {
const placeholder = select.options[0].text;
select.replaceChildren(new Option(placeholder, ''));
const values = [...new Set(rows.map((row) => row[fields[select.dataset.column]]).filter(Boolean))];
values.sort((a, b) => a.localeCompare(b, 'id')).forEach((value) => {
select.add(new Option(value, value));
});
});
});
$('#applyIndexTableFilter').on('click', function () {
$('.index-table-filter').each(function () {
const value = $.fn.dataTable.util.escapeRegex(this.value);
tablePetani.column(Number(this.dataset.column)).search(value ? '^' + value + '$' : '', true, false);
});
tablePetani.draw();
});
$('#btn-reset-filter').on('click', function () {
$('.index-table-filter').val('');
$('#indexFarmerSearch').val('');
tablePetani.search('').columns().search('').draw();
});
$('#table-petani tbody').on('click', 'tr.clickable-row', function() {
const url = $(this).data('url');
if (url) {
window.location.href = url;
}
});
})
</script>
<script>
document.addEventListener('shown.bs.tab', function (event) {
const filterTab = document.getElementById('profile-filter-tab');
const target = event.target?.getAttribute('data-bs-target');
if (filterTab && target === '#profile-tab-demografi-petani') {
filterTab.value = 'demografi-petani';
}
if (filterTab && target === '#profile-tab-indeks-petani') {
filterTab.value = 'indeks-petani';
}
if (event.target?.getAttribute('data-bs-target') === '#profile-tab-indeks-petani') {
setTimeout(function () {
if (window.jQuery?.fn?.dataTable) {
window.jQuery.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
}
window.dispatchEvent(new Event('resize'));
}, 150);
}
});
</script>
<script>
document.querySelectorAll('.chart-group-header-toggle').forEach((header) => {
header.addEventListener('keydown', function (event) {
if (event.target !== header || !['Enter', ' '].includes(event.key)) return;
event.preventDefault();
header.click();
});
});
document.querySelectorAll('.chart-group-panel').forEach((panel) => {
const control = document.querySelector('.chart-group-header-toggle[aria-controls="' + panel.id + '"]');
if (!control) return;
const icon = control.querySelector('.chart-group-icon-toggle i');
const syncState = () => {
const expanded = panel.classList.contains('show');
const label = expanded ? 'Sembunyikan chart' : 'Tampilkan chart';
control.setAttribute('aria-expanded', String(expanded));
control.setAttribute('aria-label', label);
control.title = label;
icon.classList.toggle('tabler-chevron-up', expanded);
icon.classList.toggle('tabler-chevron-down', !expanded);
};
syncState();
['hidden.bs.collapse', 'shown.bs.collapse'].forEach((eventName) => {
panel.addEventListener(eventName, (event) => {
if (event.target !== panel) return;
syncState();
if (eventName === 'shown.bs.collapse') window.dispatchEvent(new Event('resize'));
});
});
});
</script>
@endpush