M5: Blazor dashboard (MudBlazor + ApexCharts)
- MudBlazor theme (dark default) + responsive drawer/appbar layout + nav. - DashboardService read model: KPIs with period-over-period deltas, category breakdown, "what cost more/less" difference view, monthly trends. - Pages: Overview (KPI cards + DeltaChip + donut + difference table), Trends (range-select bar chart), Meters (list + source status), Import (load reference dataset + CSV dry-run preview), Admin (energy types, tariffs). Charts isolated into components to avoid the ApexCharts/MudBlazor Color/Format name clashes. - ReferenceDataImporter: one-click load of all four sheets as a starter dataset (meters, tank, tariff history, category memberships) — bundled sample CSVs copied to app output. - End-to-end render test: import creates meters + consumption; overview/meters/trends/ import/admin pages all return 200 with KPI cards rendered. 92 tests green (56 Core + 36 integration). Deferred to polish: dedicated PV & oil/consumable panels, meter-detail page, full admin CRUD, prev-year trend overlay. Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
@page "/"
|
||||
@rendermode InteractiveServer
|
||||
@inject DashboardService Dash
|
||||
|
||||
<PageTitle>MeterVault — Overview</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Overview</MudText>
|
||||
|
||||
@if (_summary is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudPaper Class="pa-4" Elevation="2">
|
||||
<MudText Typo="Typo.overline" Color="Color.Secondary">This month</MudText>
|
||||
<MudText Typo="Typo.h5">@Format.Euro(_summary.Month.Current)</MudText>
|
||||
<DeltaChip Kpi="_summary.Month" />
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudPaper Class="pa-4" Elevation="2">
|
||||
<MudText Typo="Typo.overline" Color="Color.Secondary">This year</MudText>
|
||||
<MudText Typo="Typo.h5">@Format.Euro(_summary.Year.Current)</MudText>
|
||||
<DeltaChip Kpi="_summary.Year" />
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudPaper Class="pa-4" Elevation="2">
|
||||
<MudText Typo="Typo.overline" Color="Color.Secondary">Latest month with data</MudText>
|
||||
<MudText Typo="Typo.h5">@Format.Euro(_summary.LatestMonthCost)</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Class="pa-4" Elevation="2" Style="height:100%">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">What costs most (this year)</MudText>
|
||||
@if (_breakdown is { Count: > 0 })
|
||||
{
|
||||
<CategoryDonut Slices="_breakdown" />
|
||||
<MudSimpleTable Dense="true" Hover="true" Class="mt-2">
|
||||
<tbody>
|
||||
@foreach (var slice in _breakdown)
|
||||
{
|
||||
<tr>
|
||||
<td>@slice.Name</td>
|
||||
<td style="text-align:right">@Format.Euro(slice.Cost)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">No cost data yet — import a sheet or add tariffs.</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Class="pa-4" Elevation="2" Style="height:100%">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">What cost more / less (year vs last year)</MudText>
|
||||
<MudSimpleTable Dense="true" Hover="true">
|
||||
<thead>
|
||||
<tr><th>Category</th><th style="text-align:right">Now</th><th style="text-align:right">Prev</th><th style="text-align:right">Δ</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var row in _difference)
|
||||
{
|
||||
<tr>
|
||||
<td>@row.Name</td>
|
||||
<td style="text-align:right">@Format.Euro(row.Current)</td>
|
||||
<td style="text-align:right">@Format.Euro(row.Previous)</td>
|
||||
<td style="text-align:right" class="@(row.Delta >= 0 ? "mv-up" : "mv-down")">
|
||||
@Format.DirectionIcon(Math.Sign(row.Delta)) @Format.Euro(Math.Abs(row.Delta))
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
}
|
||||
|
||||
@code {
|
||||
private DashboardSummary? _summary;
|
||||
private IReadOnlyList<CategorySlice> _breakdown = [];
|
||||
private IReadOnlyList<DifferenceRow> _difference = [];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var asOf = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
_summary = await Dash.GetSummaryAsync(asOf);
|
||||
|
||||
var yearStart = new DateOnly(asOf.Year, 1, 1);
|
||||
_breakdown = await Dash.GetCategoryBreakdownAsync(yearStart, asOf.AddMonths(1));
|
||||
_difference = await Dash.GetCategoryDifferenceAsync(yearStart, yearStart.AddYears(-1), asOf.AddMonths(1));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user