perubahan query

This commit is contained in:
2026-10-15 13:02:42 +07:00
parent 9d56b67998
commit 9b5c0a7309
3 changed files with 145 additions and 33 deletions
@@ -55,6 +55,48 @@ class SummaryController extends Controller
] + $filterData + $this->productionDashboardData($request)); ] + $filterData + $this->productionDashboardData($request));
} }
public function mapInformation(Request $request)
{
if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) {
abort(403);
}
$validated = $request->validate([
'level' => ['nullable', 'in:provinsi,kabupaten,kecamatan,desa'],
'id' => ['required_with:level', 'integer'],
'provinsi_id' => ['nullable', 'integer'],
'kabupatenKota_id' => ['nullable', 'integer'],
'kecamatan_id' => ['nullable', 'integer'],
'desaKelurahan_id' => ['nullable', 'integer'],
'komoditas_id' => ['nullable', 'integer'],
'segmen' => ['nullable', 'string'],
]);
$filters = [
'provinsi_id' => null,
'kabupatenKota_id' => null,
'kecamatan_id' => null,
'desaKelurahan_id' => null,
'komoditas_id' => $validated['komoditas_id'] ?? null,
'segmen' => $validated['segmen'] ?? null,
];
$regionFilter = match ($validated['level'] ?? null) {
'provinsi' => 'provinsi_id',
'kabupaten' => 'kabupatenKota_id',
'kecamatan' => 'kecamatan_id',
'desa' => 'desaKelurahan_id',
default => null,
};
if ($regionFilter) {
$filters[$regionFilter] = $validated['id'];
}
$scopedRequest = Request::create('/dashboard/produktivitas/map-information', 'GET', $filters);
return response()->json($this->productionMapInformationData($scopedRequest));
}
public function pasar(Request $request) public function pasar(Request $request)
{ {
if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) { if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) {
@@ -181,6 +223,7 @@ class SummaryController extends Controller
$healthSourceName = $tanaman->isNotEmpty() ? 'tanaman' : 'lahan'; $healthSourceName = $tanaman->isNotEmpty() ? 'tanaman' : 'lahan';
$landMetrics = $this->karakteristikLandMetrics($request); $landMetrics = $this->karakteristikLandMetrics($request);
$totalPetani = $landMetrics['total_petani'];
$totalLuasLahan = $landMetrics['total_luas_lahan']; $totalLuasLahan = $landMetrics['total_luas_lahan'];
$avgLuasPerPetani = $landMetrics['ratarata_luas_lahan']; $avgLuasPerPetani = $landMetrics['ratarata_luas_lahan'];
$actualProductivity = $this->actualProductivityByCommodityChart($request); $actualProductivity = $this->actualProductivityByCommodityChart($request);
@@ -190,6 +233,14 @@ class SummaryController extends Controller
$aksesPasar = $this->filteredAksesPasar($request); $aksesPasar = $this->filteredAksesPasar($request);
$kpiCards = [ $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, ',', '.'), 'value' => number_format($totalLuasLahan, 2, ',', '.'),
'unit' => 'ha', 'unit' => 'ha',
@@ -268,34 +319,34 @@ class SummaryController extends Controller
private function productionMapInformationData(Request $request): array private function productionMapInformationData(Request $request): array
{ {
$landMetrics = $this->karakteristikLandMetrics($request); $filteredMapLahan = (clone $this->filteredLahanQuery($request))
$filteredPetaniIds = $this->filteredPetaniIdsQuery($request);
$commodityRows = DB::table('petani')
->whereNull('petani.deleted_at')
->whereIn('petani.id', clone $filteredPetaniIds)
->leftJoin('master_komoditas', 'master_komoditas.id', '=', 'petani.komoditas_id')
->selectRaw("COALESCE(master_komoditas.nama, 'Belum ditentukan') as nama")
->selectRaw('COUNT(DISTINCT petani.id) as jumlah')
->groupBy('master_komoditas.nama')
->orderByDesc('jumlah')
->get()
->map(fn ($row) => ['nama' => $row->nama, 'jumlah' => (int) $row->jumlah])
->all();
$lahan = (clone $this->filteredLahanQuery($request))
->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id') ->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id')
->leftJoin('petani', 'petani.id', '=', 'map_lahan.petani_id') ->leftJoin('petani', 'petani.id', '=', 'map_lahan.petani_id')
->tap(fn ($query) => $this->whereActiveMapOwner($query)) ->tap(fn ($query) => $this->whereActiveMapOwner($query));
->get([ $mapLahanRows = (clone $filteredMapLahan)->get([
'map_lahan.id', 'map_lahan.id',
'map_lahan.petani_id',
'map_lahan.luas_lahan',
'map_lahan.serangan_hama', 'map_lahan.serangan_hama',
'map_lahan.penyakit_tanaman', 'map_lahan.penyakit_tanaman',
'map_lahan.jenis_hama', 'map_lahan.jenis_hama',
'map_lahan.jenis_penyakit', 'map_lahan.jenis_penyakit',
]); ]);
$petaniIds = $mapLahanRows->pluck('petani_id')->filter()->unique()->values();
$commodityRows = (clone $filteredMapLahan)
->leftJoin('master_komoditas as map_komoditas', 'map_komoditas.id', '=', 'map_lahan.komoditas_id')
->whereNotNull('map_lahan.petani_id')
->selectRaw("COALESCE(map_komoditas.nama, 'Belum ditentukan') as nama")
->selectRaw('COUNT(DISTINCT map_lahan.petani_id) as jumlah')
->groupBy('map_komoditas.nama')
->orderByDesc('jumlah')
->get()
->map(fn ($row) => ['nama' => $row->nama, 'jumlah' => (int) $row->jumlah])
->all();
$tanamanByLahan = DB::table('map_tanaman') $tanamanByLahan = DB::table('map_tanaman')
->whereNull('map_tanaman.deleted_at') ->whereNull('map_tanaman.deleted_at')
->whereIn('map_tanaman.lahan_id', $lahan->pluck('id')) ->whereIn('map_tanaman.lahan_id', $mapLahanRows->pluck('id'))
->get([ ->get([
'map_tanaman.lahan_id', 'map_tanaman.lahan_id',
'map_tanaman.serangan_hama', 'map_tanaman.serangan_hama',
@@ -306,7 +357,7 @@ class SummaryController extends Controller
->groupBy('lahan_id'); ->groupBy('lahan_id');
$optCounts = []; $optCounts = [];
foreach ($lahan as $land) { foreach ($mapLahanRows as $land) {
foreach ($this->productionOptDetails($land, $tanamanByLahan->get($land->id, collect())) as $opt) { foreach ($this->productionOptDetails($land, $tanamanByLahan->get($land->id, collect())) as $opt) {
$optCounts[$opt] = ($optCounts[$opt] ?? 0) + 1; $optCounts[$opt] = ($optCounts[$opt] ?? 0) + 1;
} }
@@ -318,14 +369,18 @@ class SummaryController extends Controller
->where('is_utama', true) ->where('is_utama', true)
->whereNull('deleted_at') ->whereNull('deleted_at')
->select('id'); ->select('id');
$scorePetaniIds = DB::table('petani')
->whereIn('petani.id', $petaniIds)
->whereIn('petani.komoditas_id', $mainCommodityIds)
->select('petani.id')
->pluck('petani.id');
$scoreQuery = DB::table('petani') $scoreQuery = DB::table('petani')
->leftJoin('petani_skor', function ($join) { ->leftJoin('petani_skor', function ($join) {
$join->on('petani_skor.petani_id', '=', 'petani.id') $join->on('petani_skor.petani_id', '=', 'petani.id')
->whereNull('petani_skor.deleted_at'); ->whereNull('petani_skor.deleted_at');
}) })
->whereNull('petani.deleted_at') ->whereNull('petani.deleted_at')
->whereIn('petani.id', clone $filteredPetaniIds) ->whereIn('petani.id', $scorePetaniIds);
->whereIn('petani.komoditas_id', $mainCommodityIds);
$averageIndex = (clone $scoreQuery)->avg('petani_skor.skor_kesiapan_adopsi'); $averageIndex = (clone $scoreQuery)->avg('petani_skor.skor_kesiapan_adopsi');
$segmentCase = "CASE $segmentCase = "CASE
WHEN petani_skor.skor_kesiapan_adopsi >= 4 THEN 'Penggerak' WHEN petani_skor.skor_kesiapan_adopsi >= 4 THEN 'Penggerak'
@@ -362,8 +417,8 @@ class SummaryController extends Controller
}; };
return [ return [
'jumlah_petani' => $landMetrics['total_petani'], 'jumlah_petani' => $petaniIds->count(),
'total_luas_lahan' => $landMetrics['total_luas_lahan'], 'total_luas_lahan' => (float) $mapLahanRows->sum('luas_lahan'),
'petani_per_komoditas' => $commodityRows, 'petani_per_komoditas' => $commodityRows,
'opt' => collect($optCounts) 'opt' => collect($optCounts)
->map(fn ($jumlah, $nama) => ['nama' => $nama, 'jumlah' => $jumlah]) ->map(fn ($jumlah, $nama) => ['nama' => $nama, 'jumlah' => $jumlah])
@@ -717,7 +717,7 @@
<div class="row g-4 mb-4"> <div class="row g-4 mb-4">
@foreach ($kpiCards as $card) @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 card-border-shadow-{{ $card['variant'] }} h-100 produksi-kpi-card">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center gap-4"> <div class="d-flex align-items-center gap-4">
@@ -764,12 +764,12 @@
<div class="row g-4 mb-4"> <div class="row g-4 mb-4">
<div class="col-12"> <div class="col-12">
<div class="card h-100 produksi-chart-card"> <div class="card h-100 produksi-chart-card">
<div class="card-header"> <!-- <div class="card-header">
<div class="card-title mb-0"> <div class="card-title mb-0">
<h5 class="m-0 me-2">Peta Sebaran & Praktik Budidaya</h5> <h5 class="m-0 me-2">Peta Sebaran & Praktik Budidaya</h5>
<small class="text-body-secondary">Petani, luas lahan, komoditas, OPT, dan praktik budidaya berdasarkan filter aktif</small> <small class="text-body-secondary">Petani, luas lahan, komoditas, OPT, dan praktik budidaya berdasarkan filter aktif</small>
</div> </div>
</div> </div> -->
<div class="card-body"> <div class="card-body">
<div class="d-flex flex-column gap-4"> <div class="d-flex flex-column gap-4">
<section> <section>
@@ -1077,6 +1077,8 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const chartData = @json($chartData); const chartData = @json($chartData);
const mapData = @json($mapData); const mapData = @json($mapData);
const dashboardFilters = @json($dashboardFilters);
const mapInformationUrl = @json(route('dashboard.produktivitas.map-information'));
const mapInformation = mapData.information || {}; const mapInformation = mapData.information || {};
const wilayah = @json($wilayah); const wilayah = @json($wilayah);
const headingColor = config.colors.headingColor; const headingColor = config.colors.headingColor;
@@ -1090,6 +1092,7 @@ document.addEventListener('DOMContentLoaded', function () {
let productionMapRendered = false; let productionMapRendered = false;
let cultivationMapRendered = false; let cultivationMapRendered = false;
let actualProductivityRendered = false; let actualProductivityRendered = false;
let mapInformationRequestId = 0;
function hasData(data) { function hasData(data) {
return data && Array.isArray(data.series) && data.series.some(function (value) { return data && Array.isArray(data.series) && data.series.some(function (value) {
@@ -1119,7 +1122,7 @@ document.addEventListener('DOMContentLoaded', function () {
const indexValue = mapInformation.indeks === null || mapInformation.indeks === undefined const indexValue = mapInformation.indeks === null || mapInformation.indeks === undefined
? '-' ? '-'
: `${formatNumber(mapInformation.indeks, 2)} / 5`; : `${formatNumber(mapInformation.indeks, 2)} / 5`;
const renderBars = function (rows, emptyLabel) { const renderBars = function (rows, emptyLabel, unitLabel) {
if (!rows.length) { if (!rows.length) {
return `<div class="produksi-map-information-empty">${emptyLabel}</div>`; return `<div class="produksi-map-information-empty">${emptyLabel}</div>`;
} }
@@ -1134,7 +1137,7 @@ document.addEventListener('DOMContentLoaded', function () {
<div> <div>
<div class="produksi-map-information-bar-label"> <div class="produksi-map-information-bar-label">
<span>${escapeHtml(row.nama || 'Belum ditentukan')}</span> <span>${escapeHtml(row.nama || 'Belum ditentukan')}</span>
<strong>${formatNumber(value, 0)}</strong> <strong>${formatNumber(value, 0)} ${unitLabel}</strong>
</div> </div>
<div class="produksi-map-information-track" aria-hidden="true"> <div class="produksi-map-information-track" aria-hidden="true">
<div class="produksi-map-information-fill" style="width: ${width}%"></div> <div class="produksi-map-information-fill" style="width: ${width}%"></div>
@@ -1177,17 +1180,50 @@ document.addEventListener('DOMContentLoaded', function () {
</div> </div>
<section> <section>
<span class="produksi-map-information-section-title">Jumlah Petani / Komoditas</span> <span class="produksi-map-information-section-title">Jumlah Petani / Komoditas</span>
<div class="produksi-map-information-bars">${renderBars(commodityRows, 'Belum ada data komoditas.')}</div> <div class="produksi-map-information-bars">${renderBars(commodityRows, 'Belum ada data komoditas.', 'petani')}</div>
</section> </section>
${includeOpt ? ` ${includeOpt ? `
<section> <section>
<span class="produksi-map-information-section-title">OPT (Organisme Pengganggu Tanaman)</span> <span class="produksi-map-information-section-title">OPT (Organisme Pengganggu Tanaman)</span>
<div class="produksi-map-information-bars">${renderBars(optRows, 'Belum ada data OPT.')}</div> <div class="produksi-map-information-bars">${renderBars(optRows, 'Belum ada data OPT.', 'lahan')}</div>
</section> </section>
` : ''} ` : ''}
`; `;
} }
async function loadProductionMapInformation(level = null, id = null) {
const requestId = ++mapInformationRequestId;
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 !== mapInformationRequestId) return;
Object.assign(mapInformation, information);
renderProductionMapInformation('produksi-map-information');
renderProductionMapInformation('produksi-budidaya-map-information', false);
} catch (error) {
console.error('Gagal memuat informasi wilayah pada peta.', error);
}
}
function toGroupedBarData(data, seriesName) { function toGroupedBarData(data, seriesName) {
if (!data || !Array.isArray(data.labels) || !Array.isArray(data.series)) { if (!data || !Array.isArray(data.labels) || !Array.isArray(data.series)) {
return data; return data;
@@ -1410,6 +1446,8 @@ document.addEventListener('DOMContentLoaded', function () {
empty: null, empty: null,
group: null, group: null,
history: [], history: [],
informationId: null,
informationLevel: null,
layers: {}, layers: {},
map: null, map: null,
}; };
@@ -1447,6 +1485,9 @@ document.addEventListener('DOMContentLoaded', function () {
if (previous) { if (previous) {
renderProductionMapLevel(previous.level, previous.parentId); renderProductionMapLevel(previous.level, previous.parentId);
productionMapState.informationLevel = previous.informationLevel;
productionMapState.informationId = previous.informationId;
loadProductionMapInformation(previous.informationLevel, previous.informationId);
if (previous.bounds?.isValid()) { if (previous.bounds?.isValid()) {
productionMapState.map.fitBounds(previous.bounds, { padding: [30, 30] }); productionMapState.map.fitBounds(previous.bounds, { padding: [30, 30] });
@@ -1534,7 +1575,12 @@ document.addEventListener('DOMContentLoaded', function () {
level: nextLevel, level: nextLevel,
parentId, parentId,
bounds: productionMapState.group.getBounds(), bounds: productionMapState.group.getBounds(),
informationLevel: productionMapState.informationLevel,
informationId: productionMapState.informationId,
}); });
productionMapState.informationLevel = nextLevel;
productionMapState.informationId = item.id;
loadProductionMapInformation(nextLevel, item.id);
renderProductionMapLevel(nextProductionMapLevel(nextLevel), item.id, event.target.getBounds()); renderProductionMapLevel(nextProductionMapLevel(nextLevel), item.id, event.target.getBounds());
}); });
} }
@@ -1627,6 +1673,8 @@ document.addEventListener('DOMContentLoaded', function () {
empty: null, empty: null,
group: null, group: null,
history: [], history: [],
informationId: null,
informationLevel: null,
layers: {}, layers: {},
map: null, map: null,
}; };
@@ -1664,6 +1712,9 @@ document.addEventListener('DOMContentLoaded', function () {
if (previous) { if (previous) {
renderCultivationMapLevel(previous.level, previous.parentId); renderCultivationMapLevel(previous.level, previous.parentId);
cultivationMapState.informationLevel = previous.informationLevel;
cultivationMapState.informationId = previous.informationId;
loadProductionMapInformation(previous.informationLevel, previous.informationId);
if (previous.bounds?.isValid()) { if (previous.bounds?.isValid()) {
cultivationMapState.map.fitBounds(previous.bounds, { padding: [30, 30] }); cultivationMapState.map.fitBounds(previous.bounds, { padding: [30, 30] });
@@ -1751,7 +1802,12 @@ document.addEventListener('DOMContentLoaded', function () {
level: nextLevel, level: nextLevel,
parentId, parentId,
bounds: cultivationMapState.group.getBounds(), bounds: cultivationMapState.group.getBounds(),
informationLevel: cultivationMapState.informationLevel,
informationId: cultivationMapState.informationId,
}); });
cultivationMapState.informationLevel = nextLevel;
cultivationMapState.informationId = item.id;
loadProductionMapInformation(nextLevel, item.id);
renderCultivationMapLevel(nextProductionMapLevel(nextLevel), item.id, event.target.getBounds()); renderCultivationMapLevel(nextProductionMapLevel(nextLevel), item.id, event.target.getBounds());
}); });
} }
+1
View File
@@ -41,6 +41,7 @@ Route::middleware('auth')
// Home // Home
Route::get('/dashboard/produktivitas/map-information', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'mapInformation'])->name('dashboard.produktivitas.map-information');
Route::get('/dashboard/produktivitas', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'produktivitas'])->name('dashboard.produktivitas'); Route::get('/dashboard/produktivitas', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'produktivitas'])->name('dashboard.produktivitas');
Route::get('/dashboard/pasar', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'pasar'])->name('dashboard.pasar'); Route::get('/dashboard/pasar', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'pasar'])->name('dashboard.pasar');