diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb index 7b09c06..9024837 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb @@ -244,6 +244,16 @@ "allTickets": "All Tickets", "assignTicket": "Assign Ticket", "changeStatus": "Change Status", + "multiSelectFilter": "Multi-Select Filter", + "selectFilterOptions": "Select Filter Options", + "noFilterOptionsAvailable": "No filter options available", + "marketing": "Marketing", + "marketingDescription": "Manage referrals and marketing codes", + "referralCode": "Referral Code", + "referralList": "Referral List", + "today": "Today", + "thisMonth": "This Month", + "total": "Total", "internalMessage": "Internal Message", "user": "User", "operator": "Operator", diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb index ef983f5..98711be 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb @@ -243,6 +243,16 @@ "allTickets": "تمام تیکت‌ها", "assignTicket": "تخصیص تیکت", "changeStatus": "تغییر وضعیت", + "multiSelectFilter": "فیلتر چندتایی", + "selectFilterOptions": "انتخاب گزینه‌های فیلتر", + "noFilterOptionsAvailable": "هیچ گزینه فیلتری در دسترس نیست", + "marketing": "بازاریابی", + "marketingDescription": "مدیریت معرفی‌ها و کدهای بازاریابی", + "referralCode": "کد معرفی", + "referralList": "لیست معرفی‌ها", + "today": "امروز", + "thisMonth": "این ماه", + "total": "کل", "internalMessage": "پیام داخلی", "user": "کاربر", "operator": "اپراتور", diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart index fff9143..6dc0308 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart @@ -467,7 +467,7 @@ abstract class AppLocalizations { /// No description provided for @thisMonth. /// /// In en, this message translates to: - /// **'This month'** + /// **'This Month'** String get thisMonth; /// No description provided for @total. @@ -1448,6 +1448,36 @@ abstract class AppLocalizations { /// **'Change Status'** String get changeStatus; + /// No description provided for @multiSelectFilter. + /// + /// In en, this message translates to: + /// **'Multi-Select Filter'** + String get multiSelectFilter; + + /// No description provided for @selectFilterOptions. + /// + /// In en, this message translates to: + /// **'Select Filter Options'** + String get selectFilterOptions; + + /// No description provided for @noFilterOptionsAvailable. + /// + /// In en, this message translates to: + /// **'No filter options available'** + String get noFilterOptionsAvailable; + + /// No description provided for @marketingDescription. + /// + /// In en, this message translates to: + /// **'Manage referrals and marketing codes'** + String get marketingDescription; + + /// No description provided for @referralCode. + /// + /// In en, this message translates to: + /// **'Referral Code'** + String get referralCode; + /// No description provided for @internalMessage. /// /// In en, this message translates to: diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart index 56952d8..ac5cf50 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart @@ -195,7 +195,7 @@ class AppLocalizationsEn extends AppLocalizations { String get today => 'Today'; @override - String get thisMonth => 'This month'; + String get thisMonth => 'This Month'; @override String get total => 'Total'; @@ -697,6 +697,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get changeStatus => 'Change Status'; + @override + String get multiSelectFilter => 'Multi-Select Filter'; + + @override + String get selectFilterOptions => 'Select Filter Options'; + + @override + String get noFilterOptionsAvailable => 'No filter options available'; + + @override + String get marketingDescription => 'Manage referrals and marketing codes'; + + @override + String get referralCode => 'Referral Code'; + @override String get internalMessage => 'Internal Message'; diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart index ae69310..8bdf30f 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart @@ -695,6 +695,21 @@ class AppLocalizationsFa extends AppLocalizations { @override String get changeStatus => 'تغییر وضعیت'; + @override + String get multiSelectFilter => 'فیلتر چندتایی'; + + @override + String get selectFilterOptions => 'انتخاب گزینه‌های فیلتر'; + + @override + String get noFilterOptionsAvailable => 'هیچ گزینه فیلتری در دسترس نیست'; + + @override + String get marketingDescription => 'مدیریت معرفی‌ها و کدهای بازاریابی'; + + @override + String get referralCode => 'کد معرفی'; + @override String get internalMessage => 'پیام داخلی'; diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart b/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart index 10c2e1a..27f74f4 100644 --- a/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart @@ -290,6 +290,7 @@ class _MarketingPageState extends State { searchable: true, width: ColumnWidth.medium, showTime: false, + filterType: ColumnFilterType.dateRange, ), ], searchFields: ['first_name', 'last_name', 'email'], diff --git a/hesabixUI/hesabix_ui/lib/widgets/data_table/ENHANCED_FILTERS_README.md b/hesabixUI/hesabix_ui/lib/widgets/data_table/ENHANCED_FILTERS_README.md new file mode 100644 index 0000000..8b4c3d7 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/data_table/ENHANCED_FILTERS_README.md @@ -0,0 +1,188 @@ +# Enhanced Filters System + +سیستم فیلتر پیشرفته برای DataTableWidget که امکان استفاده از انواع مختلف فیلتر را فراهم می‌کند. + +## ویژگی‌ها + +### 🔍 انواع فیلتر + +1. **فیلتر متنی** (پیش‌فرض): جستجوی متنی با انواع مختلف +2. **فیلتر بازه زمانی**: انتخاب بازه تاریخ برای ستون‌های تاریخ +3. **فیلتر چندتایی**: انتخاب چندین گزینه با چک باکس + +### 🎯 نحوه استفاده + +#### 1. ستون تاریخ با فیلتر بازه زمانی + +```dart +DateColumn( + 'created_at', + 'تاریخ ایجاد', + filterType: ColumnFilterType.dateRange, +) +``` + +#### 2. ستون اولویت با فیلتر چندتایی + +```dart +TextColumn( + 'priority', + 'اولویت', + filterType: ColumnFilterType.multiSelect, + filterOptions: [ + FilterOption( + value: 'normal', + label: 'عادی', + icon: Icons.circle, + color: Colors.green, + ), + FilterOption( + value: 'special', + label: 'ویژه', + icon: Icons.star, + color: Colors.orange, + ), + FilterOption( + value: 'urgent', + label: 'فوری', + icon: Icons.priority_high, + color: Colors.red, + ), + ], +) +``` + +#### 3. ستون متنی با فیلتر پیش‌فرض + +```dart +TextColumn( + 'name', + 'نام', + // filterType پیش‌فرض text است +) +``` + +## ساختار کلاس‌ها + +### ColumnFilterType + +```dart +enum ColumnFilterType { + text, // فیلتر متنی (پیش‌فرض) + dateRange, // فیلتر بازه زمانی + multiSelect, // فیلتر چندتایی +} +``` + +### FilterOption + +```dart +class FilterOption { + final String value; // مقدار برای API + final String label; // نمایش در UI + final String? description; // توضیحات اضافی + final IconData? icon; // آیکون + final Color? color; // رنگ آیکون/متن +} +``` + +## رفتار دکمه جستجو + +- **ستون تاریخ** → DateRangePicker (از/تا تاریخ) +- **ستون با filterOptions** → CheckboxList با آیتم‌های تعریف شده +- **ستون‌های دیگر** → TextField (فعلی) + +## نمایش فیلترهای فعال + +- **تاریخ**: "تاریخ: 1403/01/01 - 1403/01/31" +- ☑️ **چندتایی**: "اولویت: عادی، ویژه" (با آیکون‌های رنگی) +- 🔍 **متنی**: "نام: احمد (شامل)" + +## ساختار فیلتر در API + +### فیلتر متنی +```json +{ + "property": "name", + "operator": "*", + "value": "احمد" +} +``` + +### فیلتر چندتایی +```json +{ + "property": "priority", + "operator": "in", + "value": ["normal", "special"] +} +``` + +### فیلتر بازه زمانی +```json +[ + { + "property": "created_at", + "operator": ">=", + "value": "2024-01-01T00:00:00.000Z" + }, + { + "property": "created_at", + "operator": "<", + "value": "2024-01-31T00:00:00.000Z" + } +] +``` + +## مثال کامل + +```dart +DataTableWidget>( + config: DataTableConfig>( + title: 'Enhanced Filters Demo', + endpoint: '/api/v1/demo/list', + columns: [ + // ستون تاریخ با فیلتر بازه زمانی + DateColumn( + 'created_at', + 'تاریخ ایجاد', + filterType: ColumnFilterType.dateRange, + ), + + // ستون اولویت با فیلتر چندتایی + TextColumn( + 'priority', + 'اولویت', + filterType: ColumnFilterType.multiSelect, + filterOptions: [ + FilterOption(value: 'normal', label: 'عادی'), + FilterOption(value: 'special', label: 'ویژه'), + FilterOption(value: 'urgent', label: 'فوری'), + ], + ), + + // ستون نام با فیلتر متنی (پیش‌فرض) + TextColumn('name', 'نام'), + ], + showColumnSearch: true, + showActiveFilters: true, + ), + fromJson: (json) => json, + calendarController: calendarController, +) +``` + +## مزایا + +- ✅ **سازگاری کامل**: با سیستم فعلی کاملاً سازگار +- ✅ **انعطاف‌پذیری**: توسعه‌دهنده کنترل کامل دارد +- ✅ **قابلیت استفاده**: UI مناسب برای هر نوع فیلتر +- ✅ **قابلیت توسعه**: آسان برای اضافه کردن انواع جدید +- ✅ **عملکرد**: فیلترها در سمت سرور پردازش می‌شوند + +## نکات مهم + +1. **فیلتر پیش‌فرض**: اگر `filterType` تعیین نشود، فیلتر متنی استفاده می‌شود +2. **فیلتر چندتایی**: نیاز به `filterOptions` دارد +3. **فیلتر بازه زمانی**: فقط برای ستون‌های تاریخ مناسب است +4. **API**: سرور باید انواع مختلف فیلتر را پشتیبانی کند diff --git a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_config.dart b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_config.dart index 21fbbe8..cd65c28 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_config.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_config.dart @@ -9,6 +9,30 @@ enum ColumnWidth { extraLarge, } +/// Types of column filters +enum ColumnFilterType { + text, // Text filter (default) + dateRange, // Date range filter + multiSelect, // Multi-select filter with checkboxes +} + +/// Filter option for multi-select filters +class FilterOption { + final String value; // Value for API + final String label; // Display label + final String? description; // Additional description + final IconData? icon; // Icon + final Color? color; // Icon/text color + + const FilterOption({ + required this.value, + required this.label, + this.description, + this.icon, + this.color, + }); +} + /// Base class for all column types abstract class DataTableColumn { final String key; @@ -17,6 +41,8 @@ abstract class DataTableColumn { final bool searchable; final ColumnWidth width; final String? tooltip; + final ColumnFilterType? filterType; + final List? filterOptions; const DataTableColumn({ required this.key, @@ -25,6 +51,8 @@ abstract class DataTableColumn { this.searchable = true, this.width = ColumnWidth.medium, this.tooltip, + this.filterType, + this.filterOptions, }); } @@ -42,6 +70,8 @@ class TextColumn extends DataTableColumn { super.searchable = true, super.width = ColumnWidth.medium, super.tooltip, + super.filterType, + super.filterOptions, this.formatter, this.textAlign, this.maxLines, @@ -64,6 +94,8 @@ class NumberColumn extends DataTableColumn { super.searchable = true, super.width = ColumnWidth.medium, super.tooltip, + super.filterType, + super.filterOptions, this.formatter, this.textAlign = TextAlign.end, this.decimalPlaces, @@ -86,6 +118,8 @@ class DateColumn extends DataTableColumn { super.searchable = true, super.width = ColumnWidth.medium, super.tooltip, + super.filterType, + super.filterOptions, this.formatter, this.textAlign = TextAlign.center, this.showTime = false, @@ -105,6 +139,8 @@ class ActionColumn extends DataTableColumn { super.searchable = false, super.width = ColumnWidth.small, super.tooltip, + super.filterType, + super.filterOptions, required this.actions, this.showOnHover = true, }) : super(key: key, label: label); @@ -122,6 +158,8 @@ class CustomColumn extends DataTableColumn { super.searchable = true, super.width = ColumnWidth.medium, super.tooltip, + super.filterType, + super.filterOptions, this.builder, this.formatter, }) : super(key: key, label: label); @@ -227,14 +265,14 @@ class DataTableConfig { this.title, this.subtitle, this.showSearch = true, - this.showFilters = true, + this.showFilters = false, this.showPagination = true, this.showColumnSearch = true, this.defaultPageSize = 20, this.pageSizeOptions = const [10, 20, 50, 100], this.enableSorting = true, this.enableGlobalSearch = true, - this.enableDateRangeFilter = true, + this.enableDateRangeFilter = false, this.onRowTap, this.onRowDoubleTap, this.customRowBuilder, @@ -279,20 +317,20 @@ class DataTableConfig { this.initialColumnSettings, this.onColumnSettingsChanged, this.customHeaderActions, - this.showFiltersButton = true, + this.showFiltersButton = false, }); /// Get column width as double double getColumnWidth(ColumnWidth width) { switch (width) { case ColumnWidth.small: - return 100.0; + return 120.0; case ColumnWidth.medium: - return 150.0; + return 180.0; case ColumnWidth.large: - return 200.0; + return 250.0; case ColumnWidth.extraLarge: - return 300.0; + return 350.0; } } diff --git a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_search_dialog.dart b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_search_dialog.dart index bcc345d..e1d590e 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_search_dialog.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_search_dialog.dart @@ -5,6 +5,7 @@ import 'data_table_config.dart'; import 'helpers/data_table_utils.dart'; import 'package:hesabix_ui/core/calendar_controller.dart'; import 'package:hesabix_ui/core/date_utils.dart'; +import 'package:hesabix_ui/widgets/jalali_date_picker.dart'; /// Dialog for column search class DataTableSearchDialog extends StatefulWidget { @@ -12,8 +13,13 @@ class DataTableSearchDialog extends StatefulWidget { final String columnLabel; final String searchValue; final String searchType; + final ColumnFilterType? filterType; + final List? filterOptions; final Function(String value, String type) onApply; + final Function(List values)? onApplyMultiSelect; + final Function(DateTime? fromDate, DateTime? toDate)? onApplyDateRange; final VoidCallback onClear; + final CalendarController? calendarController; const DataTableSearchDialog({ super.key, @@ -21,8 +27,13 @@ class DataTableSearchDialog extends StatefulWidget { required this.columnLabel, required this.searchValue, required this.searchType, + this.filterType, + this.filterOptions, required this.onApply, + this.onApplyMultiSelect, + this.onApplyDateRange, required this.onClear, + this.calendarController, }); @override @@ -32,6 +43,9 @@ class DataTableSearchDialog extends StatefulWidget { class _DataTableSearchDialogState extends State { late TextEditingController _controller; late String _selectedType; + Set _selectedValues = {}; + DateTime? _fromDate; + DateTime? _toDate; @override void initState() { @@ -54,73 +68,281 @@ class _DataTableSearchDialogState extends State { return AlertDialog( title: Row( children: [ - Icon(Icons.search, color: theme.primaryColor, size: 20), + Icon(_getFilterIcon(), color: theme.primaryColor, size: 20), const SizedBox(width: 8), - Text(t.searchInColumn(widget.columnLabel)), + Text(_getFilterTitle(t)), ], ), content: Column( mainAxisSize: MainAxisSize.min, - children: [ - // Search type dropdown - DropdownButtonFormField( - value: _selectedType, - decoration: InputDecoration( - labelText: t.searchType, - border: const OutlineInputBorder(), - isDense: true, - ), - items: [ - DropdownMenuItem(value: '*', child: Text(t.contains)), - DropdownMenuItem(value: '*?', child: Text(t.startsWith)), - DropdownMenuItem(value: '?*', child: Text(t.endsWith)), - DropdownMenuItem(value: '=', child: Text(t.exactMatch)), - ], - onChanged: (value) { - if (value != null) { - setState(() { - _selectedType = value; - }); - } - }, - ), - const SizedBox(height: 16), - // Search value input - TextField( - controller: _controller, - decoration: InputDecoration( - labelText: t.searchValue, - border: const OutlineInputBorder(), - isDense: true, - ), - autofocus: true, - ), - ], + children: _buildFilterContent(t, theme), ), - actions: [ + actions: _buildFilterActions(t), + ); + } + + IconData _getFilterIcon() { + switch (widget.filterType) { + case ColumnFilterType.dateRange: + return Icons.date_range; + case ColumnFilterType.multiSelect: + return Icons.checklist; + default: + return Icons.search; + } + } + + String _getFilterTitle(AppLocalizations t) { + switch (widget.filterType) { + case ColumnFilterType.dateRange: + return t.dateRangeFilter; + case ColumnFilterType.multiSelect: + return t.multiSelectFilter; + default: + return t.searchInColumn(widget.columnLabel); + } + } + + List _buildFilterContent(AppLocalizations t, ThemeData theme) { + switch (widget.filterType) { + case ColumnFilterType.dateRange: + return _buildDateRangeContent(t, theme); + case ColumnFilterType.multiSelect: + return _buildMultiSelectContent(t, theme); + default: + return _buildTextFilterContent(t, theme); + } + } + + List _buildTextFilterContent(AppLocalizations t, ThemeData theme) { + return [ + // Search type dropdown + DropdownButtonFormField( + value: _selectedType, + decoration: InputDecoration( + labelText: t.searchType, + border: const OutlineInputBorder(), + isDense: true, + ), + items: [ + DropdownMenuItem(value: '*', child: Text(t.contains)), + DropdownMenuItem(value: '*?', child: Text(t.startsWith)), + DropdownMenuItem(value: '?*', child: Text(t.endsWith)), + DropdownMenuItem(value: '=', child: Text(t.exactMatch)), + ], + onChanged: (value) { + if (value != null) { + setState(() { + _selectedType = value; + }); + } + }, + ), + const SizedBox(height: 16), + // Search value input + TextField( + controller: _controller, + decoration: InputDecoration( + labelText: t.searchValue, + border: const OutlineInputBorder(), + isDense: true, + ), + autofocus: true, + ), + ]; + } + + List _buildDateRangeContent(AppLocalizations t, ThemeData theme) { + final isJalali = widget.calendarController?.isJalali ?? false; + + return [ + // From date + ListTile( + leading: const Icon(Icons.calendar_today), + title: Text(t.dateFrom), + subtitle: Text(_fromDate != null + ? HesabixDateUtils.formatForDisplay(_fromDate!, isJalali) + : t.selectDate), + onTap: () async { + final date = isJalali + ? await showJalaliDatePicker( + context: context, + initialDate: _fromDate ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now().add(const Duration(days: 365)), + helpText: t.dateFrom, + ) + : await showDatePicker( + context: context, + initialDate: _fromDate ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (date != null) { + setState(() { + _fromDate = date; + }); + } + }, + ), + // To date + ListTile( + leading: const Icon(Icons.calendar_today), + title: Text(t.dateTo), + subtitle: Text(_toDate != null + ? HesabixDateUtils.formatForDisplay(_toDate!, isJalali) + : t.selectDate), + onTap: () async { + final date = isJalali + ? await showJalaliDatePicker( + context: context, + initialDate: _toDate ?? _fromDate ?? DateTime.now(), + firstDate: _fromDate ?? DateTime(2000), + lastDate: DateTime.now().add(const Duration(days: 365)), + helpText: t.dateTo, + ) + : await showDatePicker( + context: context, + initialDate: _toDate ?? _fromDate ?? DateTime.now(), + firstDate: _fromDate ?? DateTime(2000), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (date != null) { + setState(() { + _toDate = date; + }); + } + }, + ), + ]; + } + + List _buildMultiSelectContent(AppLocalizations t, ThemeData theme) { + if (widget.filterOptions == null || widget.filterOptions!.isEmpty) { + return [ + Text( + t.noFilterOptionsAvailable, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ]; + } + + return [ + Text( + t.selectFilterOptions, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + ...widget.filterOptions!.map((option) => CheckboxListTile( + title: Row( + children: [ + if (option.icon != null) ...[ + Icon( + option.icon, + size: 16, + color: option.color, + ), + const SizedBox(width: 8), + ], + Text(option.label), + ], + ), + subtitle: option.description != null + ? Text(option.description!) + : null, + value: _selectedValues.contains(option.value), + onChanged: (bool? value) { + setState(() { + if (value == true) { + _selectedValues.add(option.value); + } else { + _selectedValues.remove(option.value); + } + }); + }, + )), + ]; + } + + List _buildFilterActions(AppLocalizations t) { + final hasActiveFilter = _hasActiveFilter(); + + return [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(t.cancel), + ), + if (hasActiveFilter) TextButton( onPressed: () { + widget.onClear(); Navigator.of(context).pop(); }, - child: Text(t.cancel), + child: Text(t.clear), ), - if (widget.searchValue.isNotEmpty) - TextButton( - onPressed: () { - widget.onClear(); - Navigator.of(context).pop(); - }, - child: Text(t.clear), - ), - FilledButton( - onPressed: () { - widget.onApply(_controller.text.trim(), _selectedType); - Navigator.of(context).pop(); - }, - child: Text(t.applyColumnFilter), - ), - ], - ); + FilledButton( + onPressed: _canApplyFilter() ? _applyFilter : null, + child: Text(_getApplyButtonText(t)), + ), + ]; + } + + bool _hasActiveFilter() { + switch (widget.filterType) { + case ColumnFilterType.dateRange: + return _fromDate != null || _toDate != null; + case ColumnFilterType.multiSelect: + return _selectedValues.isNotEmpty; + default: + return widget.searchValue.isNotEmpty; + } + } + + bool _canApplyFilter() { + switch (widget.filterType) { + case ColumnFilterType.dateRange: + return _fromDate != null && _toDate != null; + case ColumnFilterType.multiSelect: + return _selectedValues.isNotEmpty; + default: + return _controller.text.trim().isNotEmpty; + } + } + + String _getApplyButtonText(AppLocalizations t) { + switch (widget.filterType) { + case ColumnFilterType.dateRange: + return t.applyFilter; + case ColumnFilterType.multiSelect: + return t.applyFilter; + default: + return t.applyColumnFilter; + } + } + + void _applyFilter() { + switch (widget.filterType) { + case ColumnFilterType.dateRange: + if (widget.onApplyDateRange != null) { + widget.onApplyDateRange!(_fromDate, _toDate); + } + break; + case ColumnFilterType.multiSelect: + if (widget.onApplyMultiSelect != null) { + widget.onApplyMultiSelect!(_selectedValues.toList()); + } + break; + default: + widget.onApply(_controller.text.trim(), _selectedType); + break; + } + Navigator.of(context).pop(); } } @@ -247,6 +469,9 @@ class _DataTableDateRangeDialogState extends State { class ActiveFiltersWidget extends StatelessWidget { final Map columnSearchValues; final Map columnSearchTypes; + final Map> columnMultiSelectValues; + final Map columnDateFromValues; + final Map columnDateToValues; final DateTime? fromDate; final DateTime? toDate; final List columns; @@ -258,6 +483,9 @@ class ActiveFiltersWidget extends StatelessWidget { super.key, required this.columnSearchValues, required this.columnSearchTypes, + required this.columnMultiSelectValues, + required this.columnDateFromValues, + required this.columnDateToValues, this.fromDate, this.toDate, required this.columns, @@ -272,6 +500,8 @@ class ActiveFiltersWidget extends StatelessWidget { final theme = Theme.of(context); final hasFilters = columnSearchValues.isNotEmpty || + columnMultiSelectValues.isNotEmpty || + columnDateFromValues.isNotEmpty || (fromDate != null && toDate != null); if (!hasFilters) return const SizedBox.shrink(); @@ -309,7 +539,7 @@ class ActiveFiltersWidget extends StatelessWidget { spacing: 6, runSpacing: 3, children: [ - // Column filters + // Text search filters ...columnSearchValues.entries.map((entry) { final columnName = entry.key; final searchValue = entry.value; @@ -330,7 +560,64 @@ class ActiveFiltersWidget extends StatelessWidget { ); }), - // Date range filter + // Multi-select filters + ...columnMultiSelectValues.entries.map((entry) { + final columnName = entry.key; + final selectedValues = entry.value; + final columnLabel = DataTableUtils.getColumnLabel(columnName, columns); + final filterOptions = DataTableUtils.getColumnFilterOptions(columnName, columns); + + String displayText = '$columnLabel: '; + if (filterOptions != null) { + final selectedLabels = selectedValues.map((value) { + final option = filterOptions.firstWhere( + (opt) => opt.value == value, + orElse: () => FilterOption(value: value, label: value), + ); + return option.label; + }).join(', '); + displayText += selectedLabels; + } else { + displayText += selectedValues.join(', '); + } + + return Chip( + label: Text(displayText), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => onRemoveColumnFilter(columnName), + backgroundColor: theme.primaryColor.withValues(alpha: 0.1), + deleteIconColor: theme.primaryColor, + labelStyle: TextStyle( + color: theme.primaryColor, + fontSize: 12, + ), + ); + }), + + // Date range filters + ...columnDateFromValues.entries.map((entry) { + final columnName = entry.key; + final fromDate = entry.value; + final toDate = columnDateToValues[columnName]; + final columnLabel = DataTableUtils.getColumnLabel(columnName, columns); + + if (fromDate != null && toDate != null) { + return Chip( + label: Text('$columnLabel: ${HesabixDateUtils.formatForDisplay(fromDate, calendarController?.isJalali ?? false)} - ${HesabixDateUtils.formatForDisplay(toDate, calendarController?.isJalali ?? false)}'), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => onRemoveColumnFilter(columnName), + backgroundColor: theme.primaryColor.withValues(alpha: 0.1), + deleteIconColor: theme.primaryColor, + labelStyle: TextStyle( + color: theme.primaryColor, + fontSize: 12, + ), + ); + } + return const SizedBox.shrink(); + }), + + // Legacy date range filter if (fromDate != null && toDate != null) Chip( label: Text('${t.dateFrom}: ${HesabixDateUtils.formatForDisplay(fromDate!, calendarController?.isJalali ?? false)} - ${t.dateTo}: ${HesabixDateUtils.formatForDisplay(toDate!, calendarController?.isJalali ?? false)}'), diff --git a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart index 24ffa5c..5b43e2f 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart @@ -7,7 +7,6 @@ import 'package:dio/dio.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'package:hesabix_ui/core/api_client.dart'; import 'package:hesabix_ui/core/calendar_controller.dart'; -import 'package:hesabix_ui/widgets/date_input_field.dart'; import 'data_table_config.dart'; import 'data_table_search_dialog.dart'; import 'column_settings_dialog.dart'; @@ -34,7 +33,6 @@ class DataTableWidget extends StatefulWidget { class _DataTableWidgetState extends State> { // Data state List _items = []; - bool _loading = false; bool _loadingList = false; String? _error; @@ -44,17 +42,19 @@ class _DataTableWidgetState extends State> { int _total = 0; int _totalPages = 0; - // Search and filter state + // Search state final TextEditingController _searchCtrl = TextEditingController(); Timer? _searchDebounce; - bool _showFilters = false; - DateTime? _fromDate; - DateTime? _toDate; // Column search state final Map _columnSearchValues = {}; final Map _columnSearchTypes = {}; final Map _columnSearchControllers = {}; + + // Enhanced filter state + final Map> _columnMultiSelectValues = {}; + final Map _columnDateFromValues = {}; + final Map _columnDateToValues = {}; // Sorting state String? _sortBy; @@ -69,9 +69,13 @@ class _DataTableWidgetState extends State> { List _visibleColumns = []; bool _isLoadingColumnSettings = false; + // Scroll controller for horizontal scrolling + late ScrollController _horizontalScrollController; + @override void initState() { super.initState(); + _horizontalScrollController = ScrollController(); _limit = widget.config.defaultPageSize; _setupSearchListener(); _loadColumnSettings(); @@ -82,6 +86,7 @@ class _DataTableWidgetState extends State> { void dispose() { _searchCtrl.dispose(); _searchDebounce?.cancel(); + _horizontalScrollController.dispose(); for (var controller in _columnSearchControllers.values) { controller.dispose(); } @@ -208,19 +213,7 @@ class _DataTableWidgetState extends State> { List _buildFilters() { final filters = []; - // Date range filters - if (widget.config.enableDateRangeFilter && - widget.config.dateRangeField != null && - _fromDate != null && - _toDate != null) { - filters.addAll(DataTableUtils.createDateRangeFilters( - widget.config.dateRangeField!, - _fromDate!, - _toDate!, - )); - } - - // Column search filters + // Text search filters for (var entry in _columnSearchValues.entries) { final columnName = entry.key; final searchValue = entry.value.trim(); @@ -235,10 +228,43 @@ class _DataTableWidgetState extends State> { } } + // Multi-select filters + for (var entry in _columnMultiSelectValues.entries) { + final columnName = entry.key; + final selectedValues = entry.value; + + if (selectedValues.isNotEmpty) { + filters.add(DataTableUtils.createMultiSelectFilter( + columnName, + selectedValues, + )); + } + } + + // Date range filters + for (var entry in _columnDateFromValues.entries) { + final columnName = entry.key; + final fromDate = entry.value; + final toDate = _columnDateToValues[columnName]; + + if (fromDate != null && toDate != null) { + filters.addAll(DataTableUtils.createDateRangeFilter( + columnName, + fromDate, + toDate, + )); + } + } + return filters; } void _openColumnSearchDialog(String columnName, String columnLabel) { + // Get column configuration + final column = widget.config.getColumnByKey(columnName); + final filterType = column?.filterType; + final filterOptions = column?.filterOptions; + // Initialize controller if not exists if (!_columnSearchControllers.containsKey(columnName)) { _columnSearchControllers[columnName] = TextEditingController( @@ -256,6 +282,9 @@ class _DataTableWidgetState extends State> { columnLabel: columnLabel, searchValue: _columnSearchValues[columnName] ?? '', searchType: _columnSearchTypes[columnName] ?? '*', + filterType: filterType, + filterOptions: filterOptions, + calendarController: widget.calendarController, onApply: (value, type) { setState(() { _columnSearchValues[columnName] = value; @@ -264,10 +293,28 @@ class _DataTableWidgetState extends State> { _page = 1; _fetchData(); }, + onApplyMultiSelect: (values) { + setState(() { + _columnMultiSelectValues[columnName] = values; + }); + _page = 1; + _fetchData(); + }, + onApplyDateRange: (fromDate, toDate) { + setState(() { + _columnDateFromValues[columnName] = fromDate; + _columnDateToValues[columnName] = toDate; + }); + _page = 1; + _fetchData(); + }, onClear: () { setState(() { _columnSearchValues.remove(columnName); _columnSearchTypes.remove(columnName); + _columnMultiSelectValues.remove(columnName); + _columnDateFromValues.remove(columnName); + _columnDateToValues.remove(columnName); _columnSearchControllers[columnName]?.clear(); }); _page = 1; @@ -279,21 +326,22 @@ class _DataTableWidgetState extends State> { bool _hasActiveFilters() { - return _fromDate != null || - _toDate != null || - _searchCtrl.text.isNotEmpty || - _columnSearchValues.isNotEmpty; + return _searchCtrl.text.isNotEmpty || + _columnSearchValues.isNotEmpty || + _columnMultiSelectValues.isNotEmpty || + _columnDateFromValues.isNotEmpty; } void _clearAllFilters() { setState(() { - _fromDate = null; - _toDate = null; _searchCtrl.clear(); _sortBy = null; _sortDesc = false; _columnSearchValues.clear(); _columnSearchTypes.clear(); + _columnMultiSelectValues.clear(); + _columnDateFromValues.clear(); + _columnDateToValues.clear(); _selectedRows.clear(); for (var controller in _columnSearchControllers.values) { controller.clear(); @@ -302,9 +350,6 @@ class _DataTableWidgetState extends State> { _page = 1; _fetchData(); // Call the callback if provided - if (widget.config.onDateRangeClear != null) { - widget.config.onDateRangeClear!(); - } if (widget.config.onRowSelectionChanged != null) { widget.config.onRowSelectionChanged!(_selectedRows); } @@ -481,21 +526,6 @@ class _DataTableWidgetState extends State> { } }); - // Add date range filter - if (_fromDate != null && _toDate != null && widget.config.dateRangeField != null) { - final start = DateTime(_fromDate!.year, _fromDate!.month, _fromDate!.day); - final endExclusive = DateTime(_toDate!.year, _toDate!.month, _toDate!.day).add(const Duration(days: 1)); - filters.add({ - 'property': widget.config.dateRangeField!, - 'operator': '>=', - 'value': start.toIso8601String(), - }); - filters.add({ - 'property': widget.config.dateRangeField!, - 'operator': '<', - 'value': endExclusive.toIso8601String(), - }); - } final queryInfo = { 'sort_by': _sortBy, @@ -692,9 +722,9 @@ class _DataTableWidgetState extends State> { const SizedBox(height: 16), ], - // Search and Filters - if (widget.config.showSearch || widget.config.showFilters) ...[ - _buildSearchAndFilters(t, theme), + // Search + if (widget.config.showSearch) ...[ + _buildSearch(t, theme), const SizedBox(height: 12), ], @@ -703,14 +733,20 @@ class _DataTableWidgetState extends State> { ActiveFiltersWidget( columnSearchValues: _columnSearchValues, columnSearchTypes: _columnSearchTypes, - fromDate: _fromDate, - toDate: _toDate, + columnMultiSelectValues: _columnMultiSelectValues, + columnDateFromValues: _columnDateFromValues, + columnDateToValues: _columnDateToValues, + fromDate: null, + toDate: null, columns: widget.config.columns, calendarController: widget.calendarController, onRemoveColumnFilter: (columnName) { setState(() { _columnSearchValues.remove(columnName); _columnSearchTypes.remove(columnName); + _columnMultiSelectValues.remove(columnName); + _columnDateFromValues.remove(columnName); + _columnDateToValues.remove(columnName); _columnSearchControllers[columnName]?.clear(); }); _page = 1; @@ -771,22 +807,6 @@ class _DataTableWidgetState extends State> { ], const Spacer(), - // Filter buttons - if (widget.config.showFilters && widget.config.showFiltersButton) ...[ - Tooltip( - message: _showFilters ? t.hideFilters : t.showFilters, - child: IconButton( - onPressed: () { - setState(() { - _showFilters = !_showFilters; - }); - }, - icon: Icon(_showFilters ? Icons.filter_list_off : Icons.filter_list), - tooltip: _showFilters ? t.hideFilters : t.showFilters, - ), - ), - const SizedBox(width: 4), - ], // Clear filters button (only show when filters are applied) if (widget.config.showClearFiltersButton && _hasActiveFilters()) ...[ @@ -822,11 +842,6 @@ class _DataTableWidgetState extends State> { case 'refresh': _fetchData(); break; - case 'filters': - setState(() { - _showFilters = !_showFilters; - }); - break; case 'columnSettings': _openColumnSettingsDialog(); break; @@ -844,17 +859,6 @@ class _DataTableWidgetState extends State> { ], ), ), - if (widget.config.showFilters && widget.config.showFiltersButton) - PopupMenuItem( - value: 'filters', - child: Row( - children: [ - Icon(_showFilters ? Icons.filter_list_off : Icons.filter_list, size: 20), - const SizedBox(width: 8), - Text(_showFilters ? t.hideFilters : t.showFilters), - ], - ), - ), if (widget.config.showColumnSettingsButton && widget.config.enableColumnSettings) PopupMenuItem( value: 'columnSettings', @@ -1151,128 +1155,23 @@ class _DataTableWidgetState extends State> { ); } - Widget _buildSearchAndFilters(AppLocalizations t, ThemeData theme) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: theme.dividerColor.withValues(alpha: 0.3)), - ), - child: Column( + Widget _buildSearch(AppLocalizations t, ThemeData theme) { + return Row( children: [ - // Main controls row - Row( - children: [ - if (widget.config.showSearch) ...[ - Expanded( - child: TextField( - controller: _searchCtrl, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search, size: 18), - hintText: t.searchInNameEmail, - border: const OutlineInputBorder(), - isDense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - ), - ), - ), - const SizedBox(width: 8), - ], - ], - ), - - // Date range filters (if enabled and expanded) - if (widget.config.showFilters && - widget.config.enableDateRangeFilter && - widget.config.dateRangeField != null && - _showFilters) ...[ - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: theme.dividerColor.withValues(alpha: 0.2)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.date_range, color: theme.primaryColor, size: 16), - const SizedBox(width: 6), - Text( - t.dateRangeFilter, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: theme.primaryColor, - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: widget.calendarController != null ? DateInputField( - value: _fromDate, - onChanged: (date) { - setState(() { - _fromDate = date; - }); - }, - labelText: t.dateFrom, - calendarController: widget.calendarController!, - enabled: !_loading, - ) : const SizedBox.shrink(), - ), - const SizedBox(width: 8), - Expanded( - child: widget.calendarController != null ? DateInputField( - value: _toDate, - onChanged: (date) { - setState(() { - _toDate = date; - }); - }, - labelText: t.dateTo, - calendarController: widget.calendarController!, - enabled: !_loading, - ) : const SizedBox.shrink(), - ), - const SizedBox(width: 8), - FilledButton.icon( - onPressed: _loading || _fromDate == null || _toDate == null ? null : () { - _page = 1; - _fetchData(); - // Call the callback if provided - if (widget.config.onDateRangeApply != null) { - widget.config.onDateRangeApply!(_fromDate, _toDate); - } - }, - icon: _loading - ? const SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.check, size: 16), - label: Text(t.applyFilter), - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - minimumSize: const Size(0, 32), - ), - ), - ], - ), - ], + Expanded( + child: TextField( + controller: _searchCtrl, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search, size: 18), + hintText: t.searchInNameEmail, + border: const OutlineInputBorder(), + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), ), ), - ], + ), ], - ), - ); + ); } Widget _buildDataTable(AppLocalizations t, ThemeData theme) { @@ -1371,6 +1270,7 @@ class _DataTableWidgetState extends State> { ) : const SizedBox.shrink(), size: ColumnSize.S, + fixedWidth: 50.0, )); } @@ -1385,6 +1285,7 @@ class _DataTableWidgetState extends State> { ), ), size: ColumnSize.S, + fixedWidth: 60.0, )); } @@ -1408,14 +1309,19 @@ class _DataTableWidgetState extends State> { enabled: widget.config.enableSorting && column.sortable, ), size: DataTableUtils.getColumnSize(column.width), + fixedWidth: DataTableUtils.getColumnWidth(column.width), ); })); - return DataTable2( - columnSpacing: 12, - horizontalMargin: 12, - minWidth: widget.config.minTableWidth ?? 600, - columns: columns, + return Scrollbar( + controller: _horizontalScrollController, + thumbVisibility: true, + child: DataTable2( + columnSpacing: 8, + horizontalMargin: 8, + minWidth: widget.config.minTableWidth ?? 600, + horizontalScrollController: _horizontalScrollController, + columns: columns, rows: _items.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; @@ -1474,6 +1380,7 @@ class _DataTableWidgetState extends State> { cells: cells, ); }).toList(), + ), ); } @@ -1573,57 +1480,71 @@ class _ColumnHeaderWithSearch extends StatelessWidget { onTap: enabled ? () => onSort(sortBy) : null, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - text, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - color: isActive ? theme.colorScheme.primary : theme.colorScheme.onSurface, - ), - ), - if (enabled) ...[ - const SizedBox(width: 4), - if (isActive) - Icon( - sortDesc ? Icons.arrow_downward : Icons.arrow_upward, - size: 16, - color: theme.colorScheme.primary, - ) - else - Icon( - Icons.unfold_more, - size: 16, - color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + child: Container( + width: double.infinity, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + text, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: isActive ? theme.colorScheme.primary : theme.colorScheme.onSurface, + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (enabled) ...[ + const SizedBox(width: 4), + if (isActive) + Icon( + sortDesc ? Icons.arrow_downward : Icons.arrow_upward, + size: 16, + color: theme.colorScheme.primary, + ) + else + Icon( + Icons.unfold_more, + size: 16, + color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + ), + ], + ], ), + ), + const SizedBox(width: 8), + // Search button + InkWell( + onTap: onSearch, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: hasActiveFilter + ? theme.colorScheme.primaryContainer + : theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + border: hasActiveFilter + ? Border.all(color: theme.colorScheme.primary.withValues(alpha: 0.3)) + : null, + ), + child: Icon( + Icons.search, + size: 14, + color: hasActiveFilter + ? theme.colorScheme.onPrimaryContainer + : theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7), + ), + ), + ), ], - const SizedBox(width: 8), - // Search button - InkWell( - onTap: onSearch, - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: hasActiveFilter - ? theme.colorScheme.primaryContainer - : theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(12), - border: hasActiveFilter - ? Border.all(color: theme.colorScheme.primary.withValues(alpha: 0.3)) - : null, - ), - child: Icon( - Icons.search, - size: 14, - color: hasActiveFilter - ? theme.colorScheme.onPrimaryContainer - : theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7), - ), - ), - ), - ], + ), ), ), ); diff --git a/hesabixUI/hesabix_ui/lib/widgets/data_table/helpers/data_table_utils.dart b/hesabixUI/hesabix_ui/lib/widgets/data_table/helpers/data_table_utils.dart index e0141e4..7f9d84b 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/data_table/helpers/data_table_utils.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/data_table/helpers/data_table_utils.dart @@ -182,6 +182,42 @@ class DataTableUtils { ); } + /// Create filter item for multi-select + static FilterItem createMultiSelectFilter( + String field, + List values, + ) { + return FilterItem( + property: field, + operator: 'in', + value: values, + ); + } + + /// Create filter item for date range + static List createDateRangeFilter( + String field, + DateTime startDate, + DateTime endDate, + ) { + final start = DateTime(startDate.year, startDate.month, startDate.day); + final endExclusive = DateTime(endDate.year, endDate.month, endDate.day) + .add(const Duration(days: 1)); + + return [ + FilterItem( + property: field, + operator: '>=', + value: start.toIso8601String(), + ), + FilterItem( + property: field, + operator: '<', + value: endExclusive.toIso8601String(), + ), + ]; + } + /// Get column label by key static String getColumnLabel(String key, List columns) { final column = columns.firstWhere( @@ -191,6 +227,34 @@ class DataTableUtils { return column.label; } + /// Get column filter type + static ColumnFilterType? getColumnFilterType(String key, List columns) { + final column = columns.firstWhere( + (col) => col.key == key, + orElse: () => TextColumn(key, key), + ); + return column.filterType; + } + + /// Get column filter options + static List? getColumnFilterOptions(String key, List columns) { + final column = columns.firstWhere( + (col) => col.key == key, + orElse: () => TextColumn(key, key), + ); + return column.filterOptions; + } + + /// Check if column has date range filter + static bool isDateRangeFilter(String key, List columns) { + return getColumnFilterType(key, columns) == ColumnFilterType.dateRange; + } + + /// Check if column has multi-select filter + static bool isMultiSelectFilter(String key, List columns) { + return getColumnFilterType(key, columns) == ColumnFilterType.multiSelect; + } + /// Check if column is searchable static bool isColumnSearchable(String key, List columns) { final column = columns.firstWhere(