feat: slicing laporan capaian bps rw bulanan

main-dlh
shola 2025-12-01 13:35:51 +07:00
parent 6ea542ae6d
commit a1cb3b776d
No known key found for this signature in database
GPG Key ID: FA9358FFDCCD05D9
4 changed files with 889 additions and 14 deletions

View File

@ -0,0 +1,153 @@
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
namespace BpsRwApp.Controllers
{
[Route("[controller]/[action]")]
public class LaporanCapaianBulananController : AppControllerBase
{
public IActionResult Index()
{
return View();
}
[HttpGet]
public IActionResult GetData(string? bulanAwal, string? bulanAkhir)
{
var culture = new CultureInfo("id-ID");
// Default date range
var startDate = string.IsNullOrEmpty(bulanAwal)
? DateTime.Now.AddMonths(-1)
: DateTime.Parse(bulanAwal + "-01");
var endDate = string.IsNullOrEmpty(bulanAkhir)
? DateTime.Now
: DateTime.Parse(bulanAkhir + "-01");
// Periode text
string periode = startDate.ToString("MMMM", culture);
if (!(startDate.Year == endDate.Year && startDate.Month == endDate.Month))
periode += " - " + endDate.ToString("MMMM yyyy", culture);
// Labels bulan
var labels = new List<string>();
var cursor = new DateTime(startDate.Year, startDate.Month, 1);
var last = new DateTime(endDate.Year, endDate.Month, 1);
while (cursor <= last)
{
labels.Add(cursor.ToString("MMMM", culture));
cursor = cursor.AddMonths(1);
}
int count = labels.Count;
// seed data
int Seed(DateTime date) => date.Year * 100 + date.Month;
var rand = new Random(Seed(startDate));
// Kepatuhan PJLP
var kepatuhanCeklis = Enumerable.Range(0, count).Select(i => 85 + (i * 2)).ToArray();
var kepatuhanBelumCeklis = Enumerable.Range(0, count).Select(i => 15 - (i * 1)).ToArray();
// Rumah Memilah
int baseKonsisten = 5;
int baseTidak = 12;
int target = 18;
var rmKonsisten = new List<int>();
var rmTidak = new List<int>();
var rmTarget = new List<int>();
for (int i = 0; i < count; i++)
{
rmKonsisten.Add(baseKonsisten + i);
rmTidak.Add(baseTidak - (i / 2));
rmTarget.Add(target);
}
// SATPEL
var satpel1Sudah = Enumerable.Range(0, count).Select(i => 100 + (i * 10)).ToArray();
var satpel1Belum = Enumerable.Range(0, count).Select(i => 25 - (i * 2)).ToArray();
var satpel2Sudah = Enumerable.Range(0, count).Select(i => 90 + (i * 12)).ToArray();
var satpel2Belum = Enumerable.Range(0, count).Select(i => 28 - (i * 2)).ToArray();
// Volume Sampah
var mudah = Enumerable.Range(0, count).Select(i => 20000 + (i * 800)).ToArray();
var daur = Enumerable.Range(0, count).Select(i => 8000 + (i * 600)).ToArray();
var b3 = Enumerable.Range(0, count).Select(i => 3000 + (i * 50)).ToArray();
var residu = Enumerable.Range(0, count).Select(i => 8000 + (i * 200)).ToArray();
var data = new
{
periode,
labels,
kepatuhanPjlp = new
{
title = "Tingkat Kepatuhan PJLP",
subtitle = "Tingkat Kepatuhan PJLP Pendamping BPS RW dalam Menjalankan Instruksi Kepala Dinas",
location = "DKI Jakarta",
labels,
ceklis = kepatuhanCeklis,
belumCeklis = kepatuhanBelumCeklis
},
capaianRumah = new
{
title = "Capaian Rumah Memilah",
subtitle = $"Capaian Rumah Memilah Periode {periode}",
location = "DKI Jakarta",
labels,
rumahMemilahKonsisten = rmKonsisten,
rumahMemilahTidakKonsisten = rmTidak,
target = rmTarget
},
validasiSatpel = new[]
{
new
{
validator = "SATPEL",
title = "Status Validasi SATPEL",
subtitle = $"Status Validasi SATPEL Periode {periode}",
labels,
sudah = satpel1Sudah,
belum = satpel1Belum,
description = labels.Select(l =>
$"Periode bulan {l} terdapat {rand.Next(8, 15)} aktifitas ceklis yang tidak tervalidasi"
).ToArray()
},
new
{
validator = "SATPEL",
title = "Status Validasi SATPEL",
subtitle = $"Status Validasi SATPEL Periode {periode}",
labels,
sudah = satpel2Sudah,
belum = satpel2Belum,
description = labels.Select(l =>
$"Periode bulan {l} terdapat {rand.Next(8, 15)} aktifitas ceklis yang tidak tervalidasi"
).ToArray()
}
},
volumeSampah = new
{
title = "Volume Sampah",
subtitle = $"Volume Sampah Periode {periode}",
labels,
mudahTerurai = mudah,
materialDaur = daur,
b3,
residu
}
};
return Json(data);
}
}
}

