WorkTracker/Services/WorkDays/CouchbaseLiteWorkDayService.cs
Marco cab549ab3a 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
2026-04-20 16:11:27 +02:00

662 lines
27 KiB
C#

using Couchbase.Lite;
using WorkTracker.Domain;
using WorkTracker.Services.Festivities;
using WorkTracker.Services.Settings;
using WorkTracker.Services.Storage;
namespace WorkTracker.Services.WorkDays;
public sealed class CouchbaseLiteWorkDayService : IWorkDayService
{
private readonly Collection workDaysCollection;
private readonly IAppSettingsService appSettingsService;
private readonly IItalianFestivitySource festivitySource;
public CouchbaseLiteWorkDayService(
CouchbaseLiteDatabaseProvider databaseProvider,
IAppSettingsService appSettingsService,
IItalianFestivitySource festivitySource)
{
workDaysCollection = databaseProvider.WorkDays;
this.appSettingsService = appSettingsService;
this.festivitySource = festivitySource;
}
public Task<WorkDayDocument?> GetAsync(DateOnly date, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var id = date.ToString("yyyy-MM-dd");
var doc = workDaysCollection.GetDocument(id);
return Task.FromResult(doc is not null ? Map(doc) : null);
}
public async Task<WorkUnitDocument?> GetWorkUnitAsync(DateOnly date, string workUnitId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var day = await GetAsync(date, cancellationToken);
return day?.WorkUnits.FirstOrDefault(unit => string.Equals(unit.Id, workUnitId, StringComparison.Ordinal));
}
public async Task<CalendarEventDocument?> GetCalendarEventAsync(DateOnly date, string calendarEventId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var day = await GetAsync(date, cancellationToken);
return day?.CalendarEvents.FirstOrDefault(calendarEvent => string.Equals(calendarEvent.Id, calendarEventId, StringComparison.Ordinal));
}
public async Task<WorkUnitDocument> SaveWorkUnitAsync(DateOnly date, WorkUnitDocument workUnit, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var settings = await appSettingsService.GetAsync(cancellationToken);
var day = await GetOrCreateDayAsync(date, cancellationToken);
var now = DateTimeOffset.UtcNow;
var existingIndex = day.WorkUnits.FindIndex(unit => string.Equals(unit.Id, workUnit.Id, StringComparison.Ordinal));
var existingCreatedAt = existingIndex >= 0 ? day.WorkUnits[existingIndex].CreatedAtUtc : now;
workUnit.Id = string.IsNullOrWhiteSpace(workUnit.Id) ? Guid.NewGuid().ToString("N") : workUnit.Id;
workUnit.Label = string.IsNullOrWhiteSpace(workUnit.Label) ? "Work unit" : workUnit.Label.Trim();
workUnit.ManualWorkedHours = Math.Max(0m, workUnit.ManualWorkedHours);
workUnit.CoeffSnapshot = new CoeffSnapshotDocument
{
StandardWorkHoursPerDay = settings.StandardWorkHoursPerDay,
HourlyGrossRate = settings.HourlyGrossRate,
ProfitabilityCoefficient = settings.ProfitabilityCoefficient,
InpsRate = settings.InpsRate,
SubstituteTaxRate = settings.SubstituteTaxRate
};
workUnit.CreatedAtUtc = existingCreatedAt;
workUnit.UpdatedAtUtc = now;
Compute(workUnit);
if (existingIndex >= 0)
{
day.WorkUnits[existingIndex] = workUnit;
}
else
{
day.WorkUnits.Add(workUnit);
}
day.UpdatedAtUtc = now;
SortEntries(day);
SaveDocument(day);
return workUnit;
}
public async Task<CalendarEventDocument> SaveCalendarEventAsync(DateOnly date, CalendarEventDocument calendarEvent, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var day = await GetOrCreateDayAsync(date, cancellationToken);
var now = DateTimeOffset.UtcNow;
var existingIndex = day.CalendarEvents.FindIndex(entry => string.Equals(entry.Id, calendarEvent.Id, StringComparison.Ordinal));
var existingCreatedAt = existingIndex >= 0 ? day.CalendarEvents[existingIndex].CreatedAtUtc : now;
calendarEvent.Id = string.IsNullOrWhiteSpace(calendarEvent.Id) ? Guid.NewGuid().ToString("N") : calendarEvent.Id;
calendarEvent.Description = string.IsNullOrWhiteSpace(calendarEvent.Description)
? "Calendar entry"
: calendarEvent.Description.Trim();
calendarEvent.CreatedAtUtc = existingCreatedAt;
calendarEvent.UpdatedAtUtc = now;
Compute(calendarEvent);
if (existingIndex >= 0)
{
day.CalendarEvents[existingIndex] = calendarEvent;
}
else
{
day.CalendarEvents.Add(calendarEvent);
}
day.UpdatedAtUtc = now;
SortEntries(day);
SaveDocument(day);
return calendarEvent;
}
public async Task<bool> DeleteWorkUnitAsync(DateOnly date, string workUnitId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var day = await GetAsync(date, cancellationToken);
if (day is null)
{
return false;
}
var removed = day.WorkUnits.RemoveAll(unit => string.Equals(unit.Id, workUnitId, StringComparison.Ordinal));
if (removed == 0)
{
return false;
}
return DeleteOrSaveDay(day);
}
public async Task<bool> DeleteCalendarEventAsync(DateOnly date, string calendarEventId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var day = await GetAsync(date, cancellationToken);
if (day is null)
{
return false;
}
var removed = day.CalendarEvents.RemoveAll(calendarEvent => string.Equals(calendarEvent.Id, calendarEventId, StringComparison.Ordinal));
if (removed == 0)
{
return false;
}
return DeleteOrSaveDay(day);
}
public Task<IReadOnlyList<WorkDayDocument>> GetRangeAsync(DateOnly from, DateOnly to, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var results = new List<WorkDayDocument>();
for (var date = from; date <= to; date = date.AddDays(1))
{
var id = date.ToString("yyyy-MM-dd");
var doc = workDaysCollection.GetDocument(id);
if (doc is not null)
{
results.Add(Map(doc));
}
}
return Task.FromResult<IReadOnlyList<WorkDayDocument>>(results);
}
public async Task<MonthlySummaryModel> GetMonthlySummaryAsync(int year, int month, bool includePreview, CancellationToken cancellationToken = default)
{
var from = new DateOnly(year, month, 1);
var to = from.AddMonths(1).AddDays(-1);
var days = await GetRangeAsync(from, to, cancellationToken);
var includedUnits = days
.SelectMany(day => day.WorkUnits.Where(unit => includePreview || !unit.IsPreview).Select(unit => new { day.Date, Unit = unit }))
.ToList();
var previewUnits = days
.SelectMany(day => day.WorkUnits.Where(unit => unit.IsPreview).Select(unit => new { day.Date, Unit = unit }))
.ToList();
return new MonthlySummaryModel
{
Year = year,
Month = month,
TotalWorkedHours = includedUnits.Sum(item => item.Unit.ManualWorkedHours),
TotalPreviewWorkedHours = previewUnits.Sum(item => item.Unit.ManualWorkedHours),
CountedWorkUnits = includedUnits.Count,
PreviewWorkUnits = previewUnits.Count,
OfficeDays = includedUnits.Where(item => item.Unit.Location == WorkUnitLocation.Office).Select(item => item.Date).Distinct().Count(),
HomeDays = includedUnits.Where(item => item.Unit.Location == WorkUnitLocation.Home).Select(item => item.Date).Distinct().Count(),
HolidayDays = CountDaysWithEvent(days, CalendarEventType.Holiday),
SickDays = CountDaysWithEvent(days, CalendarEventType.Illness),
DaysOff = CountDaysWithEvent(days, CalendarEventType.DayOff),
ClosureDays = CountDaysWithEvent(days, CalendarEventType.Closure),
TotalHoursOff = days.Sum(day => GetHoursOff(day, includePreview)),
TotalGrossIncome = includedUnits.Sum(item => item.Unit.GrossIncome),
TotalNetIncome = includedUnits.Sum(item => item.Unit.NetIncome),
TotalWorkingDays = includedUnits.Select(item => item.Date).Distinct().Count()
};
}
public async Task<int> GenerateMonthlyPreviewWorkUnitsAsync(int year, int month, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var settings = await appSettingsService.GetAsync(cancellationToken);
var festivities = festivitySource.GetFestivities(year);
var from = new DateOnly(year, month, 1);
var to = from.AddMonths(1).AddDays(-1);
var createdDays = 0;
for (var date = from; date <= to; date = date.AddDays(1))
{
cancellationToken.ThrowIfCancellationRequested();
if (date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday || festivities.Contains(date))
{
continue;
}
var day = await GetOrCreateDayAsync(date, cancellationToken);
if (day.WorkUnits.Count > 0 || day.CalendarEvents.Any(entry => IsNonWorkingEvent(entry.EventType)))
{
continue;
}
day.WorkUnits.Add(CreatePreviewWorkUnit("Morning", new TimeOnly(8, 30), new TimeOnly(13, 0), settings));
day.WorkUnits.Add(CreatePreviewWorkUnit("Afternoon", new TimeOnly(14, 0), new TimeOnly(17, 30), settings));
day.UpdatedAtUtc = DateTimeOffset.UtcNow;
SortEntries(day);
SaveDocument(day);
createdDays++;
}
return createdDays;
}
private void SaveDocument(WorkDayDocument day)
{
var doc = new MutableDocument(day.Id);
doc.SetString("date", day.Date.ToString("yyyy-MM-dd"));
doc.SetBoolean("isWeekend", day.IsWeekend);
doc.SetBoolean("isItalianFestivity", day.IsItalianFestivity);
var workUnits = new MutableArrayObject();
foreach (var unit in day.WorkUnits)
{
var entry = new MutableDictionaryObject();
entry.SetString("id", unit.Id);
entry.SetString("label", unit.Label);
entry.SetInt("location", (int)unit.Location);
entry.SetString("startTime", unit.StartTime?.ToString("HH:mm"));
entry.SetString("endTime", unit.EndTime?.ToString("HH:mm"));
entry.SetBoolean("isPreview", unit.IsPreview);
entry.SetDouble("manualWorkedHours", decimal.ToDouble(unit.ManualWorkedHours));
entry.SetDouble("calculatedWorkedHours", decimal.ToDouble(unit.CalculatedWorkedHours));
entry.SetDouble("workedHoursDelta", decimal.ToDouble(unit.WorkedHoursDelta));
entry.SetDouble("grossIncome", decimal.ToDouble(unit.GrossIncome));
entry.SetDouble("netIncome", decimal.ToDouble(unit.NetIncome));
entry.SetString("notes", unit.Notes);
entry.SetDouble("coeff_standardWorkHoursPerDay", decimal.ToDouble(unit.CoeffSnapshot.StandardWorkHoursPerDay));
entry.SetDouble("coeff_hourlyGrossRate", decimal.ToDouble(unit.CoeffSnapshot.HourlyGrossRate));
entry.SetDouble("coeff_profitabilityCoefficient", decimal.ToDouble(unit.CoeffSnapshot.ProfitabilityCoefficient));
entry.SetDouble("coeff_inpsRate", decimal.ToDouble(unit.CoeffSnapshot.InpsRate));
entry.SetDouble("coeff_substituteTaxRate", decimal.ToDouble(unit.CoeffSnapshot.SubstituteTaxRate));
entry.SetString("createdAtUtc", unit.CreatedAtUtc.ToString("O"));
entry.SetString("updatedAtUtc", unit.UpdatedAtUtc.ToString("O"));
workUnits.AddDictionary(entry);
}
var calendarEvents = new MutableArrayObject();
foreach (var calendarEvent in day.CalendarEvents)
{
var entry = new MutableDictionaryObject();
entry.SetString("id", calendarEvent.Id);
entry.SetInt("eventType", (int)calendarEvent.EventType);
entry.SetString("description", calendarEvent.Description);
entry.SetString("startTime", calendarEvent.StartTime?.ToString("HH:mm"));
entry.SetString("endTime", calendarEvent.EndTime?.ToString("HH:mm"));
if (calendarEvent.DurationHours.HasValue)
{
entry.SetDouble("durationHours", decimal.ToDouble(calendarEvent.DurationHours.Value));
}
entry.SetString("createdAtUtc", calendarEvent.CreatedAtUtc.ToString("O"));
entry.SetString("updatedAtUtc", calendarEvent.UpdatedAtUtc.ToString("O"));
calendarEvents.AddDictionary(entry);
}
doc.SetArray("workUnits", workUnits);
doc.SetArray("calendarEvents", calendarEvents);
doc.SetString("createdAtUtc", day.CreatedAtUtc.ToString("O"));
doc.SetString("updatedAtUtc", day.UpdatedAtUtc.ToString("O"));
workDaysCollection.Save(doc);
}
private bool DeleteOrSaveDay(WorkDayDocument day)
{
if (day.WorkUnits.Count == 0 && day.CalendarEvents.Count == 0)
{
var existing = workDaysCollection.GetDocument(day.Id);
if (existing is null)
{
return false;
}
workDaysCollection.Delete(existing);
return true;
}
day.UpdatedAtUtc = DateTimeOffset.UtcNow;
SortEntries(day);
SaveDocument(day);
return true;
}
private static WorkDayDocument Map(Document doc)
{
if (!doc.Contains("workUnits") && !doc.Contains("calendarEvents"))
{
return MapLegacy(doc);
}
var workUnits = new List<WorkUnitDocument>();
var workUnitsArray = doc.GetArray("workUnits");
if (workUnitsArray is not null)
{
for (var i = 0; i < workUnitsArray.Count; i++)
{
var unit = workUnitsArray.GetDictionary(i);
if (unit is not null)
{
workUnits.Add(MapWorkUnit(unit));
}
}
}
var calendarEvents = new List<CalendarEventDocument>();
var calendarEventsArray = doc.GetArray("calendarEvents");
if (calendarEventsArray is not null)
{
for (var i = 0; i < calendarEventsArray.Count; i++)
{
var calendarEvent = calendarEventsArray.GetDictionary(i);
if (calendarEvent is not null)
{
calendarEvents.Add(MapCalendarEvent(calendarEvent));
}
}
}
return new WorkDayDocument
{
Id = doc.Id,
Date = DateOnly.ParseExact(doc.GetString("date") ?? doc.Id, "yyyy-MM-dd"),
IsWeekend = doc.GetBoolean("isWeekend"),
IsItalianFestivity = doc.GetBoolean("isItalianFestivity"),
WorkUnits = workUnits,
CalendarEvents = calendarEvents,
CreatedAtUtc = ReadDateTimeOffset(doc, "createdAtUtc"),
UpdatedAtUtc = ReadDateTimeOffset(doc, "updatedAtUtc")
};
}
private async Task<WorkDayDocument> GetOrCreateDayAsync(DateOnly date, CancellationToken cancellationToken)
{
var existing = await GetAsync(date, cancellationToken);
if (existing is not null)
{
existing.IsWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
existing.IsItalianFestivity = festivitySource.GetFestivities(date.Year).Contains(date);
existing.Id = date.ToString("yyyy-MM-dd");
existing.Date = date;
return existing;
}
return new WorkDayDocument
{
Id = date.ToString("yyyy-MM-dd"),
Date = date,
IsWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
IsItalianFestivity = festivitySource.GetFestivities(date.Year).Contains(date),
CreatedAtUtc = DateTimeOffset.UtcNow,
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static void Compute(WorkUnitDocument unit)
{
unit.CalculatedWorkedHours = CalculateDuration(unit.StartTime, unit.EndTime) ?? 0m;
unit.WorkedHoursDelta = unit.ManualWorkedHours - unit.CalculatedWorkedHours;
var coeff = unit.CoeffSnapshot;
unit.GrossIncome = unit.ManualWorkedHours * coeff.HourlyGrossRate;
var taxableBase = unit.GrossIncome * coeff.ProfitabilityCoefficient;
unit.NetIncome = unit.GrossIncome - (taxableBase * coeff.InpsRate) - (taxableBase * coeff.SubstituteTaxRate);
}
private static void Compute(CalendarEventDocument calendarEvent)
{
calendarEvent.DurationHours = CalculateDuration(calendarEvent.StartTime, calendarEvent.EndTime);
}
private static decimal? CalculateDuration(TimeOnly? startTime, TimeOnly? endTime)
{
if (!startTime.HasValue || !endTime.HasValue || endTime <= startTime)
{
return null;
}
return Math.Round((decimal)(endTime.Value - startTime.Value).TotalHours, 2, MidpointRounding.AwayFromZero);
}
private static int CountDaysWithEvent(IEnumerable<WorkDayDocument> days, CalendarEventType eventType)
{
return days.Count(day => day.CalendarEvents.Any(calendarEvent => calendarEvent.EventType == eventType));
}
private static decimal GetHoursOff(WorkDayDocument day, bool includePreview)
{
var includedUnits = day.WorkUnits.Where(unit => includePreview || !unit.IsPreview).ToList();
if (includedUnits.Count == 0)
{
return 0m;
}
var standardHours = includedUnits[0].CoeffSnapshot.StandardWorkHoursPerDay;
var countedHours = includedUnits.Sum(unit => unit.ManualWorkedHours);
return Math.Max(0m, standardHours - countedHours);
}
private static bool IsNonWorkingEvent(CalendarEventType eventType)
{
return eventType is CalendarEventType.DayOff or CalendarEventType.Closure or CalendarEventType.Holiday or CalendarEventType.Illness;
}
private static WorkUnitDocument CreatePreviewWorkUnit(string label, TimeOnly startTime, TimeOnly endTime, AppSettingsDocument settings)
{
var workUnit = new WorkUnitDocument
{
Id = Guid.NewGuid().ToString("N"),
Label = label,
Location = WorkUnitLocation.Office,
StartTime = startTime,
EndTime = endTime,
IsPreview = true,
ManualWorkedHours = Math.Round((decimal)(endTime - startTime).TotalHours, 2, MidpointRounding.AwayFromZero),
CoeffSnapshot = new CoeffSnapshotDocument
{
StandardWorkHoursPerDay = settings.StandardWorkHoursPerDay,
HourlyGrossRate = settings.HourlyGrossRate,
ProfitabilityCoefficient = settings.ProfitabilityCoefficient,
InpsRate = settings.InpsRate,
SubstituteTaxRate = settings.SubstituteTaxRate
},
CreatedAtUtc = DateTimeOffset.UtcNow,
UpdatedAtUtc = DateTimeOffset.UtcNow
};
Compute(workUnit);
return workUnit;
}
private static void SortEntries(WorkDayDocument day)
{
day.WorkUnits = day.WorkUnits
.OrderBy(unit => unit.StartTime ?? TimeOnly.MaxValue)
.ThenBy(unit => unit.Label, StringComparer.CurrentCultureIgnoreCase)
.ToList();
day.CalendarEvents = day.CalendarEvents
.OrderBy(calendarEvent => calendarEvent.StartTime ?? TimeOnly.MaxValue)
.ThenBy(calendarEvent => calendarEvent.Description, StringComparer.CurrentCultureIgnoreCase)
.ToList();
}
private static WorkUnitDocument MapWorkUnit(DictionaryObject unit)
{
var workUnit = new WorkUnitDocument
{
Id = unit.GetString("id") ?? Guid.NewGuid().ToString("N"),
Label = unit.GetString("label") ?? "Work unit",
Location = unit.Contains("location") ? (WorkUnitLocation)unit.GetInt("location") : WorkUnitLocation.Office,
StartTime = ReadTimeOnly(unit, "startTime"),
EndTime = ReadTimeOnly(unit, "endTime"),
IsPreview = unit.GetBoolean("isPreview"),
ManualWorkedHours = ReadDecimal(unit, "manualWorkedHours", 0m),
CalculatedWorkedHours = ReadDecimal(unit, "calculatedWorkedHours", 0m),
WorkedHoursDelta = ReadDecimal(unit, "workedHoursDelta", 0m),
GrossIncome = ReadDecimal(unit, "grossIncome", 0m),
NetIncome = ReadDecimal(unit, "netIncome", 0m),
Notes = unit.GetString("notes"),
CoeffSnapshot = new CoeffSnapshotDocument
{
StandardWorkHoursPerDay = ReadDecimal(unit, "coeff_standardWorkHoursPerDay", 8m),
HourlyGrossRate = ReadDecimal(unit, "coeff_hourlyGrossRate", 17.5m),
ProfitabilityCoefficient = ReadDecimal(unit, "coeff_profitabilityCoefficient", 0.67m),
InpsRate = ReadDecimal(unit, "coeff_inpsRate", 0.2607m),
SubstituteTaxRate = ReadDecimal(unit, "coeff_substituteTaxRate", 0.15m)
},
CreatedAtUtc = ReadDateTimeOffset(unit, "createdAtUtc"),
UpdatedAtUtc = ReadDateTimeOffset(unit, "updatedAtUtc")
};
Compute(workUnit);
return workUnit;
}
private static CalendarEventDocument MapCalendarEvent(DictionaryObject calendarEvent)
{
var entry = new CalendarEventDocument
{
Id = calendarEvent.GetString("id") ?? Guid.NewGuid().ToString("N"),
EventType = calendarEvent.Contains("eventType") ? (CalendarEventType)calendarEvent.GetInt("eventType") : CalendarEventType.Generic,
Description = calendarEvent.GetString("description") ?? "Calendar entry",
StartTime = ReadTimeOnly(calendarEvent, "startTime"),
EndTime = ReadTimeOnly(calendarEvent, "endTime"),
DurationHours = calendarEvent.Contains("durationHours") ? ReadDecimal(calendarEvent, "durationHours", 0m) : null,
CreatedAtUtc = ReadDateTimeOffset(calendarEvent, "createdAtUtc"),
UpdatedAtUtc = ReadDateTimeOffset(calendarEvent, "updatedAtUtc")
};
Compute(entry);
return entry;
}
private static WorkDayDocument MapLegacy(Document doc)
{
var date = DateOnly.ParseExact(doc.GetString("date") ?? doc.Id, "yyyy-MM-dd");
var dayType = doc.Contains("dayType") ? (DayType)doc.GetInt("dayType") : DayType.None;
var day = new WorkDayDocument
{
Id = doc.Id,
Date = date,
IsWeekend = doc.GetBoolean("isWeekend"),
IsItalianFestivity = doc.GetBoolean("isItalianFestivity"),
CreatedAtUtc = ReadDateTimeOffset(doc, "createdAtUtc"),
UpdatedAtUtc = ReadDateTimeOffset(doc, "updatedAtUtc")
};
var coeffSnapshot = new CoeffSnapshotDocument
{
StandardWorkHoursPerDay = ReadDecimal(doc, "coeff_standardWorkHoursPerDay", 8m),
HourlyGrossRate = ReadDecimal(doc, "coeff_hourlyGrossRate", 17.5m),
ProfitabilityCoefficient = ReadDecimal(doc, "coeff_profitabilityCoefficient", 0.67m),
InpsRate = ReadDecimal(doc, "coeff_inpsRate", 0.2607m),
SubstituteTaxRate = ReadDecimal(doc, "coeff_substituteTaxRate", 0.15m)
};
if (dayType is DayType.Work or DayType.Home)
{
var workUnit = new WorkUnitDocument
{
Id = "legacy",
Label = "Legacy entry",
Location = dayType == DayType.Home ? WorkUnitLocation.Home : WorkUnitLocation.Office,
StartTime = ReadTimeOnly(doc, "startTime"),
EndTime = ReadTimeOnly(doc, "actualExitTime") ?? ReadTimeOnly(doc, "projectedExitTime"),
IsPreview = false,
ManualWorkedHours = ReadDecimal(doc, "workedHoursFinal", ReadDecimal(doc, "workedHoursBase", 0m)),
GrossIncome = ReadDecimal(doc, "grossIncome", 0m),
NetIncome = ReadDecimal(doc, "netIncome", 0m),
Notes = doc.GetString("notes"),
CoeffSnapshot = coeffSnapshot,
CreatedAtUtc = day.CreatedAtUtc,
UpdatedAtUtc = day.UpdatedAtUtc
};
Compute(workUnit);
day.WorkUnits.Add(workUnit);
}
else if (dayType != DayType.None)
{
var calendarEvent = new CalendarEventDocument
{
Id = "legacy",
EventType = MapLegacyEventType(dayType),
Description = string.IsNullOrWhiteSpace(doc.GetString("notes")) ? $"Legacy {dayType}" : doc.GetString("notes")!,
CreatedAtUtc = day.CreatedAtUtc,
UpdatedAtUtc = day.UpdatedAtUtc
};
Compute(calendarEvent);
day.CalendarEvents.Add(calendarEvent);
}
return day;
}
private static CalendarEventType MapLegacyEventType(DayType dayType)
{
return dayType switch
{
DayType.DayOff => CalendarEventType.DayOff,
DayType.Closure => CalendarEventType.Closure,
DayType.Holiday => CalendarEventType.Holiday,
DayType.Illness => CalendarEventType.Illness,
_ => CalendarEventType.Generic
};
}
private static TimeOnly? ReadTimeOnly(Document doc, string key)
{
var value = doc.GetString(key);
return !string.IsNullOrEmpty(value) && TimeOnly.TryParseExact(value, "HH:mm", out var time)
? time
: null;
}
private static TimeOnly? ReadTimeOnly(DictionaryObject doc, string key)
{
var value = doc.GetString(key);
return !string.IsNullOrEmpty(value) && TimeOnly.TryParseExact(value, "HH:mm", out var time)
? time
: null;
}
private static decimal ReadDecimal(Document doc, string key, decimal defaultValue)
{
return doc.Contains(key)
? Convert.ToDecimal(doc.GetDouble(key))
: defaultValue;
}
private static decimal ReadDecimal(DictionaryObject doc, string key, decimal defaultValue)
{
return doc.Contains(key)
? Convert.ToDecimal(doc.GetDouble(key))
: defaultValue;
}
private static DateTimeOffset ReadDateTimeOffset(Document doc, string key)
{
var value = doc.GetString(key);
return !string.IsNullOrEmpty(value) && DateTimeOffset.TryParse(value, out var dt)
? dt
: DateTimeOffset.UtcNow;
}
private static DateTimeOffset ReadDateTimeOffset(DictionaryObject doc, string key)
{
var value = doc.GetString(key);
return !string.IsNullOrEmpty(value) && DateTimeOffset.TryParse(value, out var dt)
? dt
: DateTimeOffset.UtcNow;
}
}