WorkTracker/Components/Pages/CalendarView.razor

190 lines
6 KiB
Text
Raw Normal View History

@page "/calendar"
@page "/calendar/{YearMonth}"
@attribute [Authorize]
@rendermode InteractiveServer
@inject IWorkDayService WorkDayService
@inject IItalianFestivitySource FestivitySource
@inject NavigationManager Navigation
<PageTitle>Calendar</PageTitle>
<h1>Calendar</h1>
<div class="d-flex align-items-center gap-2 mb-3">
<button class="btn btn-outline-secondary btn-sm" @onclick="PreviousMonth">&laquo; Prev</button>
<h2 class="h5 mb-0">@firstOfMonth.ToString("MMMM yyyy")</h2>
<button class="btn btn-outline-secondary btn-sm" @onclick="NextMonth">Next &raquo;</button>
</div>
@if (loading)
{
<p><em>Loading...</em></p>
}
else
{
<div class="table-responsive">
<table class="table table-bordered calendar-table">
<thead class="table-dark">
<tr>
<th>Mon</th>
<th>Tue</th>
<th>Wed</th>
<th>Thu</th>
<th>Fri</th>
<th>Sat</th>
<th>Sun</th>
</tr>
</thead>
<tbody>
@foreach (var week in weeks)
{
<tr>
@foreach (var cell in week)
{
@if (cell is null)
{
<td class="calendar-cell bg-light"></td>
}
else
{
<td class="calendar-cell @GetCellClass(cell)" @onclick="() => NavigateToDay(cell.Date)" role="button">
<div class="calendar-day-number">@cell.Date.Day</div>
@if (cell.Entry is not null)
{
<span class="badge @GetBadgeClass(cell.Entry.DayType)">@cell.Entry.DayType</span>
<div class="calendar-hours">@cell.Entry.WorkedHoursFinal.ToString("N1")h</div>
}
</td>
}
}
</tr>
}
</tbody>
</table>
</div>
<div class="mt-3">
<h3 class="h6">Legend</h3>
<div class="d-flex flex-wrap gap-2">
<span class="badge bg-primary">Work</span>
<span class="badge bg-success">Home</span>
<span class="badge bg-warning text-dark">Closure</span>
<span class="badge bg-info text-dark">Illness</span>
<span class="badge bg-secondary">DayOff</span>
<span class="badge bg-danger">Holiday</span>
</div>
</div>
}
@code {
[Parameter] public string? YearMonth { get; set; }
private DateOnly firstOfMonth;
private bool loading = true;
private List<CalendarCell?[]> weeks = [];
private IReadOnlyCollection<DateOnly> festivities = [];
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();
}
private async Task LoadMonth()
{
loading = true;
festivities = FestivitySource.GetFestivities(firstOfMonth.Year);
var lastDay = firstOfMonth.AddMonths(1).AddDays(-1);
var entries = await WorkDayService.GetRangeAsync(firstOfMonth, lastDay);
var lookup = entries.ToDictionary(e => e.Date);
// Build calendar grid (ISO weeks: Monday = 0)
weeks = [];
var currentWeek = new CalendarCell?[7];
var dayOfWeek = ((int)firstOfMonth.DayOfWeek + 6) % 7; // Mon=0
for (var d = firstOfMonth; d <= lastDay; d = d.AddDays(1))
{
currentWeek[dayOfWeek] = new CalendarCell
{
Date = d,
IsWeekend = d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
IsFestivity = festivities.Contains(d),
Entry = lookup.GetValueOrDefault(d)
};
dayOfWeek++;
if (dayOfWeek == 7)
{
weeks.Add(currentWeek);
currentWeek = new CalendarCell?[7];
dayOfWeek = 0;
}
}
if (dayOfWeek > 0)
{
weeks.Add(currentWeek);
}
loading = false;
}
private async Task PreviousMonth()
{
firstOfMonth = firstOfMonth.AddMonths(-1);
await LoadMonth();
}
private async Task NextMonth()
{
firstOfMonth = firstOfMonth.AddMonths(1);
await LoadMonth();
}
private void NavigateToDay(DateOnly date) =>
Navigation.NavigateTo($"/workday/{date:yyyy-MM-dd}");
private string GetCellClass(CalendarCell cell)
{
if (cell.IsWeekend || cell.IsFestivity) return "calendar-weekend";
if (cell.Entry is null) return "";
return cell.Entry.DayType switch
{
DayType.Closure => "calendar-closure",
DayType.Illness => "calendar-illness",
DayType.DayOff => "calendar-dayoff",
DayType.Holiday => "calendar-holiday",
_ => ""
};
}
private static string GetBadgeClass(DayType type) => type switch
{
DayType.Work => "bg-primary",
DayType.Home => "bg-success",
DayType.Closure => "bg-warning text-dark",
DayType.Illness => "bg-info text-dark",
DayType.DayOff => "bg-secondary",
DayType.Holiday => "bg-danger",
_ => "bg-light text-dark"
};
private sealed class CalendarCell
{
public DateOnly Date { get; set; }
public bool IsWeekend { get; set; }
public bool IsFestivity { get; set; }
public WorkDayDocument? Entry { get; set; }
}
}