progress in datatable

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

View file

@ -244,6 +244,16 @@
"allTickets": "All Tickets",
"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",

View file

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

View file

@ -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:

View file

@ -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';

View file

@ -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 => 'پیام داخلی';

View file

@ -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'],

View file

@ -0,0 +1,188 @@
# Enhanced Filters System
سیستم فیلتر پیشرفته برای DataTableWidget که امکان استفاده از انواع مختلف فیلتر را فراهم می‌کند.
## ویژگی‌ها
### 🔍 انواع فیلتر
1. **فیلتر متنی** (پیش‌فرض): جستجوی متنی با انواع مختلف
2. **فیلتر بازه زمانی**: انتخاب بازه تاریخ برای ستون‌های تاریخ
3. **فیلتر چندتایی**: انتخاب چندین گزینه با چک باکس
### 🎯 نحوه استفاده
#### 1. ستون تاریخ با فیلتر بازه زمانی
```dart
DateColumn(
'created_at',
'تاریخ ایجاد',
filterType: ColumnFilterType.dateRange,
)
```
#### 2. ستون اولویت با فیلتر چندتایی
```dart
TextColumn(
'priority',
'اولویت',
filterType: ColumnFilterType.multiSelect,
filterOptions: [
FilterOption(
value: 'normal',
label: 'عادی',
icon: Icons.circle,
color: Colors.green,
),
FilterOption(
value: 'special',
label: 'ویژه',
icon: Icons.star,
color: Colors.orange,
),
FilterOption(
value: 'urgent',
label: 'فوری',
icon: Icons.priority_high,
color: Colors.red,
),
],
)
```
#### 3. ستون متنی با فیلتر پیش‌فرض
```dart
TextColumn(
'name',
'نام',
// filterType پیش‌فرض text است
)
```
## ساختار کلاس‌ها
### ColumnFilterType
```dart
enum ColumnFilterType {
text, // فیلتر متنی (پیش‌فرض)
dateRange, // فیلتر بازه زمانی
multiSelect, // فیلتر چندتایی
}
```
### FilterOption
```dart
class FilterOption {
final String value; // مقدار برای API
final String label; // نمایش در UI
final String? description; // توضیحات اضافی
final IconData? icon; // آیکون
final Color? color; // رنگ آیکون/متن
}
```
## رفتار دکمه جستجو
- **ستون تاریخ** → DateRangePicker (از/تا تاریخ)
- **ستون با filterOptions** → CheckboxList با آیتم‌های تعریف شده
- **ستون‌های دیگر** → TextField (فعلی)
## نمایش فیلترهای فعال
- **تاریخ**: "تاریخ: 1403/01/01 - 1403/01/31"
- ☑️ **چندتایی**: "اولویت: عادی، ویژه" (با آیکون‌های رنگی)
- 🔍 **متنی**: "نام: احمد (شامل)"
## ساختار فیلتر در API
### فیلتر متنی
```json
{
"property": "name",
"operator": "*",
"value": "احمد"
}
```
### فیلتر چندتایی
```json
{
"property": "priority",
"operator": "in",
"value": ["normal", "special"]
}
```
### فیلتر بازه زمانی
```json
[
{
"property": "created_at",
"operator": ">=",
"value": "2024-01-01T00:00:00.000Z"
},
{
"property": "created_at",
"operator": "<",
"value": "2024-01-31T00:00:00.000Z"
}
]
```
## مثال کامل
```dart
DataTableWidget<Map<String, dynamic>>(
config: DataTableConfig<Map<String, dynamic>>(
title: 'Enhanced Filters Demo',
endpoint: '/api/v1/demo/list',
columns: [
// ستون تاریخ با فیلتر بازه زمانی
DateColumn(
'created_at',
'تاریخ ایجاد',
filterType: ColumnFilterType.dateRange,
),
// ستون اولویت با فیلتر چندتایی
TextColumn(
'priority',
'اولویت',
filterType: ColumnFilterType.multiSelect,
filterOptions: [
FilterOption(value: 'normal', label: 'عادی'),
FilterOption(value: 'special', label: 'ویژه'),
FilterOption(value: 'urgent', label: 'فوری'),
],
),
// ستون نام با فیلتر متنی (پیش‌فرض)
TextColumn('name', 'نام'),
],
showColumnSearch: true,
showActiveFilters: true,
),
fromJson: (json) => json,
calendarController: calendarController,
)
```
## مزایا
- ✅ **سازگاری کامل**: با سیستم فعلی کاملاً سازگار
- ✅ **انعطاف‌پذیری**: توسعه‌دهنده کنترل کامل دارد
- ✅ **قابلیت استفاده**: UI مناسب برای هر نوع فیلتر
- ✅ **قابلیت توسعه**: آسان برای اضافه کردن انواع جدید
- ✅ **عملکرد**: فیلترها در سمت سرور پردازش می‌شوند
## نکات مهم
1. **فیلتر پیش‌فرض**: اگر `filterType` تعیین نشود، فیلتر متنی استفاده می‌شود
2. **فیلتر چندتایی**: نیاز به `filterOptions` دارد
3. **فیلتر بازه زمانی**: فقط برای ستون‌های تاریخ مناسب است
4. **API**: سرور باید انواع مختلف فیلتر را پشتیبانی کند

