Implements a provider-agnostic calendar sync feature with Nextcloud as the first alternative to Google Calendar. - Add ICalendarService interface and provider-agnostic API endpoints - Implement NextcloudCalendarService using CalDAV (Ical.Net) - Add CalendarEventBuilder to share event/transport logic across providers - Add Nextcloud sync button to the UI (Index page) - Centralise TargetFramework, Nullable and ImplicitUsings in Directory.Build.props (net9.0) - Add .worktrees to .gitignore and Docker/Nextcloud config docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
53 lines
1.9 KiB
C#
53 lines
1.9 KiB
C#
using ShiftScheduler.Shared;
|
|
|
|
namespace ShiftScheduler.Services;
|
|
|
|
public record CalendarEventData(string Summary, string Description);
|
|
|
|
public static class CalendarEventBuilder
|
|
{
|
|
public static CalendarEventData BuildEventContent(Shift shift, TransportConnection? transport)
|
|
{
|
|
var description = transport != null
|
|
? $"Transport: {FormatTransportInfo(transport)}"
|
|
: string.Empty;
|
|
return new CalendarEventData(shift.Name, description);
|
|
}
|
|
|
|
public static string FormatTransportInfo(TransportConnection transport)
|
|
{
|
|
var departure = transport.DepartureTime.ToString("HH:mm");
|
|
var arrival = transport.ArrivalTime.ToString("HH:mm");
|
|
return $"{transport.Platform}: {departure} → {arrival} ({(int)transport.Duration.TotalMinutes} Minutes)";
|
|
}
|
|
|
|
public static async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, int maxRetries = 3)
|
|
{
|
|
var delay = TimeSpan.FromSeconds(1);
|
|
|
|
for (var attempt = 0; attempt <= maxRetries; attempt++)
|
|
{
|
|
try
|
|
{
|
|
return await operation();
|
|
}
|
|
catch (HttpRequestException ex) when (ex.Message.Contains("429") && attempt < maxRetries)
|
|
{
|
|
await Task.Delay(delay);
|
|
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2);
|
|
}
|
|
catch (Exception ex) when (ex.Message.Contains("Rate Limit") && attempt < maxRetries)
|
|
{
|
|
await Task.Delay(delay);
|
|
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2);
|
|
}
|
|
catch (Exception ex) when (ex.Message.Contains("Forbidden") && ex.Message.Contains("quota") && attempt < maxRetries)
|
|
{
|
|
await Task.Delay(delay);
|
|
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2);
|
|
}
|
|
}
|
|
|
|
return await operation();
|
|
}
|
|
}
|