From d00f5ea8b76ef38facf15630c3717ad211696542 Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Mon, 13 Apr 2026 20:39:23 +0200 Subject: [PATCH] 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 --- Services/GoogleCalendarService.cs | 25 +++++++++++++++++++++++++ Services/NextcloudCalendarService.cs | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/Services/GoogleCalendarService.cs b/Services/GoogleCalendarService.cs index 0a3f17a..c80a141 100644 --- a/Services/GoogleCalendarService.cs +++ b/Services/GoogleCalendarService.cs @@ -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 + { + ["shiftSchedulerEvent"] = "true", + ["shiftName"] = eventData.Summary + } + } + }; + } + private static Event CreateGoogleEvent(CalendarEventData eventData, DateTimeOffset startTime, DateTimeOffset endTime) { return new Event diff --git a/Services/NextcloudCalendarService.cs b/Services/NextcloudCalendarService.cs index ff15a15..6c76661 100644 --- a/Services/NextcloudCalendarService.cs +++ b/Services/NextcloudCalendarService.cs @@ -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();