import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_app/Components/check_input_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/single_select_container.dart'; import 'package:manager_app/Components/string_input_container.dart'; import 'package:manager_app/Components/message_notification.dart'; import 'package:manager_app/Components/confirmation_dialog.dart'; import 'package:provider/provider.dart'; import 'package:manager_app/app_context.dart'; import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_app/Models/managerContext.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'showNewOrUpdateEventAgenda.dart'; enum _AgendaDateFilter { all, thisMonth, thisYear, past } class AgendaConfig extends StatefulWidget { final String? color; final String? label; final AgendaDTO initialValue; final ValueChanged onChanged; const AgendaConfig({ Key? key, this.color, this.label, required this.initialValue, required this.onChanged, }) : super(key: key); @override _AgendaConfigState createState() => _AgendaConfigState(); } class _AgendaConfigState extends State { late AgendaDTO agendaDTO; List events = []; String _searchFilter = ''; _AgendaDateFilter _dateFilter = _AgendaDateFilter.all; List get _filteredEvents { final now = DateTime.now(); var filtered = events.where((event) { switch (_dateFilter) { case _AgendaDateFilter.thisMonth: return event.dateFrom != null && event.dateFrom!.year == now.year && event.dateFrom!.month == now.month; case _AgendaDateFilter.thisYear: return event.dateFrom != null && event.dateFrom!.year == now.year; case _AgendaDateFilter.past: return event.dateFrom != null && event.dateFrom!.isBefore(now); case _AgendaDateFilter.all: return true; } }).toList(); if (_searchFilter.isNotEmpty) { filtered = filtered.where((event) { final label = _labelFor(event); return label.toLowerCase().contains(_searchFilter.toLowerCase()); }).toList(); } filtered.sort((a, b) { if (a.dateFrom == null && b.dateFrom == null) return 0; if (a.dateFrom == null) return 1; if (b.dateFrom == null) return -1; return a.dateFrom!.compareTo(b.dateFrom!); }); return filtered; } String _labelFor(EventAgendaDTO event) { if (event.label != null && event.label!.isNotEmpty) { return (event.label!.firstWhere((t) => t.language == 'FR', orElse: () => event.label![0])) .value ?? ''; } return ''; } /// Groups events synced from the same external agenda that share the same /// address and dates: the sync creates one EventAgendaDTO per language /// instead of one multilingual event, so this recombines them for display. String? _syncGroupKey(EventAgendaDTO event) { if (event.isSynced != true) return null; final dates = "${event.dateFrom?.toIso8601String()}|${event.dateTo?.toIso8601String()}"; final address = event.address?.address?.trim(); if (address != null && address.isNotEmpty) { return "addr:$address|$dates"; } // No address to match on (common for these synced events): fall back to // an identical title, which only happens when the source repeats the // same untranslated name per language pass. final label = _labelFor(event).trim(); if (label.isNotEmpty) { return "label:$label|$dates"; } return null; } List> get _displayGroups { final groups = >[]; final groupByKey = >{}; for (final event in _filteredEvents) { final key = _syncGroupKey(event); if (key == null) { groups.add([event]); continue; } final existing = groupByKey[key]; if (existing != null) { existing.add(event); } else { final group = [event]; groupByKey[key] = group; groups.add(group); } } return groups; } @override void initState() { super.initState(); agendaDTO = widget.initialValue; WidgetsBinding.instance.addPostFrameCallback((_) => _loadFromApi()); } Future _loadFromApi() async { if (agendaDTO.id == null || !mounted) return; final appContext = Provider.of(context, listen: false); final api = (appContext.getContext() as ManagerAppContext) .clientAPI! .sectionAgendaApi!; try { final fetched = await api.sectionAgendaGetAllEventAgendaFromSection(agendaDTO.id!); if (fetched == null || !mounted) return; setState(() { events = List.from(fetched); }); } catch (e) { // Silently keep empty on error } } SectionAgendaApi _api(BuildContext ctx) { final appContext = Provider.of(ctx, listen: false); return (appContext.getContext() as ManagerAppContext) .clientAPI! .sectionAgendaApi!; } @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; var mapProviderIn = ""; switch (agendaDTO.agendaMapProvider) { case MapProvider.Google: mapProviderIn = "Google"; break; case MapProvider.MapBox: mapProviderIn = "MapBox"; break; default: mapProviderIn = "Google"; break; } return Column( mainAxisSize: MainAxisSize.min, children: [ _buildAgendaHeader(size, mapProviderIn), ...[ const Divider(height: 32), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocalizations.of(context)!.agendaEventsLabel, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ElevatedButton.icon( icon: const Icon(Icons.add), label: Text(AppLocalizations.of(context)!.addEvent), onPressed: agendaDTO.id == null ? null : () { showNewOrUpdateEventAgenda( context, null, agendaDTO.id!, (newEvent) async { try { final created = await _api(context) .sectionAgendaCreateEventAgenda( agendaDTO.id!, newEvent); if (created != null && mounted) { setState(() => events.add(created)); showNotification(kSuccess, kWhite, AppLocalizations.of(context)!.agendaEventCreatedSuccess, context, null); } } catch (e) { showNotification( kError, kWhite, AppLocalizations.of(context)!.agendaEventCreateError, context, null); rethrow; } }, ); }, style: ElevatedButton.styleFrom( backgroundColor: kSuccess, foregroundColor: kWhite, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 280, child: StringInputContainer( label: AppLocalizations.of(context)!.searchLabel, onChanged: (v) => setState(() => _searchFilter = v), ), ), const SizedBox(width: 16), Wrap( spacing: 8, children: [ _buildDateFilterChip( _AgendaDateFilter.all, AppLocalizations.of(context)!.agendaFilterAll), _buildDateFilterChip( _AgendaDateFilter.thisMonth, AppLocalizations.of(context)!.agendaFilterThisMonth), _buildDateFilterChip( _AgendaDateFilter.thisYear, AppLocalizations.of(context)!.agendaFilterThisYear), _buildDateFilterChip( _AgendaDateFilter.past, AppLocalizations.of(context)!.agendaFilterPast), ], ), ], ), ), Container( height: 600, padding: const EdgeInsets.symmetric(vertical: 8), child: events.isEmpty ? Center( child: Text(AppLocalizations.of(context)!.noEvents, style: TextStyle(fontStyle: FontStyle.italic))) : Builder(builder: (context) { final groups = _displayGroups; return ListView.builder( itemCount: groups.length, itemBuilder: (context, index) { final group = groups[index]; final primary = group.firstWhere( (e) => e.label?.any((t) => t.language == 'FR') ?? false, orElse: () => group.first); return Card( elevation: 2, margin: const EdgeInsets.symmetric( horizontal: 16, vertical: 6), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), child: ListTile( contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), leading: CircleAvatar( backgroundColor: kPrimaryColor.withOpacity(0.1), child: const Icon(Icons.event, color: kPrimaryColor), ), title: HtmlWidget( (primary.label != null && primary.label!.isNotEmpty) ? (primary.label!.firstWhere( (t) => t.language == 'FR', orElse: () => primary.label![0])) .value ?? "${AppLocalizations.of(context)!.agendaEventFallback} $index" : "${AppLocalizations.of(context)!.agendaEventFallback} $index", textStyle: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Padding( padding: const EdgeInsets.only(top: 4), child: Text([ _formatEventDate(primary), primary.address?.address ?? AppLocalizations.of(context)!.noAddress, ].where((s) => s.isNotEmpty).join(' • ')), ), trailing: group.length > 1 ? _buildLanguageChips(group) : Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.edit, color: kPrimaryColor), onPressed: () => _editEvent(context, primary), ), IconButton( icon: const Icon(Icons.delete, color: kError), onPressed: () => _deleteEvent(context, primary), ), ], ), ), ); }, ); }), ), ], ], ); } void _editEvent(BuildContext context, EventAgendaDTO event) { showNewOrUpdateEventAgenda( context, event, agendaDTO.id ?? "", (updatedEvent) async { try { final result = await _api(context).sectionAgendaUpdateEventAgenda(updatedEvent); if (result != null && mounted) { setState(() => events[events.indexWhere((e) => e.id == event.id)] = result); showNotification(kSuccess, kWhite, AppLocalizations.of(context)!.agendaEventUpdatedSuccess, context, null); } } catch (e) { showNotification(kError, kWhite, AppLocalizations.of(context)!.agendaEventUpdateError, context, null); rethrow; } }, ); } void _deleteEvent(BuildContext context, EventAgendaDTO event) { showConfirmationDialog( AppLocalizations.of(context)!.agendaEventDeleteConfirm, () {}, () async { try { if (event.id != null) { await _api(context).sectionAgendaDeleteEventAgenda(event.id!); } if (mounted) { setState(() => events.removeWhere((e) => e.id == event.id)); showNotification(kSuccess, kWhite, AppLocalizations.of(context)!.agendaEventDeletedSuccess, context, null); } } catch (e) { showNotification(kError, kWhite, AppLocalizations.of(context)!.agendaEventDeleteError, context, null); } }, context, ); } Widget _buildLanguageChips(List group) { return Wrap( spacing: 4, children: group.map((event) { final language = (event.label != null && event.label!.isNotEmpty) ? event.label![0].language ?? '?' : '?'; return InputChip( label: Text(language.toUpperCase(), style: const TextStyle(fontSize: 12)), onPressed: () => _editEvent(context, event), onDeleted: () => _deleteEvent(context, event), deleteIconColor: kError, ); }).toList(), ); } Widget _buildDateFilterChip(_AgendaDateFilter filter, String label) { return ChoiceChip( label: Text(label), selected: _dateFilter == filter, selectedColor: kPrimaryColor.withOpacity(0.2), onSelected: (_) => setState(() => _dateFilter = filter), ); } String _formatEventDate(EventAgendaDTO event) { if (event.dateFrom == null) return ''; final dateFormat = DateFormat('dd/MM/yyyy'); final from = dateFormat.format(event.dateFrom!.toLocal()); if (event.dateTo == null || event.dateTo!.isAtSameMomentAs(event.dateFrom!)) { return from; } return "$from - ${dateFormat.format(event.dateTo!.toLocal())}"; } Widget _buildAgendaHeader(Size size, String mapProviderIn) { return Container( padding: const EdgeInsets.all(16.0), margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 2, blurRadius: 5, offset: const Offset(0, 3), ), ], border: Border.all(color: kPrimaryColor.withOpacity(0.2)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ CheckInputContainer( label: AppLocalizations.of(context)!.onlineLabel, isChecked: agendaDTO.isOnlineAgenda ?? true, onChanged: (value) { setState(() { agendaDTO.isOnlineAgenda = value; widget.onChanged(agendaDTO); }); }, ), CheckInputContainer( label: AppLocalizations.of(context)!.mapViewLabel, isChecked: agendaDTO.agendaMapProvider != null, onChanged: (value) { setState(() { if (value) { agendaDTO.agendaMapProvider = MapProvider.Google; } else { agendaDTO.agendaMapProvider = null; } widget.onChanged(agendaDTO); }); }, ), if (agendaDTO.agendaMapProvider != null) SingleSelectContainer( label: AppLocalizations.of(context)!.mapServiceLabel, color: Colors.black, initialValue: mapProviderIn, inputValues: const ["Google", "MapBox"], onChanged: (String value) { setState(() { switch (value) { case "Google": agendaDTO.agendaMapProvider = MapProvider.Google; break; case "MapBox": agendaDTO.agendaMapProvider = MapProvider.MapBox; break; } widget.onChanged(agendaDTO); }); }, ), if (agendaDTO.isOnlineAgenda == true) MultiStringInputContainer( label: AppLocalizations.of(context)!.jsonFilesLabel, resourceTypes: const [ResourceType.Json, ResourceType.JsonUrl], modalLabel: AppLocalizations.of(context)!.jsonLabel, color: kPrimaryColor, initialValue: agendaDTO.resourceIds ?? [], isTitle: false, onGetResult: (value) { setState(() { agendaDTO.resourceIds = value; widget.onChanged(agendaDTO); }); }, maxLines: 1, ), ], ), ); } }