190 lines
6 KiB
Text
190 lines
6 KiB
Text
|
|
@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">« Prev</button>
|
||
|
|
<h2 class="h5 mb-0">@firstOfMonth.ToString("MMMM yyyy")</h2>
|
||
|
|
<button class="btn btn-outline-secondary btn-sm" @onclick="NextMonth">Next »</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; }
|
||
|
|
}
|
||
|
|
}
|