This commit is contained in:
2026-10-15 15:14:57 +07:00
3 changed files with 519 additions and 27 deletions
+352 -27
View File
@@ -241,6 +241,151 @@
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);
overscroll-behavior: contain;
overflow-y: auto;
padding: .875rem;
pointer-events: auto;
position: absolute;
right: 1rem;
top: 1rem;
width: min(350px, calc(100% - 5.5rem));
z-index: 700;
}
.produksi-map-information-overlay:focus-visible {
outline: 2px solid var(--bs-primary, #7367f0);
outline-offset: 2px;
}
.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 {
display: none;
left: 50px;
@@ -479,6 +624,13 @@
left: 16px;
top: 16px;
}
.produksi-map-information-overlay {
left: auto;
right: .75rem;
top: 1rem;
width: min(320px, calc(100% - 1.5rem));
}
}
</style>
@endpush
@@ -571,7 +723,7 @@
<div class="row g-4 mb-4">
@foreach ($kpiCards as $card)
<div class="col-xl-4 col-md-4">
<div class="col-12 col-sm-6 col-xl-4">
<div class="card card-border-shadow-{{ $card['variant'] }} h-100 produksi-kpi-card">
<div class="card-body">
<div class="d-flex align-items-center gap-4">
@@ -606,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, OPT, dan praktik budidaya berdasarkan filter wilayah.</p>
<p class="text-body-secondary mb-0">Lahan, tanaman, dan OPT 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>
@@ -618,51 +770,55 @@
<div class="row g-4 mb-4">
<div class="col-12">
<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-title mb-0">
<h5 class="m-0 me-2">Peta Sebaran</h5>
<small class="text-body-secondary">Lahan dan tanaman 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>
<button type="button" class="btn btn-outline-secondary produksi-map-layer-btn" data-map-layer="topo">Topographic</button>
<button type="button" class="btn btn-outline-secondary produksi-map-layer-btn" data-map-layer="parcels">Parcels</button>
</div>
</div>
<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>
</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>
<button type="button" class="btn btn-outline-secondary produksi-map-layer-btn" data-map-layer="topo">Topographic</button>
<button type="button" class="btn btn-outline-secondary produksi-map-layer-btn" data-map-layer="parcels">Parcels</button>
</div>
</div>
<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">
<i class="icon-base ti tabler-arrow-left me-1"></i>
Kembali ke Atas
</button>
<div id="produksi-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 sebaran">
<div class="produksi-map-information" id="produksi-map-information"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-12">
<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-title mb-0">
<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 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 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>
@@ -921,6 +1077,13 @@
document.addEventListener('DOMContentLoaded', function () {
const chartData = @json($chartData);
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 wilayah = @json($wilayah);
const headingColor = config.colors.headingColor;
const legendColor = config.colors.bodyColor;
@@ -933,6 +1096,7 @@ document.addEventListener('DOMContentLoaded', function () {
let productionMapRendered = false;
let cultivationMapRendered = false;
let actualProductivityRendered = false;
const mapInformationRequestIds = { production: 0, cultivation: 0 };
function hasData(data) {
return data && Array.isArray(data.series) && data.series.some(function (value) {
@@ -950,6 +1114,130 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
function renderProductionMapInformation(elementId, information, includeOpt = true) {
const element = document.getElementById(elementId);
if (!element) return;
const commodityRows = Array.isArray(information.petani_per_komoditas)
? information.petani_per_komoditas
: [];
const optRows = Array.isArray(information.opt) ? information.opt : [];
const indexValue = information.indeks === null || information.indeks === undefined
? '-'
: `${formatNumber(information.indeks, 2)} / 5`;
const renderBars = function (rows, emptyLabel, unitLabel) {
if (!rows.length) {
return `<div class="produksi-map-information-empty">${emptyLabel}</div>`;
}
const maxValue = Math.max(...rows.map(row => Number(row.jumlah || 0)), 1);
return rows.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)} ${unitLabel}</strong>
</div>
<div class="produksi-map-information-track" aria-hidden="true">
<div class="produksi-map-information-fill" style="width: ${width}%"></div>
</div>
</div>
`;
}).join('');
};
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(information.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-pin"></i></span>
<div>
<div class="produksi-map-information-summary-value">${formatNumber(information.jumlah_lahan || 0, 0)}</div>
<div class="produksi-map-information-summary-label">Jumlah Lahan</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(information.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(information.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(information.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.', '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>
` : ''}
`;
}
async function loadProductionMapInformation(mapType, level = null, id = null) {
const requestId = ++mapInformationRequestIds[mapType];
const params = new URLSearchParams();
Object.entries(dashboardFilters || {}).forEach(function ([key, value]) {
if (value !== null && value !== undefined && value !== '') {
params.set(key, value);
}
});
if (level && id !== null && id !== undefined) {
params.set('level', level);
params.set('id', id);
}
try {
const response = await fetch(`${mapInformationUrl}?${params.toString()}`, {
headers: { Accept: 'application/json' },
});
if (!response.ok) return;
const information = await response.json();
if (requestId !== mapInformationRequestIds[mapType]) return;
Object.assign(mapInformation[mapType], information);
if (mapType === 'production') {
renderProductionMapInformation('produksi-map-information', mapInformation.production);
} else {
renderProductionMapInformation('produksi-budidaya-map-information', mapInformation.cultivation, false);
}
} catch (error) {
console.error('Gagal memuat informasi wilayah pada peta.', error);
}
}
function toGroupedBarData(data, seriesName) {
if (!data || !Array.isArray(data.labels) || !Array.isArray(data.series)) {
return data;
@@ -1172,10 +1460,23 @@ document.addEventListener('DOMContentLoaded', function () {
empty: null,
group: null,
history: [],
informationId: null,
informationLevel: null,
layers: {},
map: null,
};
function enableMapInformationInteraction(mapElement) {
const informationOverlay = mapElement.querySelector('.produksi-map-information-overlay');
if (!informationOverlay) return;
L.DomEvent.disableScrollPropagation(informationOverlay);
L.DomEvent.disableClickPropagation(informationOverlay);
informationOverlay.addEventListener('click', function () {
informationOverlay.focus({ preventScroll: true });
});
}
function initProductionMap() {
const element = document.getElementById('produksi-sebaran-map');
@@ -1201,6 +1502,7 @@ document.addEventListener('DOMContentLoaded', function () {
productionMapState.group = L.featureGroup().addTo(productionMapState.map);
productionMapState.backButton = document.getElementById('produksi-map-back');
productionMapState.empty = document.getElementById('produksi-map-empty');
enableMapInformationInteraction(element);
L.control.zoom({ position: 'topleft' }).addTo(productionMapState.map);
@@ -1209,6 +1511,9 @@ document.addEventListener('DOMContentLoaded', function () {
if (previous) {
renderProductionMapLevel(previous.level, previous.parentId);
productionMapState.informationLevel = previous.informationLevel;
productionMapState.informationId = previous.informationId;
loadProductionMapInformation('production', previous.informationLevel, previous.informationId);
if (previous.bounds?.isValid()) {
productionMapState.map.fitBounds(previous.bounds, { padding: [30, 30] });
@@ -1296,7 +1601,12 @@ document.addEventListener('DOMContentLoaded', function () {
level: nextLevel,
parentId,
bounds: productionMapState.group.getBounds(),
informationLevel: productionMapState.informationLevel,
informationId: productionMapState.informationId,
});
productionMapState.informationLevel = nextLevel;
productionMapState.informationId = item.id;
loadProductionMapInformation('production', nextLevel, item.id);
renderProductionMapLevel(nextProductionMapLevel(nextLevel), item.id, event.target.getBounds());
});
}
@@ -1389,6 +1699,8 @@ document.addEventListener('DOMContentLoaded', function () {
empty: null,
group: null,
history: [],
informationId: null,
informationLevel: null,
layers: {},
map: null,
};
@@ -1418,6 +1730,7 @@ document.addEventListener('DOMContentLoaded', function () {
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);
@@ -1426,6 +1739,9 @@ document.addEventListener('DOMContentLoaded', function () {
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] });
@@ -1513,7 +1829,12 @@ document.addEventListener('DOMContentLoaded', function () {
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());
});
}
@@ -1955,6 +2276,9 @@ document.addEventListener('DOMContentLoaded', function () {
cultivationMapRendered = true;
}
renderProductionMapInformation('produksi-map-information', mapInformation.production);
renderProductionMapInformation('produksi-budidaya-map-information', mapInformation.cultivation, false);
if (typeof L === 'undefined') {
const mapEmpty = document.getElementById('produksi-map-empty');
const cultivationMapEmpty = document.getElementById('produksi-budidaya-map-empty');
@@ -2041,9 +2365,10 @@ document.addEventListener('DOMContentLoaded', function () {
const control = document.querySelector('.chart-group-header-toggle[aria-controls="' + panel.id + '"]');
if (!control) return;
const icon = control.querySelector('.chart-group-icon-toggle i');
const groupLabel = panel.id === 'peta-sebaran-panel' ? 'peta' : 'chart';
const syncState = function () {
const expanded = panel.classList.contains('show');
const label = expanded ? 'Sembunyikan chart' : 'Tampilkan chart';
const label = expanded ? `Sembunyikan ${groupLabel}` : `Tampilkan ${groupLabel}`;
control.setAttribute('aria-expanded', String(expanded));
control.setAttribute('aria-label', label);
control.title = label;