table generia search
This commit is contained in:
parent
c20b12c261
commit
5a572be3c6
@ -1,5 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/asset_model.dart';
|
||||
import '../../data/repositories/asset_repository_impl.dart';
|
||||
@ -94,7 +96,7 @@ class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> {
|
||||
void setSearch(String search) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
applyQuery(current.query.copyWith(search: search, page: 1));
|
||||
applyQuery(current.query.copyWith(search: TableSearch.normalize(search), page: 1));
|
||||
}
|
||||
|
||||
void setPage(int page) {
|
||||
|
||||
@ -477,6 +477,7 @@ class _AssetDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Asset Code',
|
||||
flex: 1,
|
||||
searchText: (asset) => asset.assetCode ?? '',
|
||||
cellBuilder: (_, asset) {
|
||||
final code = asset.assetCode;
|
||||
if (code == null || code.isEmpty) return const Text('—');
|
||||
@ -486,21 +487,26 @@ class _AssetDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Asset Name',
|
||||
flex: 2,
|
||||
searchText: (asset) => asset.assetName,
|
||||
cellBuilder: (_, asset) => Text(asset.assetName),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Category',
|
||||
flex: 2,
|
||||
searchText: (asset) => asset.assetCategoryName ?? '',
|
||||
cellBuilder: (_, asset) => Text(asset.assetCategoryName ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Plant',
|
||||
flex: 1,
|
||||
searchText: (asset) => asset.plantName ?? '',
|
||||
cellBuilder: (_, asset) => Text(asset.plantName ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Warranty',
|
||||
flex: 1,
|
||||
searchText: (asset) =>
|
||||
DateFormatter.displayDate(asset.warrantyExpiryDate),
|
||||
cellBuilder: (_, asset) => Text(
|
||||
DateFormatter.displayDate(asset.warrantyExpiryDate),
|
||||
),
|
||||
@ -508,6 +514,8 @@ class _AssetDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Status',
|
||||
flex: 1,
|
||||
searchText: (asset) =>
|
||||
asset.status?.toLowerCase().replaceAll(' ', '_') ?? 'active',
|
||||
cellBuilder: (_, asset) => AppStatusChip(
|
||||
status: asset.status?.toLowerCase().replaceAll(' ', '_') ?? 'active',
|
||||
compact: true,
|
||||
@ -518,6 +526,7 @@ class _AssetDataTable extends StatelessWidget {
|
||||
label: 'Actions',
|
||||
flex: 1,
|
||||
alignment: Alignment.centerRight,
|
||||
enableSearch: false,
|
||||
cellBuilder: (_, asset) => AppTableActions(
|
||||
children: [
|
||||
AppTableActionIcon(
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/audit_log_model.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../data/repositories/audit_repository_impl.dart';
|
||||
@ -143,7 +145,7 @@ class AuditLogsListNotifier extends AsyncNotifier<AuditLogsListState> {
|
||||
void setSearch(String search) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
applyQuery(current.query.copyWith(search: search, page: 1));
|
||||
applyQuery(current.query.copyWith(search: TableSearch.normalize(search), page: 1));
|
||||
}
|
||||
|
||||
void setPage(int page) {
|
||||
|
||||
@ -418,6 +418,7 @@ class _AuditDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'When',
|
||||
flex: 2,
|
||||
searchText: (row) => DateFormatter.displayDateTime(row.performedAt),
|
||||
cellBuilder: (_, row) => AppTableCell.text(
|
||||
DateFormatter.displayDateTime(row.performedAt),
|
||||
),
|
||||
@ -425,6 +426,7 @@ class _AuditDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Action',
|
||||
flex: 1,
|
||||
searchText: (row) => row.action,
|
||||
cellBuilder: (_, row) => AppTableCell.child(
|
||||
AppStatusChip(
|
||||
status: row.action,
|
||||
@ -436,26 +438,36 @@ class _AuditDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Table',
|
||||
flex: 2,
|
||||
searchText: (row) => row.tableName,
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.tableName),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Record',
|
||||
flex: 1,
|
||||
searchText: (row) => row.recordId ?? '',
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.recordId),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Performed By',
|
||||
flex: 2,
|
||||
searchText: (row) => row.performerLabel,
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.performerLabel),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Request ID',
|
||||
flex: 2,
|
||||
searchText: (row) => row.requestId ?? '',
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.requestId),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Changes',
|
||||
flex: 1,
|
||||
searchText: (row) {
|
||||
final parts = <String>[];
|
||||
if (row.hasOldValue) parts.add('old');
|
||||
if (row.hasNewValue) parts.add('new');
|
||||
return parts.isEmpty ? '' : parts.join(' / ');
|
||||
},
|
||||
cellBuilder: (_, row) {
|
||||
final parts = <String>[];
|
||||
if (row.hasOldValue) parts.add('old');
|
||||
@ -469,6 +481,7 @@ class _AuditDataTable extends StatelessWidget {
|
||||
label: 'Actions',
|
||||
flex: 1,
|
||||
alignment: Alignment.centerRight,
|
||||
enableSearch: false,
|
||||
cellBuilder: (_, row) => AppTableActions(
|
||||
children: [
|
||||
AppTableActionIcon(
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/grn_model.dart';
|
||||
import '../../data/repositories/grn_repository_impl.dart';
|
||||
|
||||
@ -93,7 +95,7 @@ class GrnListNotifier extends AutoDisposeAsyncNotifier<GrnListState> {
|
||||
void setSearch(String search) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
applyQuery(current.query.copyWith(search: search, page: 1));
|
||||
applyQuery(current.query.copyWith(search: TableSearch.normalize(search), page: 1));
|
||||
}
|
||||
|
||||
void setStatusFilter(String? status) {
|
||||
|
||||
@ -239,26 +239,31 @@ class _GrnDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'GRN Number',
|
||||
flex: 2,
|
||||
searchText: (grn) => grn.grnNumber ?? '',
|
||||
cellBuilder: (_, grn) => Text(grn.grnNumber ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Date',
|
||||
flex: 1,
|
||||
searchText: (grn) => DateFormatter.displayDate(grn.grnDate),
|
||||
cellBuilder: (_, grn) => Text(DateFormatter.displayDate(grn.grnDate)),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'PO Number',
|
||||
flex: 2,
|
||||
searchText: (grn) => grn.poNumber ?? '',
|
||||
cellBuilder: (_, grn) => Text(grn.poNumber ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Vendor',
|
||||
flex: 2,
|
||||
searchText: (grn) => grn.vendorName ?? '',
|
||||
cellBuilder: (_, grn) => Text(grn.vendorName ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Status',
|
||||
flex: 1,
|
||||
searchText: (grn) => grn.status,
|
||||
cellBuilder: (_, grn) => GrnStatusChip(
|
||||
status: grn.status,
|
||||
compact: true,
|
||||
@ -268,6 +273,7 @@ class _GrnDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Actions',
|
||||
flex: 1,
|
||||
enableSearch: false,
|
||||
cellBuilder: (_, grn) => AppTableActions(
|
||||
children: [
|
||||
AppTableActionIcon(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../assets/data/repositories/asset_repository_impl.dart';
|
||||
import '../../data/repositories/master_repository_impl.dart';
|
||||
|
||||
@ -277,12 +277,14 @@ class _MasterListTable extends StatelessWidget {
|
||||
(field) => AppDataColumn<Map<String, dynamic>>(
|
||||
label: field.label,
|
||||
flex: _columnFlex(field),
|
||||
searchText: (row) => masterCellValue(row, field),
|
||||
cellBuilder: (_, row) => Text(masterCellValue(row, field)),
|
||||
),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Status',
|
||||
flex: 1,
|
||||
searchText: (row) => masterStatusValue(row),
|
||||
cellBuilder: (_, row) => AppStatusChip(
|
||||
status: masterStatusValue(row),
|
||||
compact: true,
|
||||
@ -293,6 +295,7 @@ class _MasterListTable extends StatelessWidget {
|
||||
label: 'Actions',
|
||||
flex: 1,
|
||||
alignment: Alignment.centerRight,
|
||||
enableSearch: false,
|
||||
cellBuilder: (_, row) {
|
||||
final id = row['id']?.toString();
|
||||
return AppTableActions(
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/purchase_order_model.dart';
|
||||
import '../../../grn/presentation/providers/grn_lookups_provider.dart';
|
||||
import '../../data/repositories/purchase_order_repository_impl.dart';
|
||||
@ -95,7 +97,7 @@ class PurchaseOrdersListNotifier
|
||||
void setSearch(String search) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
applyQuery(current.query.copyWith(search: search, page: 1));
|
||||
applyQuery(current.query.copyWith(search: TableSearch.normalize(search), page: 1));
|
||||
}
|
||||
|
||||
void setStatusFilter(String? status) {
|
||||
|
||||
@ -332,27 +332,32 @@ class _PoDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'PO Number',
|
||||
flex: 2,
|
||||
searchText: (order) => order.poNo ?? '',
|
||||
cellBuilder: (_, order) => Text(order.poNo ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Date',
|
||||
flex: 1,
|
||||
searchText: (order) => DateFormatter.displayDate(order.poDate),
|
||||
cellBuilder: (_, order) => Text(DateFormatter.displayDate(order.poDate)),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Vendor',
|
||||
flex: 2,
|
||||
searchText: (order) => order.vendorName ?? '',
|
||||
cellBuilder: (_, order) => Text(order.vendorName ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Total',
|
||||
flex: 1,
|
||||
searchText: (order) => CurrencyFormatter.format(order.totalAmount),
|
||||
cellBuilder: (_, order) =>
|
||||
Text(CurrencyFormatter.format(order.totalAmount)),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Status',
|
||||
flex: 1,
|
||||
searchText: (order) => order.status,
|
||||
cellBuilder: (_, order) => PoStatusChip(
|
||||
status: order.status,
|
||||
compact: true,
|
||||
@ -363,6 +368,7 @@ class _PoDataTable extends StatelessWidget {
|
||||
label: 'Actions',
|
||||
flex: 1,
|
||||
alignment: Alignment.centerRight,
|
||||
enableSearch: false,
|
||||
cellBuilder: (_, order) => AppTableActions(
|
||||
children: [
|
||||
AppTableActionIcon(
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../domain/entities/rbac_entities.dart';
|
||||
|
||||
enum RbacTab { users, roles, permissions }
|
||||
@ -114,7 +116,7 @@ class RbacNotifier extends StateNotifier<RbacState> {
|
||||
void setTab(RbacTab tab) => state = state.copyWith(selectedTab: tab, currentPage: 0);
|
||||
|
||||
void setSearch(String query) =>
|
||||
state = state.copyWith(searchQuery: query, currentPage: 0);
|
||||
state = state.copyWith(searchQuery: TableSearch.normalize(query), currentPage: 0);
|
||||
|
||||
void setRoleFilter(String filter) =>
|
||||
state = state.copyWith(roleFilter: filter, currentPage: 0);
|
||||
|
||||
@ -1425,6 +1425,7 @@ class _PermissionMatrixTable extends ConsumerWidget {
|
||||
AppDataColumn(
|
||||
label: 'Module',
|
||||
flex: 3,
|
||||
searchText: (module) => '${module.name} ${module.code}',
|
||||
cellBuilder: (context, module) {
|
||||
final index = matrix.modules.indexOf(module);
|
||||
final appearance =
|
||||
@ -1463,6 +1464,10 @@ class _PermissionMatrixTable extends ConsumerWidget {
|
||||
label: permissionActionLabel(action),
|
||||
flex: 1,
|
||||
alignment: Alignment.center,
|
||||
searchText: (module) {
|
||||
final checked = module.granted[action] ?? false;
|
||||
return checked ? 'yes granted true' : 'no denied false';
|
||||
},
|
||||
cellBuilder: (context, module) {
|
||||
final checked = module.granted[action] ?? false;
|
||||
return Checkbox(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../data/repositories/reports_repository_impl.dart';
|
||||
import '../../domain/entities/depreciation_report.dart';
|
||||
|
||||
@ -627,26 +627,31 @@ class _ReportTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Asset Code',
|
||||
flex: 2,
|
||||
searchText: (row) => row.assetCode ?? '',
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.assetCode),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Asset Name',
|
||||
flex: 3,
|
||||
searchText: (row) => row.assetName ?? '',
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.assetName),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Category',
|
||||
flex: 2,
|
||||
searchText: (row) => row.categoryName ?? '',
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.categoryName),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Plant',
|
||||
flex: 2,
|
||||
searchText: (row) => row.plantName ?? '',
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.plantName),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Purchase Date',
|
||||
flex: 2,
|
||||
searchText: (row) => DateFormatter.displayDate(row.purchaseDate),
|
||||
cellBuilder: (_, row) =>
|
||||
AppTableCell.text(DateFormatter.displayDate(row.purchaseDate)),
|
||||
),
|
||||
@ -654,6 +659,7 @@ class _ReportTable extends StatelessWidget {
|
||||
label: 'Purchase Cost',
|
||||
flex: 2,
|
||||
alignment: Alignment.centerRight,
|
||||
searchText: (row) => CurrencyFormatter.format(row.purchaseCost),
|
||||
cellBuilder: (_, row) => AppTableCell.text(
|
||||
CurrencyFormatter.format(row.purchaseCost),
|
||||
textAlign: TextAlign.right,
|
||||
@ -662,12 +668,14 @@ class _ReportTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Method',
|
||||
flex: 2,
|
||||
searchText: (row) => row.depreciationMethod ?? '',
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.depreciationMethod),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Rate %',
|
||||
flex: 1,
|
||||
alignment: Alignment.centerRight,
|
||||
searchText: (row) => row.depreciationRate?.toStringAsFixed(2) ?? '',
|
||||
cellBuilder: (_, row) => AppTableCell.text(
|
||||
row.depreciationRate?.toStringAsFixed(2),
|
||||
textAlign: TextAlign.right,
|
||||
@ -677,6 +685,8 @@ class _ReportTable extends StatelessWidget {
|
||||
label: 'Annual',
|
||||
flex: 2,
|
||||
alignment: Alignment.centerRight,
|
||||
searchText: (row) =>
|
||||
CurrencyFormatter.format(row.annualDepreciation),
|
||||
cellBuilder: (_, row) => AppTableCell.text(
|
||||
CurrencyFormatter.format(row.annualDepreciation),
|
||||
textAlign: TextAlign.right,
|
||||
@ -686,6 +696,8 @@ class _ReportTable extends StatelessWidget {
|
||||
label: 'Accumulated',
|
||||
flex: 2,
|
||||
alignment: Alignment.centerRight,
|
||||
searchText: (row) =>
|
||||
CurrencyFormatter.format(row.accumulatedDepreciation),
|
||||
cellBuilder: (_, row) => AppTableCell.text(
|
||||
CurrencyFormatter.format(row.accumulatedDepreciation),
|
||||
textAlign: TextAlign.right,
|
||||
@ -695,6 +707,7 @@ class _ReportTable extends StatelessWidget {
|
||||
label: 'Book Value',
|
||||
flex: 2,
|
||||
alignment: Alignment.centerRight,
|
||||
searchText: (row) => CurrencyFormatter.format(row.bookValue),
|
||||
cellBuilder: (_, row) => AppTableCell.text(
|
||||
CurrencyFormatter.format(row.bookValue),
|
||||
textAlign: TextAlign.right,
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/permission_matrix_models.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../data/repositories/role_repository_impl.dart';
|
||||
@ -125,7 +127,7 @@ class RolesListNotifier extends AsyncNotifier<RolesListState> {
|
||||
void setSearch(String search) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
state = AsyncData(current.copyWith(search: search, page: 1));
|
||||
state = AsyncData(current.copyWith(search: TableSearch.normalize(search), page: 1));
|
||||
}
|
||||
|
||||
void setPage(int page) {
|
||||
|
||||
@ -84,53 +84,36 @@ class _MatrixGrid extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AppCard(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
columns: [
|
||||
const DataColumn(label: Text('Module')),
|
||||
...matrix.actionColumns.map(
|
||||
(action) => DataColumn(label: Text(permissionActionLabel(action))),
|
||||
),
|
||||
],
|
||||
rows: matrix.modules
|
||||
.map(
|
||||
(row) => DataRow(
|
||||
cells: [
|
||||
DataCell(AppTableCell.text(row.name)),
|
||||
...matrix.actionColumns.map(
|
||||
(action) => DataCell(
|
||||
_actionToggle(
|
||||
roleId,
|
||||
row,
|
||||
action,
|
||||
row.granted[action] ?? false,
|
||||
ref,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
return AppDataTable<PermissionMatrixModuleRow>(
|
||||
wrapInCard: true,
|
||||
columns: [
|
||||
AppDataColumn(
|
||||
label: 'Module',
|
||||
flex: 3,
|
||||
searchText: (row) => row.name,
|
||||
cellBuilder: (_, row) => AppTableCell.text(row.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionToggle(
|
||||
String roleId,
|
||||
PermissionMatrixModuleRow row,
|
||||
String action,
|
||||
bool value,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
return Checkbox(
|
||||
value: value,
|
||||
onChanged: (checked) => ref
|
||||
.read(permissionMatrixProvider(roleId).notifier)
|
||||
.toggleAction(row.moduleId, action, checked ?? false),
|
||||
...matrix.actionColumns.map(
|
||||
(action) => AppDataColumn(
|
||||
label: permissionActionLabel(action),
|
||||
flex: 1,
|
||||
alignment: Alignment.center,
|
||||
searchText: (row) {
|
||||
final checked = row.granted[action] ?? false;
|
||||
return checked ? 'yes granted true' : 'no denied false';
|
||||
},
|
||||
cellBuilder: (_, row) => Checkbox(
|
||||
value: row.granted[action] ?? false,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: (checked) => ref
|
||||
.read(permissionMatrixProvider(roleId).notifier)
|
||||
.toggleAction(row.moduleId, action, checked ?? false),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: matrix.modules,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,21 +120,24 @@ class _RoleDataTable extends StatelessWidget {
|
||||
return AppDataTable<RoleCardModel>(
|
||||
wrapInCard: false,
|
||||
columns: [
|
||||
AppDataColumn(label: 'Role Name', flex: 2, cellBuilder: (_, r) => Text(r.name)),
|
||||
AppDataColumn(label: 'Role Name', flex: 2, searchText: (r) => r.name, cellBuilder: (_, r) => Text(r.name)),
|
||||
AppDataColumn(
|
||||
label: 'Description',
|
||||
flex: 3,
|
||||
searchText: (r) => r.description ?? '',
|
||||
cellBuilder: (_, r) => Text(r.description ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Users Count',
|
||||
flex: 1,
|
||||
searchText: (r) => '${r.userCount}',
|
||||
cellBuilder: (_, r) => Text('${r.userCount}'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Actions',
|
||||
flex: 1,
|
||||
alignment: Alignment.centerRight,
|
||||
enableSearch: false,
|
||||
cellBuilder: (_, r) => AppTableActions(
|
||||
children: [
|
||||
AppTableActionIcon(
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../data/repositories/user_repository_impl.dart';
|
||||
@ -147,7 +149,7 @@ class UsersListNotifier extends AsyncNotifier<UsersListState> {
|
||||
void setSearch(String search) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
applyQuery(current.query.copyWith(search: search, page: 1));
|
||||
applyQuery(current.query.copyWith(search: TableSearch.normalize(search), page: 1));
|
||||
}
|
||||
|
||||
void setPage(int page) {
|
||||
|
||||
@ -43,6 +43,8 @@ class UserRichDataTable extends StatelessWidget {
|
||||
label: 'User',
|
||||
sortKey: 'full_name',
|
||||
flex: 3,
|
||||
searchText: (user) =>
|
||||
'${user.fullName} ${user.email}'.trim(),
|
||||
cellBuilder: (_, user) => UserTableUserCell(user: user),
|
||||
),
|
||||
AppDataColumn(
|
||||
@ -50,22 +52,27 @@ class UserRichDataTable extends StatelessWidget {
|
||||
sortKey: 'employee_code',
|
||||
flex: 1,
|
||||
alignment: Alignment.centerRight,
|
||||
searchText: (user) => user.employeeCode,
|
||||
cellBuilder: (_, user) => EmployeeCodeBadge(code: user.employeeCode),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Role',
|
||||
flex: 2,
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
searchText: (user) => user.roleNames.join(' '),
|
||||
cellBuilder: (_, user) => UserRolesCell(roles: user.roleNames),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Department',
|
||||
flex: 2,
|
||||
searchText: (user) => user.departmentLabel,
|
||||
cellBuilder: (_, user) => Text(user.departmentLabel),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Last Login',
|
||||
flex: 2,
|
||||
searchText: (user) =>
|
||||
DateFormatter.formatUserLastLogin(user.lastLoginAt),
|
||||
cellBuilder: (_, user) => Text(
|
||||
DateFormatter.formatUserLastLogin(user.lastLoginAt),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
@ -76,6 +83,7 @@ class UserRichDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Status',
|
||||
flex: 1,
|
||||
searchText: (user) => user.status,
|
||||
cellBuilder: (_, user) => AppStatusChip(
|
||||
status: user.status,
|
||||
compact: true,
|
||||
@ -86,6 +94,7 @@ class UserRichDataTable extends StatelessWidget {
|
||||
label: 'Actions',
|
||||
flex: 1,
|
||||
alignment: Alignment.centerRight,
|
||||
enableSearch: false,
|
||||
cellBuilder: (context, user) => actionsBuilder(context, user),
|
||||
),
|
||||
],
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/vendor_model.dart';
|
||||
import '../../data/repositories/vendor_repository_impl.dart';
|
||||
|
||||
@ -93,7 +95,7 @@ class VendorsListNotifier extends AutoDisposeAsyncNotifier<VendorsListState> {
|
||||
void setSearch(String search) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
applyQuery(current.query.copyWith(search: search, page: 1));
|
||||
applyQuery(current.query.copyWith(search: TableSearch.normalize(search), page: 1));
|
||||
}
|
||||
|
||||
void setStatusFilter(String? status) {
|
||||
|
||||
@ -272,26 +272,32 @@ class _VendorDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Code',
|
||||
flex: 1,
|
||||
searchText: (vendor) => vendor.vendorCode ?? '',
|
||||
cellBuilder: (_, vendor) => Text(vendor.vendorCode ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Name',
|
||||
flex: 2,
|
||||
searchText: (vendor) => vendor.vendorName,
|
||||
cellBuilder: (_, vendor) => Text(vendor.vendorName),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Type',
|
||||
flex: 2,
|
||||
searchText: (vendor) => vendorTypeLabel(vendor.vendorType),
|
||||
cellBuilder: (_, vendor) => Text(vendorTypeLabel(vendor.vendorType)),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'GSTIN',
|
||||
flex: 2,
|
||||
searchText: (vendor) => vendor.gstin ?? '',
|
||||
cellBuilder: (_, vendor) => Text(vendor.gstin ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Status',
|
||||
flex: 1,
|
||||
searchText: (vendor) =>
|
||||
vendor.status ?? (vendor.isActive ? 'active' : 'inactive'),
|
||||
cellBuilder: (_, vendor) => AppStatusChip(
|
||||
status: vendor.status ?? (vendor.isActive ? 'active' : 'inactive'),
|
||||
compact: true,
|
||||
@ -302,6 +308,7 @@ class _VendorDataTable extends StatelessWidget {
|
||||
label: 'Actions',
|
||||
flex: 1,
|
||||
alignment: Alignment.centerRight,
|
||||
enableSearch: false,
|
||||
cellBuilder: (_, vendor) => AppTableActions(
|
||||
children: [
|
||||
AppTableActionIcon(
|
||||
|
||||
@ -2,9 +2,15 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_card.dart';
|
||||
|
||||
/// Fixed height for every row in [AppDataTable] and themed [DataTable] widgets.
|
||||
/// Fixed height for every data row in [AppDataTable] and themed [DataTable] widgets.
|
||||
const double kAppTableRowHeight = 52;
|
||||
|
||||
/// Fixed column-filter row height (first row under the header).
|
||||
const double kAppTableFilterRowHeight = 52;
|
||||
|
||||
/// Horizontal gap between column search fields.
|
||||
const double kAppTableFilterGap = 8;
|
||||
|
||||
class AppDataColumn<T> {
|
||||
const AppDataColumn({
|
||||
required this.label,
|
||||
@ -13,6 +19,8 @@ class AppDataColumn<T> {
|
||||
this.flex = 1,
|
||||
this.alignment = Alignment.centerLeft,
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.searchText,
|
||||
this.enableSearch,
|
||||
});
|
||||
|
||||
final String label;
|
||||
@ -21,6 +29,15 @@ class AppDataColumn<T> {
|
||||
final int flex;
|
||||
final Alignment alignment;
|
||||
final EdgeInsets padding;
|
||||
|
||||
/// Text used for this column's filter. When null and [enableSearch] is not
|
||||
/// forced on, the column has no search input (e.g. Actions).
|
||||
final String Function(T row)? searchText;
|
||||
|
||||
/// When null, search is enabled only if [searchText] is provided.
|
||||
final bool? enableSearch;
|
||||
|
||||
bool get isSearchable => enableSearch ?? searchText != null;
|
||||
}
|
||||
|
||||
/// Helpers for table cell content — single-line text with ellipsis and tooltip.
|
||||
@ -49,7 +66,7 @@ class AppTableCell {
|
||||
static Widget child(Widget widget) => widget;
|
||||
}
|
||||
|
||||
class AppDataTable<T> extends StatelessWidget {
|
||||
class AppDataTable<T> extends StatefulWidget {
|
||||
const AppDataTable({
|
||||
super.key,
|
||||
required this.columns,
|
||||
@ -58,6 +75,7 @@ class AppDataTable<T> extends StatelessWidget {
|
||||
this.sortAscending = true,
|
||||
this.onSort,
|
||||
this.emptyMessage = 'No records found',
|
||||
this.noMatchMessage = 'No matching records',
|
||||
this.wrapInCard = true,
|
||||
this.shrinkWrap = false,
|
||||
});
|
||||
@ -68,70 +86,152 @@ class AppDataTable<T> extends StatelessWidget {
|
||||
final bool sortAscending;
|
||||
final void Function(String column, bool ascending)? onSort;
|
||||
final String emptyMessage;
|
||||
final String noMatchMessage;
|
||||
final bool wrapInCard;
|
||||
|
||||
/// Set true when the table is placed inside another scrollable.
|
||||
final bool shrinkWrap;
|
||||
|
||||
@override
|
||||
State<AppDataTable<T>> createState() => _AppDataTableState<T>();
|
||||
}
|
||||
|
||||
class _AppDataTableState<T> extends State<AppDataTable<T>> {
|
||||
/// Column index → search query (raw, including spaces until applied).
|
||||
final Map<int, String> _queries = {};
|
||||
final Map<int, TextEditingController> _controllers = {};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in _controllers.values) {
|
||||
c.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
TextEditingController _controllerFor(int index) {
|
||||
return _controllers.putIfAbsent(index, TextEditingController.new);
|
||||
}
|
||||
|
||||
List<T> get _filteredRows {
|
||||
final active = <int, String>{};
|
||||
for (final entry in _queries.entries) {
|
||||
final q = entry.value.trim().toLowerCase();
|
||||
if (q.isEmpty) continue;
|
||||
final col = widget.columns[entry.key];
|
||||
if (!col.isSearchable || col.searchText == null) continue;
|
||||
active[entry.key] = q;
|
||||
}
|
||||
if (active.isEmpty) return widget.rows;
|
||||
|
||||
return widget.rows.where((row) {
|
||||
for (final entry in active.entries) {
|
||||
final col = widget.columns[entry.key];
|
||||
final value = (col.searchText!(row)).trim().toLowerCase();
|
||||
if (!value.contains(entry.value)) return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
bool get _hasActiveColumnFilters =>
|
||||
_queries.values.any((q) => q.trim().isNotEmpty);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (rows.isEmpty) {
|
||||
if (widget.rows.isEmpty) {
|
||||
final empty = Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
emptyMessage,
|
||||
widget.emptyMessage,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!wrapInCard) return empty;
|
||||
if (!widget.wrapInCard) return empty;
|
||||
return AppCard(clipBehavior: Clip.antiAlias, child: empty);
|
||||
}
|
||||
|
||||
final filtered = _filteredRows;
|
||||
final showFilterRow = widget.columns.any((c) => c.isSearchable);
|
||||
|
||||
final header = _TableHeaderRow<T>(
|
||||
columns: columns,
|
||||
sortColumn: sortColumn,
|
||||
sortAscending: sortAscending,
|
||||
onSort: onSort,
|
||||
columns: widget.columns,
|
||||
sortColumn: widget.sortColumn,
|
||||
sortAscending: widget.sortAscending,
|
||||
onSort: widget.onSort,
|
||||
);
|
||||
|
||||
final body = ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: shrinkWrap,
|
||||
clipBehavior: Clip.none,
|
||||
physics: shrinkWrap ? const NeverScrollableScrollPhysics() : null,
|
||||
itemCount: rows.length,
|
||||
itemBuilder: (context, index) => _TableDataRow<T>(
|
||||
columns: columns,
|
||||
row: rows[index],
|
||||
),
|
||||
);
|
||||
final filterRow = showFilterRow
|
||||
? _TableFilterRow<T>(
|
||||
columns: widget.columns,
|
||||
controllerFor: _controllerFor,
|
||||
queryFor: (i) => _queries[i] ?? '',
|
||||
onQueryChanged: (index, value) {
|
||||
setState(() => _queries[index] = value);
|
||||
},
|
||||
)
|
||||
: null;
|
||||
|
||||
// Header stays fixed; body scrolls in the remaining space and is clipped so
|
||||
// rows cannot paint over footers (e.g. pagination) outside the table.
|
||||
final table = shrinkWrap
|
||||
final Widget body;
|
||||
if (filtered.isEmpty) {
|
||||
body = Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
_hasActiveColumnFilters
|
||||
? widget.noMatchMessage
|
||||
: widget.emptyMessage,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
body = ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
clipBehavior: Clip.none,
|
||||
physics: widget.shrinkWrap
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, index) => _TableDataRow<T>(
|
||||
columns: widget.columns,
|
||||
row: filtered[index],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final pinned = <Widget>[
|
||||
header,
|
||||
if (filterRow != null) filterRow,
|
||||
];
|
||||
|
||||
final table = widget.shrinkWrap
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
header,
|
||||
...pinned,
|
||||
body,
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
header,
|
||||
...pinned,
|
||||
Expanded(
|
||||
child: ClipRect(child: body),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (!wrapInCard) return table;
|
||||
if (!widget.wrapInCard) return table;
|
||||
|
||||
return AppCard(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
@ -157,71 +257,240 @@ class _TableHeaderRow<T> extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SizedBox(
|
||||
height: kAppTableRowHeight,
|
||||
width: double.infinity,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.12),
|
||||
return Material(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
elevation: 1,
|
||||
shadowColor: theme.colorScheme.shadow.withValues(alpha: 0.08),
|
||||
child: SizedBox(
|
||||
height: kAppTableRowHeight,
|
||||
width: double.infinity,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: columns.map((col) {
|
||||
final isSorted =
|
||||
col.sortKey != null && col.sortKey == sortColumn;
|
||||
final label = Text(
|
||||
col.label.toUpperCase(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.6,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
|
||||
Widget header = label;
|
||||
if (col.sortKey != null && onSort != null) {
|
||||
header = InkWell(
|
||||
onTap: () => onSort!(
|
||||
col.sortKey!,
|
||||
isSorted ? !sortAscending : true,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(child: label),
|
||||
if (isSorted) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
sortAscending
|
||||
? Icons.arrow_upward
|
||||
: Icons.arrow_downward,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
flex: col.flex,
|
||||
child: Padding(
|
||||
padding: col.padding,
|
||||
child: Align(
|
||||
alignment: col.alignment,
|
||||
child: header,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: columns.map((col) {
|
||||
final isSorted = col.sortKey != null && col.sortKey == sortColumn;
|
||||
final label = Text(
|
||||
col.label.toUpperCase(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.6,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget header = label;
|
||||
if (col.sortKey != null && onSort != null) {
|
||||
header = InkWell(
|
||||
onTap: () => onSort!(
|
||||
col.sortKey!,
|
||||
isSorted ? !sortAscending : true,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(child: label),
|
||||
if (isSorted) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
sortAscending
|
||||
? Icons.arrow_upward
|
||||
: Icons.arrow_downward,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
/// Fixed first row under the header — one search field per searchable column.
|
||||
class _TableFilterRow<T> extends StatelessWidget {
|
||||
const _TableFilterRow({
|
||||
required this.columns,
|
||||
required this.controllerFor,
|
||||
required this.queryFor,
|
||||
required this.onQueryChanged,
|
||||
});
|
||||
|
||||
return Expanded(
|
||||
flex: col.flex,
|
||||
child: Padding(
|
||||
padding: col.padding,
|
||||
child: Align(
|
||||
alignment: col.alignment,
|
||||
child: header,
|
||||
final List<AppDataColumn<T>> columns;
|
||||
final TextEditingController Function(int index) controllerFor;
|
||||
final String Function(int index) queryFor;
|
||||
final void Function(int index, String value) onQueryChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Material(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.35),
|
||||
child: SizedBox(
|
||||
height: kAppTableFilterRowHeight,
|
||||
width: double.infinity,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i = 0; i < columns.length; i++) ...[
|
||||
if (i > 0) const SizedBox(width: kAppTableFilterGap),
|
||||
Expanded(
|
||||
flex: columns[i].flex,
|
||||
child: Padding(
|
||||
padding: columns[i].padding,
|
||||
child: columns[i].isSearchable
|
||||
? _ColumnSearchField(
|
||||
controller: controllerFor(i),
|
||||
query: queryFor(i),
|
||||
onChanged: (v) => onQueryChanged(i, v),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ColumnSearchField extends StatelessWidget {
|
||||
const _ColumnSearchField({
|
||||
required this.controller,
|
||||
required this.query,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String query;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final outline = theme.colorScheme.outline.withValues(alpha: 0.28);
|
||||
final hasQuery = query.trim().isNotEmpty;
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.colorScheme.shadow.withValues(alpha: 0.04),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
onChanged: onChanged,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
cursorHeight: 14,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: 'Search…',
|
||||
hintStyle: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.55),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search_rounded,
|
||||
size: 16,
|
||||
color: hasQuery
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
minWidth: 34,
|
||||
minHeight: 34,
|
||||
),
|
||||
suffixIcon: hasQuery
|
||||
? IconButton(
|
||||
tooltip: 'Clear',
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 28,
|
||||
minHeight: 28,
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.close_rounded,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
onChanged('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
minWidth: 28,
|
||||
minHeight: 28,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.surface,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: outline),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: outline),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user