diff --git a/lib/app_shell.dart b/lib/app_shell.dart index 053c720..32e4736 100644 --- a/lib/app_shell.dart +++ b/lib/app_shell.dart @@ -780,7 +780,9 @@ class _HomeShellState extends State { setState(() => workspaceSyncing = true); try { - final baseUri = Uri.parse(normalizeJarvisUrl(widget.jarvisSettings.apiUrl)); + final baseUri = Uri.parse( + normalizeJarvisUrl(widget.jarvisSettings.apiUrl), + ); final response = await http .get( baseUri.resolve('/api/workspace/state'), @@ -847,7 +849,9 @@ class _HomeShellState extends State { setState(() => workspaceSyncing = true); try { - final baseUri = Uri.parse(normalizeJarvisUrl(widget.jarvisSettings.apiUrl)); + final baseUri = Uri.parse( + normalizeJarvisUrl(widget.jarvisSettings.apiUrl), + ); final stateJson = jsonEncode({ 'version': 1, 'updatedAt': DateTime.now().toIso8601String(), @@ -879,9 +883,9 @@ class _HomeShellState extends State { void _showWorkspaceMessage(String message) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } Future _registerJarvisDevice( @@ -1133,6 +1137,15 @@ class _HomeShellState extends State { } void _assignSyncedEventToPerson(CalendarEvent event, Person person) { + if (widget.googleConnections.isNotEmpty) { + final currentConnection = widget.googleConnections.first; + final nextEventPersonIds = {...currentConnection.eventPersonIds}; + nextEventPersonIds[importedEventKey(event)] = person.id; + widget.onGoogleConnectionsChanged([ + currentConnection.copyWith(eventPersonIds: nextEventPersonIds), + ]); + } + final nextSyncedEvents = widget.syncedEvents .map( (item) => sameImportedEvent(item, event) diff --git a/lib/data.dart b/lib/data.dart index 51af728..76ffff3 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -310,6 +310,7 @@ class GoogleCalendarConnection { required this.accountEmail, required this.enabledCalendarIds, this.calendarPersonIds = const {}, + this.eventPersonIds = const {}, required this.lastSync, }); @@ -317,12 +318,14 @@ class GoogleCalendarConnection { final String accountEmail; final List enabledCalendarIds; final Map calendarPersonIds; + final Map eventPersonIds; final DateTime? lastSync; GoogleCalendarConnection copyWith({ String? accountEmail, List? enabledCalendarIds, Map? calendarPersonIds, + Map? eventPersonIds, DateTime? lastSync, }) { return GoogleCalendarConnection( @@ -330,6 +333,7 @@ class GoogleCalendarConnection { accountEmail: accountEmail ?? this.accountEmail, enabledCalendarIds: enabledCalendarIds ?? this.enabledCalendarIds, calendarPersonIds: calendarPersonIds ?? this.calendarPersonIds, + eventPersonIds: eventPersonIds ?? this.eventPersonIds, lastSync: lastSync ?? this.lastSync, ); } @@ -337,6 +341,7 @@ class GoogleCalendarConnection { factory GoogleCalendarConnection.fromJson(Map json) { final rawCalendarIds = json['enabledCalendarIds']; final rawCalendarPersonIds = json['calendarPersonIds']; + final rawEventPersonIds = json['eventPersonIds']; return GoogleCalendarConnection( id: json['id'] as String? ?? uniqueId(), accountEmail: json['accountEmail'] as String? ?? '', @@ -348,6 +353,11 @@ class GoogleCalendarConnection { (key, value) => MapEntry(key.toString(), value.toString()), ) : const {}, + eventPersonIds: rawEventPersonIds is Map + ? rawEventPersonIds.map( + (key, value) => MapEntry(key.toString(), value.toString()), + ) + : const {}, lastSync: DateTime.tryParse(json['lastSync'] as String? ?? ''), ); } @@ -357,6 +367,7 @@ class GoogleCalendarConnection { 'accountEmail': accountEmail, 'enabledCalendarIds': enabledCalendarIds, 'calendarPersonIds': calendarPersonIds, + 'eventPersonIds': eventPersonIds, 'lastSync': lastSync?.toIso8601String(), }; } @@ -380,16 +391,15 @@ class GoogleCalendarInfo { final backgroundColor = json['backgroundColor'] as String?; return GoogleCalendarInfo( id: json['id'] as String? ?? 'primary', - name: json['summaryOverride'] as String? ?? + name: + json['summaryOverride'] as String? ?? json['summary'] as String? ?? 'N\u00e9vtelen Google napt\u00e1r', - description: [ - json['description'] as String?, - json['accessRole'] as String?, - ] - .whereType() - .where((part) => part.trim().isNotEmpty) - .join(' \u00b7 '), + description: + [json['description'] as String?, json['accessRole'] as String?] + .whereType() + .where((part) => part.trim().isNotEmpty) + .join(' \u00b7 '), color: parseGoogleColor(backgroundColor), primary: json['primary'] == true, ); diff --git a/lib/google_calendar_page.dart b/lib/google_calendar_page.dart index 16bdc6a..937c47e 100644 --- a/lib/google_calendar_page.dart +++ b/lib/google_calendar_page.dart @@ -26,7 +26,8 @@ class AccountsPage extends StatefulWidget { State createState() => _AccountsPageState(); } -class _AccountsPageState extends State with WidgetsBindingObserver { +class _AccountsPageState extends State + with WidgetsBindingObserver { List googleCalendars = []; bool loadingCalendars = false; bool syncing = false; @@ -89,7 +90,10 @@ class _AccountsPageState extends State with WidgetsBindingObserver Uri _jarvisUri(String path, [Map query = const {}]) { final baseUri = Uri.parse(normalizeJarvisUrl(widget.jarvisSettings.apiUrl)); - return baseUri.replace(path: path, queryParameters: query.isEmpty ? null : query); + return baseUri.replace( + path: path, + queryParameters: query.isEmpty ? null : query, + ); } String? _jarvisConfigError() { @@ -121,7 +125,9 @@ class _AccountsPageState extends State with WidgetsBindingObserver .get(_jarvisUri('/api/google/oauth/status'), headers: _jarvisHeaders) .timeout(const Duration(seconds: 10)); if (response.statusCode != 200) { - throw Exception('JARVIS status ${response.statusCode}: ${response.body}'); + throw Exception( + 'JARVIS status ${response.statusCode}: ${response.body}', + ); } final payload = jsonDecode(response.body); if (payload is! Map) { @@ -137,6 +143,7 @@ class _AccountsPageState extends State with WidgetsBindingObserver accountEmail: email ?? current?.accountEmail ?? 'Google fi\u00f3k', enabledCalendarIds: current?.enabledCalendarIds ?? const [], calendarPersonIds: current?.calendarPersonIds ?? const {}, + eventPersonIds: current?.eventPersonIds ?? const {}, lastSync: current?.lastSync, ), ]); @@ -152,7 +159,8 @@ class _AccountsPageState extends State with WidgetsBindingObserver if (!mounted) return; setState(() { loadingCalendars = false; - statusMessage = 'JARVIS Google kapcsolat nem el\u00e9rhet\u0151: $error'; + statusMessage = + 'JARVIS Google kapcsolat nem el\u00e9rhet\u0151: $error'; }); } } @@ -160,7 +168,8 @@ class _AccountsPageState extends State with WidgetsBindingObserver Future _startGoogleConnection() async { setState(() { loadingCalendars = true; - statusMessage = 'Google \u00f6sszek\u00f6t\u00e9s ind\u00edt\u00e1sa JARVIS-on...'; + statusMessage = + 'Google \u00f6sszek\u00f6t\u00e9s ind\u00edt\u00e1sa JARVIS-on...'; }); final configError = _jarvisConfigError(); @@ -176,17 +185,27 @@ class _AccountsPageState extends State with WidgetsBindingObserver .get(_jarvisUri('/api/google/oauth/start'), headers: _jarvisHeaders) .timeout(const Duration(seconds: 10)); if (response.statusCode != 200) { - throw Exception('JARVIS OAuth ${response.statusCode}: ${response.body}'); + throw Exception( + 'JARVIS OAuth ${response.statusCode}: ${response.body}', + ); } final payload = jsonDecode(response.body); - if (payload is! Map) throw Exception('Hib\u00e1s JARVIS v\u00e1lasz.'); + if (payload is! Map) { + throw Exception('Hib\u00e1s JARVIS v\u00e1lasz.'); + } final url = payload['authorizationUrl'] as String?; - if (url == null || url.isEmpty) throw Exception('Hi\u00e1nyzik az OAuth URL.'); + if (url == null || url.isEmpty) { + throw Exception('Hi\u00e1nyzik az OAuth URL.'); + } final launched = await launchUrl( Uri.parse(url), mode: LaunchMode.externalApplication, ); - if (!launched) throw Exception('Nem siker\u00fclt megnyitni a b\u00f6ng\u00e9sz\u0151t.'); + if (!launched) { + throw Exception( + 'Nem siker\u00fclt megnyitni a b\u00f6ng\u00e9sz\u0151t.', + ); + } if (!mounted) return; setState(() { waitingForGoogleAuthReturn = true; @@ -198,7 +217,8 @@ class _AccountsPageState extends State with WidgetsBindingObserver if (!mounted) return; setState(() { loadingCalendars = false; - statusMessage = 'Google \u00f6sszek\u00f6t\u00e9s nem indult el: $error'; + statusMessage = + 'Google \u00f6sszek\u00f6t\u00e9s nem indult el: $error'; }); } } @@ -243,6 +263,7 @@ class _AccountsPageState extends State with WidgetsBindingObserver accountEmail: 'Google Calendar', enabledCalendarIds: const [], calendarPersonIds: const {}, + eventPersonIds: const {}, lastSync: null, ), ]); @@ -290,6 +311,17 @@ class _AccountsPageState extends State with WidgetsBindingObserver .toList(); final synced = []; final syncDetails = []; + final eventPersonOverrides = {...current.eventPersonIds}; + for (final cachedEvent in widget.cachedEvents) { + if (!cachedEvent.isSynced || cachedEvent.personId == null) continue; + final defaultPersonId = + current.calendarPersonIds[cachedEvent.calendarId]; + if (cachedEvent.personId == defaultPersonId) continue; + eventPersonOverrides.putIfAbsent( + importedEventKey(cachedEvent), + () => cachedEvent.personId!, + ); + } final now = DateTime.now(); final start = DateTime( now.year, @@ -299,19 +331,18 @@ class _AccountsPageState extends State with WidgetsBindingObserver final end = start.add(const Duration(days: 180)); for (final calendar in selectedCalendars) { - final uri = _jarvisUri( - '/api/google/events', - { - 'calendarId': calendar.id, - 'timeMin': start.toUtc().toIso8601String(), - 'timeMax': end.toUtc().toIso8601String(), - }, - ); + final uri = _jarvisUri('/api/google/events', { + 'calendarId': calendar.id, + 'timeMin': start.toUtc().toIso8601String(), + 'timeMax': end.toUtc().toIso8601String(), + }); final response = await http .get(uri, headers: _jarvisHeaders) .timeout(const Duration(seconds: 20)); if (response.statusCode != 200) { - throw Exception('${calendar.name}: Events API ${response.statusCode}'); + throw Exception( + '${calendar.name}: Events API ${response.statusCode}', + ); } final payload = jsonDecode(response.body); @@ -323,13 +354,12 @@ class _AccountsPageState extends State with WidgetsBindingObserver syncDetails.add('${calendar.name}: ${events.length}'); for (final event in events) { if (event['status'] == 'cancelled') continue; - synced.add( - CalendarEvent.fromGoogleEvent( - event, - calendar, - personId: current.calendarPersonIds[calendar.id], - ), - ); + final imported = CalendarEvent.fromGoogleEvent(event, calendar); + final eventKey = importedEventKey(imported); + final assignedPersonId = + eventPersonOverrides[eventKey] ?? + current.calendarPersonIds[calendar.id]; + synced.add(imported.copyWith(personId: assignedPersonId)); } } @@ -339,7 +369,12 @@ class _AccountsPageState extends State with WidgetsBindingObserver }); widget.onSyncedEventsChanged(synced); - widget.onChanged([current.copyWith(lastSync: DateTime.now())]); + widget.onChanged([ + current.copyWith( + eventPersonIds: eventPersonOverrides, + lastSync: DateTime.now(), + ), + ]); setState(() { syncing = false; statusMessage = @@ -369,9 +404,7 @@ class _AccountsPageState extends State with WidgetsBindingObserver action: FilledButton.icon( onPressed: loadingCalendars ? null : _startGoogleConnection, icon: const Icon(Icons.open_in_browser), - label: Text( - hasConnection ? text.googleReconnect : text.googleConnect, - ), + label: Text(hasConnection ? text.googleReconnect : text.googleConnect), ), child: ListView( padding: const EdgeInsets.only(bottom: 24), @@ -457,9 +490,7 @@ class _AccountsPageState extends State with WidgetsBindingObserver ), const SizedBox(height: 8), OutlinedButton.icon( - onPressed: loadingCalendars - ? null - : _loadGoogleConnectionStatus, + onPressed: loadingCalendars ? null : _loadGoogleConnectionStatus, icon: const Icon(Icons.refresh), label: const Text('Google napt\u00e1rlista friss\u00edt\u00e9se'), ), diff --git a/lib/utils.dart b/lib/utils.dart index 3a4f7d0..5ca408a 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -86,6 +86,23 @@ Color parseGoogleColor(String? value) { String uniqueId() => DateTime.now().microsecondsSinceEpoch.toString(); +String importedEventKey(CalendarEvent event) { + final calendarId = event.calendarId?.trim(); + final externalId = event.externalId?.trim(); + if (calendarId != null && + calendarId.isNotEmpty && + externalId != null && + externalId.isNotEmpty) { + return '$calendarId::$externalId'; + } + + final fallbackCalendar = calendarId?.isNotEmpty == true + ? calendarId! + : 'local'; + final date = event.date.toIso8601String().split('T').first; + return '$fallbackCalendar::${event.title}::$date::${event.start}'; +} + String normalizeJarvisUrl(String value) { final trimmed = value.trim(); if (trimmed.isEmpty) return JarvisSettings.defaults().apiUrl;