All checks were successful
Publish Container / publish (push) Successful in 3m17s
205 lines
No EOL
9.1 KiB
Text
205 lines
No EOL
9.1 KiB
Text
@page "/yearly-summary"
|
|
@page "/yearly-summary/{Year:int}"
|
|
@attribute [Authorize]
|
|
@rendermode InteractiveServer
|
|
|
|
@using System.Globalization
|
|
@inject global::WorkTracker.Services.WorkDays.IWorkDayService WorkDayService
|
|
@inject IJSRuntime JS
|
|
@inject NavigationManager Navigation
|
|
|
|
<PageTitle>Yearly Summary</PageTitle>
|
|
|
|
<h1>Yearly Summary</h1>
|
|
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<button class="btn btn-outline-secondary btn-sm" @onclick="PreviousYear">« Prev</button>
|
|
<h2 class="h5 mb-0">@currentYear</h2>
|
|
<button class="btn btn-outline-secondary btn-sm" @onclick="NextYear">Next »</button>
|
|
<a class="btn btn-outline-primary btn-sm ms-auto" href="summary">Monthly Summary</a>
|
|
</div>
|
|
|
|
<div class="form-check mb-3">
|
|
<input id="include-preview-yearly" type="checkbox" class="form-check-input" checked="@includePreview" @onchange="OnIncludePreviewChanged" />
|
|
<label class="form-check-label" for="include-preview-yearly">Include preview work units in totals</label>
|
|
</div>
|
|
|
|
@if (loading)
|
|
{
|
|
<p><em>Loading...</em></p>
|
|
}
|
|
else
|
|
{
|
|
<div class="yearly-summary-card card border-0 shadow-sm">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered table-sm align-middle mb-0 yearly-summary-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="yearly-summary-sticky-column">Month</th>
|
|
<th class="text-end yearly-summary-header-cell">Working Days</th>
|
|
<th class="text-end yearly-summary-header-cell">Worked Hours</th>
|
|
<th class="text-end yearly-summary-header-cell">Hours Off</th>
|
|
<th class="text-end yearly-summary-header-cell text-success">Gross Income</th>
|
|
<th class="text-end yearly-summary-header-cell text-primary">Net Income</th>
|
|
<th class="text-end yearly-summary-header-cell">Office Days</th>
|
|
<th class="text-end yearly-summary-header-cell">Home Days</th>
|
|
<th class="text-end yearly-summary-header-cell">Holidays</th>
|
|
<th class="text-end yearly-summary-header-cell">Closure Days</th>
|
|
<th class="text-end yearly-summary-header-cell">Days Off</th>
|
|
<th class="text-end yearly-summary-header-cell">Sick Days</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var month in summaryByMonth)
|
|
{
|
|
<tr>
|
|
<th scope="row" class="yearly-summary-sticky-column">
|
|
<a href="summary/@GetYearMonth(month)">@GetMonthLabel(month)</a>
|
|
</th>
|
|
<td class="text-end">@FormatCount(month.TotalWorkingDays)</td>
|
|
<td class="text-end">@FormatHoursCell(month.TotalWorkedHours)</td>
|
|
<td class="text-end">@FormatHoursCell(month.TotalHoursOff)</td>
|
|
<td class="text-end text-success">@FormatCurrency(month.TotalGrossIncome)</td>
|
|
<td class="text-end text-primary">@FormatCurrency(month.TotalNetIncome)</td>
|
|
<td class="text-end">@FormatCount(month.OfficeDays)</td>
|
|
<td class="text-end">@FormatCount(month.HomeDays)</td>
|
|
<td class="text-end">@FormatCount(month.HolidayDays)</td>
|
|
<td class="text-end">@FormatCount(month.ClosureDays)</td>
|
|
<td class="text-end">@FormatCount(month.DaysOff)</td>
|
|
<td class="text-end">@FormatCount(month.SickDays)</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr class="yearly-summary-total-row">
|
|
<th class="yearly-summary-sticky-column">Total</th>
|
|
<td class="text-end">@FormatCount(Totals.TotalWorkingDays)</td>
|
|
<td class="text-end">@FormatHoursCell(Totals.TotalWorkedHours)</td>
|
|
<td class="text-end">@FormatHoursCell(Totals.TotalHoursOff)</td>
|
|
<td class="text-end text-success">@FormatCurrency(Totals.TotalGrossIncome)</td>
|
|
<td class="text-end text-primary">@FormatCurrency(Totals.TotalNetIncome)</td>
|
|
<td class="text-end">@FormatCount(Totals.OfficeDays)</td>
|
|
<td class="text-end">@FormatCount(Totals.HomeDays)</td>
|
|
<td class="text-end">@FormatCount(Totals.HolidayDays)</td>
|
|
<td class="text-end">@FormatCount(Totals.ClosureDays)</td>
|
|
<td class="text-end">@FormatCount(Totals.DaysOff)</td>
|
|
<td class="text-end">@FormatCount(Totals.SickDays)</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
[Parameter] public int? Year { get; set; }
|
|
|
|
private static readonly CultureInfo ItalianCulture = CultureInfo.GetCultureInfo("it-IT");
|
|
private const string IncludePreviewPreferenceKey = "worktracker.includePreviewWorkUnits";
|
|
|
|
private int currentYear;
|
|
private bool loading = true;
|
|
private bool includePreview;
|
|
private IReadOnlyList<global::WorkTracker.Domain.MonthlySummaryModel> summaryByMonth = [];
|
|
|
|
private global::WorkTracker.Domain.MonthlySummaryModel Totals => new()
|
|
{
|
|
Year = currentYear,
|
|
TotalWorkingDays = summaryByMonth.Sum(item => item.TotalWorkingDays),
|
|
CountedWorkUnits = summaryByMonth.Sum(item => item.CountedWorkUnits),
|
|
TotalWorkedHours = summaryByMonth.Sum(item => item.TotalWorkedHours),
|
|
TotalPreviewWorkedHours = summaryByMonth.Sum(item => item.TotalPreviewWorkedHours),
|
|
PreviewWorkUnits = summaryByMonth.Sum(item => item.PreviewWorkUnits),
|
|
TotalHoursOff = summaryByMonth.Sum(item => item.TotalHoursOff),
|
|
TotalGrossIncome = summaryByMonth.Sum(item => item.TotalGrossIncome),
|
|
TotalNetIncome = summaryByMonth.Sum(item => item.TotalNetIncome),
|
|
OfficeDays = summaryByMonth.Sum(item => item.OfficeDays),
|
|
HomeDays = summaryByMonth.Sum(item => item.HomeDays),
|
|
HolidayDays = summaryByMonth.Sum(item => item.HolidayDays),
|
|
ClosureDays = summaryByMonth.Sum(item => item.ClosureDays),
|
|
DaysOff = summaryByMonth.Sum(item => item.DaysOff),
|
|
SickDays = summaryByMonth.Sum(item => item.SickDays)
|
|
};
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
currentYear = Year ?? DateTime.Today.Year;
|
|
await LoadSummary();
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (!firstRender)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var savedIncludePreview = await JS.InvokeAsync<bool?>("workTrackerPreferences.getBool", IncludePreviewPreferenceKey);
|
|
if (savedIncludePreview.HasValue && savedIncludePreview.Value != includePreview)
|
|
{
|
|
includePreview = savedIncludePreview.Value;
|
|
await LoadSummary();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
private async Task LoadSummary()
|
|
{
|
|
loading = true;
|
|
summaryByMonth = await WorkDayService.GetYearlySummaryAsync(currentYear, includePreview);
|
|
loading = false;
|
|
}
|
|
|
|
private async Task OnIncludePreviewChanged(ChangeEventArgs e)
|
|
{
|
|
includePreview = e.Value is bool value && value;
|
|
await JS.InvokeVoidAsync("workTrackerPreferences.setBool", IncludePreviewPreferenceKey, includePreview);
|
|
await LoadSummary();
|
|
}
|
|
|
|
private Task PreviousYear()
|
|
{
|
|
currentYear--;
|
|
Navigation.NavigateTo($"/yearly-summary/{currentYear}");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private Task NextYear()
|
|
{
|
|
currentYear++;
|
|
Navigation.NavigateTo($"/yearly-summary/{currentYear}");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private static string GetMonthLabel(global::WorkTracker.Domain.MonthlySummaryModel month)
|
|
{
|
|
return new DateOnly(month.Year, month.Month, 1).ToString("MMMM", ItalianCulture);
|
|
}
|
|
|
|
private static string GetYearMonth(global::WorkTracker.Domain.MonthlySummaryModel month)
|
|
{
|
|
return $"{month.Year:D4}-{month.Month:D2}";
|
|
}
|
|
|
|
private static string FormatHours(decimal value)
|
|
{
|
|
return DurationFormatter.FormatHours(value);
|
|
}
|
|
|
|
private static string FormatHoursCell(decimal value)
|
|
{
|
|
return DurationFormatter.FormatHours(value, blankWhenZero: true);
|
|
}
|
|
|
|
private static string FormatCount(int value)
|
|
{
|
|
return value == 0 ? string.Empty : value.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private static string FormatCurrency(decimal value)
|
|
{
|
|
return value == 0m ? string.Empty : $"€{value.ToString("N2", ItalianCulture)}";
|
|
}
|
|
} |