@page "/workday"
@page "/workday/{DateStr}"
@attribute [Authorize]
@rendermode InteractiveServer
@inject IWorkDayService WorkDayService
@inject IAppSettingsService AppSettingsService
@inject IItalianFestivitySource FestivitySource
@inject NavigationManager Navigation
Work Day
Work Day Entry
@if (!loaded)
{
Loading...
}
else
{
Computed values
@(projectedExitTime?.ToString("HH:mm") ?? "—")
@workedHoursBase.ToString("N2")h
@workedHoursFinal.ToString("N2")h
@hoursOff.ToString("N2")h
€@grossIncome.ToString("N2")
€@netIncome.ToString("N2")
@if (!string.IsNullOrWhiteSpace(statusMessage))
{
@statusMessage
}
}
@code {
[Parameter] public string? DateStr { get; set; }
private bool loaded;
private DateOnly selectedDate = DateOnly.FromDateTime(DateTime.Today);
private DayType selectedDayType = DayType.None;
private string? startTimeStr;
private string? actualExitTimeStr;
private decimal extraHoursDelta;
private string? notes;
private string? statusMessage;
// Computed preview
private TimeOnly? projectedExitTime;
private decimal workedHoursBase;
private decimal workedHoursFinal;
private decimal hoursOff;
private decimal grossIncome;
private decimal netIncome;
private bool isWeekend;
private bool isFestivity;
// Loaded from settings
private AppSettingsDocument settings = new();
private IReadOnlyCollection festivities = [];
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(DateStr) && DateOnly.TryParseExact(DateStr, "yyyy-MM-dd", out var parsed))
{
selectedDate = parsed;
}
settings = await AppSettingsService.GetAsync();
festivities = FestivitySource.GetFestivities(selectedDate.Year);
await LoadExistingEntry();
RecomputeFlags();
RecomputePreview();
loaded = true;
}
private async Task LoadExistingEntry()
{
var existing = await WorkDayService.GetAsync(selectedDate);
if (existing is not null)
{
selectedDayType = existing.DayType;
startTimeStr = existing.StartTime?.ToString("HH:mm");
actualExitTimeStr = existing.ActualExitTime?.ToString("HH:mm");
extraHoursDelta = existing.ExtraHoursDelta;
notes = existing.Notes;
}
else
{
selectedDayType = DayType.None;
startTimeStr = null;
actualExitTimeStr = null;
extraHoursDelta = 0;
notes = null;
}
}
private async Task OnDateChanged(ChangeEventArgs e)
{
if (DateOnly.TryParse(e.Value?.ToString(), out var d))
{
selectedDate = d;
festivities = FestivitySource.GetFestivities(selectedDate.Year);
await LoadExistingEntry();
RecomputeFlags();
RecomputePreview();
statusMessage = null;
}
}
private void OnDayTypeChanged(ChangeEventArgs e)
{
if (Enum.TryParse(e.Value?.ToString(), out var dt))
{
selectedDayType = dt;
RecomputePreview();
statusMessage = null;
}
}
private void OnStartTimeChanged(ChangeEventArgs e)
{
startTimeStr = e.Value?.ToString();
RecomputePreview();
statusMessage = null;
}
private void OnActualExitChanged(ChangeEventArgs e)
{
actualExitTimeStr = e.Value?.ToString();
statusMessage = null;
}
private void OnExtraDeltaChanged(ChangeEventArgs e)
{
if (decimal.TryParse(e.Value?.ToString(), out var val))
{
extraHoursDelta = val;
}
RecomputePreview();
statusMessage = null;
}
private void RecomputeFlags()
{
isWeekend = selectedDate.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
isFestivity = festivities.Contains(selectedDate);
}
private void RecomputePreview()
{
TimeOnly? start = null;
if (!string.IsNullOrEmpty(startTimeStr) && TimeOnly.TryParse(startTimeStr, out var s))
{
start = s;
}
if (selectedDayType is DayType.Work or DayType.Home)
{
workedHoursBase = settings.StandardWorkHoursPerDay;
if (start.HasValue)
{
var totalHours = settings.StandardWorkHoursPerDay + settings.LunchBreakHours;
projectedExitTime = start.Value.Add(TimeSpan.FromHours((double)totalHours));
}
else
{
projectedExitTime = null;
}
}
else
{
workedHoursBase = 0;
projectedExitTime = null;
}
workedHoursFinal = workedHoursBase + extraHoursDelta;
hoursOff = selectedDayType is DayType.Work or DayType.Home
? Math.Max(0, settings.StandardWorkHoursPerDay - workedHoursFinal)
: 0;
grossIncome = workedHoursFinal * settings.HourlyGrossRate;
var taxableBase = grossIncome * settings.ProfitabilityCoefficient;
netIncome = grossIncome - (taxableBase * settings.InpsRate) - (taxableBase * settings.SubstituteTaxRate);
}
private async Task SaveAsync()
{
TimeOnly? start = null;
TimeOnly? exit = null;
if (!string.IsNullOrEmpty(startTimeStr) && TimeOnly.TryParse(startTimeStr, out var s))
{
start = s;
}
if (!string.IsNullOrEmpty(actualExitTimeStr) && TimeOnly.TryParse(actualExitTimeStr, out var e2))
{
exit = e2;
}
var workDay = new WorkDayDocument
{
Date = selectedDate,
DayType = selectedDayType,
StartTime = start,
ActualExitTime = exit,
ExtraHoursDelta = extraHoursDelta,
Notes = notes
};
var saved = await WorkDayService.SaveAsync(workDay);
// Update preview with saved computed values
projectedExitTime = saved.ProjectedExitTime;
workedHoursBase = saved.WorkedHoursBase;
workedHoursFinal = saved.WorkedHoursFinal;
hoursOff = saved.HoursOff;
grossIncome = saved.GrossIncome;
netIncome = saved.NetIncome;
isWeekend = saved.IsWeekend;
isFestivity = saved.IsItalianFestivity;
statusMessage = $"Saved at {DateTime.Now:t}";
}
}