Refactor AppSettingsDocument and CoeffSnapshotDocument: Remove LunchBreakHours property

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
This commit is contained in:
Marco 2026-04-20 16:11:27 +02:00
commit cab549ab3a
22 changed files with 1725 additions and 356 deletions

View file

@ -29,12 +29,10 @@ else
<tr>
<th>Date</th>
<th>Day</th>
<th>Type</th>
<th>Start</th>
<th>Projected</th>
<th>Actual</th>
<th class="text-end">Worked</th>
<th class="text-end">Extra</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>
@ -47,24 +45,42 @@ else
<tr class="@GetRowClass(row)">
<td>@row.Date.ToString("dd")</td>
<td>@row.Date.ToString("ddd")</td>
@if (row.Entry is not null)
{
<td>@row.Entry.DayType</td>
<td>@(row.Entry.StartTime?.ToString("HH:mm") ?? "")</td>
<td>@(row.Entry.ProjectedExitTime?.ToString("HH:mm") ?? "")</td>
<td>@(row.Entry.ActualExitTime?.ToString("HH:mm") ?? "")</td>
<td class="text-end">@row.Entry.WorkedHoursFinal.ToString("N2")</td>
<td class="text-end">@FormatDelta(row.Entry.ExtraHoursDelta)</td>
<td class="text-end">@row.Entry.HoursOff.ToString("N2")</td>
<td class="text-end">@row.Entry.GrossIncome.ToString("N2")</td>
<td class="text-end">@row.Entry.NetIncome.ToString("N2")</td>
}
else
{
<td colspan="9" class="text-muted">—</td>
}
<td>
<a href="workday/@row.Date.ToString("yyyy-MM-dd")" class="btn btn-sm btn-outline-primary">Edit</a>
@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>
}
@ -136,23 +152,93 @@ else
{
if (row.IsWeekend || row.IsFestivity) return "table-danger";
if (row.Entry is null) return "";
return row.Entry.DayType switch
if (row.Entry.CalendarEvents.Any(entry => entry.EventType == CalendarEventType.Holiday))
{
DayType.Closure => "table-warning",
DayType.Illness => "table-info",
DayType.DayOff => "table-secondary",
DayType.Holiday => "table-success",
DayType.Home => "table-light",
_ => ""
};
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 string FormatDelta(decimal d) => d switch
private static decimal GetCountedHours(CalendarDayRow row)
{
> 0 => $"+{d:N2}",
< 0 => d.ToString("N2"),
_ => "—"
};
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
{