progress in datatable

This commit is contained in:
Hesabix 2025-09-21 16:00:34 +03:30
parent 6a07d3ede5
commit 4e07795467
11 changed files with 898 additions and 319 deletions

View file

@ -244,6 +244,16 @@
"allTickets": "All Tickets", "allTickets": "All Tickets",
"assignTicket": "Assign Ticket", "assignTicket": "Assign Ticket",
"changeStatus": "Change Status", "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", "internalMessage": "Internal Message",
"user": "User", "user": "User",
"operator": "Operator", "operator": "Operator",

View file

@ -243,6 +243,16 @@
"allTickets": "تمام تیکت‌ها", "allTickets": "تمام تیکت‌ها",
"assignTicket": "تخصیص تیکت", "assignTicket": "تخصیص تیکت",
"changeStatus": "تغییر وضعیت", "changeStatus": "تغییر وضعیت",
"multiSelectFilter": "فیلتر چندتایی",
"selectFilterOptions": "انتخاب گزینه‌های فیلتر",
"noFilterOptionsAvailable": "هیچ گزینه فیلتری در دسترس نیست",
"marketing": "بازاریابی",
"marketingDescription": "مدیریت معرفی‌ها و کدهای بازاریابی",
"referralCode": "کد معرفی",
"referralList": "لیست معرفی‌ها",
"today": "امروز",
"thisMonth": "این ماه",
"total": "کل",
"internalMessage": "پیام داخلی", "internalMessage": "پیام داخلی",
"user": "کاربر", "user": "کاربر",
"operator": "اپراتور", "operator": "اپراتور",

View file

@ -467,7 +467,7 @@ abstract class AppLocalizations {
/// No description provided for @thisMonth. /// No description provided for @thisMonth.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'This month'** /// **'This Month'**
String get thisMonth; String get thisMonth;
/// No description provided for @total. /// No description provided for @total.
@ -1448,6 +1448,36 @@ abstract class AppLocalizations {
/// **'Change Status'** /// **'Change Status'**
String get changeStatus; 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. /// No description provided for @internalMessage.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -195,7 +195,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get today => 'Today'; String get today => 'Today';
@override @override
String get thisMonth => 'This month'; String get thisMonth => 'This Month';
@override @override
String get total => 'Total'; String get total => 'Total';
@ -697,6 +697,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get changeStatus => 'Change Status'; 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 @override
String get internalMessage => 'Internal Message'; String get internalMessage => 'Internal Message';

View file

@ -695,6 +695,21 @@ class AppLocalizationsFa extends AppLocalizations {
@override @override
String get changeStatus => 'تغییر وضعیت'; String get changeStatus => 'تغییر وضعیت';
@override
String get multiSelectFilter => 'فیلتر چندتایی';
@override
String get selectFilterOptions => 'انتخاب گزینه‌های فیلتر';
@override
String get noFilterOptionsAvailable => 'هیچ گزینه فیلتری در دسترس نیست';
@override
String get marketingDescription => 'مدیریت معرفی‌ها و کدهای بازاریابی';
@override
String get referralCode => 'کد معرفی';
@override @override
String get internalMessage => 'پیام داخلی'; String get internalMessage => 'پیام داخلی';

View file

@ -290,6 +290,7 @@ class _MarketingPageState extends State<MarketingPage> {
searchable: true, searchable: true,
width: ColumnWidth.medium, width: ColumnWidth.medium,
showTime: false, showTime: false,
filterType: ColumnFilterType.dateRange,
), ),
], ],
searchFields: ['first_name', 'last_name', 'email'], searchFields: ['first_name', 'last_name', 'email'],

View file

