progress in datatable
This commit is contained in:
parent
6a07d3ede5
commit
4e07795467
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": "اپراتور",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 => 'پیام داخلی';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'],
|
||||||
|
|
|
||||||
|
|
@ -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**: سرور باید انواع مختلف فیلتر را پشتیبانی کند
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,14 +68,54 @@ 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),
|
||||||
|
),
|
||||||
|
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
|
// Search type dropdown
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedType,
|
value: _selectedType,
|
||||||
|
|
@ -95,16 +149,136 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
),
|
),
|
||||||
autofocus: 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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
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(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(t.cancel),
|
child: Text(t.cancel),
|
||||||
),
|
),
|
||||||
if (widget.searchValue.isNotEmpty)
|
if (hasActiveFilter)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.onClear();
|
widget.onClear();
|
||||||
|
|
@ -113,14 +287,62 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
child: Text(t.clear),
|
child: Text(t.clear),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: _canApplyFilter() ? _applyFilter : null,
|
||||||
widget.onApply(_controller.text.trim(), _selectedType);
|
child: Text(_getApplyButtonText(t)),
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: Text(t.applyColumnFilter),
|
|
||||||
),
|
),
|
||||||
],
|
];
|
||||||
);
|
}
|
||||||
|
|
||||||
|
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<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)}'),
|
||||||
|
|
|
||||||
|
|
@ -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,18 +42,20 @@ 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;
|
||||||
bool _sortDesc = false;
|
bool _sortDesc = false;
|
||||||
|
|
@ -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,20 +1155,9 @@ 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
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (widget.config.showSearch) ...[
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchCtrl,
|
controller: _searchCtrl,
|
||||||
|
|
@ -1177,101 +1170,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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,13 +1309,18 @@ 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,
|
||||||
|
child: DataTable2(
|
||||||
|
columnSpacing: 8,
|
||||||
|
horizontalMargin: 8,
|
||||||
minWidth: widget.config.minTableWidth ?? 600,
|
minWidth: widget.config.minTableWidth ?? 600,
|
||||||
|
horizontalScrollController: _horizontalScrollController,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
rows: _items.asMap().entries.map((entry) {
|
rows: _items.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
|
|
@ -1474,6 +1380,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
cells: cells,
|
cells: cells,
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1573,15 +1480,25 @@ 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: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Flexible(
|
||||||
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: isActive ? theme.colorScheme.primary : theme.colorScheme.onSurface,
|
color: isActive ? theme.colorScheme.primary : theme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (enabled) ...[
|
if (enabled) ...[
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
|
|
@ -1598,6 +1515,9 @@ class _ColumnHeaderWithSearch extends StatelessWidget {
|
||||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
// Search button
|
// Search button
|
||||||
InkWell(
|
InkWell(
|
||||||
|
|
@ -1626,6 +1546,7 @@ class _ColumnHeaderWithSearch extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue