@page "/calendar" @page "/calendar/{YearMonth}" @attribute [Authorize] @rendermode InteractiveServer @inject IWorkDayService WorkDayService @inject IItalianFestivitySource FestivitySource @inject IJSRuntime JS Calendar

Calendar

@firstOfMonth.ToString("MMMM yyyy")

@if (!string.IsNullOrWhiteSpace(statusMessage)) {
@statusMessage
} @if (loading) {

Loading...

} else {
@foreach (var week in weeks) { @foreach (var cell in week) { } }
Mon Tue Wed Thu Fri Sat Sun
@cell.Date.Day
@if (!cell.IsCurrentMonth) {
@cell.Date.ToString("MMM")
}
@foreach (var workUnit in cell.Entry?.WorkUnits ?? []) { } @foreach (var calendarEvent in cell.Entry?.CalendarEvents ?? []) { }
@FormatHours(GetDayTotalHours(cell.Entry, includePreviewTotals))
@if (IsActiveCell(cell.Date)) {
@FormatDisplayDate(cell.Date)
Select an existing entry or create a new one.
@if ((cell.Entry?.WorkUnits.Count ?? 0) == 0 && (cell.Entry?.CalendarEvents.Count ?? 0) == 0) {
No entries for this day.
} @foreach (var workUnit in cell.Entry?.WorkUnits ?? []) { } @foreach (var calendarEvent in cell.Entry?.CalendarEvents ?? []) { }
}

Legend

Office work unit Home work unit Preview work unit Closure Day off Holiday
@if (monthTotals is not null) {

Month Totals

Worked Hours
@FormatHours(monthTotals.TotalWorkedHours)
Counted Units
@monthTotals.CountedWorkUnits
Hours Off
@FormatHours(monthTotals.TotalHoursOff)
Gross Income
€@monthTotals.TotalGrossIncome.ToString("N2")
Net Income
€@monthTotals.TotalNetIncome.ToString("N2")
} } @if (workUnitModalDate.HasValue) { } @if (calendarEventModalDate.HasValue) { }
@code { [Parameter] public string? YearMonth { get; set; } private const string IncludePreviewPreferenceKey = "worktracker.includePreviewWorkUnits"; private DateOnly firstOfMonth; private bool loading = true; private List weeks = []; private readonly Dictionary> festivitiesByYear = []; private DateOnly? activeDate; private bool includePreviewTotals; private MonthlySummaryModel? monthTotals; private string? statusMessage; private DateOnly? workUnitModalDate; private string? workUnitModalId; private DateOnly? calendarEventModalDate; private string? calendarEventModalId; protected override async Task OnInitializedAsync() { if (!string.IsNullOrEmpty(YearMonth) && DateTime.TryParseExact(YearMonth, "yyyy-MM", null, System.Globalization.DateTimeStyles.None, out var parsed)) { firstOfMonth = new DateOnly(parsed.Year, parsed.Month, 1); } else { firstOfMonth = new DateOnly(DateTime.Today.Year, DateTime.Today.Month, 1); } await LoadMonth(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) { return; } var savedIncludePreview = await JS.InvokeAsync("workTrackerPreferences.getBool", IncludePreviewPreferenceKey); if (savedIncludePreview.HasValue && savedIncludePreview.Value != includePreviewTotals) { includePreviewTotals = savedIncludePreview.Value; await LoadMonth(); await InvokeAsync(StateHasChanged); } } private async Task LoadMonth() { loading = true; activeDate = null; var lastDay = firstOfMonth.AddMonths(1).AddDays(-1); var gridStart = GetGridStart(firstOfMonth); var gridEnd = GetGridEnd(lastDay); var entries = await WorkDayService.GetRangeAsync(gridStart, gridEnd); var lookup = entries.ToDictionary(e => e.Date); monthTotals = await WorkDayService.GetMonthlySummaryAsync(firstOfMonth.Year, firstOfMonth.Month, includePreviewTotals); weeks = []; for (var weekStart = gridStart; weekStart <= gridEnd; weekStart = weekStart.AddDays(7)) { var week = new CalendarCell[7]; for (var columnIndex = 0; columnIndex < 7; columnIndex++) { var date = weekStart.AddDays(columnIndex); week[columnIndex] = new CalendarCell { Date = date, ColumnIndex = columnIndex, IsCurrentMonth = date.Month == firstOfMonth.Month && date.Year == firstOfMonth.Year, IsWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday, IsFestivity = GetFestivities(date.Year).Contains(date), Entry = lookup.GetValueOrDefault(date) }; } weeks.Add(week); } loading = false; } private async Task OnIncludePreviewTotalsChanged(ChangeEventArgs e) { includePreviewTotals = e.Value is bool value && value; await JS.InvokeVoidAsync("workTrackerPreferences.setBool", IncludePreviewPreferenceKey, includePreviewTotals); await LoadMonth(); } private async Task PreviousMonth() { firstOfMonth = firstOfMonth.AddMonths(-1); statusMessage = null; await LoadMonth(); } private async Task NextMonth() { firstOfMonth = firstOfMonth.AddMonths(1); statusMessage = null; await LoadMonth(); } private void TogglePopup(DateOnly date) { activeDate = activeDate == date ? null : date; } private void ClosePopup() { activeDate = null; } private static string GetPopupClass(CalendarCell cell) { if (cell.ColumnIndex == 0) { return "calendar-popup-left"; } return cell.ColumnIndex >= 5 ? "calendar-popup-right" : string.Empty; } private bool IsActiveCell(DateOnly date) => activeDate == date; private static bool IsToday(DateOnly date) => date == DateOnly.FromDateTime(DateTime.Today); private void CreateWorkUnit(DateOnly date) { OpenWorkUnit(date, null); } private void CreateCalendarEvent(DateOnly date) { OpenCalendarEvent(date, null); } private void OpenWorkUnit(DateOnly date, string? workUnitId) { activeDate = null; workUnitModalDate = date; workUnitModalId = workUnitId; } private void OpenCalendarEvent(DateOnly date, string? eventId) { activeDate = null; calendarEventModalDate = date; calendarEventModalId = eventId; } private async Task HandleEditorSavedAsync() { CloseWorkUnitModal(); CloseCalendarEventModal(); statusMessage = null; await LoadMonth(); } private void CloseWorkUnitModal() { workUnitModalDate = null; workUnitModalId = null; } private void CloseCalendarEventModal() { calendarEventModalDate = null; calendarEventModalId = null; } private async Task GeneratePreviewWorkUnitsAsync() { var confirmed = await JS.InvokeAsync("confirm", $"Generate preview work units for {firstOfMonth:MMMM yyyy}? Existing work-unit days will be left unchanged."); if (!confirmed) { return; } var createdDays = await WorkDayService.GenerateMonthlyPreviewWorkUnitsAsync(firstOfMonth.Year, firstOfMonth.Month); statusMessage = createdDays == 0 ? "No preview work units were created. Every eligible day already had work units or a blocking calendar event." : $"Created preview work units for {createdDays} day(s)."; await LoadMonth(); } private string GetCellClass(CalendarCell cell) { if (cell.IsWeekend || cell.IsFestivity) { return "calendar-weekend"; } var eventType = GetDominantEventType(cell.Entry); if (eventType.HasValue) { return eventType.Value switch { CalendarEventType.Closure => "calendar-closure", CalendarEventType.Illness => "calendar-illness", CalendarEventType.DayOff => "calendar-dayoff", CalendarEventType.Holiday => "calendar-holiday", _ => string.Empty }; } return string.Empty; } private static CalendarEventType? GetDominantEventType(WorkDayDocument? day) { if (day is null || day.CalendarEvents.Count == 0) { return null; } if (day.CalendarEvents.Any(entry => entry.EventType == CalendarEventType.Holiday)) { return CalendarEventType.Holiday; } if (day.CalendarEvents.Any(entry => entry.EventType == CalendarEventType.Closure)) { return CalendarEventType.Closure; } if (day.CalendarEvents.Any(entry => entry.EventType == CalendarEventType.DayOff)) { return CalendarEventType.DayOff; } if (day.CalendarEvents.Any(entry => entry.EventType == CalendarEventType.Illness)) { return CalendarEventType.Illness; } return null; } private static string GetWorkUnitClass(WorkUnitDocument workUnit) { if (workUnit.IsPreview) { return workUnit.Location == WorkUnitLocation.Home ? "calendar-item-preview-home" : "calendar-item-preview-office"; } return workUnit.Location == WorkUnitLocation.Home ? "calendar-item-home" : "calendar-item-office"; } private static string GetCalendarEventClass(CalendarEventDocument calendarEvent) => calendarEvent.EventType switch { CalendarEventType.Holiday => "calendar-item-holiday", CalendarEventType.Closure => "calendar-item-closure", CalendarEventType.DayOff => "calendar-item-dayoff", CalendarEventType.Illness => "calendar-item-illness", _ => "calendar-item-generic" }; private static string FormatWorkUnit(WorkUnitDocument workUnit) { var hours = FormatHours(workUnit.ManualWorkedHours); var timeRange = workUnit.StartTime.HasValue && workUnit.EndTime.HasValue ? $"{workUnit.StartTime:HH:mm}-{workUnit.EndTime:HH:mm}" : hours; return workUnit.IsPreview ? $"{timeRange} preview" : timeRange; } private static decimal GetDayTotalHours(WorkDayDocument? day, bool includePreview) { if (day is null) { return 0m; } return day.WorkUnits .Where(unit => includePreview || !unit.IsPreview) .Sum(unit => unit.ManualWorkedHours); } private static string FormatHours(decimal value) { return DurationFormatter.FormatHours(value); } private static string FormatDisplayDate(DateOnly date) { return date.ToString("dddd dd/MM/yyyy"); } private IReadOnlyCollection GetFestivities(int year) { if (!festivitiesByYear.TryGetValue(year, out var festivities)) { festivities = FestivitySource.GetFestivities(year); festivitiesByYear[year] = festivities; } return festivities; } private static DateOnly GetGridStart(DateOnly firstDayOfMonth) { var offset = ((int)firstDayOfMonth.DayOfWeek + 6) % 7; return firstDayOfMonth.AddDays(-offset); } private static DateOnly GetGridEnd(DateOnly lastDayOfMonth) { var offset = (7 - (((int)lastDayOfMonth.DayOfWeek + 6) % 7) - 1 + 7) % 7; return lastDayOfMonth.AddDays(offset); } private static DateOnly GetCalendarEventOwnerDate(CalendarEventDocument calendarEvent, DateOnly fallbackDate) { return calendarEvent.StartDate == default ? fallbackDate : calendarEvent.StartDate; } private sealed class CalendarCell { public DateOnly Date { get; set; } public int ColumnIndex { get; set; } public bool IsCurrentMonth { get; set; } public bool IsWeekend { get; set; } public bool IsFestivity { get; set; } public WorkDayDocument? Entry { get; set; } } }