Files
database-pertani-web/resources/views/dashboard/indikator-pertanian.blade.php
T
2026-10-16 10:29:42 +07:00

2569 lines
100 KiB
PHP

@extends('layout.main')
@push('style')
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<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(111, 214, 143, .65);
box-shadow: 0 0 0 .2rem rgba(111, 214, 143, .20);
}
.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: .2rem;
margin-bottom: .25rem !important;
overflow-x: auto;
scrollbar-width: none;
}
.indicator-tab-content {
padding-top: 0 !important;
}
.indicator-tab-pane-grid {
margin-top: .75rem !important;
}
.indicator-tabs::-webkit-scrollbar {
display: none;
}
.indicator-tabs .nav-item {
flex: 0 0 auto;
}
.indicator-tabs .nav-link {
align-items: center;
display: flex;
justify-content: center;
min-height: 38px;
padding-left: .75rem;
padding-right: .75rem;
white-space: nowrap;
width: auto;
}
.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;
}
.indicator-overview-card .avatar {
flex: 0 0 auto;
}
.indicator-overview-card-value {
color: var(--bs-heading-color);
font-size: 1.35rem;
font-weight: 700;
line-height: 1.2;
overflow-wrap: anywhere;
}
.indicator-commodity-list {
display: grid;
gap: .75rem;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.indicator-commodity-chip {
align-items: center;
background: rgba(111, 214, 143, .20);
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
display: flex;
gap: .75rem;
min-height: 58px;
padding: .75rem;
}
.indicator-commodity-icon {
align-items: center;
background-color: rgba(111, 214, 143, .24);
border-radius: var(--bs-border-radius);
color: var(--bs-success);
display: inline-flex;
flex: 0 0 36px;
height: 36px;
justify-content: center;
width: 36px;
}
.indicator-commodity-name {
color: var(--bs-heading-color);
font-weight: 600;
line-height: 1.2;
}
.indicator-commodity-count {
color: var(--bs-heading-color);
font-size: 1.2rem;
font-weight: 600;
line-height: 1.1;
}
.indicator-map {
background: var(--bs-paper-bg);
border-radius: var(--bs-border-radius);
min-height: 520px;
overflow: hidden;
position: relative;
width: 100%;
}
.indicator-map-back {
display: none;
left: 50px;
position: absolute;
top: 20px;
z-index: 1000;
}
.leaflet-tooltip.leaflet-modern-tooltip {
background-color: transparent;
border: none;
box-shadow: none;
color: #333;
font-family: 'Public Sans', sans-serif;
padding: 0;
}
.leaflet-modern-tooltip:before {
border: none !important;
}
.modern-map-card {
background: #fff;
border: 1px solid rgba(0, 0, 0, .05);
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, .12);
padding: 16px;
width: 190px;
}
.card-header-region {
margin-bottom: 8px;
}
.region-label {
color: #888;
display: block;
font-size: 11px;
font-weight: 500;
letter-spacing: .5px;
margin-bottom: 2px;
text-transform: uppercase;
}
.region-name {
color: #1e1e1e;
font-size: 15px;
font-weight: 700;
line-height: 1.2;
margin: 0;
}
.stat-icon {
align-items: center;
border-radius: 8px;
display: flex;
font-size: 16px;
height: 32px;
justify-content: center;
width: 32px;
}
.bg-success-light {
background-color: rgba(104, 193, 124, .20);
}
.bg-primary-light {
background-color: rgba(95, 199, 214, .20);
}
.stat-value {
color: #222;
font-size: 14px;
line-height: 1;
margin-top: 2px;
}
.action-prompt {
font-size: 12px;
}
.leaflet-modern-tooltip:hover .action-prompt i {
transform: translateX(3px);
}
.leaflet-popup-wrapper.leaflet-modern-popup-wrapper {
box-shadow: none;
}
.leaflet-modern-popup-wrapper .leaflet-popup-content-wrapper {
border: 1px solid rgba(0, 0, 0, .05);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, .12);
overflow: hidden;
padding: 0 !important;
}
.leaflet-modern-popup-wrapper .leaflet-popup-content {
margin: 0 !important;
padding: 0 !important;
}
.leaflet-modern-popup-wrapper .leaflet-popup-tip {
background: #f8f9fa;
box-shadow: 0 1px 3px rgba(0, 0, 0, .05);
}
.modern-lahan-popup {
font-family: 'Public Sans', sans-serif;
min-width: 270px;
}
.modern-lahan-popup .h5 {
font-size: 16px;
margin: 0;
}
.modern-lahan-popup small {
font-size: 11px;
}
.header-farmer {
border-bottom: 1px solid rgba(0, 0, 0, .06);
}
.rounded-top-12 {
border-radius: 12px 12px 0 0;
}
.rounded-bottom-12 {
border-radius: 0 0 12px 12px;
}
.details-body {
background-color: #f8f9fa;
}
.label-header {
letter-spacing: .5px;
}
.farmer-name {
line-height: 1.2;
}
.icon-stat-container {
align-items: center;
display: flex;
height: 36px;
justify-content: center;
width: 36px;
}
.label-detail {
margin-bottom: 0;
}
.value-detail {
font-size: 14px;
line-height: 1.1;
margin-top: 1px;
}
.value-commodity {
font-size: 13px;
line-height: 1.2;
}
.indicator-map-empty {
align-items: center;
color: var(--bs-secondary-color);
display: flex;
justify-content: center;
min-height: 390px;
text-align: center;
}
.indicator-agro-list {
display: grid;
gap: .75rem;
}
.indicator-agro-item {
align-items: center;
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
display: flex;
gap: .75rem;
min-height: 58px;
padding: .75rem;
}
.indicator-agro-icon {
align-items: center;
border-radius: var(--bs-border-radius);
display: inline-flex;
flex: 0 0 36px;
height: 36px;
justify-content: center;
width: 36px;
}
.indicator-agro-value {
color: var(--bs-heading-color);
font-weight: 600;
line-height: 1.25;
}
.indicator-agro-value-list {
display: grid;
gap: .25rem;
justify-items: end;
margin: 0;
padding: 0;
max-width: 420px;
}
.indicator-agro-value-list li {
list-style: none;
white-space: normal;
overflow-wrap: anywhere;
}
.indicator-agro-value-list li::before {
color: var(--bs-success);
content: "•";
font-weight: 700;
margin-right: .35rem;
}
@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: auto;
}
.indicator-commodity-list {
grid-template-columns: 1fr;
}
.indicator-agro-item {
align-items: flex-start;
}
.indicator-agro-value {
text-align: left !important;
}
.indicator-agro-value-list {
justify-items: start;
}
}
</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="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>
Ringkasan Pertanian
</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>
Profil Lahan &amp; Agroekologi
</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>
Budidaya &amp; Kesehatan Tanaman
</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>
Produksi, Panen &amp; Mutu
</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>
Rantai Nilai &amp; 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="indicator-tab-pane-grid">
<div class="row g-6">
@foreach (array_slice($overviewData['kpiCards'], 0, 4) as $card)
<div class="col-xl-3 col-sm-6" data-overview-card="{{ $loop->index }}">
<div class="card card-border-shadow-{{ $card['variant'] }} h-100 indicator-overview-card">
<div class="card-body">
<div class="d-flex align-items-center gap-3 mb-2">
<div class="avatar">
<span class="avatar-initial rounded bg-label-{{ $card['variant'] }}">
<i class="icon-base ti {{ $card['icon'] }} icon-26px" data-overview-icon></i>
</span>
</div>
<div class="indicator-overview-card-value" data-overview-value>{{ $card['value'] }}</div>
</div>
<p class="mb-1" data-overview-label>{{ $card['label'] }}</p>
<p class="mb-0">
<small class="text-body-secondary" data-overview-description>{{ $card['description'] }}</small>
</p>
</div>
</div>
</div>
@endforeach
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Komoditas yang Dibudidayakan</h5>
<small class="text-body-secondary">Fokus komoditas utama program</small>
</div>
</div>
<div class="card-body">
<div class="indicator-commodity-list" id="indicator-commodity-focus-list">
@foreach ($overviewData['commodityFocus'] as $commodity)
<span class="indicator-commodity-chip">
<span class="indicator-commodity-icon">
<i class="icon-base ti tabler-plant-2 icon-22px"></i>
</span>
<span class="flex-grow-1">
<span class="indicator-commodity-name d-block">{{ $commodity['label'] }}</span>
<small class="text-body-secondary">Komoditas budidaya</small>
</span>
<span class="text-end">
<span class="indicator-commodity-count d-block">{{ number_format($commodity['total'], 0, ',', '.') }}</span>
<small class="text-body-secondary">lahan</small>
</span>
</span>
@endforeach
</div>
</div>
</div>
</div>
</div>
<div class="row g-6 mt-4">
@foreach (array_slice($overviewData['kpiCards'], 4) as $card)
<div class="col-xl-3 col-sm-6" data-overview-card="{{ $loop->index + 4 }}">
<div class="card card-border-shadow-{{ $card['variant'] }} h-100 indicator-overview-card">
<div class="card-body">
<div class="d-flex align-items-center gap-3 mb-2">
<div class="avatar">
<span class="avatar-initial rounded bg-label-{{ $card['variant'] }}">
<i class="icon-base ti {{ $card['icon'] }} icon-26px" data-overview-icon></i>
</span>
</div>
<div class="indicator-overview-card-value" data-overview-value>{{ $card['value'] }}</div>
</div>
<p class="mb-1" data-overview-label>{{ $card['label'] }}</p>
<p class="mb-0">
<small class="text-body-secondary" data-overview-description>{{ $card['description'] }}</small>
</p>
</div>
</div>
</div>
@endforeach
</div>
<div class="row g-6 mt-4">
<div class="col-xl-8">
<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">Peta Sebaran Kebun</h5>
<small class="text-body-secondary">Titik dan polygon kebun/lahan terdata</small>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary active indicator-map-layer-btn" data-map-target="farm" data-map-layer="satellite">Satellite</button>
<button type="button" class="btn btn-outline-secondary indicator-map-layer-btn" data-map-target="farm" data-map-layer="topo">Topographic</button>
<button type="button" class="btn btn-outline-secondary indicator-map-layer-btn" data-map-target="farm" data-map-layer="parcels">Parcels</button>
</div>
</div>
<div class="card-body">
<div id="indicator-farm-map" class="indicator-map">
<button type="button" id="indicator-farm-map-back" class="btn btn-sm btn-dark shadow indicator-map-back">
<i class="icon-base ti tabler-arrow-left me-1"></i>
Kembali ke Atas
</button>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<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">Komposisi 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>
</div>
</div>
<div class="tab-pane fade" id="indicator-tab-produksi" role="tabpanel">
<div class="indicator-tab-pane-grid">
<div class="row g-6">
@foreach ($landProfileData['conditionCards'] as $card)
<div class="col-xl-3 col-sm-6" data-land-condition-card="{{ $loop->index }}">
<div class="card card-border-shadow-{{ $card['variant'] }} h-100 indicator-overview-card">
<div class="card-body">
<div class="d-flex align-items-center gap-3 mb-2">
<div class="avatar">
<span class="avatar-initial rounded bg-label-{{ $card['variant'] }}">
<i class="icon-base ti {{ $card['icon'] }} icon-26px" data-land-condition-icon></i>
</span>
</div>
<div class="indicator-overview-card-value" data-land-condition-value>{{ $card['value'] }}</div>
</div>
<p class="mb-1" data-land-condition-label>{{ $card['label'] }}</p>
<p class="mb-0">
<small class="text-body-secondary" data-land-condition-description>{{ $card['description'] }}</small>
</p>
</div>
</div>
</div>
@endforeach
</div>
<div class="row g-6 mt-4">
<div class="col-xl-9">
<div class="card h-100">
<div class="card-header">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Kondisi Agroekologi</h5>
<small class="text-body-secondary">Parameter lokasi dan lingkungan lahan</small>
</div>
</div>
<div class="card-body">
<div class="indicator-agro-list" id="indicator-agroecology-list">
@foreach ($landProfileData['agroecologyItems'] as $item)
<div class="indicator-agro-item">
<span class="indicator-agro-icon bg-label-{{ $item['status'] === 'available' ? 'success' : 'secondary' }}">
<i class="icon-base ti {{ $item['status'] === 'available' ? 'tabler-droplet' : 'tabler-database-off' }} icon-22px"></i>
</span>
<span class="flex-grow-1">
<span class="indicator-commodity-name d-block">{{ $item['label'] }}</span>
<small class="text-body-secondary">{{ $item['status'] === 'available' ? 'Data tersedia' : 'Field belum tersedia' }}</small>
</span>
<span class="indicator-agro-value text-end">
@php
$agroValueItems = collect(explode(', ', $item['value']))
->filter()
->values();
@endphp
@if ($agroValueItems->count() > 1)
<ul class="indicator-agro-value-list">
@foreach ($agroValueItems as $valueItem)
<li>{{ $valueItem }}</li>
@endforeach
</ul>
@else
{{ $item['value'] }}
@endif
</span>
</div>
@endforeach
</div>
</div>
</div>
</div>
<div class="col-xl-3">
<div class="row g-6">
@foreach ($landProfileData['agroforestryCards'] as $card)
<div class="col-12" data-agroforestry-card="{{ $loop->index }}">
<div class="card card-border-shadow-{{ $card['variant'] }} h-100 indicator-overview-card">
<div class="card-body">
<div class="d-flex align-items-center gap-3 mb-2">
<div class="avatar">
<span class="avatar-initial rounded bg-label-{{ $card['variant'] }}">
<i class="icon-base ti {{ $card['icon'] }} icon-26px" data-agroforestry-icon></i>
</span>
</div>
<div class="indicator-overview-card-value" data-agroforestry-value>{{ $card['value'] }}</div>
</div>
<p class="mb-1" data-agroforestry-label>{{ $card['label'] }}</p>
<p class="mb-0">
<small class="text-body-secondary" data-agroforestry-description>{{ $card['description'] }}</small>
</p>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
<div class="row g-6 mt-4">
<div class="col-xl-8">
<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">GIS Map dan Layer Kebun</h5>
<small class="text-body-secondary">Sebaran spasial kebun/lahan terdata</small>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary active indicator-map-layer-btn" data-map-target="landProfile" data-map-layer="satellite">Satellite</button>
<button type="button" class="btn btn-outline-secondary indicator-map-layer-btn" data-map-target="landProfile" data-map-layer="topo">Topographic</button>
<button type="button" class="btn btn-outline-secondary indicator-map-layer-btn" data-map-target="landProfile" data-map-layer="parcels">Parcels</button>
</div>
</div>
<div class="card-body">
<div id="indicator-land-profile-map" class="indicator-map">
<button type="button" id="indicator-land-profile-map-back" class="btn btn-sm btn-dark shadow indicator-map-back">
<i class="icon-base ti tabler-arrow-left me-1"></i>
Kembali ke Atas
</button>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<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 Kondisi Lahan</h5>
<small class="text-body-secondary">Status kepemilikan lahan terdata</small>
</div>
</div>
<div class="card-body">
<div id="chart-profile-status-lahan" class="indicator-chart-sm"></div>
</div>
</div>
</div>
</div>
<div class="row g-6 mt-4">
<div class="col-xl-4 col-lg-6">
<div class="card h-100">
<div class="card-header">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Luas Lahan per Komoditas</h5>
<small class="text-body-secondary">Akumulasi luas lahan</small>
</div>
</div>
<div class="card-body">
<div id="chart-profile-luas-komoditas" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6">
<div class="card h-100">
<div class="card-header">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Jumlah Tanaman</h5>
<small class="text-body-secondary">Akumulasi per komoditas</small>
</div>
</div>
<div class="card-body">
<div id="chart-profile-tanaman-komoditas" class="indicator-chart"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6">
<div class="card h-100">
<div class="card-header">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Umur Tanaman</h5>
<small class="text-body-secondary">Distribusi umur tanaman</small>
</div>
</div>
<div class="card-body">
<div id="chart-profile-umur-tanaman" class="indicator-chart"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@foreach ([
'indicator-tab-input-produksi' => 'Budidaya & Kesehatan Tanaman',
'indicator-tab-panen' => 'Produksi, Panen & Mutu',
'indicator-tab-akses-pasar' => 'Rantai Nilai & 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 src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
let overviewData = @json($overviewData);
let landProfileData = @json($landProfileData);
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;
let farmMap = null;
let farmMapLayer = null;
let landProfileChartsRendered = false;
let landProfileMap = null;
let landProfileMapLayer = null;
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 renderOverviewData() {
const cards = overviewData.kpiCards || [];
document.querySelectorAll('[data-overview-card]').forEach(function (card) {
const item = cards[Number(card.dataset.overviewCard)];
if (!item) {
return;
}
const value = card.querySelector('[data-overview-value]');
const label = card.querySelector('[data-overview-label]');
const description = card.querySelector('[data-overview-description]');
const icon = card.querySelector('[data-overview-icon]');
if (value) {
value.textContent = item.value;
}
if (label) {
label.textContent = item.label;
}
if (description) {
description.textContent = item.description;
}
if (icon && item.icon) {
icon.className = `icon-base ti ${item.icon} icon-26px`;
}
});
const commodityList = document.getElementById('indicator-commodity-focus-list');
if (commodityList) {
const commodities = overviewData.commodityFocus || [];
commodityList.innerHTML = commodities.map(function (commodity) {
return `
<span class="indicator-commodity-chip">
<span class="indicator-commodity-icon">
<i class="icon-base ti tabler-plant-2 icon-22px"></i>
</span>
<span class="flex-grow-1">
<span class="indicator-commodity-name d-block">${commodity.label}</span>
<small class="text-body-secondary">Komoditas budidaya</small>
</span>
<span class="text-end">
<span class="indicator-commodity-count d-block">${formatNumber(commodity.total || 0, 0)}</span>
<small class="text-body-secondary">lahan</small>
</span>
</span>
`;
}).join('');
}
}
function updateMetricCards(selector, cards, datasetKey, attributePrefix) {
document.querySelectorAll(selector).forEach(function (card) {
const item = cards[Number(card.dataset[datasetKey])];
if (!item) {
return;
}
const value = card.querySelector(`[data-${attributePrefix}-value]`);
const label = card.querySelector(`[data-${attributePrefix}-label]`);
const description = card.querySelector(`[data-${attributePrefix}-description]`);
const icon = card.querySelector(`[data-${attributePrefix}-icon]`);
if (value) {
value.textContent = item.value;
}
if (label) {
label.textContent = item.label;
}
if (description) {
description.textContent = item.description;
}
if (icon && item.icon) {
icon.className = `icon-base ti ${item.icon} icon-26px`;
}
});
}
function renderLandProfileData() {
updateMetricCards('[data-land-condition-card]', landProfileData.conditionCards || [], 'landConditionCard', 'land-condition');
updateMetricCards('[data-agroforestry-card]', landProfileData.agroforestryCards || [], 'agroforestryCard', 'agroforestry');
const agroecologyList = document.getElementById('indicator-agroecology-list');
if (agroecologyList) {
agroecologyList.innerHTML = (landProfileData.agroecologyItems || []).map(function (item) {
const isAvailable = item.status === 'available';
const valueParts = String(item.value || '')
.split(', ')
.filter(Boolean);
const valueMarkup = valueParts.length > 1
? `<ul class="indicator-agro-value-list">${valueParts.map(function (part) {
return `<li>${part}</li>`;
}).join('')}</ul>`
: (item.value || '');
return `
<div class="indicator-agro-item">
<span class="indicator-agro-icon bg-label-${isAvailable ? 'success' : 'secondary'}">
<i class="icon-base ti ${isAvailable ? 'tabler-droplet' : 'tabler-database-off'} icon-22px"></i>
</span>
<span class="flex-grow-1">
<span class="indicator-commodity-name d-block">${item.label}</span>
<small class="text-body-secondary">${isAvailable ? 'Data tersedia' : 'Field belum tersedia'}</small>
</span>
<span class="indicator-agro-value text-end">${valueMarkup}</span>
</div>
`;
}).join('');
}
}
const indicatorMapStates = {};
function ensureLatLon(coords) {
if (Array.isArray(coords) && typeof coords[0] === 'number') {
if (Math.abs(coords[0]) > 90) {
return [coords[1], coords[0]];
}
return [coords[0], coords[1]];
}
return Array.isArray(coords) ? coords.map(ensureLatLon) : coords;
}
function makeLayerFromPolygon(polygon, style) {
if (!polygon || polygon === 'null') {
return null;
}
let geojsonData = polygon;
if (typeof geojsonData === 'string') {
try {
geojsonData = JSON.parse(geojsonData);
} catch (error) {
return null;
}
}
if (!geojsonData || geojsonData === 'null') {
return null;
}
if (geojsonData.type) {
return L.geoJSON(geojsonData, {
style: style,
coordsToLatLng: function (coords) {
const a = coords[0];
const b = coords[1];
if (Math.abs(a) > 90) {
return L.latLng(b, a);
}
if (Math.abs(b) > 90) {
return L.latLng(a, b);
}
return L.latLng(b, a);
},
});
}
if (Array.isArray(geojsonData)) {
return L.polygon(ensureLatLon(geojsonData), style);
}
return null;
}
function initialMapLevel() {
const levels = overviewData.map?.levels || {};
if (filters.desa !== 'all' && (levels.desa || []).length) {
return 'desa';
}
if (filters.kecamatan !== 'all' && (levels.kecamatan || []).length) {
return 'kecamatan';
}
if (filters.kabupaten !== 'all' && (levels.kabupaten || []).length) {
return 'kabupaten';
}
if ((levels.provinsi || []).length) {
return 'provinsi';
}
if ((levels.kecamatan || []).length) {
return 'kecamatan';
}
if ((levels.desa || []).length) {
return 'desa';
}
return 'lahan';
}
function mapNextLevel(level) {
return level === 'provinsi'
? 'kabupaten'
: level === 'kabupaten'
? 'kecamatan'
: level === 'kecamatan'
? 'desa'
: 'lahan';
}
function regionTooltip(item) {
return `
<div class="modern-map-card">
<div class="card-header-region">
<span class="region-label">Wilayah</span>
<h6 class="region-name">${item.nama}</h6>
</div>
<div class="card-body-stats border-top border-bottom py-2 my-2">
<div class="d-flex align-items-center mb-2">
<div class="stat-icon bg-success-light text-success me-2 rounded">
<i class="ti tabler-map"></i>
</div>
<div class="flex-grow-1">
<small class="text-muted text-uppercase fw-medium" style="font-size: 10px;">Jumlah Lahan</small>
<div class="stat-value fw-bold">${formatNumber(item.total_lahan || 0, 0)}</div>
</div>
</div>
<div class="d-flex align-items-center">
<div class="stat-icon bg-primary-light text-primary me-2 rounded">
<i class="ti tabler-area"></i>
</div>
<div class="flex-grow-1">
<small class="text-muted text-uppercase fw-medium" style="font-size: 10px;">Total Luas</small>
<div class="stat-value fw-bold">${formatNumber(item.total_luas || 0, 2)} <small class="text-muted">Ha</small></div>
</div>
</div>
</div>
<div class="text-center mt-2">
<span class="action-prompt text-primary fw-medium small">
Klik untuk detail <i class="ti tabler-chevron-right ms-1"></i>
</span>
</div>
</div>
`;
}
function landPopup(item) {
const listTanaman = item.tanaman && item.tanaman.length ? item.tanaman.join(', ') : '';
const komoditasText = listTanaman || '<span class="text-danger fw-medium small">Belum terdaftar</span>';
const cropIcon = listTanaman ? 'ti tabler-plant' : 'ti tabler-leaf-off';
const cropIconColor = listTanaman ? 'text-success' : 'text-danger';
return `
<div class="modern-lahan-popup">
<div class="p-3 bg-white rounded-top-12 header-farmer">
<small class="label-header text-uppercase text-muted fw-semibold">Pemilik Lahan</small>
<h6 class="mb-0 fw-bold farmer-name h5">${item.nama_petani || 'N/A'}</h6>
</div>
<div class="p-3 bg-light rounded-bottom-12 details-body">
<div class="row g-2 mb-3">
<div class="col-12 d-flex align-items-center">
<div class="icon-stat-container rounded-circle bg-white text-muted border me-2">
<i class="ti tabler-hash fs-5"></i>
</div>
<div>
<small class="d-block text-muted text-uppercase fw-medium label-detail">ID Lahan</small>
<span class="fw-bold text-dark value-detail">TR-${item.id}</span>
</div>
</div>
</div>
<div class="row g-1">
<div class="col-6 d-flex align-items-center">
<div class="icon-stat-container rounded bg-white border me-2">
<i class="${cropIcon} fs-5 ${cropIconColor}"></i>
</div>
<div class="flex-grow-1">
<small class="d-block text-muted text-uppercase fw-medium label-detail">Komoditas</small>
<span class="fw-semibold text-dark value-commodity">${komoditasText}</span>
</div>
</div>
<div class="col-6 d-flex align-items-center border-start ps-2">
<div class="icon-stat-container rounded bg-primary-light me-2">
<i class="ti tabler-dimensions fs-5 text-primary"></i>
</div>
<div>
<small class="d-block text-muted text-uppercase fw-medium label-detail">Luas Lahan</small>
<span class="fw-bold text-dark value-detail">${formatNumber(item.luas || 0, 1)} Ha</span>
</div>
</div>
</div>
</div>
</div>
`;
}
function initIndicatorMap(mapKey, elementId, backButtonId) {
if (indicatorMapStates[mapKey]) {
return indicatorMapStates[mapKey];
}
const element = document.getElementById(elementId);
if (!element || typeof L === 'undefined') {
return null;
}
const layers = {
satellite: L.tileLayer('https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', { maxZoom: 22 }),
topo: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'),
parcels: L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'),
};
const map = L.map(elementId, {
center: [-8.67, 121.0],
zoom: 10,
layers: [layers.satellite],
zoomControl: false,
});
const group = L.featureGroup().addTo(map);
const state = {
activeLayer: 'satellite',
backButton: document.getElementById(backButtonId),
currentLevel: initialMapLevel(),
currentParentId: null,
group,
history: [],
layers,
map,
};
L.control.zoom({ position: 'topleft' }).addTo(map);
state.backButton?.addEventListener('click', function () {
const previous = state.history.pop();
if (previous) {
renderIndicatorMapLevel(mapKey, previous.level, previous.parentId);
if (previous.bounds?.isValid()) {
state.map.fitBounds(previous.bounds, { padding: [30, 30] });
}
}
});
indicatorMapStates[mapKey] = state;
return state;
}
function setIndicatorMapLayer(mapKey, type, button) {
const state = indicatorMapStates[mapKey];
if (!state || !state.layers[type]) {
return;
}
Object.values(state.layers).forEach(function (layer) {
state.map.removeLayer(layer);
});
state.layers[type].addTo(state.map);
state.activeLayer = type;
document
.querySelectorAll(`.indicator-map-layer-btn[data-map-target="${mapKey}"]`)
.forEach(function (item) {
item.classList.toggle('active', item === button);
});
}
function renderIndicatorMapLevel(mapKey, level = null, parentId = null, fallbackBounds = null) {
const state = mapKey === 'landProfile'
? initIndicatorMap('landProfile', 'indicator-land-profile-map', 'indicator-land-profile-map-back')
: initIndicatorMap('farm', 'indicator-farm-map', 'indicator-farm-map-back');
if (!state) {
return;
}
const levels = overviewData.map?.levels || {};
const nextLevel = level || initialMapLevel();
let dataToRender = levels[nextLevel] || [];
if (parentId !== null) {
dataToRender = dataToRender.filter(item => String(item.parent_id) === String(parentId));
}
state.group.clearLayers();
state.currentLevel = nextLevel;
state.currentParentId = parentId;
if (nextLevel === 'lahan' && parentId !== null) {
const parentDesa = (levels.desa || []).find(item => String(item.id) === String(parentId));
const desaLayer = parentDesa
? makeLayerFromPolygon(parentDesa.polygon, {
color: '#F47C86',
dashArray: '5, 8',
fillOpacity: 0,
interactive: false,
weight: 2,
})
: null;
if (desaLayer) {
state.group.addLayer(desaLayer);
}
}
dataToRender.forEach(function (item) {
const layer = makeLayerFromPolygon(item.polygon, nextLevel === 'lahan'
? { color: '#6FD68F', fillColor: '#6FD68F', fillOpacity: .6, weight: 2 }
: { color: '#FFB46F', fillColor: '#FFB46F', fillOpacity: .3, weight: 2 });
if (!layer) {
return;
}
if (nextLevel === 'lahan') {
layer.bindPopup(landPopup(item), {
className: 'leaflet-modern-popup-wrapper',
closeButton: true,
maxWidth: 300,
});
} else {
layer.bindTooltip(regionTooltip(item), {
className: 'leaflet-modern-tooltip',
direction: 'top',
offset: [0, -10],
sticky: true,
});
layer.on('click', function (event) {
L.DomEvent.stopPropagation(event);
const targetLevel = mapNextLevel(nextLevel);
state.history.push({
level: nextLevel,
parentId,
bounds: state.group.getBounds(),
});
renderIndicatorMapLevel(mapKey, targetLevel, item.id, event.target.getBounds());
});
}
state.group.addLayer(layer);
});
if (state.backButton) {
state.backButton.style.display = state.history.length > 0 ? 'block' : 'none';
}
if (state.group.getLayers().length > 0) {
const bounds = state.group.getBounds();
if (bounds.isValid()) {
state.map.fitBounds(bounds, { padding: [30, 30], maxZoom: 18 });
}
} else if (fallbackBounds?.isValid()) {
state.map.fitBounds(fallbackBounds, { padding: [30, 30], maxZoom: 18 });
} else {
state.map.setView([-8.67, 121.0], 10);
}
setTimeout(function () {
state.map.invalidateSize();
}, 150);
}
function resetIndicatorMap(mapKey) {
const state = indicatorMapStates[mapKey];
if (state) {
state.history = [];
renderIndicatorMapLevel(mapKey, initialMapLevel(), null);
}
}
function renderFarmMap() {
resetIndicatorMap('farm');
if (!indicatorMapStates.farm) {
renderIndicatorMapLevel('farm', initialMapLevel(), null);
}
}
function renderLandProfileMap() {
resetIndicatorMap('landProfile');
if (!indicatorMapStates.landProfile) {
renderIndicatorMapLevel('landProfile', initialMapLevel(), null);
}
}
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];
}
});
overviewData = result.overviewData || {};
landProfileData = result.landProfileData || {};
chartData = result.chartData || {};
Object.values(indicatorMapStates).forEach(function (state) {
state.history = [];
state.currentLevel = initialMapLevel();
state.currentParentId = null;
});
renderOverviewData();
renderFarmMap();
productionChartsRendered = false;
landProfileChartsRendered = false;
renderLandProfileData();
if (
typeof renderLahanCharts === 'function'
&& document.querySelector('#indicator-tab-lahan-tanaman')?.classList.contains('active')
) {
renderLahanCharts();
} else {
lahanChartsDirty = true;
}
if (
typeof renderLandProfileTab === 'function'
&& document.querySelector('#indicator-tab-produksi')?.classList.contains('active')
) {
renderLandProfileTab();
}
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 () {});
document.querySelectorAll('.indicator-map-layer-btn').forEach(function (button) {
button.addEventListener('click', function () {
setIndicatorMapLayer(button.dataset.mapTarget, button.dataset.mapLayer, button);
});
});
renderOverviewData();
renderFarmMap();
renderLandProfileData();
if (typeof ApexCharts === 'undefined') {
return;
}
const appConfig = typeof config !== 'undefined' ? config : {};
const pastelChart = appConfig.chartColors || {};
const pastelSeries = pastelChart.series || ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'];
const pastelSemantic = pastelChart.semantic || {};
const headingColor = appConfig.colors?.headingColor || '#5d596c';
const legendColor = appConfig.colors?.bodyColor || '#6f6b7d';
const fontFamily = appConfig.fontFamily || 'Public Sans';
const chartColors = {
primary: pastelSemantic.primary || '#9D8EF0',
success: pastelSemantic.success || '#6FD68F',
warning: pastelSemantic.warning || '#FFB46F',
info: pastelSemantic.info || '#5FC7D6',
secondary: pastelSemantic.secondary || '#9EA6B0',
labelPrimary: 'rgba(111, 214, 143, 0.22)',
labelSuccess: 'rgba(111, 214, 143, 0.24)',
labelWarning: 'rgba(255, 180, 111, 0.22)',
labelInfo: 'rgba(95, 199, 214, 0.20)',
labelSecondary: 'rgba(179, 181, 188, 0.22)',
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 = [
...pastelSeries,
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,
style: { colors: colors.map(config.chartTextColor) },
dropShadow: { 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: ${config.chartTextColor(color)};
padding: 8px 12px;
border-radius: 5px;
font-size: 13px;
font-weight: 500;
font-family: ${fontFamily};
box-shadow: 0 4px 12px rgba(0,0,0,.15);
"
>
${label}:&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,
});
}
function renderLandProfileTab() {
renderLandProfileData();
renderLandProfileMap();
if (landProfileChartsRendered) {
return;
}
landProfileChartsRendered = true;
renderBar('#chart-profile-luas-komoditas', chartData.luasLahanPerKomoditas, {
name: 'Luas lahan',
suffix: 'ha',
colors: colorsFor(1),
height: 330,
valueDigits: 2,
});
renderDonut(
'#chart-profile-tanaman-komoditas',
chartData.jumlahTanamanPerKomoditas,
colorsFor(3),
{
totalLabel: 'Tanaman',
unit: 'tanaman',
valueDigits: 0,
}
);
renderBar('#chart-profile-umur-tanaman', chartData.umurTanaman, {
name: 'Lahan',
colors: colorsFor(5),
height: 330,
valueDigits: 0,
});
renderDonut(
'#chart-profile-status-lahan',
chartData.statusKepemilikanLahan,
colorsFor(7),
{
totalLabel: 'Lahan',
unit: 'lahan',
valueDigits: 0,
}
);
}
renderLahanCharts();
loadInitialFilterOptions();
document
.querySelector('[data-bs-target="#indicator-tab-lahan-tanaman"]')
?.addEventListener('shown.bs.tab', function () {
renderFarmMap();
if (lahanChartsDirty) {
renderLahanCharts();
}
});
document
.querySelector('[data-bs-target="#indicator-tab-produksi"]')
?.addEventListener('shown.bs.tab', renderLandProfileTab);
if (document.querySelector('#indicator-tab-produksi')?.classList.contains('active')) {
renderLandProfileTab();
}
});
</script>
@endpush