peta sebaran dan praktek budaya

This commit is contained in:
2026-10-15 11:25:38 +07:00
parent 3e172cf915
commit f95f43999a
2 changed files with 397 additions and 40 deletions
@@ -262,6 +262,116 @@ class SummaryController extends Controller
], ],
'levels' => $levels, 'levels' => $levels,
'cultivationLevels' => $this->productionCultivationMapLevelData($request, $levels), 'cultivationLevels' => $this->productionCultivationMapLevelData($request, $levels),
'information' => $this->productionMapInformationData($request),
];
}
private function productionMapInformationData(Request $request): array
{
$landMetrics = $this->karakteristikLandMetrics($request);
$filteredPetaniIds = $this->filteredPetaniIdsQuery($request);
$commodityRows = DB::table('petani')
->whereNull('petani.deleted_at')
->whereIn('petani.id', clone $filteredPetaniIds)
->leftJoin('master_komoditas', 'master_komoditas.id', '=', 'petani.komoditas_id')
->selectRaw("COALESCE(master_komoditas.nama, 'Belum ditentukan') as nama")
->selectRaw('COUNT(DISTINCT petani.id) as jumlah')
->groupBy('master_komoditas.nama')
->orderByDesc('jumlah')
->get()
->map(fn ($row) => ['nama' => $row->nama, 'jumlah' => (int) $row->jumlah])
->all();
$lahan = (clone $this->filteredLahanQuery($request))
->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id')
->leftJoin('petani', 'petani.id', '=', 'map_lahan.petani_id')
->tap(fn ($query) => $this->whereActiveMapOwner($query))
->get([
'map_lahan.id',
'map_lahan.serangan_hama',
'map_lahan.penyakit_tanaman',
'map_lahan.jenis_hama',
'map_lahan.jenis_penyakit',
]);
$tanamanByLahan = DB::table('map_tanaman')
->whereNull('map_tanaman.deleted_at')
->whereIn('map_tanaman.lahan_id', $lahan->pluck('id'))
->get([
'map_tanaman.lahan_id',
'map_tanaman.serangan_hama',
'map_tanaman.penyakit_tanaman',
'map_tanaman.jenis_hama',
'map_tanaman.jenis_penyakit',
])
->groupBy('lahan_id');
$optCounts = [];
foreach ($lahan as $land) {
foreach ($this->productionOptDetails($land, $tanamanByLahan->get($land->id, collect())) as $opt) {
$optCounts[$opt] = ($optCounts[$opt] ?? 0) + 1;
}
}
arsort($optCounts);
$mainCommodityIds = DB::table('master_komoditas')
->where('is_utama', true)
->whereNull('deleted_at')
->select('id');
$scoreQuery = DB::table('petani')
->leftJoin('petani_skor', function ($join) {
$join->on('petani_skor.petani_id', '=', 'petani.id')
->whereNull('petani_skor.deleted_at');
})
->whereNull('petani.deleted_at')
->whereIn('petani.id', clone $filteredPetaniIds)
->whereIn('petani.komoditas_id', $mainCommodityIds);
$averageIndex = (clone $scoreQuery)->avg('petani_skor.skor_kesiapan_adopsi');
$segmentCase = "CASE
WHEN petani_skor.skor_kesiapan_adopsi >= 4 THEN 'Penggerak'
WHEN petani_skor.skor_kesiapan_adopsi >= 3 THEN 'Perintis'
WHEN petani_skor.skor_kesiapan_adopsi >= 2 THEN 'Pemerhati'
WHEN petani_skor.skor_kesiapan_adopsi >= 1 THEN 'Pemula'
ELSE 'Belum Lengkap'
END";
$segmentCounts = (clone $scoreQuery)
->selectRaw("{$segmentCase} as segmen")
->selectRaw('COUNT(DISTINCT petani.id) as jumlah')
->groupByRaw($segmentCase)
->get();
$segmentPriority = [
'Penggerak' => 0,
'Perintis' => 1,
'Pemerhati' => 2,
'Pemula' => 3,
'Belum Lengkap' => 4,
];
$dominantSegment = $segmentCounts
->sort(function ($left, $right) use ($segmentPriority) {
return ((int) $right->jumlah <=> (int) $left->jumlah)
?: (($segmentPriority[$left->segmen] ?? 99) <=> ($segmentPriority[$right->segmen] ?? 99));
})
->first();
$averageSegment = match (true) {
$averageIndex === null => 'Belum Lengkap',
$averageIndex >= 4 => 'Penggerak',
$averageIndex >= 3 => 'Perintis',
$averageIndex >= 2 => 'Pemerhati',
$averageIndex >= 1 => 'Pemula',
default => 'Belum Lengkap',
};
return [
'jumlah_petani' => $landMetrics['total_petani'],
'total_luas_lahan' => $landMetrics['total_luas_lahan'],
'petani_per_komoditas' => $commodityRows,
'opt' => collect($optCounts)
->map(fn ($jumlah, $nama) => ['nama' => $nama, 'jumlah' => $jumlah])
->values()
->all(),
'indeks' => $averageIndex === null ? null : round((float) $averageIndex, 2),
'segmen' => $averageSegment,
'segmen_dominan' => $dominantSegment->segmen ?? 'Belum Lengkap',
]; ];
} }
+261 -14
View File
@@ -241,6 +241,145 @@
width: 100%; width: 100%;
} }
.produksi-map-information-overlay {
background: rgba(255, 255, 255, .94);
border: 1px solid rgba(0, 0, 0, .08);
border-radius: 12px;
box-shadow: 0 6px 24px rgba(0, 0, 0, .2);
left: auto;
max-height: calc(100% - 2rem);
overflow-y: auto;
padding: .875rem;
pointer-events: none;
position: absolute;
right: 1rem;
top: 1rem;
width: min(350px, calc(100% - 5.5rem));
z-index: 700;
}
.produksi-map-information {
display: flex;
flex-direction: column;
gap: .75rem;
}
.produksi-map-information-summary {
align-items: center;
background: rgba(var(--bs-primary-rgb, 115, 103, 240), .07);
border-radius: 10px;
display: flex;
gap: .75rem;
padding: .875rem;
}
.produksi-map-information-summary-icon {
align-items: center;
background: var(--bs-primary, #7367f0);
border-radius: 10px;
color: #fff;
display: inline-flex;
flex: 0 0 38px;
height: 38px;
justify-content: center;
width: 38px;
}
.produksi-map-information-summary-value {
color: var(--bs-heading-color);
font-size: 1.25rem;
font-weight: 700;
line-height: 1.2;
}
.produksi-map-information-summary-label,
.produksi-map-information-section-title {
color: var(--bs-heading-color);
font-size: .8125rem;
font-weight: 600;
}
.produksi-map-information-metrics {
display: grid;
gap: .4rem;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.produksi-map-information-metric {
background: rgba(var(--bs-primary-rgb, 115, 103, 240), .07);
border-radius: 8px;
min-width: 0;
padding: .5rem .625rem;
}
.produksi-map-information-metric-label {
color: var(--bs-secondary-color);
display: block;
font-size: .65rem;
line-height: 1.2;
margin-bottom: .2rem;
}
.produksi-map-information-metric-value {
color: var(--bs-heading-color);
display: block;
font-size: .75rem;
font-weight: 700;
line-height: 1.25;
overflow-wrap: anywhere;
}
.produksi-map-information-section-title {
display: block;
margin-bottom: .625rem;
}
.produksi-map-information-bars {
display: flex;
flex-direction: column;
gap: .5rem;
}
.produksi-map-information-bar-label {
align-items: baseline;
color: var(--bs-body-color);
display: flex;
font-size: .75rem;
gap: .5rem;
justify-content: space-between;
margin-bottom: .25rem;
}
.produksi-map-information-bar-label span:first-child {
min-width: 0;
overflow-wrap: anywhere;
}
.produksi-map-information-bar-label strong {
color: var(--bs-heading-color);
flex: 0 0 auto;
font-weight: 600;
}
.produksi-map-information-track {
background: var(--bs-secondary-bg, rgba(128, 128, 128, .14));
border-radius: 999px;
height: 6px;
overflow: hidden;
}
.produksi-map-information-fill {
background: var(--bs-primary, #7367f0);
border-radius: inherit;
height: 100%;
min-width: 3px;
}
.produksi-map-information-empty {
color: var(--bs-secondary-color);
font-size: .75rem;
}
.produksi-map-back { .produksi-map-back {
display: none; display: none;
left: 50px; left: 50px;
@@ -479,6 +618,13 @@
left: 16px; left: 16px;
top: 16px; top: 16px;
} }
.produksi-map-information-overlay {
left: auto;
right: .75rem;
top: 1rem;
width: min(320px, calc(100% - 1.5rem));
}
} }
</style> </style>
@endpush @endpush
@@ -618,9 +764,18 @@
<div class="row g-4 mb-4"> <div class="row g-4 mb-4">
<div class="col-12"> <div class="col-12">
<div class="card h-100 produksi-chart-card"> <div class="card h-100 produksi-chart-card">
<div class="card-header d-flex justify-content-between align-items-center gap-3 flex-wrap"> <div class="card-header">
<div class="card-title mb-0"> <div class="card-title mb-0">
<h5 class="m-0 me-2">Peta Sebaran</h5> <h5 class="m-0 me-2">Peta Sebaran & Praktik Budidaya</h5>
<small class="text-body-secondary">Petani, luas lahan, komoditas, OPT, dan praktik budidaya berdasarkan filter aktif</small>
</div>
</div>
<div class="card-body">
<div class="d-flex flex-column gap-4">
<section>
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-2">
<div>
<h6 class="mb-0">Peta Sebaran Lahan & Tanaman</h6>
<small class="text-body-secondary">Lahan dan tanaman berdasarkan filter wilayah, komoditas, dan segmen</small> <small class="text-body-secondary">Lahan dan tanaman berdasarkan filter wilayah, komoditas, dan segmen</small>
</div> </div>
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
@@ -629,26 +784,23 @@
<button type="button" class="btn btn-outline-secondary produksi-map-layer-btn" data-map-layer="parcels">Parcels</button> <button type="button" class="btn btn-outline-secondary produksi-map-layer-btn" data-map-layer="parcels">Parcels</button>
</div> </div>
</div> </div>
<div class="card-body">
<div id="produksi-sebaran-map" class="produksi-map"> <div id="produksi-sebaran-map" class="produksi-map">
<button type="button" id="produksi-map-back" class="btn btn-sm btn-dark shadow produksi-map-back"> <button type="button" id="produksi-map-back" class="btn btn-sm btn-dark shadow produksi-map-back">
<i class="icon-base ti tabler-arrow-left me-1"></i> <i class="icon-base ti tabler-arrow-left me-1"></i>
Kembali ke Atas Kembali ke Atas
</button> </button>
<div id="produksi-map-empty" class="produksi-map-empty">Belum ada poligon lahan untuk filter ini.</div> <div id="produksi-map-empty" class="produksi-map-empty">Belum ada poligon lahan untuk filter ini.</div>
<div class="produksi-map-information-overlay">
<div class="produksi-map-information" id="produksi-map-information"></div>
</div> </div>
</div> </div>
</div> </section>
</div>
</div>
<div class="row g-4 mb-4"> <section>
<div class="col-12"> <div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-2">
<div class="card h-100 produksi-chart-card"> <div>
<div class="card-header d-flex justify-content-between align-items-center gap-3 flex-wrap"> <h6 class="mb-0">Peta Praktik Budidaya</h6>
<div class="card-title mb-0"> <small class="text-body-secondary">Intensitas pemangkasan, pemupukan, dan kebiasaan praktik baik</small>
<h5 class="m-0 me-2">Peta Praktik Budidaya</h5>
<small class="text-body-secondary">Intensitas pemangkasan, pemupukan, dan kebiasaan praktik baik berdasarkan filter wilayah</small>
</div> </div>
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary active produksi-cultivation-map-layer-btn" data-map-layer="satellite">Satellite</button> <button type="button" class="btn btn-outline-secondary active produksi-cultivation-map-layer-btn" data-map-layer="satellite">Satellite</button>
@@ -656,13 +808,17 @@
<button type="button" class="btn btn-outline-secondary produksi-cultivation-map-layer-btn" data-map-layer="parcels">Parcels</button> <button type="button" class="btn btn-outline-secondary produksi-cultivation-map-layer-btn" data-map-layer="parcels">Parcels</button>
</div> </div>
</div> </div>
<div class="card-body">
<div id="produksi-budidaya-map" class="produksi-map"> <div id="produksi-budidaya-map" class="produksi-map">
<button type="button" id="produksi-budidaya-map-back" class="btn btn-sm btn-dark shadow produksi-map-back"> <button type="button" id="produksi-budidaya-map-back" class="btn btn-sm btn-dark shadow produksi-map-back">
<i class="icon-base ti tabler-arrow-left me-1"></i> <i class="icon-base ti tabler-arrow-left me-1"></i>
Kembali ke Atas Kembali ke Atas
</button> </button>
<div id="produksi-budidaya-map-empty" class="produksi-map-empty">Belum ada poligon lahan untuk filter ini.</div> <div id="produksi-budidaya-map-empty" class="produksi-map-empty">Belum ada poligon lahan untuk filter ini.</div>
<div class="produksi-map-information-overlay">
<div class="produksi-map-information" id="produksi-budidaya-map-information"></div>
</div>
</div>
</section>
</div> </div>
</div> </div>
</div> </div>
@@ -921,6 +1077,7 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const chartData = @json($chartData); const chartData = @json($chartData);
const mapData = @json($mapData); const mapData = @json($mapData);
const mapInformation = mapData.information || {};
const wilayah = @json($wilayah); const wilayah = @json($wilayah);
const headingColor = config.colors.headingColor; const headingColor = config.colors.headingColor;
const legendColor = config.colors.bodyColor; const legendColor = config.colors.bodyColor;
@@ -950,6 +1107,93 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
} }
function renderProductionMapInformation(elementId, includeOpt = true) {
const element = document.getElementById(elementId);
if (!element) return;
const commodityRows = Array.isArray(mapInformation.petani_per_komoditas)
? mapInformation.petani_per_komoditas
: [];
const optRows = Array.isArray(mapInformation.opt) ? mapInformation.opt : [];
const indexValue = mapInformation.indeks === null || mapInformation.indeks === undefined
? '-'
: `${formatNumber(mapInformation.indeks, 2)} / 5`;
const renderBars = function (rows, emptyLabel) {
if (!rows.length) {
return `<div class="produksi-map-information-empty">${emptyLabel}</div>`;
}
const visibleRows = rows.slice(0, 5);
const maxValue = Math.max(...visibleRows.map(row => Number(row.jumlah || 0)), 1);
const bars = visibleRows.map(function (row) {
const value = Number(row.jumlah || 0);
const width = Math.max(value > 0 ? 3 : 0, (value / maxValue) * 100);
return `
<div>
<div class="produksi-map-information-bar-label">
<span>${escapeHtml(row.nama || 'Belum ditentukan')}</span>
<strong>${formatNumber(value, 0)}</strong>
</div>
<div class="produksi-map-information-track" aria-hidden="true">
<div class="produksi-map-information-fill" style="width: ${width}%"></div>
</div>
</div>
`;
}).join('');
const remaining = rows.length - visibleRows.length;
return bars + (remaining > 0
? `<div class="produksi-map-information-empty">+ ${formatNumber(remaining, 0)} kategori lainnya</div>`
: '');
};
element.innerHTML = `
<div>
<div class="produksi-map-information-summary">
<span class="produksi-map-information-summary-icon"><i class="icon-base ti tabler-users"></i></span>
<div>
<div class="produksi-map-information-summary-value">${formatNumber(mapInformation.jumlah_petani || 0, 0)}</div>
<div class="produksi-map-information-summary-label">Jumlah Petani</div>
</div>
</div>
<div class="produksi-map-information-summary mt-2">
<span class="produksi-map-information-summary-icon"><i class="icon-base ti tabler-map-2"></i></span>
<div>
<div class="produksi-map-information-summary-value">${formatNumber(mapInformation.total_luas_lahan || 0, 2)} <small class="fs-6 fw-normal">ha</small></div>
<div class="produksi-map-information-summary-label">Total Luas Lahan</div>
</div>
</div>
</div>
<div class="produksi-map-information-metrics">
<div class="produksi-map-information-metric">
<span class="produksi-map-information-metric-label">Indeks</span>
<strong class="produksi-map-information-metric-value">${indexValue}</strong>
</div>
<div class="produksi-map-information-metric">
<span class="produksi-map-information-metric-label">Segmen</span>
<strong class="produksi-map-information-metric-value">${escapeHtml(mapInformation.segmen || 'Belum Lengkap')}</strong>
</div>
<div class="produksi-map-information-metric">
<span class="produksi-map-information-metric-label">Segmen Dominan</span>
<strong class="produksi-map-information-metric-value">${escapeHtml(mapInformation.segmen_dominan || 'Belum Lengkap')}</strong>
</div>
</div>
<section>
<span class="produksi-map-information-section-title">Jumlah Petani / Komoditas</span>
<div class="produksi-map-information-bars">${renderBars(commodityRows, 'Belum ada data komoditas.')}</div>
</section>
${includeOpt ? `
<section>
<span class="produksi-map-information-section-title">OPT (Organisme Pengganggu Tanaman)</span>
<div class="produksi-map-information-bars">${renderBars(optRows, 'Belum ada data OPT.')}</div>
</section>
` : ''}
`;
}
function toGroupedBarData(data, seriesName) { function toGroupedBarData(data, seriesName) {
if (!data || !Array.isArray(data.labels) || !Array.isArray(data.series)) { if (!data || !Array.isArray(data.labels) || !Array.isArray(data.series)) {
return data; return data;
@@ -1955,6 +2199,9 @@ document.addEventListener('DOMContentLoaded', function () {
cultivationMapRendered = true; cultivationMapRendered = true;
} }
renderProductionMapInformation('produksi-map-information');
renderProductionMapInformation('produksi-budidaya-map-information', false);
if (typeof L === 'undefined') { if (typeof L === 'undefined') {
const mapEmpty = document.getElementById('produksi-map-empty'); const mapEmpty = document.getElementById('produksi-map-empty');
const cultivationMapEmpty = document.getElementById('produksi-budidaya-map-empty'); const cultivationMapEmpty = document.getElementById('produksi-budidaya-map-empty');