bps-rw/Views/LaporanCapaianBulanan/Index.cshtml

710 lines
28 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

@{
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="/lib/chart.js/chart.umd.js"></script>
<script src="/lib/chartjs-plugin-annotation/chartjs-plugin-annotation.min.js"></script>
<script src="/lib/chartjs-plugin-datalabels/chartjs-plugin-datalabels.min.js"></script>
<script>
Chart.register(ChartDataLabels);
Chart.defaults.animation = { duration: 500, easing: 'easeOutQuart' };
Chart.defaults.animations = {
numbers: { duration: 500, easing: 'easeOutQuart' },
colors: { duration: 500, 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>