@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 {
@if (isWeekend || isFestivity) {
@if (isWeekend) { Weekend } @if (isFestivity) { Festivity }
}
Informational only, not used in calculations.

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}"; } }