@inject IWorkDayService WorkDayService @inject IAppSettingsService AppSettingsService @inject IJSRuntime JS
@code { [Parameter] public DateOnly Date { get; set; } [Parameter] public string? UnitId { get; set; } [Parameter] public EventCallback OnSaved { get; set; } [Parameter] public EventCallback OnClosed { get; set; } private bool loaded; private DateOnly selectedDate; private string unitId = string.Empty; private string label = "Work unit"; private WorkUnitLocation location = WorkUnitLocation.Office; private string? startTimeStr; private string? endTimeStr; private decimal manualWorkedHours; private string manualWorkedHoursStr = "00:00"; private bool isPreview; private string? notes; private string? statusMessage; private bool isExistingUnit; private WorkDayDocument? selectedDay; private string? loadKey; private decimal grossIncome; private decimal netIncome; private decimal? calculatedWorkedHours; private decimal workedHoursDelta; private decimal dayTotalHours; private int dayWorkUnitCount; private AppSettingsDocument settings = new(); protected override async Task OnParametersSetAsync() { var nextKey = $"{Date:yyyy-MM-dd}|{UnitId}"; if (nextKey == loadKey) { return; } loadKey = nextKey; loaded = false; selectedDate = Date; statusMessage = null; settings = await AppSettingsService.GetAsync(); await LoadUnitAsync(); loaded = true; } private async Task LoadUnitAsync() { if (string.IsNullOrWhiteSpace(UnitId)) { selectedDay = await WorkDayService.GetAsync(selectedDate); SetDefaults(); return; } selectedDay = await WorkDayService.GetAsync(selectedDate); var existing = await WorkDayService.GetWorkUnitAsync(selectedDate, UnitId); if (existing is not null) { unitId = existing.Id; label = existing.Label; location = existing.Location; startTimeStr = existing.StartTime?.ToString("HH:mm"); endTimeStr = existing.EndTime?.ToString("HH:mm"); manualWorkedHours = existing.ManualWorkedHours; manualWorkedHoursStr = FormatDurationHours(existing.ManualWorkedHours); isPreview = existing.IsPreview; notes = existing.Notes; isExistingUnit = true; } else { SetDefaults(); statusMessage = "The selected work unit was not found. A new unit will be created for this day."; } RecomputePreview(); } private async Task OnDateChangedAsync(DateOnly? value) { if (value.HasValue) { selectedDate = value.Value; selectedDay = await WorkDayService.GetAsync(selectedDate); statusMessage = null; RecomputePreview(); } } private Task OnStartTimeChanged(ChangeEventArgs e) { startTimeStr = e.Value?.ToString(); SyncManualHoursToCalculated(); statusMessage = null; return Task.CompletedTask; } private Task OnEndTimeChanged(ChangeEventArgs e) { endTimeStr = e.Value?.ToString(); SyncManualHoursToCalculated(); statusMessage = null; return Task.CompletedTask; } private Task OnManualWorkedHoursChanged(ChangeEventArgs e) { var rawValue = e.Value?.ToString(); if (TryParseDurationHours(rawValue, out var parsedHours)) { manualWorkedHours = parsedHours; manualWorkedHoursStr = FormatDurationHours(parsedHours); } else { manualWorkedHoursStr = FormatDurationHours(manualWorkedHours); } RecomputePreview(); statusMessage = null; return Task.CompletedTask; } private void SetDefaults() { unitId = string.Empty; label = "Work unit"; location = WorkUnitLocation.Office; startTimeStr = null; endTimeStr = null; manualWorkedHours = 0m; manualWorkedHoursStr = "00:00"; isPreview = false; notes = null; isExistingUnit = false; RecomputePreview(); } private void SyncManualHoursToCalculated() { var calculated = CalculateDuration(ParseTime(startTimeStr), ParseTime(endTimeStr)); manualWorkedHours = calculated ?? 0m; manualWorkedHoursStr = FormatDurationHours(manualWorkedHours); RecomputePreview(); } private void RecomputePreview() { calculatedWorkedHours = CalculateDuration(ParseTime(startTimeStr), ParseTime(endTimeStr)); workedHoursDelta = manualWorkedHours - (calculatedWorkedHours ?? 0m); grossIncome = manualWorkedHours * settings.HourlyGrossRate; var taxableBase = grossIncome * settings.ProfitabilityCoefficient; netIncome = grossIncome - (taxableBase * settings.InpsRate) - (taxableBase * settings.SubstituteTaxRate); RecomputeDayTotals(); } private async Task SaveAsync() { RecomputePreview(); var workUnit = new WorkUnitDocument { Id = unitId, Label = label, Location = location, StartTime = ParseTime(startTimeStr), EndTime = ParseTime(endTimeStr), ManualWorkedHours = Math.Max(0m, manualWorkedHours), IsPreview = isPreview, Notes = notes }; await WorkDayService.SaveWorkUnitAsync(selectedDate, workUnit); await OnSaved.InvokeAsync(); } private async Task DeleteAsync() { if (!isExistingUnit || string.IsNullOrWhiteSpace(unitId)) { return; } var confirmed = await JS.InvokeAsync("confirm", $"Delete work unit '{label}' on {FormatDisplayDate(selectedDate)}?\nThis cannot be undone."); if (!confirmed) { return; } var deleted = await WorkDayService.DeleteWorkUnitAsync(selectedDate, unitId); if (deleted) { await OnSaved.InvokeAsync(); return; } statusMessage = "Unable to delete the work unit."; } private Task CloseAsync() => OnClosed.InvokeAsync(); private void RecomputeDayTotals() { var existingUnits = selectedDay?.WorkUnits ?? []; dayTotalHours = existingUnits .Where(unit => !string.Equals(unit.Id, unitId, StringComparison.Ordinal)) .Sum(unit => unit.ManualWorkedHours) + manualWorkedHours; dayWorkUnitCount = existingUnits .Where(unit => !string.Equals(unit.Id, unitId, StringComparison.Ordinal)) .Count() + 1; } private static TimeOnly? ParseTime(string? value) { return !string.IsNullOrWhiteSpace(value) && TimeOnly.TryParse(value, out var parsed) ? parsed : null; } private static decimal? CalculateDuration(TimeOnly? startTime, TimeOnly? endTime) { if (!startTime.HasValue || !endTime.HasValue || endTime <= startTime) { return null; } return Math.Round((decimal)(endTime.Value - startTime.Value).TotalHours, 2, MidpointRounding.AwayFromZero); } private static string FormatDurationHours(decimal value) { var totalMinutes = (int)Math.Round(value * 60m, MidpointRounding.AwayFromZero); var hours = totalMinutes / 60; var minutes = totalMinutes % 60; return $"{hours:00}:{minutes:00}"; } private static string FormatHours(decimal? value) { return value.HasValue ? DurationFormatter.FormatHours(value.Value) : "—"; } private static string FormatSignedHours(decimal value) { if (value == 0m) { return "00:00"; } var prefix = value > 0m ? "+" : "-"; return prefix + DurationFormatter.FormatHours(Math.Abs(value)); } private static bool TryParseDurationHours(string? value, out decimal hours) { hours = 0m; if (string.IsNullOrWhiteSpace(value)) { return false; } if (TimeOnly.TryParse(value, out var parsedTime)) { hours = parsedTime.Hour + (parsedTime.Minute / 60m); return true; } if (decimal.TryParse(value, out var parsedDecimal)) { hours = Math.Max(0m, parsedDecimal); return true; } return false; } private static string FormatDisplayDate(DateOnly date) { return date.ToString("dddd dd/MM/yyyy"); } }