diff --git a/app/Http/Controllers/Web/Dashboard/SummaryController.php b/app/Http/Controllers/Web/Dashboard/SummaryController.php
index f581fd2..437d0d6 100644
--- a/app/Http/Controllers/Web/Dashboard/SummaryController.php
+++ b/app/Http/Controllers/Web/Dashboard/SummaryController.php
@@ -223,7 +223,6 @@ class SummaryController extends Controller
$healthSourceName = $tanaman->isNotEmpty() ? 'tanaman' : 'lahan';
$landMetrics = $this->karakteristikLandMetrics($request);
- $totalPetani = $landMetrics['total_petani'];
$totalLuasLahan = $landMetrics['total_luas_lahan'];
$avgLuasPerPetani = $landMetrics['ratarata_luas_lahan'];
$actualProductivity = $this->actualProductivityByCommodityChart($request);
@@ -233,14 +232,6 @@ class SummaryController extends Controller
$aksesPasar = $this->filteredAksesPasar($request);
$kpiCards = [
- [
- 'value' => number_format($totalPetani, 0, ',', '.'),
- 'unit' => 'orang',
- 'label' => 'Jumlah Petani',
- 'description' => 'Petani sesuai filter yang dipilih',
- 'icon' => 'tabler-users',
- 'variant' => 'primary',
- ],
[
'value' => number_format($totalLuasLahan, 2, ',', '.'),
'unit' => 'ha',
@@ -294,7 +285,7 @@ class SummaryController extends Controller
: $value,
),
'praktikSortasiPanen' => $this->enumOptionChart($surveyPanen, 'praktik_sortasi', PraktikSortasi::class),
- 'praktikFermentasiPascapanen' => $this->fermentationPracticeChart($aksesPasar),
+ 'praktikFermentasiPascapanen' => $this->fermentationPracticeByCommodityChart($aksesPasar),
'produktivitasAktualPerKomoditas' => $actualProductivity,
],
];
@@ -312,7 +303,6 @@ class SummaryController extends Controller
'desa' => (string) $request->input('desaKelurahan_id', ''),
],
'levels' => $levels,
- 'cultivationLevels' => $this->productionCultivationMapLevelData($request, $levels),
'information' => $this->productionMapInformationData($request),
];
}
@@ -511,6 +501,7 @@ class SummaryController extends Controller
'map_lahan.path',
'map_lahan.luas_lahan',
'map_lahan.jumlah_tanaman',
+ 'map_lahan.petani_id',
'map_lahan.umur_tanaman',
'map_lahan.serangan_hama',
'map_lahan.penyakit_tanaman',
@@ -537,58 +528,10 @@ class SummaryController extends Controller
])
->groupBy('lahan_id');
- $geoLahan = $mapLahan
- ->filter(fn ($lahan) => $this->hasMapPolygon($lahan->path))
- ->map(function ($lahan) use ($tanamanByLahan) {
- $tanamanRows = $tanamanByLahan->get($lahan->id, collect());
- $komoditas = $this->productionCommodityDetails($lahan, $tanamanRows);
- $opt = $this->productionOptDetails($lahan, $tanamanRows);
-
- return [
- 'id' => $lahan->id,
- 'parent_id' => $lahan->desa_kelurahan_id,
- 'polygon' => $lahan->path,
- 'nama_petani' => $lahan->petani_nama ?? $lahan->profile_nama ?? 'N/A',
- 'luas' => (float) $lahan->luas_lahan,
- 'komoditas' => $komoditas,
- 'opt' => $opt,
- ];
- })
- ->values()
- ->all();
-
- return [
- 'provinsi' => array_values($geoProvinsi),
- 'kabupaten' => array_values($geoKabupaten),
- 'kecamatan' => array_values($geoKecamatan),
- 'desa' => array_values($geoDesa),
- 'lahan' => $geoLahan,
- ];
- }
-
- private function productionCultivationMapLevelData(Request $request, array $baseLevels): array
- {
- $mapLahan = (clone $this->filteredLahanQuery($request))
- ->whereNotNull('map_lahan.path')
- ->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id')
- ->leftJoin('petani', 'petani.id', '=', 'map_lahan.petani_id')
- ->tap(fn ($query) => $this->whereActiveMapOwner($query))
- ->orderByDesc('map_lahan.updated_at')
- ->limit(500)
- ->get([
- 'map_lahan.id',
- 'map_lahan.desa_kelurahan_id',
- 'map_lahan.path',
- 'map_lahan.luas_lahan',
- 'map_lahan.petani_id',
- 'profiles.nama as profile_nama',
- 'petani.nama as petani_nama',
- ]);
$petaniIds = $mapLahan->pluck('petani_id')->filter()->unique()->values();
$practiceByPetani = DB::table('praktik_budidaya')
->whereNull('praktik_budidaya.deleted_at')
- ->when($petaniIds->isNotEmpty(), fn ($query) => $query->whereIn('praktik_budidaya.petani_id', $petaniIds))
- ->when($petaniIds->isEmpty(), fn ($query) => $query->whereRaw('1 = 0'))
+ ->whereIn('praktik_budidaya.petani_id', $petaniIds)
->orderByDesc('praktik_budidaya.updated_at')
->get([
'praktik_budidaya.petani_id',
@@ -601,7 +544,10 @@ class SummaryController extends Controller
$geoLahan = $mapLahan
->filter(fn ($lahan) => $this->hasMapPolygon($lahan->path))
- ->map(function ($lahan) use ($practiceByPetani) {
+ ->map(function ($lahan) use ($tanamanByLahan, $practiceByPetani) {
+ $tanamanRows = $tanamanByLahan->get($lahan->id, collect());
+ $komoditas = $this->productionCommodityDetails($lahan, $tanamanRows);
+ $opt = $this->productionOptDetails($lahan, $tanamanRows);
$practice = $practiceByPetani->get($lahan->petani_id);
return [
@@ -610,6 +556,8 @@ class SummaryController extends Controller
'polygon' => $lahan->path,
'nama_petani' => $lahan->petani_nama ?? $lahan->profile_nama ?? 'N/A',
'luas' => (float) $lahan->luas_lahan,
+ 'komoditas' => $komoditas,
+ 'opt' => $opt,
'pemangkasan' => $this->enumLabel(FrekuensiPemangkasanUtama::class, $practice->frekuensi_pemangkasan_utama ?? null, 'Belum ada data'),
'pemupukan' => $this->enumLabel(PemberianPupuk::class, $practice->pemberian_pupuk ?? null, 'Belum ada data'),
'praktik_baik' => $this->arrayValues($practice->kebiasaan_konservasi_tanah ?? null),
@@ -619,10 +567,10 @@ class SummaryController extends Controller
->all();
return [
- 'provinsi' => $baseLevels['provinsi'] ?? [],
- 'kabupaten' => $baseLevels['kabupaten'] ?? [],
- 'kecamatan' => $baseLevels['kecamatan'] ?? [],
- 'desa' => $baseLevels['desa'] ?? [],
+ 'provinsi' => array_values($geoProvinsi),
+ 'kabupaten' => array_values($geoKabupaten),
+ 'kecamatan' => array_values($geoKecamatan),
+ 'desa' => array_values($geoDesa),
'lahan' => $geoLahan,
];
}
@@ -826,7 +774,12 @@ class SummaryController extends Controller
return DB::table('akses_pasar')
->whereNull('akses_pasar.deleted_at')
->whereIn('akses_pasar.petani_id', $petaniIds)
- ->get(['praktik_fermentasi']);
+ ->leftJoin('petani as akses_petani', 'akses_petani.id', '=', 'akses_pasar.petani_id')
+ ->leftJoin('master_komoditas as akses_komoditas', 'akses_komoditas.id', '=', 'akses_petani.komoditas_id')
+ ->get([
+ 'akses_pasar.praktik_fermentasi',
+ 'akses_komoditas.nama as komoditas_nama',
+ ]);
}
private function filteredSurveyProduktivitas(Request $request)
@@ -1254,10 +1207,10 @@ class SummaryController extends Controller
];
}
- private function fermentationPracticeChart($rows): array
+ private function fermentationPracticeByCommodityChart($rows): array
{
$labels = ['Ya, rutin', 'Pernah mencoba', 'Belum pernah'];
- $counts = collect($labels)->mapWithKeys(fn ($label) => [$label => 0]);
+ $matrix = [];
foreach ($rows as $row) {
$label = $this->fermentationPracticeLabel($row->praktik_fermentasi ?? null);
@@ -1266,12 +1219,21 @@ class SummaryController extends Controller
continue;
}
- $counts[$label] = (int) $counts[$label] + 1;
+ $commodity = $row->komoditas_nama ?: 'Belum ditentukan';
+ $matrix[$commodity] ??= array_fill_keys($labels, 0);
+ $matrix[$commodity][$label]++;
}
+ ksort($matrix, SORT_NATURAL | SORT_FLAG_CASE);
+
return [
- 'labels' => $labels,
- 'series' => collect($labels)->map(fn ($label) => (int) ($counts[$label] ?? 0))->values()->all(),
+ 'labels' => array_keys($matrix),
+ 'series' => collect($labels)
+ ->map(fn ($label) => [
+ 'name' => $label,
+ 'data' => collect($matrix)->map(fn ($counts) => (int) $counts[$label])->values()->all(),
+ ])
+ ->all(),
];
}
diff --git a/resources/views/dashboard/produktivitas.blade.php b/resources/views/dashboard/produktivitas.blade.php
index 3a890da..9d1266b 100644
--- a/resources/views/dashboard/produktivitas.blade.php
+++ b/resources/views/dashboard/produktivitas.blade.php
@@ -758,7 +758,7 @@
Peta Sebaran
-
Lahan, tanaman, dan OPT berdasarkan filter wilayah.
+
Lahan, tanaman, OPT, dan praktik budidaya berdasarkan filter wilayah.
@@ -773,8 +773,8 @@
-
Peta Sebaran Lahan & Tanaman
- Lahan dan tanaman berdasarkan filter wilayah, komoditas, dan segmen
+ Peta Sebaran Lahan, Tanaman & Praktik Budidaya
+ Lahan, tanaman, OPT, pemangkasan, pemupukan, dan praktik baik berdasarkan filter wilayah, komoditas, dan segmen
@@ -795,34 +795,6 @@
-
-
-
-
-
-
-
Peta Praktik Budidaya
- Intensitas pemangkasan, pemupukan, dan kebiasaan praktik baik
-
-
-
-
-
-
-
-
-
-
Belum ada poligon lahan untuk filter ini.
-
-
-
-
-
@@ -1044,8 +1016,8 @@
@@ -1079,11 +1051,7 @@ document.addEventListener('DOMContentLoaded', function () {
const mapData = @json($mapData);
const dashboardFilters = @json($dashboardFilters);
const mapInformationUrl = @json(route('dashboard.produktivitas.map-information'));
- const initialMapInformation = mapData.information || {};
- const mapInformation = {
- production: { ...initialMapInformation },
- cultivation: { ...initialMapInformation },
- };
+ const mapInformation = { ...(mapData.information || {}) };
const wilayah = @json($wilayah);
const headingColor = config.colors.headingColor;
const legendColor = config.colors.bodyColor;
@@ -1094,9 +1062,8 @@ document.addEventListener('DOMContentLoaded', function () {
const labelColor = legendColor;
const borderColor = pastelChart.grid || 'rgba(128, 128, 128, .18)';
let productionMapRendered = false;
- let cultivationMapRendered = false;
let actualProductivityRendered = false;
- const mapInformationRequestIds = { production: 0, cultivation: 0 };
+ let mapInformationRequestId = 0;
function hasData(data) {
return data && Array.isArray(data.series) && data.series.some(function (value) {
@@ -1114,7 +1081,7 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
- function renderProductionMapInformation(elementId, information, includeOpt = true) {
+ function renderProductionMapInformation(elementId, information) {
const element = document.getElementById(elementId);
if (!element) return;
@@ -1193,17 +1160,15 @@ document.addEventListener('DOMContentLoaded', function () {
Jumlah Petani / Komoditas
${renderBars(commodityRows, 'Belum ada data komoditas.', 'petani')}
- ${includeOpt ? `
-
- OPT (Organisme Pengganggu Tanaman)
- ${renderBars(optRows, 'Belum ada data OPT.', 'lahan')}
-
- ` : ''}
+
+ OPT (Organisme Pengganggu Tanaman)
+ ${renderBars(optRows, 'Belum ada data OPT.', 'lahan')}
+
`;
}
- async function loadProductionMapInformation(mapType, level = null, id = null) {
- const requestId = ++mapInformationRequestIds[mapType];
+ async function loadProductionMapInformation(level = null, id = null) {
+ const requestId = ++mapInformationRequestId;
const params = new URLSearchParams();
Object.entries(dashboardFilters || {}).forEach(function ([key, value]) {
@@ -1225,14 +1190,10 @@ document.addEventListener('DOMContentLoaded', function () {
if (!response.ok) return;
const information = await response.json();
- if (requestId !== mapInformationRequestIds[mapType]) return;
+ if (requestId !== mapInformationRequestId) return;
- Object.assign(mapInformation[mapType], information);
- if (mapType === 'production') {
- renderProductionMapInformation('produksi-map-information', mapInformation.production);
- } else {
- renderProductionMapInformation('produksi-budidaya-map-information', mapInformation.cultivation, false);
- }
+ Object.assign(mapInformation, information);
+ renderProductionMapInformation('produksi-map-information', mapInformation);
} catch (error) {
console.error('Gagal memuat informasi wilayah pada peta.', error);
}
@@ -1425,6 +1386,9 @@ document.addEventListener('DOMContentLoaded', function () {
const opt = Array.isArray(item.opt) && item.opt.length
? item.opt.map(value => `
${escapeHtml(value)}`).join('')
: '
Belum ada data OPT';
+ const praktikBaik = Array.isArray(item.praktik_baik) && item.praktik_baik.length
+ ? item.praktik_baik.map(value => `
${escapeHtml(value)}`).join('')
+ : '
Belum ada data praktik baik';
return `
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
@@ -1513,7 +1490,7 @@ document.addEventListener('DOMContentLoaded', function () {
renderProductionMapLevel(previous.level, previous.parentId);
productionMapState.informationLevel = previous.informationLevel;
productionMapState.informationId = previous.informationId;
- loadProductionMapInformation('production', previous.informationLevel, previous.informationId);
+ loadProductionMapInformation(previous.informationLevel, previous.informationId);
if (previous.bounds?.isValid()) {
productionMapState.map.fitBounds(previous.bounds, { padding: [30, 30] });
@@ -1585,7 +1562,8 @@ document.addEventListener('DOMContentLoaded', function () {
layer.bindPopup(landPopup(item), {
className: 'produksi-map-popup',
closeButton: true,
- maxWidth: 320,
+ maxWidth: 340,
+ maxHeight: 360,
});
} else {
layer.bindTooltip(regionTooltip(item), {
@@ -1606,7 +1584,7 @@ document.addEventListener('DOMContentLoaded', function () {
});
productionMapState.informationLevel = nextLevel;
productionMapState.informationId = item.id;
- loadProductionMapInformation('production', nextLevel, item.id);
+ loadProductionMapInformation(nextLevel, item.id);
renderProductionMapLevel(nextProductionMapLevel(nextLevel), item.id, event.target.getBounds());
});
}
@@ -1641,234 +1619,6 @@ document.addEventListener('DOMContentLoaded', function () {
}, 150);
}
- function initialCultivationMapLevel() {
- const levels = mapData.cultivationLevels || {};
- const filters = mapData.filters || {};
-
- if (filters.desa && (levels.desa || []).length) return 'desa';
- if (filters.kecamatan && (levels.kecamatan || []).length) return 'kecamatan';
- if (filters.kabupaten && (levels.kabupaten || []).length) return 'kabupaten';
- if ((levels.kabupaten || []).length) return 'kabupaten';
- if ((levels.kecamatan || []).length) return 'kecamatan';
- if ((levels.desa || []).length) return 'desa';
- if ((levels.provinsi || []).length) return 'provinsi';
-
- return 'lahan';
- }
-
- function cultivationPopup(item) {
- const praktikBaik = Array.isArray(item.praktik_baik) && item.praktik_baik.length
- ? item.praktik_baik.map(value => `${escapeHtml(value)}`).join('')
- : 'Belum ada data praktik baik';
-
- return `
-
- `;
- }
-
- const cultivationMapState = {
- activeLayer: 'satellite',
- backButton: null,
- empty: null,
- group: null,
- history: [],
- informationId: null,
- informationLevel: null,
- layers: {},
- map: null,
- };
-
- function initCultivationMap() {
- const element = document.getElementById('produksi-budidaya-map');
-
- if (!element || typeof L === 'undefined') {
- return false;
- }
-
- if (cultivationMapState.map) {
- return true;
- }
-
- cultivationMapState.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'),
- };
- cultivationMapState.map = L.map('produksi-budidaya-map', {
- center: [-8.67, 121.0],
- zoom: 10,
- layers: [cultivationMapState.layers.satellite],
- zoomControl: false,
- });
- cultivationMapState.group = L.featureGroup().addTo(cultivationMapState.map);
- cultivationMapState.backButton = document.getElementById('produksi-budidaya-map-back');
- cultivationMapState.empty = document.getElementById('produksi-budidaya-map-empty');
- enableMapInformationInteraction(element);
-
- L.control.zoom({ position: 'topleft' }).addTo(cultivationMapState.map);
-
- cultivationMapState.backButton?.addEventListener('click', function () {
- const previous = cultivationMapState.history.pop();
-
- if (previous) {
- renderCultivationMapLevel(previous.level, previous.parentId);
- cultivationMapState.informationLevel = previous.informationLevel;
- cultivationMapState.informationId = previous.informationId;
- loadProductionMapInformation('cultivation', previous.informationLevel, previous.informationId);
-
- if (previous.bounds?.isValid()) {
- cultivationMapState.map.fitBounds(previous.bounds, { padding: [30, 30] });
- }
- }
- });
-
- return true;
- }
-
- function setCultivationMapLayer(type, button) {
- if (!cultivationMapState.map || !cultivationMapState.layers[type]) {
- return;
- }
-
- Object.values(cultivationMapState.layers).forEach(function (layer) {
- cultivationMapState.map.removeLayer(layer);
- });
- cultivationMapState.layers[type].addTo(cultivationMapState.map);
- cultivationMapState.activeLayer = type;
-
- document.querySelectorAll('.produksi-cultivation-map-layer-btn').forEach(function (item) {
- item.classList.toggle('active', item === button);
- });
- }
-
- function renderCultivationMapLevel(level = null, parentId = null, fallbackBounds = null) {
- if (!initCultivationMap()) {
- return;
- }
-
- const levels = mapData.cultivationLevels || {};
- const nextLevel = level || initialCultivationMapLevel();
- let dataToRender = levels[nextLevel] || [];
-
- if (parentId !== null) {
- dataToRender = dataToRender.filter(item => String(item.parent_id) === String(parentId));
- }
-
- cultivationMapState.group.clearLayers();
-
- 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) {
- cultivationMapState.group.addLayer(desaLayer);
- }
- }
-
- dataToRender.forEach(function (item) {
- const layer = makeLayerFromPolygon(item.polygon, nextLevel === 'lahan'
- ? { color: '#5FC7D6', fillColor: '#5FC7D6', fillOpacity: .58, weight: 2 }
- : { color: '#9D8EF0', fillColor: '#9D8EF0', fillOpacity: .3, weight: 2 });
-
- if (!layer) {
- return;
- }
-
- if (nextLevel === 'lahan') {
- layer.bindPopup(cultivationPopup(item), {
- className: 'produksi-map-popup',
- closeButton: true,
- maxWidth: 340,
- });
- } else {
- layer.bindTooltip(regionTooltip(item), {
- className: 'produksi-map-tooltip',
- direction: 'top',
- offset: [0, -10],
- sticky: true,
- });
-
- layer.on('click', function (event) {
- L.DomEvent.stopPropagation(event);
- cultivationMapState.history.push({
- level: nextLevel,
- parentId,
- bounds: cultivationMapState.group.getBounds(),
- informationLevel: cultivationMapState.informationLevel,
- informationId: cultivationMapState.informationId,
- });
- cultivationMapState.informationLevel = nextLevel;
- cultivationMapState.informationId = item.id;
- loadProductionMapInformation('cultivation', nextLevel, item.id);
- renderCultivationMapLevel(nextProductionMapLevel(nextLevel), item.id, event.target.getBounds());
- });
- }
-
- cultivationMapState.group.addLayer(layer);
- });
-
- if (cultivationMapState.backButton) {
- cultivationMapState.backButton.style.display = cultivationMapState.history.length > 0 ? 'block' : 'none';
- }
-
- const layersCount = cultivationMapState.group.getLayers().length;
-
- if (cultivationMapState.empty) {
- cultivationMapState.empty.style.display = layersCount > 0 ? 'none' : 'flex';
- }
-
- if (layersCount > 0) {
- const bounds = cultivationMapState.group.getBounds();
-
- if (bounds.isValid()) {
- cultivationMapState.map.fitBounds(bounds, { padding: [30, 30], maxZoom: 18 });
- }
- } else if (fallbackBounds?.isValid()) {
- cultivationMapState.map.fitBounds(fallbackBounds, { padding: [30, 30], maxZoom: 18 });
- } else {
- cultivationMapState.map.setView([-8.67, 121.0], 10);
- }
-
- setTimeout(function () {
- cultivationMapState.map.invalidateSize();
- }, 150);
- }
-
function renderDonut(selector, data, options = {}) {
const element = document.querySelector(selector);
if (!element) return;
@@ -2231,7 +1981,7 @@ document.addEventListener('DOMContentLoaded', function () {
renderGroupedBar('#chart-sumber-air-kebun', chartData.sumberAirKebun, { unit: 'petani', yTitle: 'Jumlah Petani', height: 330 });
renderGroupedBar('#chart-kebiasaan-praktik-baik', chartData.kebiasaanPraktikBaik, { unit: 'petani', yTitle: 'Jumlah Petani', height: 330 });
renderPieWithLegend('#chart-praktik-sortasi-panen', chartData.praktikSortasiPanen, { unit: 'petani' });
- renderBar('#chart-praktik-fermentasi-pascapanen', chartData.praktikFermentasiPascapanen, { height: 330, color: pastelSemantic.primary || '#9D8EF0', unit: 'petani' });
+ renderGroupedBar('#chart-praktik-fermentasi-pascapanen', chartData.praktikFermentasiPascapanen, { unit: 'petani', yTitle: 'Jumlah Petani', height: 330 });
renderActualProductivityIfVisible();
}
@@ -2261,37 +2011,15 @@ document.addEventListener('DOMContentLoaded', function () {
productionMapRendered = true;
}
- function renderCultivationMapIfOpen() {
- if (!document.getElementById('peta-sebaran-panel')?.classList.contains('show')) {
- return;
- }
-
- if (!cultivationMapRendered) {
- renderCultivationMapLevel();
- cultivationMapRendered = true;
- return;
- }
-
- cultivationMapState.map?.invalidateSize();
- cultivationMapRendered = true;
- }
-
- renderProductionMapInformation('produksi-map-information', mapInformation.production);
- renderProductionMapInformation('produksi-budidaya-map-information', mapInformation.cultivation, false);
+ renderProductionMapInformation('produksi-map-information', mapInformation);
if (typeof L === 'undefined') {
const mapEmpty = document.getElementById('produksi-map-empty');
- const cultivationMapEmpty = document.getElementById('produksi-budidaya-map-empty');
if (mapEmpty) {
mapEmpty.textContent = 'Leaflet belum dimuat.';
mapEmpty.style.display = 'flex';
}
-
- if (cultivationMapEmpty) {
- cultivationMapEmpty.textContent = 'Leaflet belum dimuat.';
- cultivationMapEmpty.style.display = 'flex';
- }
}
document.querySelectorAll('.produksi-map-layer-btn').forEach(function (button) {
@@ -2300,12 +2028,6 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
- document.querySelectorAll('.produksi-cultivation-map-layer-btn').forEach(function (button) {
- button.addEventListener('click', function () {
- setCultivationMapLayer(button.dataset.mapLayer, button);
- });
- });
-
$('#filter-provinsi, #filter-kabkota, #filter-kecamatan, #filter-desa, #filter-komoditas, #filter-segmen').select2({ width: '100%' });
$('#filter-provinsi').on('change', function () {
@@ -2388,7 +2110,6 @@ document.addEventListener('DOMContentLoaded', function () {
if (panel.id === 'peta-sebaran-panel' && typeof L !== 'undefined') {
renderProductionMapIfOpen();
- renderCultivationMapIfOpen();
}
window.dispatchEvent(new Event('resize'));
diff --git a/tests/Feature/FermentationPracticeChartTest.php b/tests/Feature/FermentationPracticeChartTest.php
new file mode 100644
index 0000000..52eca69
--- /dev/null
+++ b/tests/Feature/FermentationPracticeChartTest.php
@@ -0,0 +1,117 @@
+id();
+ $table->string('nama');
+ });
+ foreach ([
+ 'master_kabupaten_kota' => 'provinsi_id',
+ 'master_kecamatan' => 'kabupaten_kota_id',
+ 'master_desa_kelurahan' => 'kecamatan_id',
+ ] as $name => $parent) {
+ Schema::create($name, function (Blueprint $table) use ($parent) {
+ $table->id();
+ $table->integer($parent);
+ });
+ }
+ Schema::create('petani', function (Blueprint $table) {
+ $table->id();
+ $table->integer('komoditas_id')->nullable();
+ $table->integer('desa_kelurahan_id');
+ $table->softDeletes();
+ });
+ Schema::create('akses_pasar', function (Blueprint $table) {
+ $table->id();
+ $table->integer('petani_id');
+ $table->string('praktik_fermentasi')->nullable();
+ $table->softDeletes();
+ });
+ Schema::create('petani_skor', function (Blueprint $table) {
+ $table->id();
+ $table->integer('petani_id');
+ $table->float('skor_kesiapan_adopsi');
+ $table->softDeletes();
+ });
+ DB::table('master_komoditas')->insert([
+ ['id' => 1, 'nama' => 'Kakao'], ['id' => 2, 'nama' => 'Kopi'], ['id' => 3, 'nama' => 'Vanili'],
+ ]);
+ DB::table('master_kabupaten_kota')->insert(['id' => 1, 'provinsi_id' => 1]);
+ DB::table('master_kecamatan')->insert(['id' => 1, 'kabupaten_kota_id' => 1]);
+ DB::table('master_desa_kelurahan')->insert([
+ ['id' => 1, 'kecamatan_id' => 1], ['id' => 2, 'kecamatan_id' => 1],
+ ]);
+
+ foreach ([
+ [1, 1, 1, PraktikFermentasi::RUTIN_SENDIRI->value],
+ [2, 1, 1, PraktikFermentasi::RUTIN_KELOMPOK->value],
+ [3, 2, 2, PraktikFermentasi::PERNAH_SENDIRI->value],
+ [4, 2, 2, PraktikFermentasi::PERNAH_KELOMPOK->value],
+ [5, 3, 2, PraktikFermentasi::BELUM_PERNAH->value],
+ [6, 3, 2, null],
+ [7, 1, 1, ''],
+ [8, 2, 2, PraktikFermentasi::RUTIN_SENDIRI->value],
+ [9, null, 2, PraktikFermentasi::BELUM_PERNAH->value],
+ [10, 1, 1, PraktikFermentasi::RUTIN_SENDIRI->value],
+ [11, 1, 1, 'Tidak dikenal'],
+ ] as [$id, $commodity, $village, $practice]) {
+ DB::table('petani')->insert([
+ 'id' => $id, 'komoditas_id' => $commodity, 'desa_kelurahan_id' => $village,
+ 'deleted_at' => $id === 10 ? '2026-01-01' : null,
+ ]);
+ DB::table('akses_pasar')->insert([
+ 'petani_id' => $id, 'praktik_fermentasi' => $practice,
+ 'deleted_at' => $id === 8 ? '2026-01-01' : null,
+ ]);
+ }
+ DB::table('petani_skor')->insert([
+ ['petani_id' => 1, 'skor_kesiapan_adopsi' => 4.1, 'deleted_at' => null],
+ ['petani_id' => 2, 'skor_kesiapan_adopsi' => 2.6, 'deleted_at' => null],
+ ['petani_id' => 3, 'skor_kesiapan_adopsi' => 4.4, 'deleted_at' => '2026-01-01'],
+ ]);
+
+ $controller = app(SummaryController::class);
+ $query = new ReflectionMethod($controller, 'filteredAksesPasar');
+ $chart = new ReflectionMethod($controller, 'fermentationPracticeByCommodityChart');
+ $this->fermentationChart = fn (array $filters = []) => $chart->invoke(
+ $controller, $query->invoke($controller, new Request($filters)),
+ );
+});
+
+test('fermentation groups individual and collective responses by commodity', function () {
+ expect(($this->fermentationChart)())->toBe([
+ 'labels' => ['Belum ditentukan', 'Kakao', 'Kopi', 'Vanili'],
+ 'series' => [
+ ['name' => 'Ya, rutin', 'data' => [0, 2, 0, 0]],
+ ['name' => 'Pernah mencoba', 'data' => [0, 0, 2, 0]],
+ ['name' => 'Belum pernah', 'data' => [1, 0, 0, 1]],
+ ],
+ ]);
+});
+
+test('fermentation respects region and commodity filters', function () {
+ $chart = ($this->fermentationChart)(['desaKelurahan_id' => 2, 'komoditas_id' => 2]);
+ expect($chart['labels'])->toBe(['Kopi']);
+ expect(array_column($chart['series'], 'data'))->toBe([[0], [2], [0]]);
+ expect(($this->fermentationChart)(['desaKelurahan_id' => 1])['labels'])->toBe(['Kakao']);
+});
+
+test('fermentation respects segment filters and excludes deleted scores', function () {
+ $chart = ($this->fermentationChart)(['segmen' => 'Pemerhati']);
+ expect($chart['labels'])->toBe(['Kakao']);
+ expect(array_column($chart['series'], 'data'))->toBe([[1], [0], [0]]);
+ expect(($this->fermentationChart)(['segmen' => 'Penggerak'])['labels'])->toBe(['Kakao']);
+});
+
+test('fermentation returns empty grouped series when no matching responses exist', function () {
+ $chart = ($this->fermentationChart)(['provinsi_id' => 99]);
+ expect($chart['labels'])->toBe([]);
+ expect(array_column($chart['series'], 'data'))->toBe([[], [], []]);
+});
diff --git a/tests/Feature/ProductionMapTest.php b/tests/Feature/ProductionMapTest.php
new file mode 100644
index 0000000..af3a28a
--- /dev/null
+++ b/tests/Feature/ProductionMapTest.php
@@ -0,0 +1,129 @@
+ null,
+ 'master_kabupaten_kota' => 'provinsi_id',
+ 'master_kecamatan' => 'kabupaten_kota_id',
+ 'master_desa_kelurahan' => 'kecamatan_id',
+ ] as $name => $parent) {
+ Schema::create($name, function (Blueprint $table) use ($parent) {
+ $table->id();
+ $table->string('nama');
+ $table->text('path')->nullable();
+ if ($parent) {
+ $table->integer($parent);
+ }
+ $table->softDeletes();
+ });
+ DB::table($name)->insert(['id' => 1, 'nama' => 'Wilayah'] + ($parent ? [$parent => 1] : []));
+ }
+ Schema::create('master_komoditas', function (Blueprint $table) {
+ $table->id();
+ $table->string('nama');
+ });
+ foreach (['petani', 'profiles'] as $name) {
+ Schema::create($name, function (Blueprint $table) {
+ $table->id();
+ $table->string('nama');
+ $table->softDeletes();
+ });
+ }
+ foreach (['map_lahan', 'map_tanaman'] as $name) {
+ Schema::create($name, function (Blueprint $table) use ($name) {
+ $table->id();
+ $table->integer('komoditas_id');
+ $table->integer('umur_tanaman')->nullable();
+ foreach (['serangan_hama', 'penyakit_tanaman', 'jenis_hama', 'jenis_penyakit'] as $column) {
+ $table->text($column)->nullable();
+ }
+ if ($name === 'map_lahan') {
+ $table->integer('petani_id')->nullable();
+ $table->integer('profile_id')->nullable();
+ $table->integer('desa_kelurahan_id');
+ $table->text('path');
+ $table->float('luas_lahan');
+ $table->integer('jumlah_tanaman');
+ } else {
+ $table->integer('lahan_id');
+ }
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+ Schema::create('praktik_budidaya', function (Blueprint $table) {
+ $table->id();
+ $table->integer('petani_id');
+ $table->integer('frekuensi_pemangkasan_utama');
+ $table->integer('pemberian_pupuk');
+ $table->text('kebiasaan_konservasi_tanah');
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ DB::table('master_komoditas')->insert(['id' => 1, 'nama' => 'Kakao']);
+ DB::table('petani')->insert(['id' => 1, 'nama' => 'Petani A']);
+ DB::table('profiles')->insert(['id' => 1, 'nama' => 'Pemilik B']);
+ $land = [
+ 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'luas_lahan' => 1.5,
+ 'jumlah_tanaman' => 50, 'umur_tanaman' => 8,
+ 'path' => '[[[-8.1,121.1],[-8.2,121.1],[-8.1,121.2]]]',
+ 'jenis_hama' => '["Penggerek buah"]',
+ ];
+ DB::table('map_lahan')->insert($land + ['id' => 1, 'petani_id' => 1]);
+ DB::table('map_lahan')->insert($land + ['id' => 2, 'profile_id' => 1]);
+ DB::table('map_lahan')->insert($land + ['id' => 3, 'petani_id' => 1, 'deleted_at' => '2026-01-01']);
+ DB::table('map_tanaman')->insert([
+ 'lahan_id' => 1, 'komoditas_id' => 1, 'umur_tanaman' => 4,
+ 'jenis_penyakit' => '["Busuk buah"]',
+ ]);
+ foreach ([
+ ['2026-01-01', 1, null],
+ ['2026-02-01', 4, null],
+ ['2026-03-01', 5, '2026-03-02'],
+ ] as [$updatedAt, $value, $deletedAt]) {
+ DB::table('praktik_budidaya')->insert([
+ 'petani_id' => 1, 'frekuensi_pemangkasan_utama' => $value, 'pemberian_pupuk' => $value,
+ 'kebiasaan_konservasi_tanah' => '["Menumpuk daun kering/sisa tanaman di sekitar pohon"]',
+ 'updated_at' => $updatedAt, 'deleted_at' => $deletedAt,
+ ]);
+ }
+ $controller = app(SummaryController::class);
+ $method = new ReflectionMethod($controller, 'productionMapLevelData');
+ $this->mapLevels = fn (array $filters = []) => $method->invoke($controller, new Request($filters));
+});
+
+test('land map combines plants and OPT with the latest active cultivation practice', function () {
+ $lands = collect(($this->mapLevels)()['lahan'])->keyBy('id');
+
+ expect($lands->keys()->all())->toBe([1, 2]);
+ expect($lands[1])->toMatchArray([
+ 'nama_petani' => 'Petani A',
+ 'luas' => 1.5,
+ 'komoditas' => [['nama' => 'Kakao', 'jumlah' => 1, 'umur' => 4.0]],
+ 'opt' => ['Busuk Buah', 'Penggerek Buah'],
+ 'pemangkasan' => FrekuensiPemangkasanUtama::DUA_KALI_TAHUN->label(),
+ 'pemupukan' => PemberianPupuk::JENIS_DOSIS_SESUAI_WAKTU_CUKUP_TEPAT->label(),
+ 'praktik_baik' => ['Menumpuk daun kering/sisa tanaman di sekitar pohon'],
+ ]);
+ expect($lands[2])->toMatchArray([
+ 'nama_petani' => 'Pemilik B',
+ 'komoditas' => [['nama' => 'Kakao', 'jumlah' => 50, 'umur' => 8.0]],
+ 'pemangkasan' => 'Belum ada data',
+ 'pemupukan' => 'Belum ada data',
+ 'praktik_baik' => [],
+ ]);
+});
+
+test('combined map respects region and commodity filters', function () {
+ expect(($this->mapLevels)(['desaKelurahan_id' => 1, 'komoditas_id' => 1])['lahan'])->toHaveCount(2);
+ expect(($this->mapLevels)(['desaKelurahan_id' => 99])['lahan'])->toBe([]);
+ expect(($this->mapLevels)(['komoditas_id' => 99])['lahan'])->toBe([]);
+});