View file

@ -9,6 +9,30 @@ enum ColumnWidth {
extraLarge,
}
/// 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;
}
}

View file

@ -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,14 +68,54 @@ 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: [
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
DropdownButtonFormField<String>(
value: _selectedType,
@ -95,16 +149,136 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
),
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(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(t.cancel),
),
if (widget.searchValue.isNotEmpty)
if (hasActiveFilter)
TextButton(
onPressed: () {
widget.onClear();
@ -113,14 +287,62 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
child: Text(t.clear),
),
FilledButton(
onPressed: () {
widget.onApply(_controller.text.trim(), _selectedType);
Navigator.of(context).pop();
},
child: Text(t.applyColumnFilter),
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)}'),

View file

@ -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,20 +1155,9 @@ 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,
@ -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(),
size: ColumnSize.S,
fixedWidth: 50.0,
));
}
@ -1385,6 +1285,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
),
),
size: ColumnSize.S,
fixedWidth: 60.0,
));
}
@ -1408,13 +1309,18 @@ 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,
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;
@ -1474,6 +1380,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
cells: cells,
);
}).toList(),
),
);
}
@ -1573,15 +1480,25 @@ class _ColumnHeaderWithSearch extends StatelessWidget {
onTap: enabled ? () => onSort(sortBy) : null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Container(
width: double.infinity,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
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),
@ -1598,6 +1515,9 @@ class _ColumnHeaderWithSearch extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
],
],
),
),
const SizedBox(width: 8),
// Search button
InkWell(
@ -1626,6 +1546,7 @@ class _ColumnHeaderWithSearch extends StatelessWidget {
],
),
),
),
);
}
}

View file

@ -182,6 +182,42 @@ class DataTableUtils {
);
}
/// Create filter item for multi-select
static FilterItem createMultiSelectFilter(
String field,
List<String> values,
) {
return FilterItem(
property: field,
operator: 'in',
value: values,
);
}
/// Create filter item for date range
static List<FilterItem> createDateRangeFilter(
String field,
DateTime startDate,
DateTime endDate,
) {
final start = DateTime(startDate.year, startDate.month, startDate.day);
final endExclusive = DateTime(endDate.year, endDate.month, endDate.day)
.add(const Duration(days: 1));
return [
FilterItem(
property: field,
operator: '>=',
value: start.toIso8601String(),
),
FilterItem(
property: field,
operator: '<',
value: endExclusive.toIso8601String(),
),
];
}
/// Get column label by key
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(