Files
database-pertani-web/resources/views/dashboard/indikator-pertanian.blade.php
T

1650 lines
64 KiB
PHP

@extends('layout.main')
@push('style')
<style>
.indicator-page-header {
margin-bottom: 1.25rem;
}
.indicator-page-title {
color: var(--bs-heading-color);
font-size: 1.5rem;
font-weight: 700;
line-height: 1.25;
margin-bottom: .35rem;
}
.indicator-filter-row {
padding-bottom: 1.5rem;
padding-top: 1.5rem;
}
.indicator-filter-bar {
display: block;
}
.indicator-filter-line {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: .5rem;
}
.indicator-filter-section-label {
align-items: center;
display: flex;
font-weight: 500;
margin-right: .5rem;
white-space: nowrap;
}
.indicator-filter-export-action {
margin-left: auto;
}
.indicator-filter-bar .form-select,
.indicator-filter-bar .btn {
min-height: 31px;
}
.indicator-filter-bar .form-select {
background-color: var(--bs-paper-bg);
border-color: var(--bs-border-color);
border-radius: var(--bs-border-radius);
color: var(--bs-body-color);
}
.indicator-filter-bar .btn {
white-space: nowrap;
}
.indicator-filter-bar .form-select:focus {
border-color: rgba(40, 167, 69, .55);
box-shadow: 0 0 0 .2rem rgba(40, 167, 69, .12);
}
.indicator-filter-bar.is-loading .form-select,
.indicator-filter-bar.is-loading .btn {
pointer-events: none;
}
.indicator-filter-bar.is-loading .indicator-filter-select,
.indicator-filter-bar.is-loading .btn {
opacity: .65;
}
.indicator-filter-static {
align-items: center;
background-color: var(--bs-paper-bg);
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
color: var(--bs-body-color);
display: flex;
font-size: .8125rem;
min-height: 31px;
padding: .25rem .75rem;
}
.indicator-filter-bar .input-group-text {
background: var(--bs-paper-bg);
border-color: var(--bs-border-color);
color: var(--bs-secondary-color);
padding-left: .5rem;
padding-right: .5rem;
}
.indicator-summary-value {
overflow-wrap: anywhere;
}
.indicator-tabs {
gap: .5rem;
margin-bottom: .25rem !important;
overflow-x: auto;
scrollbar-width: none;
}
.indicator-tab-content {
padding-top: 0 !important;
}
.indicator-tab-pane-grid {
margin-top: 0 !important;
}
.indicator-tabs::-webkit-scrollbar {
display: none;
}
.indicator-tabs .nav-item {
flex: 1 0 auto;
}
.indicator-tabs .nav-link {
align-items: center;
display: flex;
justify-content: center;
min-height: 38px;
white-space: nowrap;
width: 100%;
}
.indicator-tab-shell.nav-align-top > .indicator-tab-content,
.indicator-tab-shell.nav-align-top .nav-pills ~ .indicator-tab-content {
background: transparent;
border: 0;
border-radius: 0;
box-shadow: none;
padding: 0;
}
#chart-petani-komoditas .apexcharts-tooltip,
#chart-luas-kecamatan .apexcharts-tooltip,
#chart-kondisi-tanaman .apexcharts-tooltip,
#chart-status-kepemilikan-lahan .apexcharts-tooltip,
#chart-serangan-hama .apexcharts-tooltip,
#chart-penyakit-tanaman .apexcharts-tooltip,
#chart-keparahan-serangan .apexcharts-tooltip,
#chart-status-hak-kelola-lahan .apexcharts-tooltip,
#chart-praktik-pemupukan .apexcharts-tooltip,
#chart-produksi-komoditas .apexcharts-tooltip,
#chart-produktivitas-komoditas .apexcharts-tooltip,
#chart-luas-panen-komoditas .apexcharts-tooltip,
#chart-status-panen .apexcharts-tooltip,
#chart-survey-produktivitas-ha .apexcharts-tooltip,
#chart-survey-produktivitas-pohon .apexcharts-tooltip {
background: transparent !important;
border: none !important;
box-shadow: none !important;
overflow: visible !important;
}
.indicator-chart {
min-height: 330px;
}
.indicator-chart-sm {
min-height: 330px;
}
@media (max-width: 767.98px) {
.indicator-filter-line {
display: grid;
grid-template-columns: 1fr;
width: 100%;
}
.indicator-filter-section-label {
margin-right: 0;
}
.indicator-filter-export-action {
margin-left: 0;
}
.indicator-filter-bar .btn {
justify-content: center;
width: 100%;
}
.indicator-tabs .nav-item {
flex-basis: 100%;
}
}
</style>
@endpush
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
<div class="indicator-page-header">
<h4 class="indicator-page-title">
Indikator Pertanian
</h4>
<p class="text-muted mb-0">
Kondisi dan performa usaha tani petani berdasarkan hasil survei
</p>
</div>
@php
$filtersByKey = collect($filterOptions)->keyBy('key');
@endphp
<div class="row g-6 indicator-filter-row">
<div class="col-12">
<div class="card card-border-shadow-primary">
<div class="card-body py-3">
<div class="indicator-filter-bar" id="indicator-filter-bar" aria-label="Filter dashboard indikator pertanian">
<div class="indicator-filter-line">
<div class="indicator-filter-section-label">
<i class="icon-base ti tabler-map-2 me-2"></i>
<span>Wilayah</span>
</div>
@foreach (['kabupaten', 'kecamatan', 'desa'] as $filterKey)
@php
$filter = $filtersByKey[$filterKey];
@endphp
<div class="filter-item">
@if (!empty($filter['readonly']))
@php
$selectedOption = collect($filter['options'])
->firstWhere('value', $initialFilters[$filter['key']]);
@endphp
<div
id="filter-{{ $filter['key'] }}"
class="form-control form-control-sm indicator-filter-static"
data-filter-key="{{ $filter['key'] }}"
data-filter-value="{{ $initialFilters[$filter['key']] }}"
aria-label="Filter {{ $filter['label'] }}"
role="text"
>
{{ $selectedOption['label'] ?? 'Ngada' }}
</div>
@else
<select
id="filter-{{ $filter['key'] }}"
class="form-select form-select-sm indicator-filter-select"
data-filter-key="{{ $filter['key'] }}"
aria-label="Filter {{ $filter['label'] }}"
@disabled($filter['key'] === 'desa' && $initialFilters['kecamatan'] === 'all')
>
@foreach ($filter['options'] as $option)
<option
value="{{ $option['value'] }}"
@if (isset($option['parent'])) data-parent="{{ $option['parent'] }}" @endif
@selected($initialFilters[$filter['key']] === $option['value'])
>
{{ $option['label'] }}
</option>
@endforeach
</select>
@endif
</div>
@endforeach
</div>
<hr class="my-3">
<div class="indicator-filter-line">
<div class="indicator-filter-section-label">
<i class="icon-base ti tabler-adjustments-horizontal me-2"></i>
<span>Data</span>
</div>
@foreach (['komoditas', 'segmentasi'] as $filterKey)
@php
$filter = $filtersByKey[$filterKey];
@endphp
<div class="filter-item">
<select
id="filter-{{ $filter['key'] }}"
class="form-select form-select-sm indicator-filter-select"
data-filter-key="{{ $filter['key'] }}"
aria-label="Filter {{ $filter['label'] }}"
>
@foreach ($filter['options'] as $option)
<option
value="{{ $option['value'] }}"
@selected($initialFilters[$filter['key']] === $option['value'])
>
{{ $option['label'] }}
</option>
@endforeach
</select>
</div>
@endforeach
<button
type="button"
class="btn btn-outline-secondary btn-sm"
id="indicator-apply-filter"
aria-label="Terapkan filter indikator pertanian"
>
<i class="icon-base ti tabler-filter me-1"></i>
Filter
</button>
<button
type="button"
class="btn btn-outline-secondary btn-sm"
id="indicator-reset-filter"
aria-label="Reset filter indikator pertanian"
>
<i class="icon-base ti tabler-refresh me-1"></i>
Reset
</button>
<div class="indicator-filter-export-action">
<button
type="button"
class="btn btn-outline-secondary btn-sm"
id="indicator-export-data"
aria-label="Export data indikator pertanian"
>
<i class="icon-base ti tabler-download me-1"></i>
Export Data
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-6" id="indicator-summary-row">
@foreach ($summaryData as $summary)
<div class="col-lg-3 col-sm-6" data-summary-card="{{ $loop->index }}">
<div class="card card-border-shadow-{{ $summary['variant'] }} 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-{{ $summary['variant'] }}">
<i class="icon-base ti {{ $summary['icon'] }} icon-28px"></i>
</span>
</div>
<h4 class="mb-0 indicator-summary-value" data-summary-value>
{{ $summary['value'] }}
</h4>
</div>
<p class="mb-1" data-summary-label>
{{ $summary['label'] }}
</p>
<p class="mb-0">
<small class="text-body-secondary" data-summary-description>
{{ $summary['description'] }}
</small>
</p>
</div>
</div>
</div>
@endforeach
</div>
<div class="nav-align-top indicator-tab-shell mt-4">
<ul class="nav nav-pills flex-column flex-md-row mb-6 row-gap-2 indicator-tabs" role="tablist">
<li class="nav-item">
<button
type="button"
class="nav-link active"
role="tab"
data-bs-toggle="tab"
data-bs-target="#indicator-tab-lahan-tanaman"
aria-controls="indicator-tab-lahan-tanaman"
aria-selected="true"
>
<i class="icon-base ti tabler-trees icon-sm me-1_5"></i>
Lahan dan Tanaman
</button>
</li>
<li class="nav-item">
<button
type="button"
class="nav-link"
role="tab"
data-bs-toggle="tab"
data-bs-target="#indicator-tab-produksi"
aria-controls="indicator-tab-produksi"
aria-selected="false"
>
<i class="icon-base ti tabler-chart-histogram icon-sm me-1_5"></i>
Produksi dan Produktivitas
</button>
</li>
<li class="nav-item">
<button
type="button"
class="nav-link"
role="tab"
data-bs-toggle="tab"
data-bs-target="#indicator-tab-input-produksi"
aria-controls="indicator-tab-input-produksi"
aria-selected="false"
>
<i class="icon-base ti tabler-package icon-sm me-1_5"></i>
Input Produksi
</button>
</li>
<li class="nav-item">
<button
type="button"
class="nav-link"
role="tab"
data-bs-toggle="tab"
data-bs-target="#indicator-tab-panen"
aria-controls="indicator-tab-panen"
aria-selected="false"
>
<i class="icon-base ti tabler-basket icon-sm me-1_5"></i>
Panen dan Pascapanen
</button>
</li>
<li class="nav-item">
<button
type="button"
class="nav-link"
role="tab"
data-bs-toggle="tab"
data-bs-target="#indicator-tab-akses-pasar"
aria-controls="indicator-tab-akses-pasar"
aria-selected="false"
>
<i class="icon-base ti tabler-building-store icon-sm me-1_5"></i>
Akses Pasar
</button>
</li>
</ul>
<div class="tab-content indicator-tab-content">
<div
class="tab-pane fade show active"
id="indicator-tab-lahan-tanaman"
role="tabpanel"
>
<div class="row g-6 indicator-tab-pane-grid">
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Distribusi Komoditas</h5>
<small class="text-body-secondary">Jumlah petani per komoditas</small>
</div>
</div>
<div class="card-body">
<div id="chart-petani-komoditas" class="indicator-chart-sm"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Luas Lahan per Kecamatan</h5>
<small class="text-body-secondary">Kecamatan dengan luas lahan terdata terbesar</small>
</div>
</div>
<div class="card-body">
<div id="chart-luas-kecamatan" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Hak Kelola Lahan</h5>
<small class="text-body-secondary">Berdasarkan survei sosial ekonomi</small>
</div>
</div>
<div class="card-body">
<div id="chart-status-hak-kelola-lahan" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Status Kepemilikan Lahan</h5>
<small class="text-body-secondary">Berdasarkan data lahan terdata</small>
</div>
</div>
<div class="card-body">
<div id="chart-status-kepemilikan-lahan" class="indicator-chart-sm"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Serangan Hama</h5>
<small class="text-body-secondary">Jenis hama pada tanaman/lahan</small>
</div>
</div>
<div class="card-body">
<div id="chart-serangan-hama" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Keparahan Serangan</h5>
<small class="text-body-secondary">Tingkat dampak hama/penyakit</small>
</div>
</div>
<div class="card-body">
<div id="chart-keparahan-serangan" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Kondisi Tanaman</h5>
<small class="text-body-secondary">Berdasarkan tanaman/lahan terdata</small>
</div>
</div>
<div class="card-body">
<div id="chart-kondisi-tanaman" class="indicator-chart-sm"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Penyakit Tanaman</h5>
<small class="text-body-secondary">Jenis penyakit pada tanaman/lahan</small>
</div>
</div>
<div class="card-body">
<div id="chart-penyakit-tanaman" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Praktik Pemupukan</h5>
<small class="text-body-secondary">Berdasarkan survei praktik budidaya</small>
</div>
</div>
<div class="card-body">
<div id="chart-praktik-pemupukan" class="indicator-chart"></div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="indicator-tab-produksi" role="tabpanel">
<div class="row g-6 indicator-tab-pane-grid">
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Total Produksi per Komoditas</h5>
<small class="text-body-secondary">Akumulasi hasil panen aktual</small>
</div>
</div>
<div class="card-body">
<div id="chart-produksi-komoditas" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Produktivitas Aktual</h5>
<small class="text-body-secondary">Rata-rata kg/ha per komoditas</small>
</div>
</div>
<div class="card-body">
<div id="chart-produktivitas-komoditas" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Luas Panen per Komoditas</h5>
<small class="text-body-secondary">Akumulasi luas panen terdata</small>
</div>
</div>
<div class="card-body">
<div id="chart-luas-panen-komoditas" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Status Panen</h5>
<small class="text-body-secondary">Jumlah catatan produksi per status</small>
</div>
</div>
<div class="card-body">
<div id="chart-status-panen" class="indicator-chart-sm"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Produktivitas Survey (ha/tahun)</h5>
<small class="text-body-secondary">Rata-rata produktivitas per komoditas</small>
</div>
</div>
<div class="card-body">
<div id="chart-survey-produktivitas-ha" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xxl-4 col-lg-6">
<div class="card 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">Produktivitas Survey (pohon/tahun)</h5>
<small class="text-body-secondary">Rata-rata produktivitas per komoditas</small>
</div>
</div>
<div class="card-body">
<div id="chart-survey-produktivitas-pohon" class="indicator-chart"></div>
</div>
</div>
</div>
</div>
</div>
@foreach ([
'indicator-tab-input-produksi' => 'Input Produksi',
'indicator-tab-panen' => 'Panen dan Pascapanen',
'indicator-tab-akses-pasar' => 'Akses Pasar',
] as $tabId => $tabLabel)
<div class="tab-pane fade" id="{{ $tabId }}" role="tabpanel">
<div class="card">
<div class="card-body text-center text-body-secondary py-5">
Belum ada grafik untuk {{ $tabLabel }}.
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
let chartData = @json($chartData);
const dashboardUrl = @json(route('dashboard.indikator-pertanian'));
const dashboardDataUrl = @json(route('dashboard.indikator-pertanian.data'));
const filterOptionsUrl = `${dashboardDataUrl}?mode=filter-options`;
const kecamatanUrlBase = @json(url('/kecamatanbykabupatenkota'));
const desaUrlBase = @json(url('/desakelurahanbykecamatan'));
const initialFilters = @json($initialFilters);
const filters = { ...initialFilters };
const filterSelects = document.querySelectorAll('.indicator-filter-select');
const filterBar = document.getElementById('indicator-filter-bar');
const kabupatenFilter = document.querySelector('[data-filter-key="kabupaten"]');
const kecamatanSelect = document.querySelector('[data-filter-key="kecamatan"]');
const desaSelect = document.querySelector('[data-filter-key="desa"]');
const komoditasSelect = document.querySelector('[data-filter-key="komoditas"]');
const segmentasiSelect = document.querySelector('[data-filter-key="segmentasi"]');
const selectedKabupaten = kabupatenFilter?.dataset.filterValue || initialFilters.kabupaten || 'all';
const applyFilterButton = document.getElementById('indicator-apply-filter');
const resetButton = document.getElementById('indicator-reset-filter');
const exportButton = document.getElementById('indicator-export-data');
const chartInstances = {};
let activeRequest = null;
let lahanChartsDirty = false;
let productionChartsRendered = false;
function buildFilterParams() {
const params = new URLSearchParams();
['kecamatan', 'desa', 'komoditas', 'segmentasi'].forEach(function (key) {
if (filters[key] && filters[key] !== 'all') {
params.set(key, filters[key]);
}
});
return params;
}
function updateAddressBar() {
const params = buildFilterParams();
const nextUrl = params.toString()
? dashboardUrl + '?' + params.toString()
: dashboardUrl;
window.history.replaceState({}, '', nextUrl);
}
function setLoadingState(isLoading) {
filterBar?.classList.toggle('is-loading', isLoading);
filterSelects.forEach(function (select) {
if (select.dataset.filterKey === 'desa') {
select.disabled = isLoading || filters.kecamatan === 'all';
return;
}
select.disabled = isLoading;
});
if (resetButton) {
resetButton.disabled = isLoading;
}
if (applyFilterButton) {
applyFilterButton.disabled = isLoading;
}
if (exportButton) {
exportButton.disabled = isLoading;
}
}
async function fetchJson(url) {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
});
if (!response.ok) {
throw new Error(`HTTP Error ${response.status}`);
}
return response.json();
}
function normalizeOptions(items, allLabel) {
return [
{ value: 'all', label: allLabel },
...items
.filter(function (item) {
return String(item.value ?? item.id ?? '') !== 'all';
})
.map(function (item) {
return {
value: String(item.value ?? item.id),
label: item.label ?? item.nama ?? '-',
};
}),
];
}
function setSelectOptions(select, options, selectedValue) {
if (!select) {
return;
}
select.innerHTML = '';
options.forEach(function (option) {
select.add(new Option(option.label, option.value));
});
select.value = options.some(function (option) {
return option.value === selectedValue;
}) ? selectedValue : 'all';
}
async function loadKecamatanOptions(selectedValue = filters.kecamatan) {
if (!kecamatanSelect || selectedKabupaten === 'all') {
return;
}
kecamatanSelect.disabled = true;
try {
const data = await fetchJson(`${kecamatanUrlBase}/${selectedKabupaten}`);
const options = normalizeOptions(data, 'Semua Kecamatan');
setSelectOptions(kecamatanSelect, options, selectedValue);
filters.kecamatan = kecamatanSelect.value;
} catch (error) {
console.error('Gagal memuat kecamatan:', error);
} finally {
kecamatanSelect.disabled = false;
}
}
async function loadDesaOptions(selectedValue = filters.desa) {
if (!desaSelect) {
return;
}
if (!filters.kecamatan || filters.kecamatan === 'all') {
setSelectOptions(desaSelect, normalizeOptions([], 'Semua Desa'), 'all');
desaSelect.disabled = true;
filters.desa = 'all';
return;
}
desaSelect.disabled = true;
try {
const data = await fetchJson(`${desaUrlBase}/${filters.kecamatan}`);
const options = normalizeOptions(data, 'Semua Desa');
setSelectOptions(desaSelect, options, selectedValue);
filters.desa = desaSelect.value;
} catch (error) {
console.error('Gagal memuat desa:', error);
} finally {
desaSelect.disabled = false;
}
}
async function loadDashboardFilterOptions() {
try {
const data = await fetchJson(filterOptionsUrl);
setSelectOptions(
komoditasSelect,
normalizeOptions(data.komoditas || [], 'Semua Komoditas'),
filters.komoditas
);
filters.komoditas = komoditasSelect?.value || 'all';
setSelectOptions(
segmentasiSelect,
normalizeOptions(data.segmentasi || [], 'Semua Segmentasi'),
filters.segmentasi
);
filters.segmentasi = segmentasiSelect?.value || 'all';
} catch (error) {
console.error('Gagal memuat opsi filter indikator:', error);
}
}
async function loadInitialFilterOptions() {
await Promise.all([
loadKecamatanOptions(filters.kecamatan),
loadDashboardFilterOptions(),
]);
await loadDesaOptions(filters.desa);
}
function updateSummaryCards(summaryData) {
document.querySelectorAll('[data-summary-card]').forEach(function (card) {
const summary = summaryData[Number(card.dataset.summaryCard)];
if (!summary) {
return;
}
const value = card.querySelector('[data-summary-value]');
const label = card.querySelector('[data-summary-label]');
const description = card.querySelector('[data-summary-description]');
if (value) {
value.textContent = summary.value;
}
if (label) {
label.textContent = summary.label;
}
if (description) {
description.textContent = summary.description;
}
});
}
async function loadIndicatorData() {
const params = buildFilterParams();
if (activeRequest) {
activeRequest.abort();
}
const requestController = new AbortController();
activeRequest = requestController;
setLoadingState(true);
try {
const response = await fetch(`${dashboardDataUrl}?${params.toString()}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
signal: requestController.signal,
});
if (!response.ok) {
throw new Error(`HTTP Error ${response.status}`);
}
const result = await response.json();
Object.assign(filters, result.filters || {});
if (kecamatanSelect && kecamatanSelect.value !== filters.kecamatan) {
kecamatanSelect.value = filters.kecamatan;
}
filterSelects.forEach(function (select) {
const key = select.dataset.filterKey;
if (filters[key] !== undefined) {
select.value = filters[key];
}
});
chartData = result.chartData || {};
updateSummaryCards(result.summaryData || []);
productionChartsRendered = false;
if (
typeof renderLahanCharts === 'function'
&& document.querySelector('#indicator-tab-lahan-tanaman')?.classList.contains('active')
) {
renderLahanCharts();
} else {
lahanChartsDirty = true;
}
if (
typeof renderProductionCharts === 'function'
&& document.querySelector('#indicator-tab-produksi')?.classList.contains('active')
) {
renderProductionCharts();
}
updateAddressBar();
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Gagal memuat indikator pertanian:', error);
}
} finally {
if (activeRequest === requestController) {
activeRequest = null;
setLoadingState(false);
}
}
}
filterSelects.forEach(function (select) {
select.addEventListener('change', async function (event) {
const key = event.target.dataset.filterKey;
filters[key] = event.target.value;
if (key === 'kecamatan') {
filters.desa = 'all';
await loadDesaOptions('all');
}
});
});
applyFilterButton?.addEventListener('click', function () {
loadIndicatorData();
});
resetButton?.addEventListener('click', async function () {
Object.assign(filters, {
kecamatan: 'all',
desa: 'all',
komoditas: 'all',
segmentasi: 'all',
});
filterSelects.forEach(function (select) {
select.value = 'all';
});
await loadDesaOptions('all');
loadIndicatorData();
});
exportButton?.addEventListener('click', function () {});
if (typeof ApexCharts === 'undefined') {
return;
}
const appConfig = typeof config !== 'undefined' ? config : {};
const headingColor = appConfig.colors?.headingColor || '#5d596c';
const legendColor = appConfig.colors?.bodyColor || '#6f6b7d';
const fontFamily = appConfig.fontFamily || 'Public Sans';
const chartColors = {
primary: appConfig.colors?.primary || '#7367f0',
success: appConfig.colors?.success || '#28c76f',
warning: appConfig.colors?.warning || '#ff9f43',
info: appConfig.colors?.info || '#00cfe8',
secondary: appConfig.colors?.secondary || '#a8aaae',
labelPrimary: appConfig.colors_label?.primary || 'rgba(115, 103, 240, 0.16)',
labelSuccess: appConfig.colors_label?.success || 'rgba(40, 199, 111, 0.16)',
labelWarning: appConfig.colors_label?.warning || 'rgba(255, 159, 67, 0.16)',
labelInfo: appConfig.colors_label?.info || 'rgba(0, 207, 232, 0.16)',
labelSecondary: appConfig.colors_label?.secondary || 'rgba(168, 170, 174, 0.16)',
body: appConfig.colors?.bodyColor || '#6f6b7d',
muted: appConfig.colors?.textMuted || '#a5a3ae',
heading: appConfig.colors?.headingColor || '#5d596c',
border: appConfig.colors?.borderColor || '#dbdade',
card: appConfig.colors?.cardColor || '#fff',
};
const surveyPalette = [
'#28a745',
'#007bff',
'#fd7e14',
'#dc3545',
'#6f42c1',
'#20c997',
chartColors.primary,
chartColors.info,
chartColors.warning,
];
const shuffleColors = function (colors) {
const shuffled = [...colors];
for (let index = shuffled.length - 1; index > 0; index -= 1) {
const randomIndex = Math.floor(Math.random() * (index + 1));
[shuffled[index], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[index]];
}
return shuffled;
};
const colorSet = shuffleColors(surveyPalette);
const colorsFor = function (offset = 0) {
return [
...colorSet.slice(offset),
...colorSet.slice(0, offset),
];
};
function hasSeries(data) {
return Array.isArray(data?.series)
&& data.series.length > 0
&& data.series.some(function (value) {
return Number(value) > 0;
});
}
function formatNumber(value, digits = 0) {
return Number(value || 0).toLocaleString('id-ID', {
maximumFractionDigits: digits,
minimumFractionDigits: digits,
});
}
function wrapAxisLabel(value, maxCharacters = 13) {
const words = String(value || '').split(/\s+/).filter(Boolean);
const lines = [];
let currentLine = '';
words.forEach(function (word) {
const nextLine = currentLine ? currentLine + ' ' + word : word;
if (currentLine && nextLine.length > maxCharacters) {
lines.push(currentLine);
currentLine = word;
return;
}
currentLine = nextLine;
});
if (currentLine) {
lines.push(currentLine);
}
return lines.length > 1 ? lines : (lines[0] || '');
}
function renderDonut(selector, data, colors, options = {}) {
const element = document.querySelector(selector);
if (!element) {
return;
}
if (chartInstances[selector]) {
chartInstances[selector].destroy();
delete chartInstances[selector];
}
element.innerHTML = '';
if (!hasSeries(data)) {
element.innerHTML = '<div class="text-center text-body-secondary py-5">Belum ada data.</div>';
return;
}
const total = data.series.reduce(function (sum, value) {
return sum + Number(value || 0);
}, 0);
const unit = options.unit || '';
const valueDigits = options.valueDigits || 0;
chartInstances[selector] = new ApexCharts(element, {
chart: {
type: 'donut',
height: options.height || 330,
parentHeightOffset: 0,
toolbar: { show: false },
},
labels: data.labels,
series: data.series,
colors: colors,
stroke: { width: 0 },
dataLabels: { enabled: false },
legend: {
show: true,
position: 'bottom',
offsetY: 10,
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 400,
labels: {
colors: legendColor,
useSeriesColors: false,
},
markers: {
size: 4,
strokeWidth: 0,
},
itemMargin: {
horizontal: 12,
vertical: 5,
},
formatter: function (seriesName, chartOptions) {
const value = Number(chartOptions.w.globals.series[chartOptions.seriesIndex] || 0);
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
return `${seriesName}: ${formatNumber(value, valueDigits)}${unit ? ' ' + unit : ''} (${percentage}%)`;
},
},
tooltip: {
enabled: true,
followCursor: true,
custom: function ({ series, seriesIndex, w }) {
const value = Number(series[seriesIndex] || 0);
const label = w.globals.labels[seriesIndex];
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
const color = w.globals.colors[seriesIndex];
return `
<div
style="
background: ${color};
color: #fff;
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}:&nbsp;
<strong>
${formatNumber(value, valueDigits)}${unit ? ' ' + unit : ''}
(${percentage}%)
</strong>
</div>
`;
},
},
plotOptions: {
pie: {
donut: {
size: options.donutSize || '68%',
labels: {
show: true,
value: {
fontSize: options.valueFontSize || '30px',
fontFamily: fontFamily,
color: headingColor,
fontWeight: 500,
offsetY: -15,
formatter: function (value) {
return formatNumber(value, valueDigits);
},
},
name: {
offsetY: 25,
fontFamily: fontFamily,
},
total: {
show: true,
showAlways: true,
fontSize: '13px',
fontFamily: fontFamily,
color: legendColor,
label: options.totalLabel || 'Total',
formatter: function () {
return formatNumber(total, valueDigits);
},
},
},
},
},
},
responsive: [
{
breakpoint: 1400,
options: {
chart: { height: 350 },
legend: { position: 'bottom' },
},
},
{
breakpoint: 576,
options: {
chart: { height: 330 },
legend: { position: 'bottom' },
},
},
],
states: {
hover: {
filter: {
type: 'lighten',
value: 0.08,
},
},
active: {
filter: {
type: 'darken',
value: 0.08,
},
},
},
});
chartInstances[selector].render();
}
function renderBar(selector, data, options = {}) {
const element = document.querySelector(selector);
if (!element) {
return;
}
if (chartInstances[selector]) {
chartInstances[selector].destroy();
delete chartInstances[selector];
}
element.innerHTML = '';
if (!hasSeries(data)) {
element.innerHTML = '<div class="text-center text-body-secondary py-5">Belum ada data.</div>';
return;
}
const total = data.series.reduce(function (sum, value) {
return sum + Number(value || 0);
}, 0);
const valueDigits = options.valueDigits ?? 0;
const suffix = options.suffix || '';
const isHorizontal = options.horizontal || false;
const wrappedLabels = data.labels.map(function (label) {
return wrapAxisLabel(label, options.labelLineLength || 13);
});
const maxLabelLines = wrappedLabels.reduce(function (maximum, label) {
return Math.max(maximum, Array.isArray(label) ? label.length : 1);
}, 1);
const labelMaxHeight = Math.max(82, Math.min(maxLabelLines * 18, 160));
const chartHeight = isHorizontal
? options.height || 330
: (options.height || 330) + Math.max(maxLabelLines - 3, 0) * 16;
chartInstances[selector] = new ApexCharts(element, {
chart: {
type: 'bar',
height: chartHeight,
parentHeightOffset: 0,
toolbar: { show: false },
},
series: [{
name: options.name || 'Total',
data: data.series,
}],
colors: options.colors || colorsFor(options.colorOffset || 0),
plotOptions: {
bar: {
borderRadius: 4,
borderRadiusApplication: 'around',
horizontal: isHorizontal,
columnWidth: options.columnWidth || '44%',
barHeight: options.barHeight || '50%',
distributed: true,
dataLabels: {
position: 'top',
},
},
},
dataLabels: {
enabled: true,
textAnchor: isHorizontal ? 'start' : 'middle',
offsetX: isHorizontal ? 6 : 0,
offsetY: isHorizontal ? 0 : -18,
style: {
fontSize: '12px',
fontFamily: fontFamily,
fontWeight: 500,
colors: [headingColor],
},
formatter: function (value) {
const percentage = total > 0 ? ((Number(value || 0) / total) * 100).toFixed(1) : '0.0';
return `${formatNumber(value, valueDigits)}${suffix ? ' ' + suffix : ''} (${percentage}%)`;
},
},
grid: {
borderColor: chartColors.border,
strokeDashArray: 5,
xaxis: { lines: { show: isHorizontal } },
yaxis: { lines: { show: !isHorizontal } },
padding: {
top: 18,
right: isHorizontal ? 55 : 16,
left: 8,
bottom: isHorizontal ? 0 : Math.max(maxLabelLines - 2, 0) * 8,
},
},
xaxis: {
categories: isHorizontal ? data.labels : wrappedLabels,
axisBorder: { show: false },
axisTicks: { show: false },
labels: {
rotate: 0,
rotateAlways: false,
trim: false,
hideOverlappingLabels: false,
maxHeight: labelMaxHeight,
style: {
colors: legendColor,
fontSize: '12px',
fontFamily: fontFamily,
},
formatter: function (value) {
if (isHorizontal && valueDigits === 0 && !Number.isInteger(Number(value))) {
return '';
}
return isHorizontal ? formatNumber(value, valueDigits) : value;
},
},
tooltip: { enabled: false },
title: {
text: isHorizontal ? suffix || options.name || '' : '',
style: {
color: legendColor,
fontSize: '12px',
fontFamily: fontFamily,
fontWeight: 400,
},
},
},
yaxis: {
labels: {
maxWidth: isHorizontal ? options.labelWidth || 180 : undefined,
style: {
colors: legendColor,
fontSize: '12px',
fontFamily: fontFamily,
},
formatter: function (value) {
return isHorizontal ? value : formatNumber(value, valueDigits);
},
},
title: {
text: isHorizontal ? '' : suffix || options.name || '',
style: {
color: legendColor,
fontSize: '12px',
fontFamily: fontFamily,
fontWeight: 400,
},
},
},
legend: { show: false },
tooltip: {
enabled: true,
followCursor: true,
intersect: false,
shared: false,
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
const value = Number(series[seriesIndex][dataPointIndex] || 0);
const label = data.labels[dataPointIndex] || w.globals.labels[dataPointIndex];
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
const color = w.globals.colors[dataPointIndex] || options.color || chartColors.primary;
return `
<div
style="
background: ${color};
color: #fff;
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}:&nbsp;
<strong>
${formatNumber(value, valueDigits)}${suffix ? ' ' + suffix : ''}
(${percentage}%)
</strong>
</div>
`;
},
},
states: {
hover: {
filter: {
type: 'lighten',
value: 0.08,
},
},
active: {
filter: {
type: 'darken',
value: 0.08,
},
},
},
});
chartInstances[selector].render();
}
function renderLahanCharts() {
lahanChartsDirty = false;
renderDonut(
'#chart-petani-komoditas',
chartData.petaniPerKomoditas,
colorsFor(0),
{
totalLabel: 'Petani',
unit: 'petani',
valueDigits: 0,
}
);
renderBar('#chart-luas-kecamatan', chartData.luasLahanPerKecamatan, {
name: 'Luas lahan',
suffix: 'ha',
colors: colorsFor(1),
height: 330,
horizontal: true,
labelWidth: 150,
valueDigits: 2,
});
renderDonut(
'#chart-kondisi-tanaman',
chartData.kondisiTanaman,
colorsFor(2),
{
totalLabel: 'Data',
valueDigits: 0,
}
);
renderDonut(
'#chart-status-kepemilikan-lahan',
chartData.statusKepemilikanLahan,
colorsFor(3),
{
totalLabel: 'Lahan',
unit: 'lahan',
valueDigits: 0,
}
);
renderBar('#chart-serangan-hama', chartData.seranganHama, {
name: 'Data',
colors: colorsFor(4),
height: 330,
valueDigits: 0,
});
renderBar('#chart-penyakit-tanaman', chartData.penyakitTanaman, {
name: 'Data',
colors: colorsFor(5),
height: 330,
valueDigits: 0,
});
renderBar('#chart-keparahan-serangan', chartData.keparahanSerangan, {
name: 'Data',
colors: colorsFor(6),
height: 330,
valueDigits: 0,
});
renderBar('#chart-status-hak-kelola-lahan', chartData.statusHakKelolaLahan, {
name: 'Petani',
colors: colorsFor(7),
height: 330,
horizontal: true,
labelWidth: 180,
valueDigits: 0,
});
renderBar('#chart-praktik-pemupukan', chartData.praktikPemupukan, {
name: 'Petani',
colors: colorsFor(8),
height: 330,
horizontal: true,
labelWidth: 180,
valueDigits: 0,
});
}
function renderProductionCharts() {
if (productionChartsRendered) {
return;
}
productionChartsRendered = true;
renderBar('#chart-produksi-komoditas', chartData.produksiPerKomoditas, {
name: 'Produksi',
suffix: 'kg',
colors: colorsFor(0),
height: 330,
horizontal: true,
labelWidth: 150,
valueDigits: 0,
});
renderBar('#chart-produktivitas-komoditas', chartData.produktivitasPerKomoditas, {
name: 'Produktivitas',
suffix: 'kg/ha',
colors: colorsFor(2),
height: 330,
horizontal: true,
labelWidth: 150,
valueDigits: 2,
});
renderBar('#chart-luas-panen-komoditas', chartData.luasPanenPerKomoditas, {
name: 'Luas panen',
suffix: 'ha',
colors: colorsFor(4),
height: 330,
horizontal: true,
labelWidth: 150,
valueDigits: 2,
});
renderDonut(
'#chart-status-panen',
chartData.statusPanen,
colorsFor(6),
{
totalLabel: 'Catatan',
valueDigits: 0,
}
);
renderBar('#chart-survey-produktivitas-ha', chartData.surveyProduktivitasHa, {
name: 'Produktivitas',
suffix: 'ha/tahun',
colors: colorsFor(1),
height: 330,
horizontal: true,
labelWidth: 150,
valueDigits: 2,
});
renderBar('#chart-survey-produktivitas-pohon', chartData.surveyProduktivitasPohon, {
name: 'Produktivitas',
suffix: 'pohon/tahun',
colors: colorsFor(3),
height: 330,
horizontal: true,
labelWidth: 150,
valueDigits: 2,
});
}
renderLahanCharts();
loadInitialFilterOptions();
document
.querySelector('[data-bs-target="#indicator-tab-lahan-tanaman"]')
?.addEventListener('shown.bs.tab', function () {
if (lahanChartsDirty) {
renderLahanCharts();
}
});
document
.querySelector('[data-bs-target="#indicator-tab-produksi"]')
?.addEventListener('shown.bs.tab', renderProductionCharts);
if (document.querySelector('#indicator-tab-produksi')?.classList.contains('active')) {
renderProductionCharts();
}
});
</script>
@endpush