feat: sync all-day events for shifts without time (Frei, holidays, etc.)

Both Google Calendar and Nextcloud CalDAV now create all-day events for
shifts that have no MorningTime or AfternoonTime configured, instead of
silently skipping them. Fixes month-boundary crash on day 31 by using
DateOnly.AddDays(1) for the end date.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claudio Schaad 2026-04-13 20:39:23 +02:00
parent 0520ce3847
commit d00f5ea8b7
2 changed files with 49 additions and 0 deletions

View file

@ -84,7 +84,12 @@ public class GoogleCalendarService(IHttpContextAccessor httpContextAccessor, ICo
var date = shiftWithTransport.Date;
if (string.IsNullOrEmpty(shift.MorningTime) && string.IsNullOrEmpty(shift.AfternoonTime))
{
var eventData = CalendarEventBuilder.BuildEventContent(shift, null);
var allDayEvent = CreateAllDayGoogleEvent(eventData, DateOnly.FromDateTime(date));
await CalendarEventBuilder.ExecuteWithRetryAsync(async () => await service.Events.Insert(allDayEvent, calendarId).ExecuteAsync());
return;
}
if (!string.IsNullOrEmpty(shift.MorningTime))
{
@ -103,6 +108,26 @@ public class GoogleCalendarService(IHttpContextAccessor httpContextAccessor, ICo
}
}
private static Event CreateAllDayGoogleEvent(CalendarEventData eventData, DateOnly date)
{
var dateStr = date.ToString("yyyy-MM-dd");
var nextDateStr = date.AddDays(1).ToString("yyyy-MM-dd");
return new Event
{
Summary = eventData.Summary,
Start = new EventDateTime { Date = dateStr },
End = new EventDateTime { Date = nextDateStr },
ExtendedProperties = new Event.ExtendedPropertiesData
{
Private__ = new Dictionary<string, string>
{
["shiftSchedulerEvent"] = "true",
["shiftName"] = eventData.Summary
}
}
};
}
private static Event CreateGoogleEvent(CalendarEventData eventData, DateTimeOffset startTime, DateTimeOffset endTime)
{
return new Event

View file

@ -162,7 +162,16 @@ public class NextcloudCalendarService(
var date = shiftWithTransport.Date;
if (string.IsNullOrEmpty(shift.MorningTime) && string.IsNullOrEmpty(shift.AfternoonTime))
{
var uid = $"shift-{SanitizeForUid(shift.Name)}-{date:yyyyMMdd}-allday";
var eventData = CalendarEventBuilder.BuildEventContent(shift, null);
var icsContent = BuildAllDayIcsContent(uid, eventData, DateOnly.FromDateTime(date));
var putRequest = CreateRequest(HttpMethod.Put, $"{calendarId}{Uri.EscapeDataString(uid)}.ics");
putRequest.Content = new StringContent(icsContent, Encoding.UTF8, "text/calendar");
(await client.SendAsync(putRequest)).EnsureSuccessStatusCode();
await Task.Delay(300);
continue;
}
if (!string.IsNullOrEmpty(shift.MorningTime))
{
@ -194,6 +203,21 @@ public class NextcloudCalendarService(
}
}
private static string BuildAllDayIcsContent(string uid, CalendarEventData eventData, DateOnly date)
{
var calendar = new Calendar();
var vevent = new CalendarEvent
{
Uid = uid,
Summary = eventData.Summary,
DtStart = new CalDateTime(date.Year, date.Month, date.Day),
DtEnd = new CalDateTime(date.AddDays(1).Year, date.AddDays(1).Month, date.AddDays(1).Day)
};
vevent.Properties.Add(new Ical.Net.CalendarProperty("X-SHIFT-SCHEDULER", "TRUE"));
calendar.Events.Add(vevent);
return new CalendarSerializer().SerializeToString(calendar)!;
}
private static string BuildIcsContent(string uid, CalendarEventData eventData, DateTimeOffset startTime, DateTimeOffset endTime)
{
var calendar = new Calendar();