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.

@@ -1044,8 +1016,8 @@
-
Praktik Fermentasi atau Pengolahan Pascapanen
- Apakah sudah ada praktik fermentasi atau pengolahan pascapanen yang dilakukan +
Praktik Fermentasi atau Pengolahan Pascapanen per Komoditas
+ Jumlah petani per komoditas berdasarkan praktik fermentasi atau pengolahan pascapanen.
@@ -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 `
    @@ -1449,6 +1413,19 @@ document.addEventListener('DOMContentLoaded', function () { OPT
    ${opt}
    +
    + Praktik Budidaya +
    +
    + Intensitas Pemangkasan + ${escapeHtml(item.pemangkasan || 'Belum ada data')} +
    +
    + Intensitas Pemupukan + ${escapeHtml(item.pemupukan || 'Belum ada data')} +
    + Kebiasaan praktik baik +
    `; @@ -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 ` -
    -
    - Pemilik Lahan -
    ${escapeHtml(item.nama_petani || 'N/A')}
    -
    -
    -
    -
    - Luas Lahan - ${formatNumber(item.luas || 0, 2)} Ha -
    -
    - ID Lahan - TR-${escapeHtml(item.id)} -
    -
    -
    - Intensitas Pemangkasan - ${escapeHtml(item.pemangkasan || 'Belum ada data')} -
    -
    - Intensitas Pemupukan - ${escapeHtml(item.pemupukan || 'Belum ada data')} -
    - Kebiasaan praktik baik - -
    -
    - `; - } - - 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([]); +});