feat: add Nextcloud sync button and update client to provider-agnostic API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dacd8bc082
commit
15bf1603b7
2 changed files with 52 additions and 76 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="btn export-btn" @onclick="ExportToIcs" disabled="@(_isLoadingInitial || _isSyncing)"><span class="oi oi-calendar" aria-hidden="true"></span> Export to ICS</button>
|
<button class="btn export-btn" @onclick="ExportToIcs" disabled="@(_isLoadingInitial || _isSyncing)"><span class="oi oi-calendar" aria-hidden="true"></span> Export to ICS</button>
|
||||||
<button class="btn export-btn" @onclick="SyncToGoogleCalendar" disabled="@(_isLoadingInitial || _isSyncing)"><span class="oi oi-cloud-upload" aria-hidden="true"></span> Sync to Google Calendar</button>
|
<button class="btn export-btn" @onclick="SyncToGoogleCalendar" disabled="@(_isLoadingInitial || _isSyncing)"><span class="oi oi-cloud-upload" aria-hidden="true"></span> Sync to Google Calendar</button>
|
||||||
|
<button class="btn export-btn" @onclick="SyncToNextcloud" disabled="@(_isLoadingInitial || _isSyncing)"><span class="oi oi-cloud-upload" aria-hidden="true"></span> Sync to Nextcloud</button>
|
||||||
<button class="btn export-btn" @onclick="ExportToPdf" disabled="@(_isLoadingInitial || _isSyncing)"><span class="oi oi-document" aria-hidden="true"></span>Export to PDF</button>
|
<button class="btn export-btn" @onclick="ExportToPdf" disabled="@(_isLoadingInitial || _isSyncing)"><span class="oi oi-document" aria-hidden="true"></span>Export to PDF</button>
|
||||||
<button class="btn config-btn" @onclick="ShowConfiguration" disabled="@(_isLoadingInitial || _isSyncing)"><span class="oi oi-cog" aria-hidden="true"></span> Configuration</button>
|
<button class="btn config-btn" @onclick="ShowConfiguration" disabled="@(_isLoadingInitial || _isSyncing)"><span class="oi oi-cog" aria-hidden="true"></span> Configuration</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -38,7 +39,7 @@
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Select Google Calendar</h5>
|
<h5 class="modal-title">Select @(_syncProvider == "nextcloud" ? "Nextcloud" : "Google") Calendar</h5>
|
||||||
<button type="button" class="btn-close" @onclick="CloseCalendarSelector"></button>
|
<button type="button" class="btn-close" @onclick="CloseCalendarSelector"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
@ -87,7 +88,7 @@ else if (_isSyncing)
|
||||||
<div class="loading-overlay">
|
<div class="loading-overlay">
|
||||||
<div class="loading-content">
|
<div class="loading-content">
|
||||||
<div class="loading-spinner">⏳</div>
|
<div class="loading-spinner">⏳</div>
|
||||||
<p>Syncing with google calendar...</p>
|
<p>Syncing with @(_syncProvider == "nextcloud" ? "Nextcloud" : "Google Calendar")...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -218,10 +219,3 @@ else
|
||||||
OnClose="HideConfiguration"
|
OnClose="HideConfiguration"
|
||||||
OnConfigurationChanged="OnConfigurationChanged" />
|
OnConfigurationChanged="OnConfigurationChanged" />
|
||||||
|
|
||||||
@code {
|
|
||||||
// Helper method to check if a specific day is loading transport data
|
|
||||||
private bool IsLoadingTransportForDay(DateTime day)
|
|
||||||
{
|
|
||||||
return _isLoadingTransportPerDay.GetValueOrDefault(day, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ namespace ShiftScheduler.Client.Pages
|
||||||
private bool _showConfigDialog = false;
|
private bool _showConfigDialog = false;
|
||||||
private bool _isSyncing = false;
|
private bool _isSyncing = false;
|
||||||
private bool _showCalendarSelector = false;
|
private bool _showCalendarSelector = false;
|
||||||
|
private string _syncProvider = string.Empty;
|
||||||
private string _errorMessage = string.Empty;
|
private string _errorMessage = string.Empty;
|
||||||
private string _successMessage = string.Empty;
|
private string _successMessage = string.Empty;
|
||||||
private List<CalendarInfo> _availableCalendars = new();
|
private List<CalendarInfo> _availableCalendars = new();
|
||||||
|
|
@ -31,14 +32,11 @@ namespace ShiftScheduler.Client.Pages
|
||||||
{
|
{
|
||||||
_isLoadingInitial = true;
|
_isLoadingInitial = true;
|
||||||
|
|
||||||
// Load shifts first to show the calendar immediately
|
|
||||||
Shifts = await HttpClient.GetFromJsonAsync<List<Shift>>("api/shift/shifts") ?? new();
|
Shifts = await HttpClient.GetFromJsonAsync<List<Shift>>("api/shift/shifts") ?? new();
|
||||||
|
|
||||||
// Show calendar immediately, don't wait for transport data
|
|
||||||
_isLoadingInitial = false;
|
_isLoadingInitial = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
// Load schedule and transport data in the background
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await LoadScheduleFromStorage();
|
await LoadScheduleFromStorage();
|
||||||
|
|
@ -64,7 +62,6 @@ namespace ShiftScheduler.Client.Pages
|
||||||
{
|
{
|
||||||
SelectedSchedule[day] = shiftName;
|
SelectedSchedule[day] = shiftName;
|
||||||
|
|
||||||
// Get transport information for this shift and date - per day loading
|
|
||||||
_isLoadingTransportPerDay[day] = true;
|
_isLoadingTransportPerDay[day] = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
|
|
@ -77,15 +74,12 @@ namespace ShiftScheduler.Client.Pages
|
||||||
{
|
{
|
||||||
var shiftWithTransport = await response.Content.ReadFromJsonAsync<ShiftWithTransport>();
|
var shiftWithTransport = await response.Content.ReadFromJsonAsync<ShiftWithTransport>();
|
||||||
if (shiftWithTransport != null)
|
if (shiftWithTransport != null)
|
||||||
{
|
|
||||||
SelectedShiftsWithTransport[day] = shiftWithTransport;
|
SelectedShiftsWithTransport[day] = shiftWithTransport;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Error fetching transport data: {ex.Message}");
|
Console.WriteLine($"Error fetching transport data: {ex.Message}");
|
||||||
// Create a fallback with just the shift data
|
|
||||||
var shift = Shifts.FirstOrDefault(s => s.Name == shiftName);
|
var shift = Shifts.FirstOrDefault(s => s.Name == shiftName);
|
||||||
if (shift != null)
|
if (shift != null)
|
||||||
{
|
{
|
||||||
|
|
@ -172,34 +166,35 @@ namespace ShiftScheduler.Client.Pages
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SyncToGoogleCalendar()
|
private Task SyncToGoogleCalendar() => StartCalendarSync("google");
|
||||||
|
private Task SyncToNextcloud() => StartCalendarSync("nextcloud");
|
||||||
|
|
||||||
|
private async Task StartCalendarSync(string provider)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_errorMessage = string.Empty;
|
_errorMessage = string.Empty;
|
||||||
_successMessage = string.Empty;
|
_successMessage = string.Empty;
|
||||||
|
_syncProvider = provider;
|
||||||
_isSyncing = true;
|
_isSyncing = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
// Get available calendars
|
var response = await HttpClient.GetAsync($"api/shift/calendars?provider={provider}");
|
||||||
var response = await HttpClient.GetAsync("api/shift/google_calendars");
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
_availableCalendars = await response.Content.ReadFromJsonAsync<List<CalendarInfo>>() ?? new();
|
_availableCalendars = await response.Content.ReadFromJsonAsync<List<CalendarInfo>>() ?? new();
|
||||||
|
|
||||||
if (_availableCalendars.Count == 1)
|
if (_availableCalendars.Count == 1)
|
||||||
{
|
{
|
||||||
// If only one calendar, sync directly
|
|
||||||
await PerformCalendarSync(_availableCalendars[0].Id);
|
await PerformCalendarSync(_availableCalendars[0].Id);
|
||||||
}
|
}
|
||||||
else if (_availableCalendars.Count > 1)
|
else if (_availableCalendars.Count > 1)
|
||||||
{
|
{
|
||||||
// Show calendar selector
|
|
||||||
_showCalendarSelector = true;
|
_showCalendarSelector = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_errorMessage = "No writable calendars found in your Google account.";
|
_errorMessage = "No writable calendars found.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -210,7 +205,7 @@ namespace ShiftScheduler.Client.Pages
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_errorMessage = $"Error accessing Google Calendar: {ex.Message}";
|
_errorMessage = $"Error accessing calendar: {ex.Message}";
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -234,27 +229,27 @@ namespace ShiftScheduler.Client.Pages
|
||||||
|
|
||||||
var request = new
|
var request = new
|
||||||
{
|
{
|
||||||
|
Provider = _syncProvider,
|
||||||
CalendarId = calendarId,
|
CalendarId = calendarId,
|
||||||
Year = SelectedDate.Year,
|
Year = SelectedDate.Year,
|
||||||
Month = SelectedDate.Month
|
Month = SelectedDate.Month
|
||||||
};
|
};
|
||||||
|
|
||||||
var response = await HttpClient.PostAsJsonAsync("api/shift/sync_to_google_calendar", request);
|
var response = await HttpClient.PostAsJsonAsync("api/shift/sync_to_calendar", request);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var result = await response.Content.ReadFromJsonAsync<dynamic>();
|
_successMessage = "Shifts synced successfully!";
|
||||||
_successMessage = "Shifts synced to Google Calendar successfully!";
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var errorContent = await response.Content.ReadAsStringAsync();
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
_errorMessage = $"Failed to sync to Google Calendar: {response.StatusCode}. {errorContent}";
|
_errorMessage = $"Failed to sync: {response.StatusCode}. {errorContent}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_errorMessage = $"Error syncing to Google Calendar: {ex.Message}";
|
_errorMessage = $"Error syncing: {ex.Message}";
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -298,8 +293,6 @@ namespace ShiftScheduler.Client.Pages
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
SelectedSchedule = await response.Content.ReadFromJsonAsync<Dictionary<DateTime, string>>() ?? new();
|
SelectedSchedule = await response.Content.ReadFromJsonAsync<Dictionary<DateTime, string>>() ?? new();
|
||||||
|
|
||||||
// Reload transport data for all selected shifts
|
|
||||||
await ReloadTransportDataForSchedule();
|
await ReloadTransportDataForSchedule();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -318,18 +311,13 @@ namespace ShiftScheduler.Client.Pages
|
||||||
private async Task ReloadTransportDataForSchedule()
|
private async Task ReloadTransportDataForSchedule()
|
||||||
{
|
{
|
||||||
SelectedShiftsWithTransport.Clear();
|
SelectedShiftsWithTransport.Clear();
|
||||||
|
|
||||||
// Clear existing per-day loading states
|
|
||||||
_isLoadingTransportPerDay.Clear();
|
_isLoadingTransportPerDay.Clear();
|
||||||
|
|
||||||
// Mark all days as loading
|
|
||||||
foreach (var kvp in SelectedSchedule)
|
foreach (var kvp in SelectedSchedule)
|
||||||
{
|
|
||||||
_isLoadingTransportPerDay[kvp.Key] = true;
|
_isLoadingTransportPerDay[kvp.Key] = true;
|
||||||
}
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
// Create parallel tasks for all transport data requests
|
|
||||||
var transportTasks = SelectedSchedule.Select(async kvp =>
|
var transportTasks = SelectedSchedule.Select(async kvp =>
|
||||||
{
|
{
|
||||||
var day = kvp.Key;
|
var day = kvp.Key;
|
||||||
|
|
@ -344,15 +332,12 @@ namespace ShiftScheduler.Client.Pages
|
||||||
{
|
{
|
||||||
var shiftWithTransport = await response.Content.ReadFromJsonAsync<ShiftWithTransport>();
|
var shiftWithTransport = await response.Content.ReadFromJsonAsync<ShiftWithTransport>();
|
||||||
if (shiftWithTransport != null)
|
if (shiftWithTransport != null)
|
||||||
{
|
|
||||||
SelectedShiftsWithTransport[day] = shiftWithTransport;
|
SelectedShiftsWithTransport[day] = shiftWithTransport;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Error reloading transport data for {day}: {ex.Message}");
|
Console.WriteLine($"Error reloading transport data for {day}: {ex.Message}");
|
||||||
// Create fallback without transport
|
|
||||||
var shift = Shifts.FirstOrDefault(s => s.Name == shiftName);
|
var shift = Shifts.FirstOrDefault(s => s.Name == shiftName);
|
||||||
if (shift != null)
|
if (shift != null)
|
||||||
{
|
{
|
||||||
|
|
@ -367,13 +352,11 @@ namespace ShiftScheduler.Client.Pages
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// Mark this day as no longer loading
|
|
||||||
_isLoadingTransportPerDay[day] = false;
|
_isLoadingTransportPerDay[day] = false;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
// Wait for all transport data to load
|
|
||||||
await Task.WhenAll(transportTasks);
|
await Task.WhenAll(transportTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,7 +375,6 @@ namespace ShiftScheduler.Client.Pages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to get transport summary for display
|
|
||||||
private string GetTransportSummary(DateTime date)
|
private string GetTransportSummary(DateTime date)
|
||||||
{
|
{
|
||||||
if (!SelectedShiftsWithTransport.TryGetValue(date, out var shiftWithTransport))
|
if (!SelectedShiftsWithTransport.TryGetValue(date, out var shiftWithTransport))
|
||||||
|
|
@ -408,20 +390,18 @@ namespace ShiftScheduler.Client.Pages
|
||||||
if (!string.IsNullOrEmpty(afternoonTransport))
|
if (!string.IsNullOrEmpty(afternoonTransport))
|
||||||
summaries.Add(afternoonTransport);
|
summaries.Add(afternoonTransport);
|
||||||
|
|
||||||
if (summaries.Count == 0) { return string.Empty; }
|
if (summaries.Count == 0) return string.Empty;
|
||||||
|
|
||||||
return "🚂 " + string.Join(" | ", summaries);
|
return "🚂 " + string.Join(" | ", summaries);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to get shift times for display
|
|
||||||
private string GetShiftTimes(DateTime date)
|
private string GetShiftTimes(DateTime date)
|
||||||
{
|
{
|
||||||
if (!SelectedSchedule.TryGetValue(date, out var shiftName))
|
if (!SelectedSchedule.TryGetValue(date, out var shiftName))
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
var shift = Shifts.FirstOrDefault(s => s.Name == shiftName);
|
var shift = Shifts.FirstOrDefault(s => s.Name == shiftName);
|
||||||
if (shift == null)
|
if (shift == null) return string.Empty;
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
var times = new List<string>();
|
var times = new List<string>();
|
||||||
|
|
||||||
|
|
@ -448,9 +428,11 @@ namespace ShiftScheduler.Client.Pages
|
||||||
|
|
||||||
private async Task OnConfigurationChanged()
|
private async Task OnConfigurationChanged()
|
||||||
{
|
{
|
||||||
// Reload shifts after configuration changes
|
|
||||||
Shifts = await HttpClient.GetFromJsonAsync<List<Shift>>("api/shift/shifts") ?? new();
|
Shifts = await HttpClient.GetFromJsonAsync<List<Shift>>("api/shift/shifts") ?? new();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsLoadingTransportForDay(DateTime day) =>
|
||||||
|
_isLoadingTransportPerDay.GetValueOrDefault(day, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue