2569 lines
100 KiB
PHP
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 & 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 & 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 & 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 & 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}:
|
|
<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}:
|
|
<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
|