update dashboard produksi

This commit is contained in:
2026-10-16 10:04:56 +07:00
parent 4370d3c3d0
commit 60d32a88e9
4 changed files with 318 additions and 389 deletions
+39 -318
View File
@@ -758,7 +758,7 @@
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Peta Sebaran</h5>
<p class="text-body-secondary mb-0">Lahan, tanaman, dan OPT berdasarkan filter wilayah.</p>
<p class="text-body-secondary mb-0">Lahan, tanaman, OPT, dan praktik budidaya berdasarkan filter wilayah.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-down"></i>
@@ -773,8 +773,8 @@
<div class="card-body">
<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>
<h6 class="mb-0">Peta Sebaran Lahan, Tanaman & Praktik Budidaya</h6>
<small class="text-body-secondary">Lahan, tanaman, OPT, pemangkasan, pemupukan, dan praktik baik berdasarkan filter wilayah, komoditas, dan segmen</small>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary active produksi-map-layer-btn" data-map-layer="satellite">Satellite</button>
@@ -795,34 +795,6 @@
</div>
</div>
</div>
<div class="col-12">
<div class="card h-100 produksi-chart-card">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-2">
<div>
<h6 class="mb-0">Peta Praktik Budidaya</h6>
<small class="text-body-secondary">Intensitas pemangkasan, pemupukan, dan kebiasaan praktik baik</small>
</div>
<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 produksi-cultivation-map-layer-btn" data-map-layer="topo">Topographic</button>
<button type="button" class="btn btn-outline-secondary produksi-cultivation-map-layer-btn" data-map-layer="parcels">Parcels</button>
</div>
</div>
<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">
<i class="icon-base ti tabler-arrow-left me-1"></i>
Kembali ke Atas
</button>
<div id="produksi-budidaya-map-empty" class="produksi-map-empty">Belum ada poligon lahan untuk filter ini.</div>
<div class="produksi-map-information-overlay" tabindex="0" role="region" aria-label="Informasi peta praktik budidaya">
<div class="produksi-map-information" id="produksi-budidaya-map-information"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -1044,8 +1016,8 @@
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Praktik Fermentasi atau Pengolahan Pascapanen</h5>
<small class="text-body-secondary">Apakah sudah ada praktik fermentasi atau pengolahan pascapanen yang dilakukan</small>
<h5 class="m-0 me-2">Praktik Fermentasi atau Pengolahan Pascapanen per Komoditas</h5>
<small class="text-body-secondary">Jumlah petani per komoditas berdasarkan praktik fermentasi atau pengolahan pascapanen.</small>
</div>
</div>
<div class="card-body">
@@ -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 () {
<span class="produksi-map-information-section-title">Jumlah Petani / Komoditas</span>
<div class="produksi-map-information-bars">${renderBars(commodityRows, 'Belum ada data komoditas.', 'petani')}</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.', 'lahan')}</div>
</section>
` : ''}
<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.', 'lahan')}</div>
</section>
`;
}
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 => `<span class="produksi-opt-chip">${escapeHtml(value)}</span>`).join('')
: '<span class="text-body-secondary small">Belum ada data OPT</span>';
const praktikBaik = Array.isArray(item.praktik_baik) && item.praktik_baik.length
? item.praktik_baik.map(value => `<li>${escapeHtml(value)}</li>`).join('')
: '<li>Belum ada data praktik baik</li>';
return `
<div class="produksi-lahan-popup">
@@ -1449,6 +1413,19 @@ document.addEventListener('DOMContentLoaded', function () {
<span class="produksi-popup-label">OPT</span>
<div class="produksi-opt-list">${opt}</div>
</div>
<div class="mt-3">
<span class="produksi-popup-label">Praktik Budidaya</span>
</div>
<div class="mb-3">
<span class="produksi-popup-label">Intensitas Pemangkasan</span>
<span class="produksi-popup-value">${escapeHtml(item.pemangkasan || 'Belum ada data')}</span>
</div>
<div class="mb-3">
<span class="produksi-popup-label">Intensitas Pemupukan</span>
<span class="produksi-popup-value">${escapeHtml(item.pemupukan || 'Belum ada data')}</span>
</div>
<span class="produksi-popup-label">Kebiasaan praktik baik</span>
<ul class="produksi-praktik-list">${praktikBaik}</ul>
</div>
</div>
`;
@@ -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 => `<li>${escapeHtml(value)}</li>`).join('')
: '<li>Belum ada data praktik baik</li>';
return `
<div class="produksi-lahan-popup">
<div class="produksi-lahan-popup-header">
<span class="produksi-popup-label">Pemilik Lahan</span>
<h6>${escapeHtml(item.nama_petani || 'N/A')}</h6>
</div>
<div class="produksi-lahan-popup-body">
<div class="row g-2 mb-3">
<div class="col-6">
<span class="produksi-popup-label">Luas Lahan</span>
<span class="produksi-popup-value">${formatNumber(item.luas || 0, 2)} Ha</span>
</div>
<div class="col-6">
<span class="produksi-popup-label">ID Lahan</span>
<span class="produksi-popup-value">TR-${escapeHtml(item.id)}</span>
</div>
</div>
<div class="mb-3">
<span class="produksi-popup-label">Intensitas Pemangkasan</span>
<span class="produksi-popup-value">${escapeHtml(item.pemangkasan || 'Belum ada data')}</span>
</div>
<div class="mb-3">
<span class="produksi-popup-label">Intensitas Pemupukan</span>
<span class="produksi-popup-value">${escapeHtml(item.pemupukan || 'Belum ada data')}</span>
</div>
<span class="produksi-popup-label">Kebiasaan praktik baik</span>
<ul class="produksi-praktik-list">${praktikBaik}</ul>
</div>
</div>
`;
}
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'));