progress in datatable
This commit is contained in:
parent
6a07d3ede5
commit
4e07795467
|
|
@ -244,6 +244,16 @@
|
|||
"allTickets": "All Tickets",
|
||||
"assignTicket": "Assign Ticket",
|
||||
"changeStatus": "Change Status",
|
||||
"multiSelectFilter": "Multi-Select Filter",
|
||||
"selectFilterOptions": "Select Filter Options",
|
||||
"noFilterOptionsAvailable": "No filter options available",
|
||||
"marketing": "Marketing",
|
||||
"marketingDescription": "Manage referrals and marketing codes",
|
||||
"referralCode": "Referral Code",
|
||||
"referralList": "Referral List",
|
||||
"today": "Today",
|
||||
"thisMonth": "This Month",
|
||||
"total": "Total",
|
||||
"internalMessage": "Internal Message",
|
||||
"user": "User",
|
||||
"operator": "Operator",
|
||||
|
|
|
|||
|
|
@ -243,6 +243,16 @@
|
|||
"allTickets": "تمام تیکتها",
|
||||
"assignTicket": "تخصیص تیکت",
|
||||
"changeStatus": "تغییر وضعیت",
|
||||
"multiSelectFilter": "فیلتر چندتایی",
|
||||
"selectFilterOptions": "انتخاب گزینههای فیلتر",
|
||||
"noFilterOptionsAvailable": "هیچ گزینه فیلتری در دسترس نیست",
|
||||
"marketing": "بازاریابی",
|
||||
"marketingDescription": "مدیریت معرفیها و کدهای بازاریابی",
|
||||
"referralCode": "کد معرفی",
|
||||
"referralList": "لیست معرفیها",
|
||||
"today": "امروز",
|
||||
"thisMonth": "این ماه",
|
||||
"total": "کل",
|
||||
"internalMessage": "پیام داخلی",
|
||||
"user": "کاربر",
|
||||
"operator": "اپراتور",
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @thisMonth.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This month'**
|
||||
/// **'This Month'**
|
||||
String get thisMonth;
|
||||
|
||||
/// No description provided for @total.
|
||||
|
|
@ -1448,6 +1448,36 @@ abstract class AppLocalizations {
|
|||
/// **'Change Status'**
|
||||
String get changeStatus;
|
||||
|
||||
/// No description provided for @multiSelectFilter.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Multi-Select Filter'**
|
||||
String get multiSelectFilter;
|
||||
|
||||
/// No description provided for @selectFilterOptions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select Filter Options'**
|
||||
String get selectFilterOptions;
|
||||
|
||||
/// No description provided for @noFilterOptionsAvailable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No filter options available'**
|
||||
String get noFilterOptionsAvailable;
|
||||
|
||||
/// No description provided for @marketingDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manage referrals and marketing codes'**
|
||||
String get marketingDescription;
|
||||
|
||||
/// No description provided for @referralCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Referral Code'**
|
||||
String get referralCode;
|
||||
|
||||
/// No description provided for @internalMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get today => 'Today';
|
||||
|
||||
@override
|
||||
String get thisMonth => 'This month';
|
||||
String get thisMonth => 'This Month';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
|
@ -697,6 +697,21 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get changeStatus => 'Change Status';
|
||||
|
||||
@override
|
||||
String get multiSelectFilter => 'Multi-Select Filter';
|
||||
|
||||
@override
|
||||
String get selectFilterOptions => 'Select Filter Options';
|
||||
|
||||
@override
|
||||
String get noFilterOptionsAvailable => 'No filter options available';
|
||||
|
||||
@override
|
||||
String get marketingDescription => 'Manage referrals and marketing codes';
|
||||
|
||||
@override
|
||||
String get referralCode => 'Referral Code';
|
||||
|
||||
@override
|
||||
String get internalMessage => 'Internal Message';
|
||||
|
||||
|
|
|
|||
|
|
@ -695,6 +695,21 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||
@override
|
||||
String get changeStatus => 'تغییر وضعیت';
|
||||
|
||||
@override
|
||||
String get multiSelectFilter => 'فیلتر چندتایی';
|
||||
|
||||
@override
|
||||
String get selectFilterOptions => 'انتخاب گزینههای فیلتر';
|
||||
|
||||
@override
|
||||
String get noFilterOptionsAvailable => 'هیچ گزینه فیلتری در دسترس نیست';
|
||||
|
||||
@override
|
||||
String get marketingDescription => 'مدیریت معرفیها و کدهای بازاریابی';
|
||||
|
||||
@override
|
||||
String get referralCode => 'کد معرفی';
|
||||
|
||||
@override
|
||||
String get internalMessage => 'پیام داخلی';
|
||||
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ class _MarketingPageState extends State<MarketingPage> {
|
|||
searchable: true,
|
||||
width: ColumnWidth.medium,
|
||||
showTime: false,
|
||||
filterType: ColumnFilterType.dateRange,
|
||||
),
|
||||
],
|
||||
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,
|
||||
}
|
||||
|
||||
/// Types of column filters
|
||||
enum ColumnFilterType {
|
||||
text, // Text filter (default)
|
||||
dateRange, // Date range filter
|
||||
multiSelect, // Multi-select filter with checkboxes
|
||||
}
|
||||
|
||||
/// Filter option for multi-select filters
|
||||
class FilterOption {
|
||||
final String value; // Value for API
|
||||
final String label; // Display label
|
||||
final String? description; // Additional description
|
||||
final IconData? icon; // Icon
|
||||
final Color? color; // Icon/text color
|
||||
|
||||
const FilterOption({
|
||||
required this.value,
|
||||
required this.label,
|
||||
this.description,
|
||||
this.icon,
|
||||
this.color,
|
||||
});
|
||||
}
|
||||
|
||||
/// Base class for all column types
|
||||
abstract class DataTableColumn {
|
||||
final String key;
|
||||
|
|
@ -17,6 +41,8 @@ abstract class DataTableColumn {
|
|||
final bool searchable;
|
||||
final ColumnWidth width;
|
||||
final String? tooltip;
|
||||
final ColumnFilterType? filterType;
|
||||
final List<FilterOption>? filterOptions;
|
||||
|
||||
const DataTableColumn({
|
||||
required this.key,
|
||||
|
|
@ -25,6 +51,8 @@ abstract class DataTableColumn {
|
|||
this.searchable = true,
|
||||
this.width = ColumnWidth.medium,
|
||||
this.tooltip,
|
||||
this.filterType,
|
||||
this.filterOptions,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +70,8 @@ class TextColumn extends DataTableColumn {
|
|||
super.searchable = true,
|
||||
super.width = ColumnWidth.medium,
|
||||
super.tooltip,
|
||||
super.filterType,
|
||||
super.filterOptions,
|
||||
this.formatter,
|
||||
this.textAlign,
|
||||
this.maxLines,
|
||||
|
|
@ -64,6 +94,8 @@ class NumberColumn extends DataTableColumn {
|
|||
super.searchable = true,
|
||||
super.width = ColumnWidth.medium,
|
||||
super.tooltip,
|
||||
super.filterType,
|
||||
super.filterOptions,
|
||||
this.formatter,
|
||||
this.textAlign = TextAlign.end,
|
||||
this.decimalPlaces,
|
||||
|
|
@ -86,6 +118,8 @@ class DateColumn extends DataTableColumn {
|
|||
super.searchable = true,
|
||||
super.width = ColumnWidth.medium,
|
||||
super.tooltip,
|
||||
super.filterType,
|
||||
super.filterOptions,
|
||||
this.formatter,
|
||||
this.textAlign = TextAlign.center,
|
||||
this.showTime = false,
|
||||
|
|
@ -105,6 +139,8 @@ class ActionColumn extends DataTableColumn {
|
|||
super.searchable = false,
|
||||
super.width = ColumnWidth.small,
|
||||
super.tooltip,
|
||||
super.filterType,
|
||||
super.filterOptions,
|
||||
required this.actions,
|
||||
this.showOnHover = true,
|
||||
}) : super(key: key, label: label);
|
||||
|
|
@ -122,6 +158,8 @@ class CustomColumn extends DataTableColumn {
|
|||
super.searchable = true,
|
||||
super.width = ColumnWidth.medium,
|
||||
super.tooltip,
|
||||
super.filterType,
|
||||
super.filterOptions,
|
||||
this.builder,
|
||||
this.formatter,
|
||||
}) : super(key: key, label: label);
|
||||
|
|
@ -227,14 +265,14 @@ class DataTableConfig<T> {
|
|||
this.title,
|
||||
this.subtitle,
|
||||
this.showSearch = true,
|
||||
this.showFilters = true,
|
||||
this.showFilters = false,
|
||||
this.showPagination = true,
|
||||
this.showColumnSearch = true,
|
||||
this.defaultPageSize = 20,
|
||||
this.pageSizeOptions = const [10, 20, 50, 100],
|
||||
this.enableSorting = true,
|
||||
this.enableGlobalSearch = true,
|
||||
this.enableDateRangeFilter = true,
|
||||
this.enableDateRangeFilter = false,
|
||||
this.onRowTap,
|
||||
this.onRowDoubleTap,
|
||||
this.customRowBuilder,
|
||||
|
|
@ -279,20 +317,20 @@ class DataTableConfig<T> {
|
|||
this.initialColumnSettings,
|
||||
this.onColumnSettingsChanged,
|
||||
this.customHeaderActions,
|
||||
this.showFiltersButton = true,
|
||||
this.showFiltersButton = false,
|
||||
});
|
||||
|
||||
/// Get column width as double
|
||||
double getColumnWidth(ColumnWidth width) {
|
||||
switch (width) {
|
||||
case ColumnWidth.small:
|
||||
return 100.0;
|
||||
return 120.0;
|
||||
case ColumnWidth.medium:
|
||||
return 150.0;
|
||||
return 180.0;
|
||||
case ColumnWidth.large:
|
||||
return 200.0;
|
||||
return 250.0;
|
||||
case ColumnWidth.extraLarge:
|
||||
return 300.0;
|
||||
return 350.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'data_table_config.dart';
|
|||
import 'helpers/data_table_utils.dart';
|
||||
import 'package:hesabix_ui/core/calendar_controller.dart';
|
||||
import 'package:hesabix_ui/core/date_utils.dart';
|
||||
import 'package:hesabix_ui/widgets/jalali_date_picker.dart';
|
||||
|
||||
/// Dialog for column search
|
||||
class DataTableSearchDialog extends StatefulWidget {
|
||||
|
|
@ -12,8 +13,13 @@ class DataTableSearchDialog extends StatefulWidget {
|
|||
final String columnLabel;
|
||||
final String searchValue;
|
||||
final String searchType;
|
||||
final ColumnFilterType? filterType;
|
||||
final List<FilterOption>? filterOptions;
|
||||
final Function(String value, String type) onApply;
|
||||
final Function(List<String> values)? onApplyMultiSelect;
|
||||
final Function(DateTime? fromDate, DateTime? toDate)? onApplyDateRange;
|
||||
final VoidCallback onClear;
|
||||
final CalendarController? calendarController;
|
||||
|
||||
const DataTableSearchDialog({
|
||||
super.key,
|
||||
|
|
@ -21,8 +27,13 @@ class DataTableSearchDialog extends StatefulWidget {
|
|||
required this.columnLabel,
|
||||
required this.searchValue,
|
||||
required this.searchType,
|
||||
this.filterType,
|
||||
this.filterOptions,
|
||||
required this.onApply,
|
||||
this.onApplyMultiSelect,
|
||||
this.onApplyDateRange,
|
||||
required this.onClear,
|
||||
this.calendarController,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -32,6 +43,9 @@ class DataTableSearchDialog extends StatefulWidget {
|
|||
class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||
late TextEditingController _controller;
|
||||
late String _selectedType;
|
||||
Set<String> _selectedValues = <String>{};
|
||||
DateTime? _fromDate;
|
||||
DateTime? _toDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -54,73 +68,281 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
|||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.search, color: theme.primaryColor, size: 20),
|
||||
Icon(_getFilterIcon(), color: theme.primaryColor, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(t.searchInColumn(widget.columnLabel)),
|
||||
Text(_getFilterTitle(t)),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Search type dropdown
|
||||
DropdownButtonFormField<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,
|
||||
),
|
||||
],
|
||||
children: _buildFilterContent(t, theme),
|
||||
),
|
||||
actions: [
|
||||
actions: _buildFilterActions(t),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getFilterIcon() {
|
||||
switch (widget.filterType) {
|
||||
case ColumnFilterType.dateRange:
|
||||
return Icons.date_range;
|
||||
case ColumnFilterType.multiSelect:
|
||||
return Icons.checklist;
|
||||
default:
|
||||
return Icons.search;
|
||||
}
|
||||
}
|
||||
|
||||
String _getFilterTitle(AppLocalizations t) {
|
||||
switch (widget.filterType) {
|
||||
case ColumnFilterType.dateRange:
|
||||
return t.dateRangeFilter;
|
||||
case ColumnFilterType.multiSelect:
|
||||
return t.multiSelectFilter;
|
||||
default:
|
||||
return t.searchInColumn(widget.columnLabel);
|
||||
}
|
||||
}
|
||||
|
||||
List<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(
|
||||
onPressed: () {
|
||||
widget.onClear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.cancel),
|
||||
child: Text(t.clear),
|
||||
),
|
||||
if (widget.searchValue.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.onClear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.clear),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
widget.onApply(_controller.text.trim(), _selectedType);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.applyColumnFilter),
|
||||
),
|
||||
],
|
||||
);
|
||||
FilledButton(
|
||||
onPressed: _canApplyFilter() ? _applyFilter : null,
|
||||
child: Text(_getApplyButtonText(t)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
bool _hasActiveFilter() {
|
||||
switch (widget.filterType) {
|
||||
case ColumnFilterType.dateRange:
|
||||
return _fromDate != null || _toDate != null;
|
||||
case ColumnFilterType.multiSelect:
|
||||
return _selectedValues.isNotEmpty;
|
||||
default:
|
||||
return widget.searchValue.isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
bool _canApplyFilter() {
|
||||
switch (widget.filterType) {
|
||||
case ColumnFilterType.dateRange:
|
||||
return _fromDate != null && _toDate != null;
|
||||
case ColumnFilterType.multiSelect:
|
||||
return _selectedValues.isNotEmpty;
|
||||
default:
|
||||
return _controller.text.trim().isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
String _getApplyButtonText(AppLocalizations t) {
|
||||
switch (widget.filterType) {
|
||||
case ColumnFilterType.dateRange:
|
||||
return t.applyFilter;
|
||||
case ColumnFilterType.multiSelect:
|
||||
return t.applyFilter;
|
||||
default:
|
||||
return t.applyColumnFilter;
|
||||
}
|
||||
}
|
||||
|
||||
void _applyFilter() {
|
||||
switch (widget.filterType) {
|
||||
case ColumnFilterType.dateRange:
|
||||
if (widget.onApplyDateRange != null) {
|
||||
widget.onApplyDateRange!(_fromDate, _toDate);
|
||||
}
|
||||
break;
|
||||
case ColumnFilterType.multiSelect:
|
||||
if (widget.onApplyMultiSelect != null) {
|
||||
widget.onApplyMultiSelect!(_selectedValues.toList());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
widget.onApply(_controller.text.trim(), _selectedType);
|
||||
break;
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +469,9 @@ class _DataTableDateRangeDialogState extends State<DataTableDateRangeDialog> {
|
|||
class ActiveFiltersWidget extends StatelessWidget {
|
||||
final Map<String, String> columnSearchValues;
|
||||
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? toDate;
|
||||
final List<DataTableColumn> columns;
|
||||
|
|
@ -258,6 +483,9 @@ class ActiveFiltersWidget extends StatelessWidget {
|
|||
super.key,
|
||||
required this.columnSearchValues,
|
||||
required this.columnSearchTypes,
|
||||
required this.columnMultiSelectValues,
|
||||
required this.columnDateFromValues,
|
||||
required this.columnDateToValues,
|
||||
this.fromDate,
|
||||
this.toDate,
|
||||
required this.columns,
|
||||
|
|
@ -272,6 +500,8 @@ class ActiveFiltersWidget extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
|
||||
final hasFilters = columnSearchValues.isNotEmpty ||
|
||||
columnMultiSelectValues.isNotEmpty ||
|
||||
columnDateFromValues.isNotEmpty ||
|
||||
(fromDate != null && toDate != null);
|
||||
|
||||
if (!hasFilters) return const SizedBox.shrink();
|
||||
|
|
@ -309,7 +539,7 @@ class ActiveFiltersWidget extends StatelessWidget {
|
|||
spacing: 6,
|
||||
runSpacing: 3,
|
||||
children: [
|
||||
// Column filters
|
||||
// Text search filters
|
||||
...columnSearchValues.entries.map((entry) {
|
||||
final columnName = entry.key;
|
||||
final searchValue = entry.value;
|
||||
|
|
@ -330,7 +560,64 @@ class ActiveFiltersWidget extends StatelessWidget {
|
|||
);
|
||||
}),
|
||||
|
||||
// Date range filter
|
||||
// Multi-select filters
|
||||
...columnMultiSelectValues.entries.map((entry) {
|
||||
final columnName = entry.key;
|
||||
final selectedValues = entry.value;
|
||||
final columnLabel = DataTableUtils.getColumnLabel(columnName, columns);
|
||||
final filterOptions = DataTableUtils.getColumnFilterOptions(columnName, columns);
|
||||
|
||||
String displayText = '$columnLabel: ';
|
||||
if (filterOptions != null) {
|
||||
final selectedLabels = selectedValues.map((value) {
|
||||
final option = filterOptions.firstWhere(
|
||||
(opt) => opt.value == value,
|
||||
orElse: () => FilterOption(value: value, label: value),
|
||||
);
|
||||
return option.label;
|
||||
}).join(', ');
|
||||
displayText += selectedLabels;
|
||||
} else {
|
||||
displayText += selectedValues.join(', ');
|
||||
}
|
||||
|
||||
return Chip(
|
||||
label: Text(displayText),
|
||||
deleteIcon: const Icon(Icons.close, size: 16),
|
||||
onDeleted: () => onRemoveColumnFilter(columnName),
|
||||
backgroundColor: theme.primaryColor.withValues(alpha: 0.1),
|
||||
deleteIconColor: theme.primaryColor,
|
||||
labelStyle: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// Date range filters
|
||||
...columnDateFromValues.entries.map((entry) {
|
||||
final columnName = entry.key;
|
||||
final fromDate = entry.value;
|
||||
final toDate = columnDateToValues[columnName];
|
||||
final columnLabel = DataTableUtils.getColumnLabel(columnName, columns);
|
||||
|
||||
if (fromDate != null && toDate != null) {
|
||||
return Chip(
|
||||
label: Text('$columnLabel: ${HesabixDateUtils.formatForDisplay(fromDate, calendarController?.isJalali ?? false)} - ${HesabixDateUtils.formatForDisplay(toDate, calendarController?.isJalali ?? false)}'),
|
||||
deleteIcon: const Icon(Icons.close, size: 16),
|
||||
onDeleted: () => onRemoveColumnFilter(columnName),
|
||||
backgroundColor: theme.primaryColor.withValues(alpha: 0.1),
|
||||
deleteIconColor: theme.primaryColor,
|
||||
labelStyle: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
|
||||
// Legacy date range filter
|
||||
if (fromDate != null && toDate != null)
|
||||
Chip(
|
||||
label: Text('${t.dateFrom}: ${HesabixDateUtils.formatForDisplay(fromDate!, calendarController?.isJalali ?? false)} - ${t.dateTo}: ${HesabixDateUtils.formatForDisplay(toDate!, calendarController?.isJalali ?? false)}'),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:dio/dio.dart';
|
|||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||
import 'package:hesabix_ui/core/api_client.dart';
|
||||
import 'package:hesabix_ui/core/calendar_controller.dart';
|
||||
import 'package:hesabix_ui/widgets/date_input_field.dart';
|
||||
import 'data_table_config.dart';
|
||||
import 'data_table_search_dialog.dart';
|
||||
import 'column_settings_dialog.dart';
|
||||
|
|
@ -34,7 +33,6 @@ class DataTableWidget<T> extends StatefulWidget {
|
|||
class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||
// Data state
|
||||
List<T> _items = [];
|
||||
bool _loading = false;
|
||||
bool _loadingList = false;
|
||||
String? _error;
|
||||
|
||||
|
|
@ -44,18 +42,20 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
int _total = 0;
|
||||
int _totalPages = 0;
|
||||
|
||||
// Search and filter state
|
||||
// Search state
|
||||
final TextEditingController _searchCtrl = TextEditingController();
|
||||
Timer? _searchDebounce;
|
||||
bool _showFilters = false;
|
||||
DateTime? _fromDate;
|
||||
DateTime? _toDate;
|
||||
|
||||
// Column search state
|
||||
final Map<String, String> _columnSearchValues = {};
|
||||
final Map<String, String> _columnSearchTypes = {};
|
||||
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
|
||||
String? _sortBy;
|
||||
bool _sortDesc = false;
|
||||
|
|
@ -69,9 +69,13 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
List<DataTableColumn> _visibleColumns = [];
|
||||
bool _isLoadingColumnSettings = false;
|
||||
|
||||
// Scroll controller for horizontal scrolling
|
||||
late ScrollController _horizontalScrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_horizontalScrollController = ScrollController();
|
||||
_limit = widget.config.defaultPageSize;
|
||||
_setupSearchListener();
|
||||
_loadColumnSettings();
|
||||
|
|
@ -82,6 +86,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
void dispose() {
|
||||
_searchCtrl.dispose();
|
||||
_searchDebounce?.cancel();
|
||||
_horizontalScrollController.dispose();
|
||||
for (var controller in _columnSearchControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
|
|
@ -208,19 +213,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
List<FilterItem> _buildFilters() {
|
||||
final filters = <FilterItem>[];
|
||||
|
||||
// Date range filters
|
||||
if (widget.config.enableDateRangeFilter &&
|
||||
widget.config.dateRangeField != null &&
|
||||
_fromDate != null &&
|
||||
_toDate != null) {
|
||||
filters.addAll(DataTableUtils.createDateRangeFilters(
|
||||
widget.config.dateRangeField!,
|
||||
_fromDate!,
|
||||
_toDate!,
|
||||
));
|
||||
}
|
||||
|
||||
// Column search filters
|
||||
// Text search filters
|
||||
for (var entry in _columnSearchValues.entries) {
|
||||
final columnName = entry.key;
|
||||
final searchValue = entry.value.trim();
|
||||
|
|
@ -235,10 +228,43 @@ class _DataTableWidgetState<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;
|
||||
}
|
||||
|
||||
void _openColumnSearchDialog(String columnName, String columnLabel) {
|
||||
// Get column configuration
|
||||
final column = widget.config.getColumnByKey(columnName);
|
||||
final filterType = column?.filterType;
|
||||
final filterOptions = column?.filterOptions;
|
||||
|
||||
// Initialize controller if not exists
|
||||
if (!_columnSearchControllers.containsKey(columnName)) {
|
||||
_columnSearchControllers[columnName] = TextEditingController(
|
||||
|
|
@ -256,6 +282,9 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
columnLabel: columnLabel,
|
||||
searchValue: _columnSearchValues[columnName] ?? '',
|
||||
searchType: _columnSearchTypes[columnName] ?? '*',
|
||||
filterType: filterType,
|
||||
filterOptions: filterOptions,
|
||||
calendarController: widget.calendarController,
|
||||
onApply: (value, type) {
|
||||
setState(() {
|
||||
_columnSearchValues[columnName] = value;
|
||||
|
|
@ -264,10 +293,28 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
_page = 1;
|
||||
_fetchData();
|
||||
},
|
||||
onApplyMultiSelect: (values) {
|
||||
setState(() {
|
||||
_columnMultiSelectValues[columnName] = values;
|
||||
});
|
||||
_page = 1;
|
||||
_fetchData();
|
||||
},
|
||||
onApplyDateRange: (fromDate, toDate) {
|
||||
setState(() {
|
||||
_columnDateFromValues[columnName] = fromDate;
|
||||
_columnDateToValues[columnName] = toDate;
|
||||
});
|
||||
_page = 1;
|
||||
_fetchData();
|
||||
},
|
||||
onClear: () {
|
||||
setState(() {
|
||||
_columnSearchValues.remove(columnName);
|
||||
_columnSearchTypes.remove(columnName);
|
||||
_columnMultiSelectValues.remove(columnName);
|
||||
_columnDateFromValues.remove(columnName);
|
||||
_columnDateToValues.remove(columnName);
|
||||
_columnSearchControllers[columnName]?.clear();
|
||||
});
|
||||
_page = 1;
|
||||
|
|
@ -279,21 +326,22 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
|
||||
|
||||
bool _hasActiveFilters() {
|
||||
return _fromDate != null ||
|
||||
_toDate != null ||
|
||||
_searchCtrl.text.isNotEmpty ||
|
||||
_columnSearchValues.isNotEmpty;
|
||||
return _searchCtrl.text.isNotEmpty ||
|
||||
_columnSearchValues.isNotEmpty ||
|
||||
_columnMultiSelectValues.isNotEmpty ||
|
||||
_columnDateFromValues.isNotEmpty;
|
||||
}
|
||||
|
||||
void _clearAllFilters() {
|
||||
setState(() {
|
||||
_fromDate = null;
|
||||
_toDate = null;
|
||||
_searchCtrl.clear();
|
||||
_sortBy = null;
|
||||
_sortDesc = false;
|
||||
_columnSearchValues.clear();
|
||||
_columnSearchTypes.clear();
|
||||
_columnMultiSelectValues.clear();
|
||||
_columnDateFromValues.clear();
|
||||
_columnDateToValues.clear();
|
||||
_selectedRows.clear();
|
||||
for (var controller in _columnSearchControllers.values) {
|
||||
controller.clear();
|
||||
|
|
@ -302,9 +350,6 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
_page = 1;
|
||||
_fetchData();
|
||||
// Call the callback if provided
|
||||
if (widget.config.onDateRangeClear != null) {
|
||||
widget.config.onDateRangeClear!();
|
||||
}
|
||||
if (widget.config.onRowSelectionChanged != null) {
|
||||
widget.config.onRowSelectionChanged!(_selectedRows);
|
||||
}
|
||||
|
|
@ -481,21 +526,6 @@ class _DataTableWidgetState<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 = {
|
||||
'sort_by': _sortBy,
|
||||
|
|
@ -692,9 +722,9 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Search and Filters
|
||||
if (widget.config.showSearch || widget.config.showFilters) ...[
|
||||
_buildSearchAndFilters(t, theme),
|
||||
// Search
|
||||
if (widget.config.showSearch) ...[
|
||||
_buildSearch(t, theme),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
|
|
@ -703,14 +733,20 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
ActiveFiltersWidget(
|
||||
columnSearchValues: _columnSearchValues,
|
||||
columnSearchTypes: _columnSearchTypes,
|
||||
fromDate: _fromDate,
|
||||
toDate: _toDate,
|
||||
columnMultiSelectValues: _columnMultiSelectValues,
|
||||
columnDateFromValues: _columnDateFromValues,
|
||||
columnDateToValues: _columnDateToValues,
|
||||
fromDate: null,
|
||||
toDate: null,
|
||||
columns: widget.config.columns,
|
||||
calendarController: widget.calendarController,
|
||||
onRemoveColumnFilter: (columnName) {
|
||||
setState(() {
|
||||
_columnSearchValues.remove(columnName);
|
||||
_columnSearchTypes.remove(columnName);
|
||||
_columnMultiSelectValues.remove(columnName);
|
||||
_columnDateFromValues.remove(columnName);
|
||||
_columnDateToValues.remove(columnName);
|
||||
_columnSearchControllers[columnName]?.clear();
|
||||
});
|
||||
_page = 1;
|
||||
|
|
@ -771,22 +807,6 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
],
|
||||
const Spacer(),
|
||||
|
||||
// Filter buttons
|
||||
if (widget.config.showFilters && widget.config.showFiltersButton) ...[
|
||||
Tooltip(
|
||||
message: _showFilters ? t.hideFilters : t.showFilters,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showFilters = !_showFilters;
|
||||
});
|
||||
},
|
||||
icon: Icon(_showFilters ? Icons.filter_list_off : Icons.filter_list),
|
||||
tooltip: _showFilters ? t.hideFilters : t.showFilters,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
|
||||
// Clear filters button (only show when filters are applied)
|
||||
if (widget.config.showClearFiltersButton && _hasActiveFilters()) ...[
|
||||
|
|
@ -822,11 +842,6 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
case 'refresh':
|
||||
_fetchData();
|
||||
break;
|
||||
case 'filters':
|
||||
setState(() {
|
||||
_showFilters = !_showFilters;
|
||||
});
|
||||
break;
|
||||
case 'columnSettings':
|
||||
_openColumnSettingsDialog();
|
||||
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)
|
||||
PopupMenuItem(
|
||||
value: 'columnSettings',
|
||||
|
|
@ -1151,128 +1155,23 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchAndFilters(AppLocalizations t, ThemeData theme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: theme.dividerColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
Widget _buildSearch(AppLocalizations t, ThemeData theme) {
|
||||
return Row(
|
||||
children: [
|
||||
// Main controls row
|
||||
Row(
|
||||
children: [
|
||||
if (widget.config.showSearch) ...[
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.search, size: 18),
|
||||
hintText: t.searchInNameEmail,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
// Date range filters (if enabled and expanded)
|
||||
if (widget.config.showFilters &&
|
||||
widget.config.enableDateRangeFilter &&
|
||||
widget.config.dateRangeField != null &&
|
||||
_showFilters) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: theme.dividerColor.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.date_range, color: theme.primaryColor, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
t.dateRangeFilter,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: widget.calendarController != null ? DateInputField(
|
||||
value: _fromDate,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_fromDate = date;
|
||||
});
|
||||
},
|
||||
labelText: t.dateFrom,
|
||||
calendarController: widget.calendarController!,
|
||||
enabled: !_loading,
|
||||
) : const SizedBox.shrink(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: widget.calendarController != null ? DateInputField(
|
||||
value: _toDate,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_toDate = date;
|
||||
});
|
||||
},
|
||||
labelText: t.dateTo,
|
||||
calendarController: widget.calendarController!,
|
||||
enabled: !_loading,
|
||||
) : const SizedBox.shrink(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton.icon(
|
||||
onPressed: _loading || _fromDate == null || _toDate == null ? null : () {
|
||||
_page = 1;
|
||||
_fetchData();
|
||||
// Call the callback if provided
|
||||
if (widget.config.onDateRangeApply != null) {
|
||||
widget.config.onDateRangeApply!(_fromDate, _toDate);
|
||||
}
|
||||
},
|
||||
icon: _loading
|
||||
? const SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.check, size: 16),
|
||||
label: Text(t.applyFilter),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
minimumSize: const Size(0, 32),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.search, size: 18),
|
||||
hintText: t.searchInNameEmail,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataTable(AppLocalizations t, ThemeData theme) {
|
||||
|
|
@ -1371,6 +1270,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
)
|
||||
: const SizedBox.shrink(),
|
||||
size: ColumnSize.S,
|
||||
fixedWidth: 50.0,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -1385,6 +1285,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
),
|
||||
),
|
||||
size: ColumnSize.S,
|
||||
fixedWidth: 60.0,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -1408,14 +1309,19 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
enabled: widget.config.enableSorting && column.sortable,
|
||||
),
|
||||
size: DataTableUtils.getColumnSize(column.width),
|
||||
fixedWidth: DataTableUtils.getColumnWidth(column.width),
|
||||
);
|
||||
}));
|
||||
|
||||
return DataTable2(
|
||||
columnSpacing: 12,
|
||||
horizontalMargin: 12,
|
||||
minWidth: widget.config.minTableWidth ?? 600,
|
||||
columns: columns,
|
||||
return Scrollbar(
|
||||
controller: _horizontalScrollController,
|
||||
thumbVisibility: true,
|
||||
child: DataTable2(
|
||||
columnSpacing: 8,
|
||||
horizontalMargin: 8,
|
||||
minWidth: widget.config.minTableWidth ?? 600,
|
||||
horizontalScrollController: _horizontalScrollController,
|
||||
columns: columns,
|
||||
rows: _items.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final item = entry.value;
|
||||
|
|
@ -1474,6 +1380,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
cells: cells,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1573,57 +1480,71 @@ class _ColumnHeaderWithSearch extends StatelessWidget {
|
|||
onTap: enabled ? () => onSort(sortBy) : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isActive ? theme.colorScheme.primary : theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (enabled) ...[
|
||||
const SizedBox(width: 4),
|
||||
if (isActive)
|
||||
Icon(
|
||||
sortDesc ? Icons.arrow_downward : Icons.arrow_upward,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
else
|
||||
Icon(
|
||||
Icons.unfold_more,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isActive ? theme.colorScheme.primary : theme.colorScheme.onSurface,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (enabled) ...[
|
||||
const SizedBox(width: 4),
|
||||
if (isActive)
|
||||
Icon(
|
||||
sortDesc ? Icons.arrow_downward : Icons.arrow_upward,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
else
|
||||
Icon(
|
||||
Icons.unfold_more,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Search button
|
||||
InkWell(
|
||||
onTap: onSearch,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: hasActiveFilter
|
||||
? theme.colorScheme.primaryContainer
|
||||
: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: hasActiveFilter
|
||||
? Border.all(color: theme.colorScheme.primary.withValues(alpha: 0.3))
|
||||
: null,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
size: 14,
|
||||
color: hasActiveFilter
|
||||
? theme.colorScheme.onPrimaryContainer
|
||||
: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
// Search button
|
||||
InkWell(
|
||||
onTap: onSearch,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: hasActiveFilter
|
||||
? theme.colorScheme.primaryContainer
|
||||
: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: hasActiveFilter
|
||||
? Border.all(color: theme.colorScheme.primary.withValues(alpha: 0.3))
|
||||
: null,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
size: 14,
|
||||
color: hasActiveFilter
|
||||
? theme.colorScheme.onPrimaryContainer
|
||||
: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
static String getColumnLabel(String key, List<DataTableColumn> columns) {
|
||||
final column = columns.firstWhere(
|
||||
|
|
@ -191,6 +227,34 @@ class DataTableUtils {
|
|||
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
|
||||
static bool isColumnSearchable(String key, List<DataTableColumn> columns) {
|
||||
final column = columns.firstWhere(
|
||||
|
|
|
|||
Loading…
Reference in a new issue