@ -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<Map<String, dynamic>>(
config: DataTableConfig<Map<String, dynamic>>(
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**: سرور باید انواع مختلف فیلتر را پشتیبانی کند

View file

@ -9,6 +9,30 @@ enum ColumnWidth {
extraLarge, 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 /// Base class for all column types
abstract class DataTableColumn { abstract class DataTableColumn {
final String key; final String key;
@ -17,6 +41,8 @@ abstract class DataTableColumn {
final bool searchable; final bool searchable;
final ColumnWidth width; final ColumnWidth width;
final String? tooltip; final String? tooltip;
final ColumnFilterType? filterType;
final List<FilterOption>? filterOptions;
const DataTableColumn({ const DataTableColumn({
required this.key, required this.key,
@ -25,6 +51,8 @@ abstract class DataTableColumn {
this.searchable = true, this.searchable = true,
this.width = ColumnWidth.medium, this.width = ColumnWidth.medium,
this.tooltip, this.tooltip,
this.filterType,
this.filterOptions,
}); });
} }
@ -42,6 +70,8 @@ class TextColumn extends DataTableColumn {
super.searchable = true, super.searchable = true,
super.width = ColumnWidth.medium, super.width = ColumnWidth.medium,
super.tooltip, super.tooltip,
super.filterType,
super.filterOptions,
this.formatter, this.formatter,
this.textAlign, this.textAlign,
this.maxLines, this.maxLines,
@ -64,6 +94,8 @@ class NumberColumn extends DataTableColumn {
super.searchable = true, super.searchable = true,
super.width = ColumnWidth.medium, super.width = ColumnWidth.medium,
super.tooltip, super.tooltip,
super.filterType,
super.filterOptions,
this.formatter, this.formatter,
this.textAlign = TextAlign.end, this.textAlign = TextAlign.end,
this.decimalPlaces, this.decimalPlaces,
@ -86,6 +118,8 @@ class DateColumn extends DataTableColumn {
super.searchable = true, super.searchable = true,
super.width = ColumnWidth.medium, super.width = ColumnWidth.medium,
super.tooltip, super.tooltip,
super.filterType,
super.filterOptions,
this.formatter, this.formatter,
this.textAlign = TextAlign.center, this.textAlign = TextAlign.center,
this.showTime = false, this.showTime = false,
@ -105,6 +139,8 @@ class ActionColumn extends DataTableColumn {
super.searchable = false, super.searchable = false,
super.width = ColumnWidth.small, super.width = ColumnWidth.small,
super.tooltip, super.tooltip,
super.filterType,
super.filterOptions,
required this.actions, required this.actions,
this.showOnHover = true, this.showOnHover = true,
}) : super(key: key, label: label); }) : super(key: key, label: label);
@ -122,6 +158,8 @@ class CustomColumn extends DataTableColumn {
super.searchable = true, super.searchable = true,
super.width = ColumnWidth.medium, super.width = ColumnWidth.medium,
super.tooltip, super.tooltip,
super.filterType,
super.filterOptions,
this.builder, this.builder,
this.formatter, this.formatter,
}) : super(key: key, label: label); }) : super(key: key, label: label);
@ -227,14 +265,14 @@ class DataTableConfig<T> {
this.title, this.title,
this.subtitle, this.subtitle,
this.showSearch = true, this.showSearch = true,
this.showFilters = true, this.showFilters = false,
this.showPagination = true, this.showPagination = true,
this.showColumnSearch = true, this.showColumnSearch = true,
this.defaultPageSize = 20, this.defaultPageSize = 20,
this.pageSizeOptions = const [10, 20, 50, 100], this.pageSizeOptions = const [10, 20, 50, 100],
this.enableSorting = true, this.enableSorting = true,
this.enableGlobalSearch = true, this.enableGlobalSearch = true,
this.enableDateRangeFilter = true, this.enableDateRangeFilter = false,
this.onRowTap, this.onRowTap,
this.onRowDoubleTap, this.onRowDoubleTap,
this.customRowBuilder, this.customRowBuilder,
@ -279,20 +317,20 @@ class DataTableConfig<T> {
this.initialColumnSettings, this.initialColumnSettings,
this.onColumnSettingsChanged, this.onColumnSettingsChanged,
this.customHeaderActions, this.customHeaderActions,
this.showFiltersButton = true, this.showFiltersButton = false,
}); });
/// Get column width as double /// Get column width as double
double getColumnWidth(ColumnWidth width) { double getColumnWidth(ColumnWidth width) {
switch (width) { switch (width) {
case ColumnWidth.small: case ColumnWidth.small:
return 100.0; return 120.0;
case ColumnWidth.medium: case ColumnWidth.medium:
return 150.0; return 180.0;
case ColumnWidth.large: case ColumnWidth.large:
return 200.0; return 250.0;
case ColumnWidth.extraLarge: case ColumnWidth.extraLarge:
return 300.0; return 350.0;
} }
} }