View File

@ -16,7 +16,7 @@
<div class="flex justify-end items-center gap-2"> <div class="flex justify-end items-center gap-2">
<span class="whitespace-nowrap text-sm font-medium text-gray-700">Filter Bulan:</span> <span class="whitespace-nowrap text-sm font-medium text-gray-700">Filter Bulan:</span>
<input type="month" id="filterBulan" class="px-4 py-2 border bg-white rounded-md w-[180px] sm:w-[200px]" <input type="month" id="filterBulan" class="px-4 py-2 border bg-white rounded-md w-[180px] sm:w-[200px]"
min="2000-01" max="@DateTime.Now.ToString("yyyy-MM")" value="@DateTime.Now.ToString("yyyy-MM")" /> min="2020-01" max="@DateTime.Now.ToString("yyyy-MM")" value="@DateTime.Now.ToString("yyyy-MM")" />
</div> </div>
</div> </div>
@ -91,14 +91,7 @@
<script> <script>
Chart.register(ChartDataLabels); Chart.register(ChartDataLabels);
Chart.defaults.animation = { Chart.defaults.animation = { duration: 1500, easing: 'easeOutQuart' };
duration: 1500,
easing: 'easeOutQuart'
};
Chart.defaults.animations = {
numbers: { duration: 1500, easing: 'easeOutQuart' },
colors: { duration: 1500, easing: 'easeOutQuart' }
};
let chartInstances = { let chartInstances = {
pieTotal: null, pieTotal: null,
@ -200,7 +193,7 @@
`; `;
detailPjlpContainer.appendChild(card); detailPjlpContainer.appendChild(card);
// Buat chart untuk wilayah ini // chart wilayah
const ctx = document.getElementById(`pie_${wilayahId}`); const ctx = document.getElementById(`pie_${wilayahId}`);
if (ctx) { if (ctx) {
chartInstances.piePerWilayah[wilayahId] = new Chart(ctx, { chartInstances.piePerWilayah[wilayahId] = new Chart(ctx, {

View File

@ -0,0 +1,710 @@
@{
ViewData["Title"] = "Capaian BPS RW DKI Terhadap Bulan Sebelumnya";
}
<div class="breadcrumbs text-sm">
<ul>
<li class="text-gray-500"><a>Laporan</a></li>
<li>Capaian BPS RW DKI Terhadap Bulan Sebelumnya</li>
</ul>
</div>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div class="prose">
<h3 class="mb-2">Capaian BPS RW DKI Terhadap Bulan Sebelumnya</h3>
</div>
<div class="justify-self-end lg:self-center">
<button class="btn rounded-full bg-white" type="button" onclick="modal_filter.showModal()">
<span class="icon icon-fill me-2">filter_list</span>
Filter
</button>
</div>
</div>
<div class="h-6"></div>
<!-- Grid 2 Kolom untuk Tingkat Kepatuhan dan Capaian Rumah -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Tingkat Kepatuhan PJLP -->
<div class="bg-white rounded-sm shadow p-6">
<h3 class="text-lg font-bold mb-1" id="titleKepatuhanPjlp">Tingkat Kepatuhan PJLP</h3>
<p class="text-md font-medium mb-1" id="subtitleKepatuhanPjlp">Tingkat Kepatuhan PJLP Pendamping BPS RW dalam Menjalankan Instruksi Kepala Dinas</p>
<p class="text-sm text-gray-800 mb-4" id="locationKepatuhanPjlp">DKI Jakarta</p>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full h-[400px]">
<canvas id="chartKepatuhanPjlp"></canvas>
</div>
</div>
</div>
<!-- Capaian Rumah Memilah -->
<div class="bg-white rounded-sm shadow p-6">
<h3 class="text-lg font-bold mb-1" id="titleCapaianRumah">Capaian Rumah Memilah</h3>
<p class="text-md font-medium mb-1" id="subtitleCapaianRumah">Capaian rumah memilah periode Mei-Oktober 2025</p>
<p class="text-sm text-gray-800 mb-4" id="locationCapaianRumah">DKI Jakarta</p>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full h-[400px]">
<canvas id="chartCapaianRumah"></canvas>
</div>
</div>
</div>
</div>
<!-- Status Validasi SATPEL -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6" id="satpelContainer"></div>
<!-- Capaian Volume Pemilahan Sampah -->
<div class="bg-white rounded-sm shadow p-6 mt-6">
<h3 class="text-lg font-bold mb-1">Capaian Volume Pemilahan Sampah</h3>
<p class="text-md font-medium mb-4">Capaian Volume Pemilahan Sampah dari Sumber / Rumah Tangga Bulan Mei Oktober 2025</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6" id="volumeSampahContainer"></div>
</div>
<!-- Filter Modal -->
<dialog id="modal_filter" class="modal modal-bottom sm:modal-middle">
<div class="modal-box w-full sm:max-w-sm">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold">Filter</h3>
<button type="button" class="btn btn-sm btn-circle btn-ghost" onclick="modal_filter.close()">✕</button>
</div>
<form id="filterForm" action="#" method="get">
<div class="grid grid-cols-2 gap-4">
<fieldset class="fieldset">
<legend class="fieldset-legend">Bulan Awal</legend>
<input type="month" id="bulanAwal" class="input w-full" name="bulanAwal" placeholder="Pilih bulan" min="2020-01" required>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Bulan Akhir</legend>
<input type="month" id="bulanAkhir" class="input w-full" name="bulanAkhir" placeholder="Pilih bulan" required>
</fieldset>
</div>
<div id="errorMessage" class="text-red-500 text-sm mt-2 hidden"></div>
<div class="modal-action">
<button type="button" class="btn" onclick="clearFilter()">Bersihkan</button>
<button type="submit" class="btn btn-neutral">Terapkan Filter</button>
</div>
</form>
</div>
</dialog>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.5.0/chart.umd.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/3.0.1/chartjs-plugin-annotation.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js"></script>
<script>
Chart.register(ChartDataLabels);
Chart.defaults.animation = { duration: 1500, easing: 'easeOutQuart' };
Chart.defaults.animations = {
numbers: { duration: 1500, easing: 'easeOutQuart' },
colors: { duration: 1500, easing: 'easeOutQuart' }
};
const chartInstances = {
kepatuhanPjlp: null,
capaianRumah: null,
satpel: {},
volumeSampah: {}
};
function updateCharts(data) {
// Destroy existing charts
[chartInstances.kepatuhanPjlp, chartInstances.capaianRumah].forEach(c => c?.destroy());
Object.values(chartInstances.satpel).forEach(c => c?.destroy());
Object.values(chartInstances.volumeSampah).forEach(c => c?.destroy());
chartInstances.satpel = {};
chartInstances.volumeSampah = {};
if (data.bulanAwal) document.getElementById('bulanAwal').value = data.bulanAwal;
if (data.bulanAkhir) document.getElementById('bulanAkhir').value = data.bulanAkhir;
renderCharts(data);
}
function renderCharts(data) {
const mapFields = (target, source) => {
target.title.textContent = source.title;
target.subtitle.textContent = source.subtitle;
target.location.textContent = source.location;
};
mapFields({
title: document.getElementById('titleKepatuhanPjlp'),
subtitle: document.getElementById('subtitleKepatuhanPjlp'),
location: document.getElementById('locationKepatuhanPjlp')
}, data.kepatuhanPjlp);
mapFields({
title: document.getElementById('titleCapaianRumah'),
subtitle: document.getElementById('subtitleCapaianRumah'),
location: document.getElementById('locationCapaianRumah')
}, data.capaianRumah);
// Kepatuhan PJLP Chart
const kepatuhanDatasets = [
{
label: 'Ceklis',
data: data.kepatuhanPjlp.ceklis,
backgroundColor: '#12B76A',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Belum Ceklis',
data: data.kepatuhanPjlp.belumCeklis,
backgroundColor: '#F04438',
barPercentage: 0.9,
categoryPercentage: 0.8
}
];
// lines for Kepatuhan PJLP
const kepatuhanAnnotations = {};
if (data.kepatuhanPjlp.labels.length > 1) {
const numBars = 2;
const categoryPercentage = 0.8;
const barWidth = categoryPercentage / numBars;
const offsetCeklis = -barWidth / 2;
const offsetBelumCeklis = barWidth / 2;
data.kepatuhanPjlp.labels.forEach((label, idx) => {
if (idx < data.kepatuhanPjlp.labels.length - 1) {
kepatuhanAnnotations[`lineCeklis${idx}`] = {
type: 'line',
xMin: idx + offsetCeklis,
xMax: idx + 1 + offsetCeklis,
yMin: data.kepatuhanPjlp.ceklis[idx],
yMax: data.kepatuhanPjlp.ceklis[idx + 1],
borderColor: '#84CC16',
borderWidth: 2,
borderDash: [5, 5]
};
kepatuhanAnnotations[`lineBelumCeklis${idx}`] = {
type: 'line',
xMin: idx + offsetBelumCeklis,
xMax: idx + 1 + offsetBelumCeklis,
yMin: data.kepatuhanPjlp.belumCeklis[idx],
yMax: data.kepatuhanPjlp.belumCeklis[idx + 1],
borderColor: '#EF4444',
borderWidth: 2,
borderDash: [5, 5]
};
}
});
}
chartInstances.kepatuhanPjlp = new Chart(document.getElementById('chartKepatuhanPjlp'), {
type: 'bar',
data: {
labels: data.kepatuhanPjlp.labels,
datasets: kepatuhanDatasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: {
padding: {
top: 20
}
},
scales: {
x: { grid: { offset: false } },
y: {
beginAtZero: true,
grace: '5%'
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
generateLabels: (chart) => {
const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
if (data.kepatuhanPjlp.labels.length > 1) {
original.push(
{
text: 'Linear Ceklis',
fillStyle: 'transparent',
strokeStyle: '#84CC16',
lineWidth: 2,
lineDash: [5, 5],
pointStyle: 'line'
},
{
text: 'Linear Belum Ceklis',
fillStyle: 'transparent',
strokeStyle: '#EF4444',
lineWidth: 2,
lineDash: [5, 5],
pointStyle: 'line'
}
);
}
return original;
}
}
},
datalabels: {
anchor: 'end',
align: 'top',
color: '#444',
font: { size: 10 }
},
annotation: {
annotations: kepatuhanAnnotations
}
}
}
});
// Capaian Rumah Chart
const capaianRumahDatasets = [
{
label: 'Rumah Memilah Konsisten',
data: data.capaianRumah.rumahMemilahKonsisten,
backgroundColor: '#84CC16',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Rumah Memilah Tidak Konsisten',
data: data.capaianRumah.rumahMemilahTidakKonsisten,
backgroundColor: '#FB923C',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Target',
data: data.capaianRumah.target,
backgroundColor: '#EF4444',
barPercentage: 0.9,
categoryPercentage: 0.8
}
];
// lines for trend - using pixel-based positioning
const capaianAnnotations = {};
if (data.capaianRumah.labels.length > 1) {
const numBars = 3;
const barPercentage = 0.9;
const categoryPercentage = 0.8;
const barWidth = categoryPercentage / numBars;
const offsetKonsisten = -barWidth;
const offsetTidakKonsisten = 0;
data.capaianRumah.labels.forEach((label, idx) => {
if (idx < data.capaianRumah.labels.length - 1) {
capaianAnnotations[`lineKonsisten${idx}`] = {
type: 'line',
xMin: idx + offsetKonsisten,
xMax: idx + 1 + offsetKonsisten,
yMin: data.capaianRumah.rumahMemilahKonsisten[idx],
yMax: data.capaianRumah.rumahMemilahKonsisten[idx + 1],
borderColor: '#84CC16',
borderWidth: 2,
borderDash: [5, 5]
};
capaianAnnotations[`lineTidakKonsisten${idx}`] = {
type: 'line',
xMin: idx + offsetTidakKonsisten,
xMax: idx + 1 + offsetTidakKonsisten,
yMin: data.capaianRumah.rumahMemilahTidakKonsisten[idx],
yMax: data.capaianRumah.rumahMemilahTidakKonsisten[idx + 1],
borderColor: '#FB923C',
borderWidth: 2,
borderDash: [5, 5]
};
}
});
}
chartInstances.capaianRumah = new Chart(document.getElementById('chartCapaianRumah'), {
type: 'bar',
data: {
labels: data.capaianRumah.labels,
datasets: capaianRumahDatasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: {
padding: {
top: 20
}
},
scales: {
x: {
grid: { offset: false }
},
y: {
beginAtZero: true,
grace: '5%'
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
generateLabels: (chart) => {
const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
// custom legend items for trend lines
if (data.capaianRumah.labels.length > 1) {
original.push(
{
text: 'Linear Rumah Memilah Konsisten',
fillStyle: 'transparent',
strokeStyle: '#84CC16',
lineWidth: 2,
lineDash: [5, 5],
pointStyle: 'line'
},
{
text: 'Linear Rumah Memilah Tidak Konsisten',
fillStyle: 'transparent',
strokeStyle: '#FB923C',
lineWidth: 2,
lineDash: [5, 5],
pointStyle: 'line'
}
);
}
return original;
}
}
},
datalabels: {
anchor: 'end',
align: 'top',
color: '#444',
font: { size: 10 }
},
annotation: {
annotations: capaianAnnotations
}
}
}
});
// SATPEL Charts
const satpelContainer = document.getElementById('satpelContainer');
satpelContainer.innerHTML = '';
data.validasiSatpel.forEach((item, i) => {
const card = document.createElement('div');
card.className = 'bg-white rounded-sm shadow p-6';
card.innerHTML = `
<h3 class="text-lg font-bold mb-1">${item.title}</h3>
<p class="text-md font-medium mb-4">${item.subtitle}</p>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full h-[400px]"><canvas id="satpel_${i}"></canvas></div>
</div>
<div class="mt-4 p-4 bg-gray-100 rounded-lg">
${item.description.map((desc, idx) =>
`<div class="text-sm">${idx + 1}. ${desc}</div>`
).join('')}
</div>
`;
satpelContainer.appendChild(card);
const satpelDatasets = [
{
label: 'Sudah',
data: item.sudah,
backgroundColor: '#84CC16',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Belum',
data: item.belum,
backgroundColor: '#FB923C',
barPercentage: 0.9,
categoryPercentage: 0.8
}
];
// lines for SATPEL
const satpelAnnotations = {};
if (item.labels.length > 1) {
const numBars = 2;
const categoryPercentage = 0.8;
const barWidth = categoryPercentage / numBars;
const offsetSudah = -barWidth / 2;
const offsetBelum = barWidth / 2;
item.labels.forEach((label, idx) => {
if (idx < item.labels.length - 1) {
satpelAnnotations[`lineSudah${idx}`] = {
type: 'line',
xMin: idx + offsetSudah,
xMax: idx + 1 + offsetSudah,
yMin: item.sudah[idx],
yMax: item.sudah[idx + 1],
borderColor: '#84CC16',
borderWidth: 2,
borderDash: [5, 5]
};
}
});
}
chartInstances.satpel[i] = new Chart(document.getElementById(`satpel_${i}`), {
type: 'bar',
data: {
labels: item.labels,
datasets: satpelDatasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: {
padding: {
top: 20
}
},
scales: {
x: { grid: { offset: false } },
y: {
beginAtZero: true,
grace: '5%'
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
generateLabels: (chart) => {
const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
if (item.labels.length > 1) {
original.push({
text: 'Linear Sudah',
fillStyle: 'transparent',
strokeStyle: '#84CC16',
lineWidth: 2,
lineDash: [5, 5],
pointStyle: 'line'
});
}
return original;
}
}
},
datalabels: {
anchor: 'end',
align: 'top',
color: '#444',
font: { size: 10 }
},
annotation: {
annotations: satpelAnnotations
}
}
}
});
});
// Volume Sampah Charts
const volumeSampahContainer = document.getElementById('volumeSampahContainer');
volumeSampahContainer.innerHTML = '';
const categories = [
{ key: 'mudahTerurai', title: 'Mudah Terurai', color: '#16A34A' },
{ key: 'materialDaur', title: 'Material Daur', color: '#EAB308' },
{ key: 'b3', title: 'B3', color: '#DC2626' },
{ key: 'residu', title: 'Residu', color: '#475467' }
];
categories.forEach((cat, i) => {
const card = document.createElement('div');
card.className = 'border border-gray-400 rounded-sm p-4';
card.innerHTML = `
<div class="mb-3">
<span class="inline-block px-3 py-1 text-sm font-semibold rounded" style="background-color: ${cat.color}; color: white;">${cat.title}</span>
</div>
<div class="w-full h-[400px]"><canvas id="volumeSampah_${i}"></canvas></div>
`;
volumeSampahContainer.appendChild(card);
const values = data.volumeSampah[cat.key];
const min = Math.min(...values), max = Math.max(...values);
const bgColors = values.map(v => {
const opacity = min === max ? 1.0 : 0.4 + (0.6 * (v - min) / (max - min));
const [r, g, b] = [cat.color.slice(1,3), cat.color.slice(3,5), cat.color.slice(5,7)].map(x => parseInt(x, 16));
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
});
chartInstances.volumeSampah[i] = new Chart(document.getElementById(`volumeSampah_${i}`), {
type: 'bar',
data: {
labels: data.volumeSampah.labels,
datasets: [{ label: cat.title, data: values, backgroundColor: bgColors }]
},
options: {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 20
}
},
scales: {
x: { grid: { display: false } },
y: {
beginAtZero: true,
grace: '5%'
}
},
plugins: {
legend: { display: false },
datalabels: {
color: '#333',
anchor: 'end',
align: 'top',
font: { size: 10 }
}
}
}
});
});
}
const getCurrentMonth = () => new Date().toISOString().slice(0, 7);
const getDefaultDateRange = () => {
const today = new Date();
const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
return {
start: lastMonth.toISOString().slice(0, 7),
end: getCurrentMonth()
};
};
const getErrorMsg = () => document.getElementById('errorMessage');
const getBulanAwalInput = () => document.getElementById('bulanAwal');
const getBulanAkhirInput = () => document.getElementById('bulanAkhir');
function loadDataByDateRange(startDate, endDate) {
const params = new URLSearchParams();
if (startDate) params.append('bulanAwal', startDate);
if (endDate) params.append('bulanAkhir', endDate);
fetch(`/LaporanCapaianBulanan/GetData?${params.toString()}`, {
headers: { 'Accept': 'application/json' }
})
.then(res => res.json())
.then(updateCharts)
.catch(err => console.error('Gagal memuat data:', err));
}
function setErrorMessage(message) {
const errorMsg = getErrorMsg();
if (message) {
errorMsg.textContent = message;
errorMsg.classList.remove('hidden');
} else {
errorMsg.classList.add('hidden');
}
}
function validateDateRange(startDate, endDate) {
const currentMonth = getCurrentMonth();
if (!startDate || !endDate) {
setErrorMessage('Bulan awal dan akhir harus diisi');
return false;
}
if (startDate < '2020-01') {
setErrorMessage('Bulan awal minimal tahun 2020');
return false;
}
if (startDate > currentMonth) {
setErrorMessage('Bulan awal tidak boleh lebih besar dari bulan berjalan');
return false;
}
if (endDate < startDate) {
setErrorMessage('Bulan akhir tidak boleh lebih kecil dari bulan awal');
return false;
}
if (endDate > currentMonth) {
setErrorMessage('Bulan akhir tidak boleh lebih besar dari bulan berjalan');
return false;
}
setErrorMessage('');
return true;
}
function updateDateInputConstraints() {
const currentMonth = getCurrentMonth();
const bulanAwalInput = getBulanAwalInput();
const bulanAkhirInput = getBulanAkhirInput();
bulanAwalInput.max = currentMonth;
if (bulanAwalInput.value) {
bulanAkhirInput.min = bulanAwalInput.value;
}
bulanAkhirInput.max = currentMonth;
}
function setDefaultValues() {
const defaultRange = getDefaultDateRange();
getBulanAwalInput().value = defaultRange.start;
getBulanAkhirInput().value = defaultRange.end;
updateDateInputConstraints();
}
function clearFilter() {
setDefaultValues();
setErrorMessage('');
modal_filter.close();
const defaultRange = getDefaultDateRange();
loadDataByDateRange(defaultRange.start, defaultRange.end);
}
document.addEventListener('DOMContentLoaded', () => {
setDefaultValues();
const defaultRange = getDefaultDateRange();
loadDataByDateRange(defaultRange.start, defaultRange.end);
document.getElementById('filterForm').addEventListener('submit', (e) => {
e.preventDefault();
const startDate = getBulanAwalInput().value;
const endDate = getBulanAkhirInput().value;
if (validateDateRange(startDate, endDate)) {
loadDataByDateRange(startDate, endDate);
modal_filter.close();
}
});
// Validasi filter
getBulanAwalInput().addEventListener('change', () => {
updateDateInputConstraints();
const startDate = getBulanAwalInput().value;
const endDate = getBulanAkhirInput().value;
if (startDate && endDate) validateDateRange(startDate, endDate);
});
getBulanAkhirInput().addEventListener('change', () => {
const startDate = getBulanAwalInput().value;
const endDate = getBulanAkhirInput().value;
if (startDate && endDate) validateDateRange(startDate, endDate);
});
});
</script>

View File

@ -53,7 +53,8 @@
</li> </li>
<li> <li>
<details @(new[] { "DataKecamatanRumahMemilah", "DataKecamatanChecklistHarian" }.Contains(controller) ? "open" : "")> <details @(new[] { "DataKecamatanRumahMemilah", "DataKecamatanChecklistHarian" }.Contains(controller) ?
"open" : "")>
<summary>DATA KECAMATAN</summary> <summary>DATA KECAMATAN</summary>
<ul> <ul>
<li> <li>
@ -167,7 +168,9 @@
</details> </details>
</li> </li>
<li> <li>
<details @(new[] { "RincianTargetRumahMemilah", "VolumeTimbulanSampah", "LaporanCapaian", "RealisasiTerhadapTarget" }.Contains(controller) ? "open" : "")> <details @(new[] { "RincianTargetRumahMemilah", "VolumeTimbulanSampah", "LaporanCapaian",
"RealisasiTerhadapTarget","LaporanCapaianBulanan","LaporanCapaianWilayah" }.Contains(controller) ?
"open" : "")>
<summary>LAPORAN</summary> <summary>LAPORAN</summary>
<ul> <ul>
<li> <li>
@ -187,9 +190,24 @@
<li> <li>
<a asp-controller="LaporanCapaian" asp-action="Index" <a asp-controller="LaporanCapaian" asp-action="Index"
class="@(controller == "LaporanCapaian" ? "menu-active" : "")"> class="@(controller == "LaporanCapaian" ? "menu-active" : "")">
<span class="icon icon-fill">flag</span> <span class="icon icon-fill">map</span>
Laporan Capaian BPS RW DKI Jakarta Laporan Capaian BPS RW DKI Jakarta
</a> </a>
<li>
<li>
<a asp-controller="LaporanCapaianBulanan" asp-action="Index"
class="@(controller == "LaporanCapaianBulanan" ? "menu-active" : "")">
<span class="icon icon-fill">trending_up</span>
Laporan Capaian BPS RW DKI Terhadap Bulan Sebelumnya
</a>
</li>
<li>
<a asp-controller="LaporanCapaianWilayah" asp-action="Index"
class="@(controller == "LaporanCapaianWilayah" ? "menu-active" : "")">
<span class="icon icon-fill">leaderboard</span>
Laporan Capaian BPS RW per Wilayah
</a>
</li>
<li> <li>
<a asp-controller="RealisasiTerhadapTarget" asp-action="Index" <a asp-controller="RealisasiTerhadapTarget" asp-action="Index"
class="@(controller == "RealisasiTerhadapTarget" ? "menu-active" : "")"> class="@(controller == "RealisasiTerhadapTarget" ? "menu-active" : "")">
@ -201,7 +219,8 @@
</details> </details>
</li> </li>
<li> <li>
<details @(new[] { "DataRw", "DataRt", "DataBankSampah", "DataKecamatan" }.Contains(controller) ? "open" : "")> <details @(new[] { "DataRw", "DataRt", "DataBankSampah", "DataKecamatan" }.Contains(controller) ? "open" :
"")>
<summary>DATA MASTER</summary> <summary>DATA MASTER</summary>
<ul> <ul>
<li> <li>