2026-03-17 22:10:19 +01:00
|
|
|
@page "/summary"
|
|
|
|
|
@page "/summary/{YearMonth}"
|
|
|
|
|
@attribute [Authorize]
|
|
|
|
|
@rendermode InteractiveServer
|
|
|
|
|
|
|
|
|
|
@inject IWorkDayService WorkDayService
|
|
|
|
|
|
|
|
|
|
<PageTitle>Monthly Summary</PageTitle>
|
|
|
|
|
|
|
|
|
|
<h1>Monthly Summary</h1>
|
|
|
|
|
|
|
|
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
|
|
|
<button class="btn btn-outline-secondary btn-sm" @onclick="PreviousMonth">« Prev</button>
|
|
|
|
|
<h2 class="h5 mb-0">@currentMonth.ToString("MMMM yyyy")</h2>
|
|
|
|
|
<button class="btn btn-outline-secondary btn-sm" @onclick="NextMonth">Next »</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-20 16:11:27 +02:00
|
|
|
<div class="form-check mb-3">
|
|
|
|
|
<input id="include-preview" type="checkbox" class="form-check-input" checked="@includePreview" @onchange="OnIncludePreviewChanged" />
|
|
|
|
|
<label class="form-check-label" for="include-preview">Include preview work units in totals</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-17 22:10:19 +01:00
|
|
|
@if (loading)
|
|
|
|
|
{
|
|
|
|
|
<p><em>Loading...</em></p>
|
|
|
|
|
}
|
|
|
|
|
else if (summary is not null)
|
|
|
|
|
{
|
|
|
|
|
<div class="row g-3">
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Working Days</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@summary.TotalWorkingDays</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-20 16:11:27 +02:00
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Counted Work Units</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@summary.CountedWorkUnits</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-17 22:10:19 +01:00
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Total Worked Hours</div>
|
2026-04-20 16:11:27 +02:00
|
|
|
<div class="fs-3 fw-bold">@FormatHours(summary.TotalWorkedHours)</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Preview Hours</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@FormatHours(summary.TotalPreviewWorkedHours)</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Preview Units</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@summary.PreviewWorkUnits</div>
|
2026-03-17 22:10:19 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Hours Off</div>
|
2026-04-20 16:11:27 +02:00
|
|
|
<div class="fs-3 fw-bold">@FormatHours(summary.TotalHoursOff)</div>
|
2026-03-17 22:10:19 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100 border-success">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Gross Income</div>
|
|
|
|
|
<div class="fs-3 fw-bold text-success">€@summary.TotalGrossIncome.ToString("N2")</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100 border-primary">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Net Income</div>
|
|
|
|
|
<div class="fs-3 fw-bold text-primary">€@summary.TotalNetIncome.ToString("N2")</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Office Days</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@summary.OfficeDays</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Home Days</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@summary.HomeDays</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Holidays</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@summary.HolidayDays</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
2026-04-20 16:11:27 +02:00
|
|
|
<div class="text-muted small">Closure Days</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@summary.ClosureDays</div>
|
2026-03-17 22:10:19 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="text-muted small">Days Off</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@summary.DaysOff</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-md-4 col-xl-3">
|
|
|
|
|
<div class="card text-center h-100">
|
|
|
|
|
<div class="card-body">
|
2026-04-20 16:11:27 +02:00
|
|
|
<div class="text-muted small">Sick Days</div>
|
|
|
|
|
<div class="fs-3 fw-bold">@summary.SickDays</div>
|
2026-03-17 22:10:19 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@code {
|
|
|
|
|
[Parameter] public string? YearMonth { get; set; }
|
|
|
|
|
|
|
|
|
|
private DateOnly currentMonth;
|
|
|
|
|
private bool loading = true;
|
2026-04-20 16:11:27 +02:00
|
|
|
private bool includePreview;
|
2026-03-17 22:10:19 +01:00
|
|
|
private MonthlySummaryModel? summary;
|
|
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrEmpty(YearMonth) && DateTime.TryParseExact(YearMonth, "yyyy-MM", null, System.Globalization.DateTimeStyles.None, out var parsed))
|
|
|
|
|
{
|
|
|
|
|
currentMonth = new DateOnly(parsed.Year, parsed.Month, 1);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
currentMonth = new DateOnly(DateTime.Today.Year, DateTime.Today.Month, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await LoadSummary();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 16:11:27 +02:00
|
|
|
private async Task OnIncludePreviewChanged(ChangeEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
includePreview = e.Value is bool value && value;
|
|
|
|
|
await LoadSummary();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 22:10:19 +01:00
|
|
|
private async Task LoadSummary()
|
|
|
|
|
{
|
|
|
|
|
loading = true;
|
2026-04-20 16:11:27 +02:00
|
|
|
summary = await WorkDayService.GetMonthlySummaryAsync(currentMonth.Year, currentMonth.Month, includePreview);
|
2026-03-17 22:10:19 +01:00
|
|
|
loading = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task PreviousMonth()
|
|
|
|
|
{
|
|
|
|
|
currentMonth = currentMonth.AddMonths(-1);
|
|
|
|
|
await LoadSummary();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task NextMonth()
|
|
|
|
|
{
|
|
|
|
|
currentMonth = currentMonth.AddMonths(1);
|
|
|
|
|
await LoadSummary();
|
|
|
|
|
}
|
2026-04-20 16:11:27 +02:00
|
|
|
|
|
|
|
|
private static string FormatHours(decimal value)
|
|
|
|
|
{
|
|
|
|
|
var totalMinutes = (int)Math.Round(value * 60m, MidpointRounding.AwayFromZero);
|
|
|
|
|
var hours = totalMinutes / 60;
|
|
|
|
|
var minutes = totalMinutes % 60;
|
|
|
|
|
return $"{hours:00}:{minutes:00}";
|
|
|
|
|
}
|
2026-03-17 22:10:19 +01:00
|
|
|
}
|