View file

@ -5,6 +5,7 @@ import 'data_table_config.dart';
import 'helpers/data_table_utils.dart'; import 'helpers/data_table_utils.dart';
import 'package:hesabix_ui/core/calendar_controller.dart'; import 'package:hesabix_ui/core/calendar_controller.dart';
import 'package:hesabix_ui/core/date_utils.dart'; import 'package:hesabix_ui/core/date_utils.dart';
import 'package:hesabix_ui/widgets/jalali_date_picker.dart';
/// Dialog for column search /// Dialog for column search
class DataTableSearchDialog extends StatefulWidget { class DataTableSearchDialog extends StatefulWidget {
@ -12,8 +13,13 @@ class DataTableSearchDialog extends StatefulWidget {
final String columnLabel; final String columnLabel;
final String searchValue; final String searchValue;
final String searchType; final String searchType;
final ColumnFilterType? filterType;
final List<FilterOption>? filterOptions;
final Function(String value, String type) onApply; final Function(String value, String type) onApply;
final Function(List<String> values)? onApplyMultiSelect;
final Function(DateTime? fromDate, DateTime? toDate)? onApplyDateRange;
final VoidCallback onClear; final VoidCallback onClear;
final CalendarController? calendarController;
const DataTableSearchDialog({ const DataTableSearchDialog({
super.key, super.key,
@ -21,8 +27,13 @@ class DataTableSearchDialog extends StatefulWidget {
required this.columnLabel, required this.columnLabel,
required this.searchValue, required this.searchValue,
required this.searchType, required this.searchType,
this.filterType,
this.filterOptions,
required this.onApply, required this.onApply,
this.onApplyMultiSelect,
this.onApplyDateRange,
required this.onClear, required this.onClear,
this.calendarController,
}); });
@override @override
@ -32,6 +43,9 @@ class DataTableSearchDialog extends StatefulWidget {
class _DataTableSearchDialogState extends State<DataTableSearchDialog> { class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
late TextEditingController _controller; late TextEditingController _controller;
late String _selectedType; late String _selectedType;
Set<String> _selectedValues = <String>{};
DateTime? _fromDate;
DateTime? _toDate;
@override @override
void initState() { void initState() {
@ -54,73 +68,281 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
return AlertDialog( return AlertDialog(
title: Row( title: Row(
children: [ children: [
Icon(Icons.search, color: theme.primaryColor, size: 20), Icon(_getFilterIcon(), color: theme.primaryColor, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(t.searchInColumn(widget.columnLabel)), Text(_getFilterTitle(t)),
], ],
), ),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: _buildFilterContent(t, theme),
// Search type dropdown
DropdownButtonFormField<String>(
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,
),
],
), ),
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<Widget> _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<Widget> _buildTextFilterContent(AppLocalizations t, ThemeData theme) {
return [
// Search type dropdown
DropdownButtonFormField<String>(
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<Widget> _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<Widget> _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<Widget> _buildFilterActions(AppLocalizations t) {
final hasActiveFilter = _hasActiveFilter();
return [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(t.cancel),
),
if (hasActiveFilter)
TextButton( TextButton(
onPressed: () { onPressed: () {
widget.onClear();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text(t.cancel), child: Text(t.clear),
), ),
if (widget.searchValue.isNotEmpty) FilledButton(
TextButton( onPressed: _canApplyFilter() ? _applyFilter : null,
onPressed: () { child: Text(_getApplyButtonText(t)),
widget.onClear(); ),
Navigator.of(context).pop(); ];
}, }
child: Text(t.clear),
), bool _hasActiveFilter() {
FilledButton( switch (widget.filterType) {
onPressed: () { case ColumnFilterType.dateRange:
widget.onApply(_controller.text.trim(), _selectedType); return _fromDate != null || _toDate != null;
Navigator.of(context).pop(); case ColumnFilterType.multiSelect:
}, return _selectedValues.isNotEmpty;
child: Text(t.applyColumnFilter), 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<DataTableDateRangeDialog> {
class ActiveFiltersWidget extends StatelessWidget { class ActiveFiltersWidget extends StatelessWidget {
final Map<String, String> columnSearchValues; final Map<String, String> columnSearchValues;
final Map<String, String> columnSearchTypes; final Map<String, String> columnSearchTypes;
final Map<String, List<String>> columnMultiSelectValues;
final Map<String, DateTime?> columnDateFromValues;
final Map<String, DateTime?> columnDateToValues;
final DateTime? fromDate; final DateTime? fromDate;
final DateTime? toDate; final DateTime? toDate;
final List<DataTableColumn> columns; final List<DataTableColumn> columns;
@ -258,6 +483,9 @@ class ActiveFiltersWidget extends StatelessWidget {
super.key, super.key,
required this.columnSearchValues, required this.columnSearchValues,
required this.columnSearchTypes, required this.columnSearchTypes,
required this.columnMultiSelectValues,
required this.columnDateFromValues,
required this.columnDateToValues,
this.fromDate, this.fromDate,
this.toDate, this.toDate,
required this.columns, required this.columns,
@ -272,6 +500,8 @@ class ActiveFiltersWidget extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final hasFilters = columnSearchValues.isNotEmpty || final hasFilters = columnSearchValues.isNotEmpty ||
columnMultiSelectValues.isNotEmpty ||
columnDateFromValues.isNotEmpty ||
(fromDate != null && toDate != null); (fromDate != null && toDate != null);
if (!hasFilters) return const SizedBox.shrink(); if (!hasFilters) return const SizedBox.shrink();
@ -309,7 +539,7 @@ class ActiveFiltersWidget extends StatelessWidget {
spacing: 6, spacing: 6,
runSpacing: 3, runSpacing: 3,
children: [ children: [
// Column filters // Text search filters
...columnSearchValues.entries.map((entry) { ...columnSearchValues.entries.map((entry) {
final columnName = entry.key; final columnName = entry.key;
final searchValue = entry.value; 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) if (fromDate != null && toDate != null)
Chip( Chip(
label: Text('${t.dateFrom}: ${HesabixDateUtils.formatForDisplay(fromDate!, calendarController?.isJalali ?? false)} - ${t.dateTo}: ${HesabixDateUtils.formatForDisplay(toDate!, calendarController?.isJalali ?? false)}'), label: Text('${t.dateFrom}: ${HesabixDateUtils.formatForDisplay(fromDate!, calendarController?.isJalali ?? false)} - ${t.dateTo}: ${HesabixDateUtils.formatForDisplay(toDate!, calendarController?.isJalali ?? false)}'),

View file

@ -7,7 +7,6 @@ import 'package:dio/dio.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'package:hesabix_ui/core/api_client.dart'; import 'package:hesabix_ui/core/api_client.dart';
import 'package:hesabix_ui/core/calendar_controller.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_config.dart';
import 'data_table_search_dialog.dart'; import 'data_table_search_dialog.dart';
import 'column_settings_dialog.dart'; import 'column_settings_dialog.dart';
@ -34,7 +33,6 @@ class DataTableWidget<T> extends StatefulWidget {
class _DataTableWidgetState<T> extends State<DataTableWidget<T>> { class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
// Data state // Data state
List<T> _items = []; List<T> _items = [];
bool _loading = false;
bool _loadingList = false; bool _loadingList = false;
String? _error; String? _error;
@ -44,17 +42,19 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
int _total = 0; int _total = 0;
int _totalPages = 0; int _totalPages = 0;
// Search and filter state // Search state
final TextEditingController _searchCtrl = TextEditingController(); final TextEditingController _searchCtrl = TextEditingController();
Timer? _searchDebounce; Timer? _searchDebounce;
bool _showFilters = false;
DateTime? _fromDate;
DateTime? _toDate;
// Column search state // Column search state
final Map<String, String> _columnSearchValues = {}; final Map<String, String> _columnSearchValues = {};
final Map<String, String> _columnSearchTypes = {}; final Map<String, String> _columnSearchTypes = {};
final Map<String, TextEditingController> _columnSearchControllers = {}; final Map<String, TextEditingController> _columnSearchControllers = {};
// Enhanced filter state
final Map<String, List<String>> _columnMultiSelectValues = {};
final Map<String, DateTime?> _columnDateFromValues = {};
final Map<String, DateTime?> _columnDateToValues = {};
// Sorting state // Sorting state
String? _sortBy; String? _sortBy;
@ -69,9 +69,13 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
List<DataTableColumn> _visibleColumns = []; List<DataTableColumn> _visibleColumns = [];
bool _isLoadingColumnSettings = false; bool _isLoadingColumnSettings = false;
// Scroll controller for horizontal scrolling
late ScrollController _horizontalScrollController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_horizontalScrollController = ScrollController();
_limit = widget.config.defaultPageSize; _limit = widget.config.defaultPageSize;
_setupSearchListener(); _setupSearchListener();
_loadColumnSettings(); _loadColumnSettings();
@ -82,6 +86,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
void dispose() { void dispose() {
_searchCtrl.dispose(); _searchCtrl.dispose();
_searchDebounce?.cancel(); _searchDebounce?.cancel();
_horizontalScrollController.dispose();
for (var controller in _columnSearchControllers.values) { for (var controller in _columnSearchControllers.values) {
controller.dispose(); controller.dispose();
} }
@ -208,19 +213,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
List<FilterItem> _buildFilters() { List<FilterItem> _buildFilters() {
final filters = <FilterItem>[]; final filters = <FilterItem>[];
// Date range filters // Text search 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
for (var entry in _columnSearchValues.entries) { for (var entry in _columnSearchValues.entries) {
final columnName = entry.key; final columnName = entry.key;
final searchValue = entry.value.trim(); final searchValue = entry.value.trim();
@ -235,10 +228,43 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
} }
} }
// 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; return filters;
} }
void _openColumnSearchDialog(String columnName, String columnLabel) { 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 // Initialize controller if not exists
if (!_columnSearchControllers.containsKey(columnName)) { if (!_columnSearchControllers.containsKey(columnName)) {
_columnSearchControllers[columnName] = TextEditingController( _columnSearchControllers[columnName] = TextEditingController(
@ -256,6 +282,9 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
columnLabel: columnLabel, columnLabel: columnLabel,
searchValue: _columnSearchValues[columnName] ?? '', searchValue: _columnSearchValues[columnName] ?? '',
searchType: _columnSearchTypes[columnName] ?? '*', searchType: _columnSearchTypes[columnName] ?? '*',
filterType: filterType,
filterOptions: filterOptions,
calendarController: widget.calendarController,
onApply: (value, type) { onApply: (value, type) {
setState(() { setState(() {
_columnSearchValues[columnName] = value; _columnSearchValues[columnName] = value;
@ -264,10 +293,28 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
_page = 1; _page = 1;
_fetchData(); _fetchData();
}, },
onApplyMultiSelect: (values) {
setState(() {
_columnMultiSelectValues[columnName] = values;
});
_page = 1;
_fetchData();
},
onApplyDateRange: (fromDate, toDate) {
setState(() {
_columnDateFromValues[columnName] = fromDate;
_columnDateToValues[columnName] = toDate;
});
_page = 1;
_fetchData();
},
onClear: () { onClear: () {
setState(() { setState(() {
_columnSearchValues.remove(columnName); _columnSearchValues.remove(columnName);
_columnSearchTypes.remove(columnName); _columnSearchTypes.remove(columnName);
_columnMultiSelectValues.remove(columnName);
_columnDateFromValues.remove(columnName);
_columnDateToValues.remove(columnName);
_columnSearchControllers[columnName]?.clear(); _columnSearchControllers[columnName]?.clear();
}); });
_page = 1; _page = 1;
@ -279,21 +326,22 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
bool _hasActiveFilters() { bool _hasActiveFilters() {
return _fromDate != null || return _searchCtrl.text.isNotEmpty ||
_toDate != null || _columnSearchValues.isNotEmpty ||
_searchCtrl.text.isNotEmpty || _columnMultiSelectValues.isNotEmpty ||
_columnSearchValues.isNotEmpty; _columnDateFromValues.isNotEmpty;
} }
void _clearAllFilters() { void _clearAllFilters() {
setState(() { setState(() {
_fromDate = null;
_toDate = null;
_searchCtrl.clear(); _searchCtrl.clear();
_sortBy = null; _sortBy = null;
_sortDesc = false; _sortDesc = false;
_columnSearchValues.clear(); _columnSearchValues.clear();
_columnSearchTypes.clear(); _columnSearchTypes.clear();
_columnMultiSelectValues.clear();
_columnDateFromValues.clear();
_columnDateToValues.clear();
_selectedRows.clear(); _selectedRows.clear();
for (var controller in _columnSearchControllers.values) { for (var controller in _columnSearchControllers.values) {
controller.clear(); controller.clear();
@ -302,9 +350,6 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
_page = 1; _page = 1;
_fetchData(); _fetchData();
// Call the callback if provided // Call the callback if provided
if (widget.config.onDateRangeClear != null) {
widget.config.onDateRangeClear!();
}
if (widget.config.onRowSelectionChanged != null) { if (widget.config.onRowSelectionChanged != null) {
widget.config.onRowSelectionChanged!(_selectedRows); widget.config.onRowSelectionChanged!(_selectedRows);
} }
@ -481,21 +526,6 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
} }
}); });
// 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 = { final queryInfo = {
'sort_by': _sortBy, 'sort_by': _sortBy,
@ -692,9 +722,9 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
// Search and Filters // Search
if (widget.config.showSearch || widget.config.showFilters) ...[ if (widget.config.showSearch) ...[
_buildSearchAndFilters(t, theme), _buildSearch(t, theme),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
@ -703,14 +733,20 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
ActiveFiltersWidget( ActiveFiltersWidget(
columnSearchValues: _columnSearchValues, columnSearchValues: _columnSearchValues,
columnSearchTypes: _columnSearchTypes, columnSearchTypes: _columnSearchTypes,
fromDate: _fromDate, columnMultiSelectValues: _columnMultiSelectValues,
toDate: _toDate, columnDateFromValues: _columnDateFromValues,
columnDateToValues: _columnDateToValues,
fromDate: null,
toDate: null,
columns: widget.config.columns, columns: widget.config.columns,
calendarController: widget.calendarController, calendarController: widget.calendarController,
onRemoveColumnFilter: (columnName) { onRemoveColumnFilter: (columnName) {
setState(() { setState(() {
_columnSearchValues.remove(columnName); _columnSearchValues.remove(columnName);
_columnSearchTypes.remove(columnName); _columnSearchTypes.remove(columnName);
_columnMultiSelectValues.remove(columnName);
_columnDateFromValues.remove(columnName);
_columnDateToValues.remove(columnName);
_columnSearchControllers[columnName]?.clear(); _columnSearchControllers[columnName]?.clear();
}); });
_page = 1; _page = 1;
@ -771,22 +807,6 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
], ],
const Spacer(), 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) // Clear filters button (only show when filters are applied)
if (widget.config.showClearFiltersButton && _hasActiveFilters()) ...[ if (widget.config.showClearFiltersButton && _hasActiveFilters()) ...[
@ -822,11 +842,6 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
case 'refresh': case 'refresh':
_fetchData(); _fetchData();
break; break;
case 'filters':
setState(() {
_showFilters = !_showFilters;
});
break;
case 'columnSettings': case 'columnSettings':
_openColumnSettingsDialog(); _openColumnSettingsDialog();
break; break;
@ -844,17 +859,6 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
], ],
), ),
), ),
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) if (widget.config.showColumnSettingsButton && widget.config.enableColumnSettings)
PopupMenuItem( PopupMenuItem(
value: 'columnSettings', value: 'columnSettings',
@ -1151,128 +1155,23 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
); );
} }
Widget _buildSearchAndFilters(AppLocalizations t, ThemeData theme) { Widget _buildSearch(AppLocalizations t, ThemeData theme) {
return Container( return Row(
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(
children: [ children: [
// Main controls row Expanded(
Row( child: TextField(
children: [ controller: _searchCtrl,
if (widget.config.showSearch) ...[ decoration: InputDecoration(
Expanded( prefixIcon: const Icon(Icons.search, size: 18),
child: TextField( hintText: t.searchInNameEmail,
controller: _searchCtrl, border: const OutlineInputBorder(),
decoration: InputDecoration( isDense: true,
prefixIcon: const Icon(Icons.search, size: 18), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
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),
),
),
],
),
],
), ),
), ),
], ),
], ],
), );
);
} }
Widget _buildDataTable(AppLocalizations t, ThemeData theme) { Widget _buildDataTable(AppLocalizations t, ThemeData theme) {
@ -1371,6 +1270,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
size: ColumnSize.S, size: ColumnSize.S,
fixedWidth: 50.0,
)); ));
} }
@ -1385,6 +1285,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
), ),
), ),
size: ColumnSize.S, size: ColumnSize.S,
fixedWidth: 60.0,
)); ));
} }
@ -1408,14 +1309,19 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
enabled: widget.config.enableSorting && column.sortable, enabled: widget.config.enableSorting && column.sortable,
), ),
size: DataTableUtils.getColumnSize(column.width), size: DataTableUtils.getColumnSize(column.width),
fixedWidth: DataTableUtils.getColumnWidth(column.width),
); );
})); }));
return DataTable2( return Scrollbar(
columnSpacing: 12, controller: _horizontalScrollController,
horizontalMargin: 12, thumbVisibility: true,
minWidth: widget.config.minTableWidth ?? 600, child: DataTable2(
columns: columns, columnSpacing: 8,
horizontalMargin: 8,
minWidth: widget.config.minTableWidth ?? 600,
horizontalScrollController: _horizontalScrollController,
columns: columns,
rows: _items.asMap().entries.map((entry) { rows: _items.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
final item = entry.value; final item = entry.value;
@ -1474,6 +1380,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
cells: cells, cells: cells,
); );
}).toList(), }).toList(),
),
); );
} }
@ -1573,57 +1480,71 @@ class _ColumnHeaderWithSearch extends StatelessWidget {
onTap: enabled ? () => onSort(sortBy) : null, onTap: enabled ? () => onSort(sortBy) : null,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row( child: Container(
mainAxisSize: MainAxisSize.min, width: double.infinity,
children: [ child: Row(
Text( mainAxisSize: MainAxisSize.max,
text, mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: theme.textTheme.titleSmall?.copyWith( children: [
fontWeight: FontWeight.w600, Expanded(
color: isActive ? theme.colorScheme.primary : theme.colorScheme.onSurface, child: Row(
), mainAxisSize: MainAxisSize.min,
), children: [
if (enabled) ...[ Flexible(
const SizedBox(width: 4), child: Text(
if (isActive) text,
Icon( style: theme.textTheme.titleSmall?.copyWith(
sortDesc ? Icons.arrow_downward : Icons.arrow_upward, fontWeight: FontWeight.w600,
size: 16, color: isActive ? theme.colorScheme.primary : theme.colorScheme.onSurface,
color: theme.colorScheme.primary, ),
) overflow: TextOverflow.ellipsis,
else ),
Icon( ),
Icons.unfold_more, if (enabled) ...[
size: 16, const SizedBox(width: 4),
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6), 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),
),
),
),
],
), ),
), ),
); );

View file

@ -182,6 +182,42 @@ class DataTableUtils {
); );
} }
/// Create filter item for multi-select
static FilterItem createMultiSelectFilter(
String field,
List<String> values,
) {
return FilterItem(
property: field,
operator: 'in',
value: values,
);
}
/// Create filter item for date range
static List<FilterItem> 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 /// Get column label by key
static String getColumnLabel(String key, List<DataTableColumn> columns) { static String getColumnLabel(String key, List<DataTableColumn> columns) {
final column = columns.firstWhere( final column = columns.firstWhere(
@ -191,6 +227,34 @@ class DataTableUtils {
return column.label; return column.label;
} }
/// Get column filter type
static ColumnFilterType? getColumnFilterType(String key, List<DataTableColumn> columns) {
final column = columns.firstWhere(
(col) => col.key == key,
orElse: () => TextColumn(key, key),
);
return column.filterType;
}
/// Get column filter options
static List<FilterOption>? getColumnFilterOptions(String key, List<DataTableColumn> 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<DataTableColumn> columns) {
return getColumnFilterType(key, columns) == ColumnFilterType.dateRange;
}
/// Check if column has multi-select filter
static bool isMultiSelectFilter(String key, List<DataTableColumn> columns) {
return getColumnFilterType(key, columns) == ColumnFilterType.multiSelect;
}
/// Check if column is searchable /// Check if column is searchable
static bool isColumnSearchable(String key, List<DataTableColumn> columns) { static bool isColumnSearchable(String key, List<DataTableColumn> columns) {
final column = columns.firstWhere( final column = columns.firstWhere(