bps-rw/Views/LaporanCapaianWilayah/Index.cshtml

1357 lines
57 KiB
Plaintext

@{
ViewData["Title"] = "Capaian BPS RW per Wilayah";
}
<div class="breadcrumbs text-sm">
<ul>
<li class="text-gray-500"><a>Laporan</a></li>
<li>Capaian BPS RW per Wilayah</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 per Wilayah</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>
<!-- Tingkat Kepatuhan PJLP -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-1">Tingkat Kepatuhan PJLP</h3>
<p class="text-md font-medium mb-1" id="descTingkatKepatuhan">Tingkat Kepatuhan PJLP Periode Mei-Oktober</p>
<p class="text-sm mb-4" id="locationTingkatKepatuhan">Kota Administrasi Jakarta Barat</p>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full" id="chartTingkatKepatuhanContainer">
<canvas id="chartTingkatKepatuhan"></canvas>
</div>
</div>
</div>
<!-- Status Validasi SATPEL -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-1">Status Validasi SATPEL</h3>
<p class="text-md font-medium mb-1" id="descStatusSatpel">Status Validasi SATPEL Periode Oktober</p>
<p class="text-sm mb-4" id="locationStatusSatpel">Kota Administrasi Jakarta Barat</p>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full min-h-80">
<canvas id="chartStatusSatpel"></canvas>
</div>
</div>
<div class="mt-4 p-4 bg-gray-100 rounded-lg">
<p class="text-sm font-semibold text-gray-700 mb-2">Terhadap status validasi:</p>
<div id="satpelValidationText" class="text-sm"></div>
</div>
</div>
</div>
<!-- Status Validasi SUDIN -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-1">Status Validasi SUDIN</h3>
<p class="text-md font-medium mb-1" id="descStatusSudin">Status Validasi SUDIN Periode Oktober</p>
<p class="text-sm mb-4" id="locationStatusSudin">Kota Administrasi Jakarta Barat</p>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full min-h-80">
<canvas id="chartStatusSudin"></canvas>
</div>
</div>
<div class="mt-4 p-4 bg-gray-100 rounded-lg">
<p class="text-sm font-semibold text-gray-700 mb-2">Terhadap status validasi:</p>
<div id="sudinValidationText" class="text-sm"></div>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-1">Capaian Rumah Memilah</h3>
<p class="text-md font-medium mb-1" id="descCapaianRumah">Capaian Rumah Memilah Periode Oktober 2025</p>
<p class="text-sm mb-4" id="locationCapaianRumah">Kota Administrasi Jakarta Barat</p>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full min-h-80">
<canvas id="chartCapaianRumah"></canvas>
</div>
</div>
<div class="mt-4 p-4 bg-gray-100 rounded-lg">
<p class="text-sm font-semibold text-gray-700 mb-2">Terhadap Tingkat kepatuhan dalam melaksanakan SK Kepala
Dinas LH Nomor e-0017 Tahun 2025 dan Instruksi Kepala Dinas LH Nomor e-0003 Tahun 2025, dalam
menjalankan program pertumbuhan rumah memilah di lingkup warga:</p>
<div id="capaianRumahText" class="text-sm"></div>
</div>
</div>
</div>
<!-- Volume Timbulan Sampah -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-1">Volume Timbulan Sampah</h3>
<p class="text-md font-medium mb-1" id="descVolumeSampah">Volume Timbulan Sampah Per Hari Per Jiwa Dalam Satu
Bulan Oktober</p>
<p class="text-sm mb-4" id="locationVolumeSampah">Kota Administrasi Jakarta Barat</p>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full" id="chartVolumeSampahContainer">
<canvas id="chartVolumeSampah"></canvas>
</div>
</div>
<div class="mt-4 p-4 bg-gray-100 rounded-lg">
<p class="text-sm text-gray-600" id="volumeSampahText">Terhadap total volume timbulan sampah organik hasil
pilahan 5367 rumah tangga memilah secara konsisten periode bulan Oktober sebanyak 28092,18 Kg.</p>
</div>
</div>
<!-- Realisasi Terhadap Target -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-1">Realisasi Terhadap Target</h3>
<p class="text-md font-medium mb-1" id="descRealisasi">Laporan Program Realisasi dan Realisasi Terhadap Target
Maret-Oktober</p>
<p class="text-sm mb-4" id="locationRealisasi">Kota Administrasi Jakarta Barat</p>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full min-h-80">
<canvas id="chartRealisasi"></canvas>
</div>
</div>
<div class="mt-4 p-4 bg-gray-100 rounded-lg">
<p class="text-sm font-semibold text-gray-700 mb-2">Terhadap realisasi target rumah memilah sebagai berikut:
</p>
<div id="realisasiTargetText" class="text-sm"></div>
<p class="text-sm text-gray-600 mt-3 font-medium">Dari diagram dapat dilihat bahwa terjadi progres
pertumbuhan rumah memilah disetiap bulannya.</p>
</div>
</div>
</div>
<!-- Tingkat Kepatuhan PJLP Periode -->
<div class="bg-white rounded-lg shadow-md p-6 mt-6">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div class="prose">
<h3 class="text-lg font-semibold mb-1" id="titleKepatuhanPeriode">Tingkat Kepatuhan PJLP Periode</h3>
<p class="text-md font-medium" id="subtitleKepatuhanPeriode">Tingkat Kepatuhan PJLP Periode</p>
<p class="text-sm mb-4" id="locationKepatuhanPeriode">Kota Administrasi Jakarta Barat</p>
</div>
<div class="flex justify-end items-start mb-4">
<input type="month" id="filterBulan" class="px-4 py-2 border bg-white rounded-md w-[180px] sm:w-[200px]"
min="2020-01" max="@DateTime.Now.ToString("yyyy-MM")" value="@DateTime.Now.ToString("yyyy-MM")" />
</div>
</div>
<div class="border border-gray-400 rounded-sm p-4">
<div class="w-full h-[400px]">
<canvas id="chartKepatuhanPeriode"></canvas>
</div>
</div>
<!-- PJLP Pendamping Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div class="border border-gray-400 rounded-lg p-4">
<p class="text-md text-gray-800 mb-2">Total PJLP Pendamping</p>
<div class="flex flex-row items-center gap-2">
<p class="text-3xl font-bold text-primary-500" id="totalPjlpPendamping">1210</p>
<p class="text-lg font-bold text-green-700" id="percenlPjlpPendamping">Pendamping</p>
</div>
</div>
<div class="border border-gray-400 rounded-lg p-4">
<p class="text-md text-gray-800 mb-2">PJLP Pendamping yang sudah melakukan Ceklis</p>
<div class="flex flex-row items-center gap-2">
<p class="text-3xl font-bold text-primary-500" id="sudahCeklisCount">605</p>
<p class="text-lg font-bold text-green-700" id="sudahCeklisPercent">Pendamping (50%)</p>
</div>
</div>
<div class="border border-gray-400 rounded-lg p-4">
<p class="text-md text-gray-800 mb-2">PJLP Pendamping yang belum melakukan Ceklis</p>
<div class="flex flex-row items-center gap-2">
<p class="text-3xl font-bold text-primary-500" id="belumCeklisCount">605</p>
<p class="text-lg font-bold text-green-700" id="belumCeklisPercent">Pendamping (50%)</p>
</div>
</div>
</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">
<fieldset class="fieldset mb-4">
<legend class="fieldset-legend">Wilayah</legend>
<select id="wilayahFilter" class="input w-full" name="wilayah">
<option value="Jakarta Pusat">Jakarta Pusat</option>
<option value="Jakarta Utara">Jakarta Utara</option>
<option value="Jakarta Barat" selected>Jakarta Barat</option>
<option value="Jakarta Timur">Jakarta Timur</option>
<option value="Jakarta Selatan">Jakarta Selatan</option>
<option value="Kepulauan Seribu">Kepulauan Seribu</option>
</select>
</fieldset>
<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' }
};
let chartInstances = {
tingkatKepatuhan: null,
statusSatpel: null,
statusSudin: null,
capaianRumah: null,
volumeSampah: null,
realisasi: null,
kepatuhanPeriode: null
};
function updateCharts(data) {
// Destroy existing charts
Object.keys(chartInstances).forEach(key => {
if (key !== 'kepatuhanPeriode' && chartInstances[key]) {
chartInstances[key].destroy();
}
});
// Update dynamic height for chartTingkatKepatuhan and chartVolumeSampah
if (data.countMonthLabels) {
const baseHeight = 320;
let dynamicHeight = baseHeight;
if (window.innerWidth >= 768) {
const additionalHeight = 70 + ((data.countMonthLabels - 2) * 23);
dynamicHeight = baseHeight + additionalHeight;
}
const containerTingkatKepatuhan = document.getElementById('chartTingkatKepatuhanContainer');
containerTingkatKepatuhan.style.minHeight = `${dynamicHeight}px`;
const containerVolumeSampah = document.getElementById('chartVolumeSampahContainer');
containerVolumeSampah.style.minHeight = `${dynamicHeight}px`;
}
renderCharts(data);
}
function updateKepatuhanPeriodeChart(data) {
// Destroy chart
if (chartInstances.kepatuhanPeriode) {
chartInstances.kepatuhanPeriode.destroy();
}
renderKepatuhanPeriodeChart(data);
}
function renderCharts(data) {
if (!data) {
console.error('No data provided to renderCharts');
return;
}
const periode = data.periode;
// Update filter
if (data.bulanAwal) document.getElementById('bulanAwal').value = data.bulanAwal;
if (data.bulanAkhir) document.getElementById('bulanAkhir').value = data.bulanAkhir;
if (data.wilayah) document.getElementById('wilayahFilter').value = data.wilayah;
// Update descriptions
document.getElementById('descTingkatKepatuhan').textContent = `Tingkat Kepatuhan PJLP Periode ${periode}`;
document.getElementById('descStatusSatpel').textContent = `Status Validasi SATPEL Periode ${periode}`;
document.getElementById('descStatusSudin').textContent = `Status Validasi SUDIN Periode ${periode}`;
document.getElementById('descCapaianRumah').textContent = `Capaian Rumah Memilah Periode ${periode}`;
document.getElementById('descVolumeSampah').textContent = `Volume Timbulan Sampah Per Hari Per Jiwa Dalam Satu Bulan ${periode}`;
document.getElementById('descRealisasi').textContent = `Laporan Program Realisasi dan Realisasi Terhadap Target ${periode}`;
// Chart Kepatuhan PJLP
const kepatuhanAnnotations = {};
// linear trend
if (data.tingkatKepatuhanPjlp && data.tingkatKepatuhanPjlp.length > 1) {
const numBars = 2;
const categoryPercentage = 0.8;
const barWidth = categoryPercentage / numBars;
const offsetCeklis = -barWidth / 2;
const offsetBelumCeklis = barWidth / 2;
data.tingkatKepatuhanPjlp.forEach((item, idx) => {
if (idx < data.tingkatKepatuhanPjlp.length - 1) {
kepatuhanAnnotations[`lineCeklis${idx}`] = {
type: 'line',
xMin: idx + offsetCeklis,
xMax: idx + 1 + offsetCeklis,
yMin: data.tingkatKepatuhanPjlp[idx].ceklis,
yMax: data.tingkatKepatuhanPjlp[idx + 1].ceklis,
borderColor: '#84CC16',
borderWidth: 2,
borderDash: [5, 5]
};
kepatuhanAnnotations[`lineBelumCeklis${idx}`] = {
type: 'line',
xMin: idx + offsetBelumCeklis,
xMax: idx + 1 + offsetBelumCeklis,
yMin: data.tingkatKepatuhanPjlp[idx].belumCeklis,
yMax: data.tingkatKepatuhanPjlp[idx + 1].belumCeklis,
borderColor: '#EF4444',
borderWidth: 2,
borderDash: [5, 5]
};
}
});
}
chartInstances.tingkatKepatuhan = new Chart(document.getElementById('chartTingkatKepatuhan'), {
type: 'bar',
data: {
labels: data.tingkatKepatuhanPjlp.map(x => x.bulan),
datasets: [
{
label: 'Ceklis',
data: data.tingkatKepatuhanPjlp.map(x => x.ceklis),
backgroundColor: '#12B76A',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Belum Ceklis',
data: data.tingkatKepatuhanPjlp.map(x => x.belumCeklis),
backgroundColor: '#F04438',
barPercentage: 0.9,
categoryPercentage: 0.8
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: {
padding: {
top: 20
}
},
scales: {
y: {
beginAtZero: true,
grace: '5%',
grid: { color: '#E5E7EB' }
},
x: {
grid: { offset: false }
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
pointStyle: "circle",
padding: 15,
generateLabels: (chart) => {
const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
if (data.tingkatKepatuhanPjlp && data.tingkatKepatuhanPjlp.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, weight: 'bold' }
},
annotation: {
annotations: kepatuhanAnnotations
}
}
}
});
// Chart Status Validasi SATPEL
const satpelAnnotations = {};
// linear trend
if (data.statusValidasiSatpel && data.statusValidasiSatpel.length > 1) {
const numBars = 2;
const categoryPercentage = 0.8;
const barWidth = categoryPercentage / numBars;
const offsetSudah = -barWidth / 2;
data.statusValidasiSatpel.forEach((item, idx) => {
if (idx < data.statusValidasiSatpel.length - 1) {
satpelAnnotations[`lineSudah${idx}`] = {
type: 'line',
xMin: idx + offsetSudah,
xMax: idx + 1 + offsetSudah,
yMin: data.statusValidasiSatpel[idx].sudah,
yMax: data.statusValidasiSatpel[idx + 1].sudah,
borderColor: '#84CC16',
borderWidth: 2,
borderDash: [5, 5]
};
}
});
}
chartInstances.statusSatpel = new Chart(document.getElementById('chartStatusSatpel'), {
type: 'bar',
data: {
labels: data.statusValidasiSatpel.map(x => x.bulan),
datasets: [
{
label: 'Sudah',
data: data.statusValidasiSatpel.map(x => x.sudah),
backgroundColor: '#12B76A',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Belum',
data: data.statusValidasiSatpel.map(x => x.belum),
backgroundColor: '#F04438',
barPercentage: 0.9,
categoryPercentage: 0.8
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: {
padding: {
top: 20
}
},
scales: {
y: {
beginAtZero: true,
grace: '5%',
grid: { color: '#E5E7EB' }
},
x: {
grid: { offset: false }
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
pointStyle: "circle",
padding: 15,
generateLabels: (chart) => {
const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
if (data.statusValidasiSatpel && data.statusValidasiSatpel.length > 1) {
original.push({
text: 'Linear Sudah',
fillStyle: 'transparent',
strokeStyle: '#12B76A',
lineWidth: 2,
lineDash: [5, 5],
pointStyle: 'line'
});
}
return original;
}
}
},
datalabels: {
anchor: 'end',
align: 'top',
color: '#444',
font: { size: 10, weight: 'bold' }
},
annotation: {
annotations: satpelAnnotations
}
}
}
});
// Chart Status Validasi SUDIN
const sudinAnnotations = {};
// linear trend
if (data.statusValidasiSudin && data.statusValidasiSudin.length > 1) {
const numBars = 2;
const categoryPercentage = 0.8;
const barWidth = categoryPercentage / numBars;
const offsetSudah = -barWidth / 2;
data.statusValidasiSudin.forEach((item, idx) => {
if (idx < data.statusValidasiSudin.length - 1) {
sudinAnnotations[`lineSudah${idx}`] = {
type: 'line',
xMin: idx + offsetSudah,
xMax: idx + 1 + offsetSudah,
yMin: data.statusValidasiSudin[idx].sudah,
yMax: data.statusValidasiSudin[idx + 1].sudah,
borderColor: '#84CC16',
borderWidth: 2,
borderDash: [5, 5]
};
}
});
}
chartInstances.statusSudin = new Chart(document.getElementById('chartStatusSudin'), {
type: 'bar',
data: {
labels: data.statusValidasiSudin.map(x => x.bulan),
datasets: [
{
label: 'Sudah',
data: data.statusValidasiSudin.map(x => x.sudah),
backgroundColor: '#84CC16',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Belum',
data: data.statusValidasiSudin.map(x => x.belum),
backgroundColor: '#FB923C',
barPercentage: 0.9,
categoryPercentage: 0.8
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: {
padding: {
top: 20
}
},
scales: {
y: {
beginAtZero: true,
grace: '5%',
grid: { color: '#E5E7EB' }
},
x: {
grid: { offset: false }
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
pointStyle: "circle",
padding: 15,
generateLabels: (chart) => {
const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
if (data.statusValidasiSudin && data.statusValidasiSudin.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, weight: 'bold' }
},
annotation: {
annotations: sudinAnnotations
}
}
}
});
// Chart Capaian Rumah Memilah
const capaianAnnotations = {};
// linear trend
if (data.capaianRumahMemilah && data.capaianRumahMemilah.length > 1) {
const numBars = 2;
const categoryPercentage = 0.8;
const barWidth = categoryPercentage / numBars;
const offsetKonsisten = -barWidth / 2;
const offsetTidakKonsisten = barWidth / 2;
data.capaianRumahMemilah.forEach((item, idx) => {
if (idx < data.capaianRumahMemilah.length - 1) {
capaianAnnotations[`lineKonsisten${idx}`] = {
type: 'line',
xMin: idx + offsetKonsisten,
xMax: idx + 1 + offsetKonsisten,
yMin: data.capaianRumahMemilah[idx].konsisten,
yMax: data.capaianRumahMemilah[idx + 1].konsisten,
borderColor: '#84CC16',
borderWidth: 2,
borderDash: [5, 5]
};
capaianAnnotations[`lineTidakKonsisten${idx}`] = {
type: 'line',
xMin: idx + offsetTidakKonsisten,
xMax: idx + 1 + offsetTidakKonsisten,
yMin: data.capaianRumahMemilah[idx].tidakKonsisten,
yMax: data.capaianRumahMemilah[idx + 1].tidakKonsisten,
borderColor: '#FB923C',
borderWidth: 2,
borderDash: [5, 5]
};
}
});
}
chartInstances.capaianRumah = new Chart(document.getElementById('chartCapaianRumah'), {
type: 'bar',
data: {
labels: data.capaianRumahMemilah.map(x => x.bulan),
datasets: [
{
label: 'Rumah Memilah Konsisten',
data: data.capaianRumahMemilah.map(x => x.konsisten),
backgroundColor: '#84CC16',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Rumah Memilah Tidak Konsisten',
data: data.capaianRumahMemilah.map(x => x.tidakKonsisten),
backgroundColor: '#FB923C',
barPercentage: 0.9,
categoryPercentage: 0.8
},
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: {
padding: {
top: 20
}
},
scales: {
y: {
beginAtZero: true,
grace: '5%',
grid: { color: '#E5E7EB' }
},
x: {
grid: { offset: false }
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
pointStyle: "circle",
padding: 15,
generateLabels: (chart) => {
const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
if (data.capaianRumahMemilah && data.capaianRumahMemilah.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, weight: 'bold' }
},
annotation: {
annotations: capaianAnnotations
}
}
}
});
// Chart Volume Timbulan Sampah
const volumeAnnotations = {};
// linear trend untuk Mudah Terurai
if (data.volumeTimbulanSampah && data.volumeTimbulanSampah.length > 1) {
const numBars = 2;
const categoryPercentage = 0.8;
const barWidth = categoryPercentage / numBars;
const offsetTerurai = -barWidth / 2;
data.volumeTimbulanSampah.forEach((item, idx) => {
if (idx < data.volumeTimbulanSampah.length - 1) {
volumeAnnotations[`lineTerurai${idx}`] = {
type: 'line',
xMin: idx + offsetTerurai,
xMax: idx + 1 + offsetTerurai,
yMin: data.volumeTimbulanSampah[idx].terurai,
yMax: data.volumeTimbulanSampah[idx + 1].terurai,
borderColor: '#84CC16',
borderWidth: 2,
borderDash: [5, 5]
};
}
});
}
chartInstances.volumeSampah = new Chart(document.getElementById('chartVolumeSampah'), {
type: 'bar',
data: {
labels: data.volumeTimbulanSampah.map(x => x.bulan),
datasets: [
{
label: 'Mudah Terurai',
data: data.volumeTimbulanSampah.map(x => x.terurai),
backgroundColor: '#84CC16',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Tidak Mudah Terurai',
data: data.volumeTimbulanSampah.map(x => x.tidakTerurai),
backgroundColor: '#FB923C',
barPercentage: 0.9,
categoryPercentage: 0.8
},
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: {
padding: {
top: 20
}
},
scales: {
y: {
beginAtZero: true,
grace: '5%',
grid: { color: '#E5E7EB' },
ticks: {
callback: function (value) {
return value.toFixed(2) + ' kg';
}
}
},
x: {
grid: { offset: false }
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
pointStyle: "circle",
padding: 15,
generateLabels: (chart) => {
const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
if (data.volumeTimbulanSampah && data.volumeTimbulanSampah.length > 1) {
original.push({
text: 'Linear Mudah Terurai',
fillStyle: 'transparent',
strokeStyle: '#84CC16',
lineWidth: 2,
lineDash: [5, 5],
pointStyle: 'line'
});
}
return original;
}
}
},
datalabels: {
anchor: 'end',
align: 'top',
color: '#444',
font: { size: 10, weight: 'bold' },
formatter: (value) => value.toFixed(2)
},
annotation: {
annotations: volumeAnnotations
}
}
}
});
// Chart Realisasi Terhadap Target
const realisasiAnnotations = {};
// linear trend
if (data.realisasiTarget && data.realisasiTarget.length > 1) {
const numBars = 3;
const categoryPercentage = 0.8;
const barWidth = categoryPercentage / numBars;
const offsetKonsisten = -barWidth;
const offsetTidakKonsisten = 0;
data.realisasiTarget.forEach((item, idx) => {
if (idx < data.realisasiTarget.length - 1) {
realisasiAnnotations[`lineKonsisten${idx}`] = {
type: 'line',
xMin: idx + offsetKonsisten,
xMax: idx + 1 + offsetKonsisten,
yMin: data.realisasiTarget[idx].rmKonsisten,
yMax: data.realisasiTarget[idx + 1].rmKonsisten,
borderColor: '#84CC16',
borderWidth: 2,
borderDash: [5, 5]
};
realisasiAnnotations[`lineTidakKonsisten${idx}`] = {
type: 'line',
xMin: idx + offsetTidakKonsisten,
xMax: idx + 1 + offsetTidakKonsisten,
yMin: data.realisasiTarget[idx].rmTidakKonsisten,
yMax: data.realisasiTarget[idx + 1].rmTidakKonsisten,
borderColor: '#F97316',
borderWidth: 2,
borderDash: [5, 5]
};
}
});
}
chartInstances.realisasi = new Chart(document.getElementById('chartRealisasi'), {
type: 'bar',
data: {
labels: data.realisasiTarget.map(x => x.bulan),
datasets: [
{
label: 'Rumah Memilah Konsisten',
data: data.realisasiTarget.map(x => x.rmKonsisten),
backgroundColor: '#84CC16',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Rumah Memilah Tidak Konsisten',
data: data.realisasiTarget.map(x => x.rmTidakKonsisten),
backgroundColor: '#F97316',
barPercentage: 0.9,
categoryPercentage: 0.8
},
{
label: 'Target',
data: data.realisasiTarget.map(x => x.target),
backgroundColor: '#C6C6C6',
barPercentage: 0.9,
categoryPercentage: 0.8
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: {
padding: {
top: 20
}
},
scales: {
y: {
beginAtZero: true,
grace: '5%',
grid: { color: '#E5E7EB' }
},
x: {
grid: { offset: false }
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
pointStyle: "circle",
padding: 15,
generateLabels: (chart) => {
const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
if (data.realisasiTarget && data.realisasiTarget.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: '#F97316',
lineWidth: 2,
lineDash: [5, 5],
pointStyle: 'line'
}
);
}
return original;
}
}
},
datalabels: {
anchor: 'end',
align: 'top',
color: '#444',
font: { size: 10, weight: 'bold' }
},
annotation: {
annotations: realisasiAnnotations
}
}
}
});
// Update Capaian Rumah Memilah Text
if (data.tingkatKepatuhanPjlp) {
const capaianRumahTextContainer = document.getElementById('capaianRumahText');
capaianRumahTextContainer.innerHTML = '';
data.tingkatKepatuhanPjlp.forEach((item, index) => {
const p = document.createElement('p');
if (item.belumCeklis === 0) {
p.textContent = `${index + 1}. Periode ${item.bulan} sebanyak ${item.ceklis} PJLP sudah menjalankan program.`;
} else {
p.textContent = `${index + 1}. Periode ${item.bulan} sebanyak ${item.ceklis} PJLP sudah menjalankan program, dan yang belum menjalankan program sebanyak ${item.belumCeklis} orang.`;
}
capaianRumahTextContainer.appendChild(p);
});
}
// Update SATPEL Validation Text
if (data.statusValidasiSatpel) {
const satpelTextContainer = document.getElementById('satpelValidationText');
satpelTextContainer.innerHTML = '';
data.statusValidasiSatpel.forEach((item, index) => {
const p = document.createElement('p');
if (item.belum === 0) {
p.textContent = `${index + 1}. Periode bulan ${item.bulan} sudah tervalidasi semua.`;
} else {
p.textContent = `${index + 1}. Periode bulan ${item.bulan} terdapat ${item.belum} belum tervalidasi.`;
}
satpelTextContainer.appendChild(p);
});
}
// Update SUDIN Validation Text
if (data.statusValidasiSudin) {
const sudinTextContainer = document.getElementById('sudinValidationText');
sudinTextContainer.innerHTML = '';
data.statusValidasiSudin.forEach((item, index) => {
const p = document.createElement('p');
if (item.belum === 0) {
p.textContent = `${index + 1}. Periode bulan ${item.bulan} sudah tervalidasi semua.`;
} else {
p.textContent = `${index + 1}. Periode bulan ${item.bulan} terdapat ${item.belum} aktifitas ceklis yang tidak tervalidasi (terlewat/diragukan kebenaran ceklis).`;
}
sudinTextContainer.appendChild(p);
});
}
// Update Volume Sampah Text
if (data.volumeSampahInfo) {
const volumeText = `Terhadap total volume timbulan sampah organik hasil pilahan ${data.volumeSampahInfo.jumlahRumah} rumah tangga memilah secara konsisten periode ${data.volumeSampahInfo.periode} sebanyak ${data.volumeSampahInfo.totalVolume} Kg.`;
document.getElementById('volumeSampahText').textContent = volumeText;
}
// Update Realisasi Target Text
if (data.realisasiTarget) {
const realisasiTextContainer = document.getElementById('realisasiTargetText');
realisasiTextContainer.innerHTML = '';
data.realisasiTarget.forEach((item, index) => {
const p = document.createElement('p');
const selisih = item.target - item.realisasi;
if (selisih > 0) {
p.textContent = `${index + 1}. Bulan ${item.bulan} dari target ${item.target} rumah tangga memilah konsisten, baru terbentuk ${item.realisasi} rumah tangga konsisten melakukan pemilahan, sehingga realisasi terhadap target masih kurang ${selisih} rumah tangga.`;
} else if (selisih < 0) {
p.textContent = `${index + 1}. Bulan ${item.bulan} dari target ${item.target} rumah tangga memilah konsisten, baru terbentuk ${item.realisasi} rumah tangga konsisten melakukan pemilahan, sehingga realisasi terhadap target sudah melewati target yaitu ${Math.abs(selisih)} rumah tangga.`;
} else {
p.textContent = `${index + 1}. Bulan ${item.bulan} dari target ${item.target} rumah tangga memilah konsisten, baru terbentuk ${item.realisasi} rumah tangga konsisten melakukan pemilahan, sehingga realisasi terhadap target sudah tercapai.`;
}
realisasiTextContainer.appendChild(p);
});
}
}
function renderKepatuhanPeriodeChart(data) {
if (!data || !data.tingkatKepatuhanPeriode) {
console.error('No data provided to renderKepatuhanPeriodeChart');
return;
}
// Update descriptions
document.getElementById('subtitleKepatuhanPeriode').textContent = `Tingkat Kepatuhan PJLP Periode ${data.periode}`;
document.getElementById('locationKepatuhanPeriode').textContent = data.locationText;
// Chart Tingkat Kepatuhan PJLP Periode
chartInstances.kepatuhanPeriode = new Chart(document.getElementById('chartKepatuhanPeriode'), {
type: 'bar',
data: {
labels: data.tingkatKepatuhanPeriode.map(x => x.wilayah),
datasets: [
{
label: 'Ceklis',
data: data.tingkatKepatuhanPeriode.map(x => x.ceklis),
backgroundColor: '#84CC16'
},
{
label: 'Belum Ceklis',
data: data.tingkatKepatuhanPeriode.map(x => x.belumCeklis),
backgroundColor: '#FB923C'
},
{
label: 'Total PJLP',
data: data.tingkatKepatuhanPeriode.map(x => x.total),
backgroundColor: '#98A2B3'
},
]
},
options: {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 20
}
},
scales: {
y: {
beginAtZero: true,
grace: '5%',
grid: { color: '#E5E7EB' }
},
x: {
grid: { display: false }
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
pointStyle: "circle",
padding: 15
}
},
datalabels: {
display: true,
anchor: 'end',
align: 'top',
color: '#444',
font: { size: 10, weight: 'bold' }
}
}
}
});
// Update PJLP Stats Cards
if (data.pjlpStats) {
document.getElementById('totalPjlpPendamping').textContent = data.pjlpStats.total;
document.getElementById('percenlPjlpPendamping').textContent = 'Pendamping';
document.getElementById('sudahCeklisCount').textContent = data.pjlpStats.sudahCeklis;
document.getElementById('sudahCeklisPercent').textContent = `Pendamping (${data.pjlpStats.persentaseSudah}%)`;
document.getElementById('belumCeklisCount').textContent = data.pjlpStats.belumCeklis;
document.getElementById('belumCeklisPercent').textContent = `Pendamping (${data.pjlpStats.persentaseBelum}%)`;
}
}
const getCurrentMonth = () => new Date().toISOString().slice(0, 7);
const getDefaultDateRange = () => {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
const startMonth = month - 2;
const startYear = startMonth < 0 ? year - 1 : year;
const adjustedStartMonth = startMonth < 0 ? startMonth + 12 : startMonth;
const endYear = year;
const endMonth = month;
const start = `${startYear}-${String(adjustedStartMonth + 1).padStart(2, '0')}`;
const end = `${endYear}-${String(endMonth + 1).padStart(2, '0')}`;
return { start, end };
};
const getErrorMsg = () => document.getElementById('errorMessage');
const getBulanAwalInput = () => document.getElementById('bulanAwal');
const getBulanAkhirInput = () => document.getElementById('bulanAkhir');
const getWilayahInput = () => document.getElementById('wilayahFilter');
function loadDataByDateRange(startDate, endDate, wilayah) {
const params = new URLSearchParams();
if (startDate) params.append('bulanAwal', startDate);
if (endDate) params.append('bulanAkhir', endDate);
if (wilayah) params.append('wilayah', wilayah);
fetch(`/LaporanCapaianWilayah/GetData?${params.toString()}`, {
headers: { 'Accept': 'application/json' }
})
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
})
.then(data => {
console.log('Data received:', data);
if (!data || !data.tingkatKepatuhanPjlp) {
console.error('Invalid data structure:', data);
return;
}
updateCharts(data);
updateLocationTexts(data.locationText);
})
.catch(err => console.error('Gagal memuat data:', err));
}
function loadDataKepatuhanPeriode(bulan, wilayah) {
const params = new URLSearchParams();
if (bulan) params.append('bulan', bulan);
if (wilayah) params.append('wilayah', wilayah);
fetch(`/LaporanCapaianWilayah/GetDataKepatuhanPeriode?${params.toString()}`, {
headers: { 'Accept': 'application/json' }
})
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
})
.then(data => {
console.log('Data Kepatuhan Periode received:', data);
if (!data || !data.tingkatKepatuhanPeriode) {
console.error('Invalid data structure:', data);
return;
}
updateKepatuhanPeriodeChart(data);
})
.catch(err => console.error('Gagal memuat data kepatuhan periode:', err));
}
function updateLocationTexts(locationText) {
document.getElementById('locationTingkatKepatuhan').textContent = locationText;
document.getElementById('locationStatusSatpel').textContent = locationText;
document.getElementById('locationStatusSudin').textContent = locationText;
document.getElementById('locationCapaianRumah').textContent = locationText;
document.getElementById('locationVolumeSampah').textContent = locationText;
document.getElementById('locationRealisasi').textContent = locationText;
}
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;
getWilayahInput().value = 'Jakarta Barat';
updateDateInputConstraints();
}
function clearFilter() {
setDefaultValues();
setErrorMessage('');
modal_filter.close();
const defaultRange = getDefaultDateRange();
const currentMonth = getCurrentMonth();
loadDataByDateRange(defaultRange.start, defaultRange.end, 'Jakarta Barat');
loadDataKepatuhanPeriode(currentMonth, 'Jakarta Barat');
}
document.addEventListener('DOMContentLoaded', () => {
setDefaultValues();
const defaultRange = getDefaultDateRange();
const currentMonth = getCurrentMonth();
loadDataByDateRange(defaultRange.start, defaultRange.end, 'Jakarta Barat');
loadDataKepatuhanPeriode(currentMonth, 'Jakarta Barat');
// Filter form
document.getElementById('filterForm').addEventListener('submit', (e) => {
e.preventDefault();
const startDate = getBulanAwalInput().value;
const endDate = getBulanAkhirInput().value;
const wilayah = getWilayahInput().value;
if (validateDateRange(startDate, endDate)) {
loadDataByDateRange(startDate, endDate, wilayah);
// update chart kepatuhan
const bulanPeriode = document.getElementById('filterBulan').value;
loadDataKepatuhanPeriode(bulanPeriode, wilayah);
modal_filter.close();
}
});
// Filter bulan Kepatuhan PJLP Periode
document.getElementById('filterBulan').addEventListener('change', (e) => {
const bulan = e.target.value;
const wilayah = getWilayahInput().value;
loadDataKepatuhanPeriode(bulan, wilayah);
});
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>