diff --git a/app/lib/backend/http/api/conversations.dart b/app/lib/backend/http/api/conversations.dart index a928c16ca3d..5ac0c3668eb 100644 --- a/app/lib/backend/http/api/conversations.dart +++ b/app/lib/backend/http/api/conversations.dart @@ -520,6 +520,8 @@ Future<(List, int, int)> searchConversationsServer( int? page, int? limit, bool includeDiscarded = true, + DateTime? startDate, + DateTime? endDate, }) async { Logger.debug(Env.apiBaseUrl); var response = await makeApiCall( @@ -531,6 +533,8 @@ Future<(List, int, int)> searchConversationsServer( 'page': page ?? 1, 'per_page': limit ?? 10, 'include_discarded': includeDiscarded, + if (startDate != null) 'start_date': startDate.toIso8601String(), + if (endDate != null) 'end_date': endDate.toIso8601String(), }), ); if (response == null) return ([], 0, 0); diff --git a/app/lib/pages/conversations/widgets/search_widget.dart b/app/lib/pages/conversations/widgets/search_widget.dart index 6926e508599..c2b4f4f5c83 100644 --- a/app/lib/pages/conversations/widgets/search_widget.dart +++ b/app/lib/pages/conversations/widgets/search_widget.dart @@ -73,22 +73,25 @@ class _SearchWidgetState extends State { } } - Future _showDatePicker(BuildContext context, {bool hasExistingFilter = false}) async { + Future _showDateRangePicker(BuildContext context, {bool hasExistingFilter = false}) async { final convoProvider = Provider.of(context, listen: false); - DateTime selectedDate = convoProvider.selectedDate ?? DateTime.now(); + DateTime? startDate = convoProvider.searchStartDate; + DateTime? endDate = convoProvider.searchEndDate; + List selectedRange = [startDate, endDate]; await showCupertinoModalPopup( context: context, builder: (BuildContext context) { return Container( - height: 420, + height: 480, padding: const EdgeInsets.only(top: 6.0), margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), color: const Color(0xFF1F1F25), child: SafeArea( top: false, child: Column( + mainAxisSize: MainAxisSize.min, children: [ - // Header with Cancel and Done buttons + // Header with Cancel and Apply buttons Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), decoration: const BoxDecoration( @@ -104,7 +107,10 @@ class _SearchWidgetState extends State { if (hasExistingFilter) { final provider = Provider.of(context, listen: false); Navigator.of(context).pop(); - await provider.clearDateFilter(); + provider.clearSearchDateRange(); + if (provider.previousQuery.isNotEmpty) { + await provider.searchConversations(provider.previousQuery); + } PlatformManager.instance.analytics.calendarFilterCleared(); } else { Navigator.of(context).pop(); @@ -115,14 +121,23 @@ class _SearchWidgetState extends State { style: const TextStyle(color: Colors.white, fontSize: 16), ), ), - const Spacer(), + Text( + 'Filter by date', + style: TextStyle(color: Colors.grey.shade400, fontSize: 14), + ), CupertinoButton( padding: EdgeInsets.zero, onPressed: () async { final provider = Provider.of(context, listen: false); Navigator.of(context).pop(); - await provider.filterConversationsByDate(selectedDate); - PlatformManager.instance.analytics.calendarFilterApplied(selectedDate); + if (provider.previousQuery.isNotEmpty) { + provider.setSearchDateRange(startDate, endDate); + await provider.searchConversations(provider.previousQuery); + } + final appliedStart = startDate; + if (appliedStart != null) { + PlatformManager.instance.analytics.calendarFilterApplied(appliedStart); + } }, child: Text( context.l10n.done, @@ -132,7 +147,7 @@ class _SearchWidgetState extends State { ], ), ), - // Date picker + // Date range picker Expanded( child: Material( color: ResponsiveHelper.backgroundSecondary, @@ -141,11 +156,13 @@ class _SearchWidgetState extends State { firstDate: DateTime(2020), lastDate: DateTime.now(), currentDate: DateTime.now(), + calendarType: CalendarDatePicker2Type.range, ), - value: [selectedDate], + value: selectedRange, onValueChanged: (dates) { if (dates.isNotEmpty) { - selectedDate = dates[0]; + startDate = dates[0]; + endDate = dates.length > 1 ? dates[1] : null; } }, ), @@ -217,11 +234,12 @@ class _SearchWidgetState extends State { // Calendar button - same height as search bar (48px) Consumer( builder: (context, convoProvider, _) { + final hasActiveFilter = convoProvider.searchStartDate != null; return Container( width: 48, height: 48, decoration: BoxDecoration( - color: convoProvider.selectedDate != null + color: hasActiveFilter ? Colors.deepPurple.withValues(alpha: 0.5) : const Color(0xFF1F1F25), borderRadius: BorderRadius.circular(24), @@ -229,13 +247,13 @@ class _SearchWidgetState extends State { child: IconButton( padding: EdgeInsets.zero, icon: Icon( - convoProvider.selectedDate != null ? FontAwesomeIcons.calendarDay : FontAwesomeIcons.calendarDays, + hasActiveFilter ? FontAwesomeIcons.calendarDay : FontAwesomeIcons.calendarDays, size: 18, - color: convoProvider.selectedDate != null ? Colors.white : Colors.white70, + color: hasActiveFilter ? Colors.white : Colors.white70, ), onPressed: () async { HapticFeedback.mediumImpact(); - await _showDatePicker(context, hasExistingFilter: convoProvider.selectedDate != null); + await _showDateRangePicker(context, hasExistingFilter: hasActiveFilter); }, ), ); diff --git a/app/lib/providers/conversation_provider.dart b/app/lib/providers/conversation_provider.dart index 11a023bcf03..5a965f2bf82 100644 --- a/app/lib/providers/conversation_provider.dart +++ b/app/lib/providers/conversation_provider.dart @@ -31,6 +31,9 @@ class ConversationProvider extends ChangeNotifier { int totalSearchPages = 1; int currentSearchPage = 1; + DateTime? searchStartDate; + DateTime? searchEndDate; + Timer? _processingConversationWatchTimer; // Add debounce mechanism for refresh @@ -103,6 +106,8 @@ class ConversationProvider extends ChangeNotifier { hasDailySummaries = false; selectedDate = null; selectedFolderId = null; + searchStartDate = null; + searchEndDate = null; previousQuery = ''; totalSearchPages = 1; currentSearchPage = 1; @@ -152,7 +157,12 @@ class ConversationProvider extends ChangeNotifier { } previousQuery = query; - var (convos, current, total) = await searchConversationsServer(query, includeDiscarded: showDiscardedConversations); + var (convos, current, total) = await searchConversationsServer( + query, + includeDiscarded: showDiscardedConversations, + startDate: searchStartDate, + endDate: searchEndDate, + ); convos.sort((a, b) => (b.startedAt ?? b.createdAt).compareTo(a.startedAt ?? a.createdAt)); searchedConversations = convos; currentSearchPage = current; @@ -177,6 +187,8 @@ class ConversationProvider extends ChangeNotifier { previousQuery, page: currentSearchPage + 1, includeDiscarded: showDiscardedConversations, + startDate: searchStartDate, + endDate: searchEndDate, ); searchedConversations.addAll(newConvos); searchedConversations.sort((a, b) => (b.startedAt ?? b.createdAt).compareTo(a.startedAt ?? a.createdAt)); @@ -517,6 +529,20 @@ class ConversationProvider extends ChangeNotifier { }).toList(); } + /// Set search date range (start and end). Null = no limit on that side. + void setSearchDateRange(DateTime? start, DateTime? end) { + searchStartDate = start; + searchEndDate = end; + notifyListeners(); + } + + /// Clear the search date range filter + void clearSearchDateRange() { + searchStartDate = null; + searchEndDate = null; + notifyListeners(); + } + /// Filter conversations by a specific date Future filterConversationsByDate(DateTime date) async { selectedDate = date;