From b01fa4e4ba209ecbcab7bde8bb34c3eb95922ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 12 Feb 2026 18:04:29 +0100 Subject: [PATCH] Event details page improvements, calendar item update source. --- .../CalendarAccountSettingsPageViewModel.cs | 42 ++-- .../CalendarPageViewModel.cs | 5 +- .../Data/AccountCalendarViewModel.cs | 13 + .../EventDetailsPageViewModel.cs | 41 +++- Wino.Calendar/Views/EventDetailsPage.xaml | 223 +++++++++++++++--- Wino.Calendar/Views/EventDetailsPage.xaml.cs | 27 ++- .../Entities/Calendar/AccountCalendar.cs | 3 + .../Enums/CalendarItemUpdateSource.cs | 22 ++ .../Interfaces/IAccountCalendar.cs | 3 + Wino.Core.ViewModels/CalendarBaseViewModel.cs | 18 +- Wino.Core.ViewModels/CoreBaseViewModel.cs | 13 +- .../Extensions/GoogleIntegratorExtensions.cs | 1 + .../Extensions/OutlookIntegratorExtensions.cs | 1 + .../Requests/Calendar/AcceptEventRequest.cs | 4 +- .../Requests/Calendar/DeclineEventRequest.cs | 4 +- .../Calendar/TentativeEventRequest.cs | 4 +- .../Calendar/UpdateCalendarEventRequest.cs | 6 +- Wino.Core/Synchronizers/GmailSynchronizer.cs | 51 +++- .../Synchronizers/OutlookSynchronizer.cs | 47 +++- .../AccountDetailsPageViewModel.cs | 7 +- .../Services/AccountCalendarStateService.cs | 4 + .../Views/Account/AccountDetailsPage.xaml.cs | 9 +- .../Views/Calendar/EventDetailsPage.xaml.cs | 17 +- .../Calendar/CalendarItemEventMessages.cs | 5 +- Wino.Services/CalendarService.cs | 2 +- Wino.Services/DatabaseService.cs | 1 + 26 files changed, 471 insertions(+), 102 deletions(-) create mode 100644 Wino.Core.Domain/Enums/CalendarItemUpdateSource.cs diff --git a/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs index 53ebbbba..1fbf23e4 100644 --- a/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs @@ -60,29 +60,30 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode { base.OnNavigatedTo(mode, parameters); - if (parameters is not Guid accountId) + if (parameters is AccountCalendar selectedCalendar) + { + Account = await _accountService.GetAccountAsync(selectedCalendar.AccountId); + AccountCalendar = await _calendarService.GetAccountCalendarAsync(selectedCalendar.Id) ?? selectedCalendar; + } + else if (parameters is Guid accountId) + { + Account = await _accountService.GetAccountAsync(accountId); + var calendars = await _calendarService.GetAccountCalendarsAsync(accountId); + AccountCalendar = calendars.FirstOrDefault(c => c.IsPrimary) ?? calendars.FirstOrDefault(); + } + else + { return; + } - // Load account - Account = await _accountService.GetAccountAsync(accountId); - - if (Account == null) - return; - - // Load first primary calendar for this account - var calendars = await _calendarService.GetAccountCalendarsAsync(accountId); - AccountCalendar = calendars.FirstOrDefault(c => c.IsPrimary) ?? calendars.FirstOrDefault(); - - if (AccountCalendar == null) + if (Account == null || AccountCalendar == null) return; // Initialize properties from AccountCalendar AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4"; - IsSyncEnabled = AccountCalendar.IsExtended; + IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled; IsPrimaryCalendar = AccountCalendar.IsPrimary; - - // TODO: Default ShowAs is not stored in AccountCalendar yet, defaulting to Busy - SelectedDefaultShowAsOption = ShowAsOptions[2]; // Busy + SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2]; } partial void OnAccountColorHexChanged(string value) @@ -98,7 +99,7 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode { if (AccountCalendar != null) { - AccountCalendar.IsExtended = value; + AccountCalendar.IsSynchronizationEnabled = value; SaveChangesAsync(); } } @@ -114,11 +115,10 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value) { - // TODO: Default ShowAs should be stored in AccountCalendar or account preferences - // For now, this is just a placeholder as the property doesn't exist yet - if (value != null) + if (AccountCalendar != null && value != null) { - // Future: Store value.ShowAs somewhere + AccountCalendar.DefaultShowAs = value.ShowAs; + SaveChangesAsync(); } } diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index 28cb9c3d..ed0c6164 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -286,6 +286,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, Description = string.Empty, Location = Location ?? string.Empty, Title = EventName, + ShowAs = SelectedQuickEventAccountCalendar.DefaultShowAs, IsHidden = false, AssignedCalendar = SelectedQuickEventAccountCalendar }; @@ -907,9 +908,9 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, }); } - protected override async void OnCalendarItemUpdated(CalendarItem calendarItem) + protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source) { - base.OnCalendarItemUpdated(calendarItem); + base.OnCalendarItemUpdated(calendarItem, source); Debug.WriteLine($"Calendar item updated: {calendarItem.Id}"); // Series master events should not be visible on the UI. diff --git a/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs index 8d36afc4..1f41f633 100644 --- a/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs @@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; namespace Wino.Calendar.ViewModels.Data; @@ -54,6 +55,12 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i); } + public bool IsSynchronizationEnabled + { + get => AccountCalendar.IsSynchronizationEnabled; + set => SetProperty(AccountCalendar.IsSynchronizationEnabled, value, AccountCalendar, (u, i) => u.IsSynchronizationEnabled = i); + } + public Guid AccountId { get => AccountCalendar.AccountId; @@ -65,6 +72,12 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend get => AccountCalendar.RemoteCalendarId; set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r); } + + public CalendarItemShowAs DefaultShowAs + { + get => AccountCalendar.DefaultShowAs; + set => SetProperty(AccountCalendar.DefaultShowAs, value, AccountCalendar, (u, s) => u.DefaultShowAs = s); + } public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; } public MailAccount MailAccount { get => MailAccount; set => MailAccount = value; } } diff --git a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs index 4b3733ea..2e91dd0e 100644 --- a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs @@ -105,6 +105,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel public bool IncludeRsvpMessage => !string.IsNullOrEmpty(RsvpMessage); [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IncludeRsvpMessage))] public partial string RsvpMessage { get; set; } = string.Empty; public ObservableCollection RsvpStatusOptions { get; } = new ObservableCollection(); @@ -129,7 +130,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel CalendarItemStatus.Tentative => Translator.CalendarEventResponse_TentativeResponse, CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_DeclinedResponse, CalendarItemStatus.NotResponded => Translator.CalendarEventResponse_NotResponded, - _ => throw new NotImplementedException() + _ => Translator.CalendarEventResponse_NotResponded }; } } @@ -179,19 +180,33 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel await LoadCalendarItemTargetAsync(args); } - protected override async void OnCalendarItemUpdated(CalendarItem calendarItem) + protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source) { - base.OnCalendarItemUpdated(calendarItem); + base.OnCalendarItemUpdated(calendarItem, source); // If the current event was updated, reload it if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id) { - // Refresh the current event data by reloading from service + // Reflect client-side optimistic changes immediately; fallback to DB for server updates. + if (source == CalendarItemUpdateSource.ClientUpdated || source == CalendarItemUpdateSource.ClientReverted) + { + var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? []; + CurrentEvent = new CalendarItemViewModel(calendarItem); + + foreach (var attendee in previousAttendees) + { + CurrentEvent.Attendees.Add(attendee); + } + + return; + } + + // Refresh from DB when update comes from server sync. var refreshedEvent = await _calendarService.GetCalendarItemAsync(calendarItem.Id); if (refreshedEvent != null) { CurrentEvent = new CalendarItemViewModel(refreshedEvent); - await LoadAttendeesAsync(refreshedEvent.EventTrackingId, refreshedEvent); + await LoadAttendeesAsync(refreshedEvent.Id, refreshedEvent); } } } @@ -218,17 +233,17 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel CurrentEvent = new CalendarItemViewModel(currentEventItem); - await LoadAttendeesAsync(currentEventItem.EventTrackingId, currentEventItem); + await LoadAttendeesAsync(currentEventItem.Id, currentEventItem); // Initialize SelectedShowAsOption based on current event's ShowAs SelectedShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == currentEventItem.ShowAs) ?? ShowAsOptions[2]; // Load reminders for this calendar item - Reminders = await _calendarService.GetRemindersAsync(currentEventItem.EventTrackingId); + Reminders = await _calendarService.GetRemindersAsync(currentEventItem.Id); InitializeReminderOptions(); // Load attachments - await LoadAttachmentsAsync(currentEventItem.EventTrackingId); + await LoadAttachmentsAsync(currentEventItem.Id); } catch (Exception ex) { @@ -236,11 +251,11 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel } } - private async Task LoadAttendeesAsync(Guid eventTrackingId, CalendarItem calendarItem) + private async Task LoadAttendeesAsync(Guid calendarItemId, CalendarItem calendarItem) { CurrentEvent.Attendees.Clear(); - var attendees = await _calendarService.GetAttendeesAsync(eventTrackingId); + var attendees = await _calendarService.GetAttendeesAsync(calendarItemId); // Separate organizer from other attendees to ensure organizer is always first var organizer = attendees.FirstOrDefault(a => a.IsOrganizer); @@ -348,7 +363,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel { // Capture original state BEFORE making any changes for potential revert var originalItem = await _calendarService.GetCalendarItemAsync(CurrentEvent.CalendarItem.Id); - var originalAttendees = await _calendarService.GetAttendeesAsync(CurrentEvent.CalendarItem.EventTrackingId); + var originalAttendees = await _calendarService.GetAttendeesAsync(CurrentEvent.CalendarItem.Id); // Get selected reminder options var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList(); @@ -370,7 +385,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel } // Save reminders to database - await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.EventTrackingId, newReminders); + await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.Id, newReminders); Reminders = newReminders; // Update ShowAs if changed @@ -499,7 +514,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel await _winoRequestDelegator.ExecuteAsync(preparationRequest); // Reload attendees to get the updated status from the server - await LoadAttendeesAsync(CurrentEvent.CalendarItem.EventTrackingId, CurrentEvent.CalendarItem); + await LoadAttendeesAsync(CurrentEvent.CalendarItem.Id, CurrentEvent.CalendarItem); OnPropertyChanged(nameof(CurrentRsvpText)); OnPropertyChanged(nameof(CurrentRsvpStatus)); diff --git a/Wino.Calendar/Views/EventDetailsPage.xaml b/Wino.Calendar/Views/EventDetailsPage.xaml index c82ca17d..4247c766 100644 --- a/Wino.Calendar/Views/EventDetailsPage.xaml +++ b/Wino.Calendar/Views/EventDetailsPage.xaml @@ -6,9 +6,12 @@ xmlns:abstract="using:Wino.Calendar.Views.Abstract" xmlns:calendar="using:Wino.Core.Domain.Entities.Calendar" xmlns:calendarHelpers="using:Wino.Calendar.Helpers" + xmlns:calendarViewModels="using:Wino.Calendar.ViewModels" xmlns:coreControls="using:Wino.Core.UWP.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:data="using:Wino.Calendar.ViewModels.Data" xmlns:domain="using:Wino.Core.Domain" + xmlns:enums="using:Wino.Core.Domain.Enums" xmlns:helpers="using:Wino.Helpers" xmlns:local="using:Wino.Calendar.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -43,6 +46,7 @@ + @@ -60,16 +64,16 @@ DefaultLabelPosition="Right" IsSticky="True" OverflowButtonVisibility="Auto"> - - + - + @@ -78,7 +82,9 @@ - + @@ -86,27 +92,37 @@ - - + + - + - + - - + @@ -118,7 +134,11 @@ - + @@ -127,14 +147,33 @@ - + - + @@ -142,8 +181,51 @@ + + + + + + + + + +