feat: slicing laporan capaian bps rw bulanan
parent
6ea542ae6d
commit
a1cb3b776d
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
<div class="flex justify-end items-center gap-2">
|
||||
<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]"
|
||||
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>
|
||||
|
||||
|
|
@ -91,14 +91,7 @@
|
|||
<script>
|
||||
Chart.register(ChartDataLabels);
|
||||
|
||||
Chart.defaults.animation = {
|
||||
duration: 1500,
|
||||
easing: 'easeOutQuart'
|
||||
};
|
||||
Chart.defaults.animations = {
|
||||
numbers: { duration: 1500, easing: 'easeOutQuart' },
|
||||
colors: { duration: 1500, easing: 'easeOutQuart' }
|
||||
};
|
||||
Chart.defaults.animation = { duration: 1500, easing: 'easeOutQuart' };
|
||||
|
||||
let chartInstances = {
|
||||
pieTotal: null,
|
||||
|
|
@ -200,7 +193,7 @@
|
|||
`;
|
||||
detailPjlpContainer.appendChild(card);
|
||||
|
||||
// Buat chart untuk wilayah ini
|
||||
// chart wilayah
|
||||
const ctx = document.getElementById(`pie_${wilayahId}`);
|
||||
if (ctx) {
|
||||
chartInstances.piePerWilayah[wilayahId] = new Chart(ctx, {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -53,7 +53,8 @@
|
|||
</li>
|
||||
|
||||
<li>
|
||||
<details @(new[] { "DataKecamatanRumahMemilah", "DataKecamatanChecklistHarian" }.Contains(controller) ? "open" : "")>
|
||||
<details @(new[] { "DataKecamatanRumahMemilah", "DataKecamatanChecklistHarian" }.Contains(controller) ?
|
||||
"open" : "")>
|
||||
<summary>DATA KECAMATAN</summary>
|
||||
<ul>
|
||||
<li>
|
||||
|
|
@ -167,7 +168,9 @@
|
|||
</details>
|
||||
</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>
|
||||
<ul>
|
||||
<li>
|
||||
|
|
@ -187,9 +190,24 @@
|
|||
<li>
|
||||
<a asp-controller="LaporanCapaian" asp-action="Index"
|
||||
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
|
||||
</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>
|
||||
<a asp-controller="RealisasiTerhadapTarget" asp-action="Index"
|
||||
class="@(controller == "RealisasiTerhadapTarget" ? "menu-active" : "")">
|
||||
|
|
@ -201,7 +219,8 @@
|
|||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details @(new[] { "DataRw", "DataRt", "DataBankSampah", "DataKecamatan" }.Contains(controller) ? "open" : "")>
|
||||
<details @(new[] { "DataRw", "DataRt", "DataBankSampah", "DataKecamatan" }.Contains(controller) ? "open" :
|
||||
"")>
|
||||
<summary>DATA MASTER</summary>
|
||||
<ul>
|
||||
<li>
|
||||
|
|
|
|||
Loading…
Reference in New Issue