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