Add CalendarEventDocument and CalendarEventType enum for event management Update WorkDayDocument to include WorkUnitDocument and CalendarEventDocument lists Enhance CouchbaseLiteWorkDayService with methods for managing WorkUnit and CalendarEvent Revise MonthlySummaryModel to track preview worked hours and counted work units Improve CSS for calendar view, including responsive design and new item styles
250 lines
8.4 KiB
Text
250 lines
8.4 KiB
Text
@page "/grid"
|
|
@page "/grid/{YearMonth}"
|
|
@attribute [Authorize]
|
|
@rendermode InteractiveServer
|
|
|
|
@inject IWorkDayService WorkDayService
|
|
@inject IItalianFestivitySource FestivitySource
|
|
@inject NavigationManager Navigation
|
|
|
|
<PageTitle>Grid View</PageTitle>
|
|
|
|
<h1>Grid View</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">@currentDate.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-sm table-bordered align-middle">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Day</th>
|
|
<th>Work Units</th>
|
|
<th>Calendar Events</th>
|
|
<th class="text-end">Counted</th>
|
|
<th class="text-end">Preview</th>
|
|
<th class="text-end">Off</th>
|
|
<th class="text-end">Gross €</th>
|
|
<th class="text-end">Net €</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var row in calendarDays)
|
|
{
|
|
<tr class="@GetRowClass(row)">
|
|
<td>@row.Date.ToString("dd")</td>
|
|
<td>@row.Date.ToString("ddd")</td>
|
|
<td>
|
|
@if (row.Entry?.WorkUnits.Count > 0)
|
|
{
|
|
@foreach (var unit in row.Entry.WorkUnits)
|
|
{
|
|
<div class="small mb-1">@unit.Label: @FormatTimeRange(unit.StartTime, unit.EndTime) (@FormatHours(unit.ManualWorkedHours)@(unit.IsPreview ? ", preview" : ""))</div>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">—</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
@if (row.Entry?.CalendarEvents.Count > 0)
|
|
{
|
|
@foreach (var calendarEvent in row.Entry.CalendarEvents)
|
|
{
|
|
<div class="small mb-1">@calendarEvent.EventType: @calendarEvent.Description</div>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">—</span>
|
|
}
|
|
</td>
|
|
<td class="text-end">@FormatHours(GetCountedHours(row))</td>
|
|
<td class="text-end">@FormatHours(GetPreviewHours(row))</td>
|
|
<td class="text-end">@FormatHours(GetHoursOff(row))</td>
|
|
<td class="text-end">@GetGrossIncome(row).ToString("N2")</td>
|
|
<td class="text-end">@GetNetIncome(row).ToString("N2")</td>
|
|
<td>
|
|
<div class="d-flex gap-2">
|
|
<a href="work-unit/@row.Date.ToString("yyyy-MM-dd")" class="btn btn-sm btn-outline-primary">Unit</a>
|
|
<a href="calendar-event/@row.Date.ToString("yyyy-MM-dd")" class="btn btn-sm btn-outline-secondary">Event</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
[Parameter] public string? YearMonth { get; set; }
|
|
|
|
private DateOnly currentDate;
|
|
private bool loading = true;
|
|
private List<CalendarDayRow> calendarDays = [];
|
|
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))
|
|
{
|
|
currentDate = new DateOnly(parsed.Year, parsed.Month, 1);
|
|
}
|
|
else
|
|
{
|
|
currentDate = new DateOnly(DateTime.Today.Year, DateTime.Today.Month, 1);
|
|
}
|
|
|
|
await LoadMonth();
|
|
}
|
|
|
|
private async Task LoadMonth()
|
|
{
|
|
loading = true;
|
|
festivities = FestivitySource.GetFestivities(currentDate.Year);
|
|
|
|
var from = currentDate;
|
|
var to = currentDate.AddMonths(1).AddDays(-1);
|
|
var entries = await WorkDayService.GetRangeAsync(from, to);
|
|
var lookup = entries.ToDictionary(e => e.Date);
|
|
|
|
calendarDays = [];
|
|
for (var d = from; d <= to; d = d.AddDays(1))
|
|
{
|
|
calendarDays.Add(new CalendarDayRow
|
|
{
|
|
Date = d,
|
|
IsWeekend = d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
|
|
IsFestivity = festivities.Contains(d),
|
|
Entry = lookup.GetValueOrDefault(d)
|
|
});
|
|
}
|
|
|
|
loading = false;
|
|
}
|
|
|
|
private async Task PreviousMonth()
|
|
{
|
|
currentDate = currentDate.AddMonths(-1);
|
|
await LoadMonth();
|
|
}
|
|
|
|
private async Task NextMonth()
|
|
{
|
|
currentDate = currentDate.AddMonths(1);
|
|
await LoadMonth();
|
|
}
|
|
|
|
private string GetRowClass(CalendarDayRow row)
|
|
{
|
|
if (row.IsWeekend || row.IsFestivity) return "table-danger";
|
|
if (row.Entry is null) return "";
|
|
if (row.Entry.CalendarEvents.Any(entry => entry.EventType == CalendarEventType.Holiday))
|
|
{
|
|
return "table-success";
|
|
}
|
|
|
|
if (row.Entry.CalendarEvents.Any(entry => entry.EventType == CalendarEventType.Closure))
|
|
{
|
|
return "table-warning";
|
|
}
|
|
|
|
if (row.Entry.CalendarEvents.Any(entry => entry.EventType == CalendarEventType.Illness))
|
|
{
|
|
return "table-info";
|
|
}
|
|
|
|
if (row.Entry.CalendarEvents.Any(entry => entry.EventType == CalendarEventType.DayOff))
|
|
{
|
|
return "table-secondary";
|
|
}
|
|
|
|
if (row.Entry.WorkUnits.Any(entry => entry.Location == WorkUnitLocation.Home))
|
|
{
|
|
return "table-light";
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
private static decimal GetCountedHours(CalendarDayRow row)
|
|
{
|
|
return row.Entry?.WorkUnits.Where(unit => !unit.IsPreview).Sum(unit => unit.ManualWorkedHours) ?? 0m;
|
|
}
|
|
|
|
private static decimal GetPreviewHours(CalendarDayRow row)
|
|
{
|
|
return row.Entry?.WorkUnits.Where(unit => unit.IsPreview).Sum(unit => unit.ManualWorkedHours) ?? 0m;
|
|
}
|
|
|
|
private static decimal GetHoursOff(CalendarDayRow row)
|
|
{
|
|
if (row.Entry is null || row.Entry.WorkUnits.Count == 0)
|
|
{
|
|
return 0m;
|
|
}
|
|
|
|
var countedUnits = row.Entry.WorkUnits.Where(unit => !unit.IsPreview).ToList();
|
|
if (countedUnits.Count == 0)
|
|
{
|
|
return 0m;
|
|
}
|
|
|
|
var standardHours = countedUnits[0].CoeffSnapshot.StandardWorkHoursPerDay;
|
|
return Math.Max(0m, standardHours - countedUnits.Sum(unit => unit.ManualWorkedHours));
|
|
}
|
|
|
|
private static decimal GetGrossIncome(CalendarDayRow row)
|
|
{
|
|
return row.Entry?.WorkUnits.Where(unit => !unit.IsPreview).Sum(unit => unit.GrossIncome) ?? 0m;
|
|
}
|
|
|
|
private static decimal GetNetIncome(CalendarDayRow row)
|
|
{
|
|
return row.Entry?.WorkUnits.Where(unit => !unit.IsPreview).Sum(unit => unit.NetIncome) ?? 0m;
|
|
}
|
|
|
|
private static string FormatTimeRange(TimeOnly? startTime, TimeOnly? endTime)
|
|
{
|
|
if (startTime.HasValue && endTime.HasValue)
|
|
{
|
|
return $"{startTime:HH:mm}-{endTime:HH:mm}";
|
|
}
|
|
|
|
if (startTime.HasValue)
|
|
{
|
|
return startTime.Value.ToString("HH:mm");
|
|
}
|
|
|
|
return "No time range";
|
|
}
|
|
|
|
private static string FormatHours(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 sealed class CalendarDayRow
|
|
{
|
|
public DateOnly Date { get; set; }
|
|
public bool IsWeekend { get; set; }
|
|
public bool IsFestivity { get; set; }
|
|
public WorkDayDocument? Entry { get; set; }
|
|
}
|
|
}
|