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">
|
<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, {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue