refactor: chart Total Sampah per Bulan (BSI, BSU, Offtaker)

main
Yuri Dimas 2025-12-09 18:15:02 +07:00
parent 62912c2a43
commit 9622ee1809
No known key found for this signature in database
GPG Key ID: 9FD7E44BC294C68C
3 changed files with 247 additions and 10 deletions

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using BankSampahApp.Models;
using Microsoft.AspNetCore.Mvc;
namespace BankSampahApp.Controllers.Main
{
@ -7,12 +8,69 @@ namespace BankSampahApp.Controllers.Main
{
public IActionResult Index()
{
return View("~/Views/Main/Dashboard/Index.cshtml");
var model = BuildDashboardChartModel();
return View("~/Views/Main/Dashboard/Index.cshtml", model);
}
public IActionResult Bsu()
{
return View("~/Views/Main/Dashboard/Bsu.cshtml");
}
/// <summary>
/// Dummy chart data agar view dapat menampilkan grafik tanpa ketergantungan pada API.
/// </summary>
private static DashboardChartViewModel BuildDashboardChartModel()
{
var monthLabels = new List<string>
{
"Januari", "Februari", "Maret", "April", "Mei", "Juni",
"Juli", "Agustus", "September", "Oktober", "November", "Desember"
};
var baseDatasets = new Dictionary<string, List<int>>
{
["BSI"] = new() { 520, 540, 560, 590, 620, 650, 680, 710, 740, 770, 800, 830 },
["BSU"] = new() { 610, 630, 640, 670, 690, 720, 760, 780, 810, 830, 860, 890 },
["Offtaker"] = new() { 1050, 1080, 1100, 1140, 1180, 1220, 1260, 1300, 1340, 1380, 1420, 1480 }
};
var years = Enumerable.Range(2020, 6).ToList();
var firstYear = years.Min();
var yearlyData = new Dictionary<int, DashboardChartPayload>();
foreach (var year in years)
{
var offset = year - firstYear;
var payload = new DashboardChartPayload
{
Labels = new List<string>(monthLabels)
};
foreach (var dataset in baseDatasets)
{
var random = new Random(HashCode.Combine(year, dataset.Key));
payload.Datasets.Add(new DashboardChartDataset
{
Label = dataset.Key,
Data = dataset.Value
.Select(baseValue =>
{
var variation = random.Next(-100, 120) + offset * 30;
return Math.Max(0, baseValue + variation);
})
.ToList()
});
}
yearlyData[year] = payload;
}
return new DashboardChartViewModel
{
YearlyData = yearlyData,
SelectedYear = yearlyData.Keys.Max()
};
}
}
}

View File

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
namespace BankSampahApp.Models;
/// <summary>
/// View model yang menampung data grafik dashboard beserta opsi filter tahunan.
/// </summary>
public class DashboardChartViewModel
{
/// <summary>
/// Data grafik per tahun (key berupa tahun).
/// </summary>
public Dictionary<int, DashboardChartPayload> YearlyData { get; set; } = new();
/// <summary>
/// Tahun yang dipilih saat halaman pertama kali dimuat.
/// </summary>
public int SelectedYear { get; set; }
/// <summary>
/// Daftar tahun yang tersedia untuk filter, disortir menurun.
/// </summary>
public IEnumerable<int> AvailableYears => YearlyData.Keys.OrderByDescending(x => x);
}
/// <summary>
/// Struktur data payload untuk chart per tahun.
/// </summary>
public class DashboardChartPayload
{
/// <summary>
/// Label (umumnya nama bulan) yang ditampilkan pada sumbu X.
/// </summary>
public List<string> Labels { get; set; } = new();
/// <summary>
/// Dataset yang ditampilkan pada grafik.
/// </summary>
public List<DashboardChartDataset> Datasets { get; set; } = new();
}
/// <summary>
/// Dataset individual untuk Chart.js.
/// </summary>
public class DashboardChartDataset
{
public string Label { get; set; } = string.Empty;
public List<int> Data { get; set; } = new();
}

View File

@ -1,5 +1,11 @@
@{
@model BankSampahApp.Models.DashboardChartViewModel
@using System.Text.Json
@{
ViewData["Title"] = "Dashboard";
var chartJson = JsonSerializer.Serialize(Model.YearlyData, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
<div class="flex gap-2 flex-row justify-between md:gap-0 mt-2 mb-2">
@ -223,19 +229,40 @@
<div class="h-6"></div>
<div class="card bg-white shadow-sm">
<div class="card-body pt-4 px-3">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:gap-0">
<div class="prose">
<div class="card bg-white">
<div class="card-body">
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between md:gap-4">
<div class="prose max-w-2xl">
<span class="text-xl font-semibold text-gray-900 font-['Plus_Jakarta_Sans']">
Data Terverifikasi
Grafik Total Sampah per Bulan (BSI, BSU, Offtaker)
</span>
<p class="mt-1 text-sm text-gray-500">
Menampilkan total tonase sampah yang dikelola setiap bulan berdasarkan sumber BSI, BSU, dan Offtaker.
</p>
</div>
<fieldset class="fieldset w-full lg:w-48">
<label class="fieldset-label text-gray-500 text-xs uppercase tracking-wide" for="chartYear">
Filter Tahun
</label>
<select id="chartYear" class="select select-sm" onchange="handleYearChange(event)">
@foreach (var year in Model.AvailableYears)
{
if (year == Model.SelectedYear)
{
<option value="@year" selected>@year</option>
}
else
{
<option value="@year">@year</option>
}
}
</select>
</fieldset>
</div>
<div class="w-full overflow-x-auto">
<div class="min-w-[900px]">
<div class="h-64 md:h-96 relative">
<canvas id="chartBankSampah" class="w-full h-full"></canvas>
<canvas id="chartBankSampah" class="w-full h-full"></canvas>
</div>
</div>
</div>
@ -243,4 +270,106 @@
</div>
<script src="~/js/chart.js"></script>
<script src="/plugins/chartjs/setup.js"></script>
<script>
const chartDataByYear = @Html.Raw(chartJson);
const chartCanvas = document.getElementById('chartBankSampah');
const chartCtx = chartCanvas.getContext('2d');
let chartInstance = null;
const datasetStyles = {
BSI: {
borderColor: '#247332',
gradientStart: 'rgba(36, 115, 50, 1)',
gradientEnd: 'rgba(136, 194, 147, 0.25)'
},
BSU: {
borderColor: '#386BCA',
gradientStart: 'rgba(56, 107, 202, 1)',
gradientEnd: 'rgba(138, 164, 242, 0.25)'
},
Offtaker: {
borderColor: '#F79009',
gradientStart: 'rgba(247, 144, 9, 1)',
gradientEnd: 'rgba(255, 219, 172, 0.25)'
}
};
const defaultDatasetStyle = {
borderColor: '#0F172A',
gradientStart: 'rgba(15, 23, 42, 0.85)',
gradientEnd: 'rgba(15, 23, 42, 0.15)'
};
const buildDatasets = (payload) => payload.datasets.map((dataset) => {
const style = datasetStyles[dataset.label] ?? defaultDatasetStyle;
const gradient = chartCtx.createLinearGradient(0, 0, 0, chartCanvas.clientHeight || 400);
gradient.addColorStop(0, style.gradientStart);
gradient.addColorStop(1, style.gradientEnd);
return {
label: dataset.label,
data: dataset.data,
borderColor: style.borderColor,
backgroundColor: gradient,
borderWidth: 2,
};
});
const renderChart = (year) => {
const payload = chartDataByYear[year];
if (!payload) {
return;
}
const updatedData = {
labels: payload.labels,
datasets: buildDatasets(payload)
};
if (chartInstance) {
chartInstance.data = updatedData;
chartInstance.update();
return;
}
chartInstance = new Chart(chartCtx, {
type: 'bar',
data: updatedData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
},
tooltip: {
callbacks: {
label: (context) => `${context.dataset.label}: ${context.formattedValue} Kg`
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
beginAtZero: true,
ticks: {
callback: (value) => `${value} Kg`
}
}
}
}
});
};
window.handleYearChange = (event) => {
renderChart(event.target.value);
};
document.addEventListener('DOMContentLoaded', () => {
renderChart(@Model.SelectedYear);
});
</script>