filter changes
This commit is contained in:
parent
6721b308a9
commit
4360931be4
@ -19,6 +19,7 @@ import '../../../../shared/widgets/app_loading_view.dart';
|
|||||||
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_status_chip.dart';
|
import '../../../../shared/widgets/app_status_chip.dart';
|
||||||
import '../../../../shared/widgets/app_table_action_icon.dart';
|
import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||||
import '../../../../shared/widgets/can_permission.dart';
|
import '../../../../shared/widgets/can_permission.dart';
|
||||||
@ -39,6 +40,8 @@ class AssetListScreen extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||||
|
bool _filtersExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final assetsAsync = ref.watch(assetsListProvider);
|
final assetsAsync = ref.watch(assetsListProvider);
|
||||||
@ -79,6 +82,13 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
|||||||
title: 'Asset Master',
|
title: 'Asset Master',
|
||||||
subtitle: 'Manage plant assets, AMC, service & insurance',
|
subtitle: 'Manage plant assets, AMC, service & insurance',
|
||||||
actions: [
|
actions: [
|
||||||
|
AppSearchFilterButton(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _filtersExpanded = !_filtersExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
if (canExport)
|
if (canExport)
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: state.isExporting ? null : _exportAssets,
|
onPressed: state.isExporting ? null : _exportAssets,
|
||||||
@ -134,7 +144,8 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
AppCollapsibleFilterPanel(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: _AssetsFilterBar(
|
child: _AssetsFilterBar(
|
||||||
query: state.query,
|
query: state.query,
|
||||||
@ -147,7 +158,6 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
|||||||
onStatusChanged: notifier.setStatusFilter,
|
onStatusChanged: notifier.setStatusFilter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
if (state.assets.isEmpty)
|
if (state.assets.isEmpty)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|||||||
@ -646,7 +646,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
_SidePanelDateField(
|
_SidePanelDateField(
|
||||||
label: 'Visit Date',
|
label: 'Visit Date',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
@ -677,7 +676,10 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SidePanelFormRow(
|
SidePanelFormRow(
|
||||||
left: lookupsAsync.when(
|
left: lookupsAsync.when(
|
||||||
loading: () => const LinearProgressIndicator(),
|
loading: () => const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
|
child: LinearProgressIndicator(),
|
||||||
|
),
|
||||||
error: (_, __) => const Text('Failed to load vendors'),
|
error: (_, __) => const Text('Failed to load vendors'),
|
||||||
data: (lookups) => AppSearchableDropdown<int>(
|
data: (lookups) => AppSearchableDropdown<int>(
|
||||||
label: 'Vendor',
|
label: 'Vendor',
|
||||||
@ -702,7 +704,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
|||||||
onChanged: (v) => setState(() => _assetConditionAfter = v),
|
onChanged: (v) => setState(() => _assetConditionAfter = v),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
SidePanelFormRow(
|
SidePanelFormRow(
|
||||||
left: AppTextField(
|
left: AppTextField(
|
||||||
controller: _complaintNoController,
|
controller: _complaintNoController,
|
||||||
@ -717,7 +718,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
AppTextField(
|
AppTextField(
|
||||||
controller: _complaintDescController,
|
controller: _complaintDescController,
|
||||||
label: 'Complaint Description',
|
label: 'Complaint Description',
|
||||||
@ -737,7 +737,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
|||||||
inputFormatters: Validators.mobileInput,
|
inputFormatters: Validators.mobileInput,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
AppTextField(
|
AppTextField(
|
||||||
controller: _workDoneController,
|
controller: _workDoneController,
|
||||||
label: 'Work Done',
|
label: 'Work Done',
|
||||||
@ -769,7 +768,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
AppTextField(
|
AppTextField(
|
||||||
controller: _serviceCostController,
|
controller: _serviceCostController,
|
||||||
label: 'Service Cost',
|
label: 'Service Cost',
|
||||||
@ -1651,7 +1649,7 @@ Widget _panelFooter(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SidePanelDateField extends StatelessWidget {
|
class _SidePanelDateField extends StatefulWidget {
|
||||||
const _SidePanelDateField({
|
const _SidePanelDateField({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.value,
|
required this.value,
|
||||||
@ -1665,19 +1663,54 @@ class _SidePanelDateField extends StatelessWidget {
|
|||||||
final bool isRequired;
|
final bool isRequired;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<_SidePanelDateField> createState() => _SidePanelDateFieldState();
|
||||||
final labelText = isRequired ? '$label *' : label;
|
}
|
||||||
final displayText = value != null ? DateFormatter.displayDate(value) : '';
|
|
||||||
|
|
||||||
return TextFormField(
|
class _SidePanelDateFieldState extends State<_SidePanelDateField> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
String get _displayText =>
|
||||||
|
widget.value != null ? DateFormatter.displayDate(widget.value) : '';
|
||||||
|
|
||||||
|
String get _labelText =>
|
||||||
|
widget.isRequired ? '${widget.label} *' : widget.label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: _displayText);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _SidePanelDateField oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
final text = _displayText;
|
||||||
|
if (_controller.text != text) {
|
||||||
|
_controller.text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: TextFormField(
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
onTap: onPick,
|
onTap: widget.onPick,
|
||||||
|
controller: _controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: labelText,
|
labelText: _labelText,
|
||||||
hintText: 'Select date',
|
hintText: 'Select date',
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 20),
|
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 20),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: displayText),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import '../../../../shared/widgets/app_loading_view.dart';
|
|||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
||||||
import '../../../../shared/widgets/app_search_field.dart';
|
import '../../../../shared/widgets/app_search_field.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||||
import '../../../../shared/widgets/app_side_panel.dart';
|
import '../../../../shared/widgets/app_side_panel.dart';
|
||||||
import '../../../../shared/widgets/app_status_chip.dart';
|
import '../../../../shared/widgets/app_status_chip.dart';
|
||||||
@ -38,6 +39,7 @@ class AuditLogsScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _AuditLogsScreenState extends ConsumerState<AuditLogsScreen> {
|
class _AuditLogsScreenState extends ConsumerState<AuditLogsScreen> {
|
||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
|
bool _filtersExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -135,7 +137,14 @@ class _AuditLogsScreenState extends ConsumerState<AuditLogsScreen> {
|
|||||||
title: 'Audit Logs',
|
title: 'Audit Logs',
|
||||||
subtitle: 'System activity and change history',
|
subtitle: 'System activity and change history',
|
||||||
actions: [
|
actions: [
|
||||||
if (canExport)
|
AppSearchFilterButton(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _filtersExpanded = !_filtersExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (canExport) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: state.isExporting || !state.query.hasActiveFilter
|
onPressed: state.isExporting || !state.query.hasActiveFilter
|
||||||
? null
|
? null
|
||||||
@ -150,9 +159,11 @@ class _AuditLogsScreenState extends ConsumerState<AuditLogsScreen> {
|
|||||||
label: Text(state.isExporting ? 'Exporting...' : 'Export'),
|
label: Text(state.isExporting ? 'Exporting...' : 'Export'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AppTableShell(
|
child: AppTableShell(
|
||||||
|
toolbarExpanded: _filtersExpanded,
|
||||||
toolbar: _FiltersBar(
|
toolbar: _FiltersBar(
|
||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
filters: state.filters,
|
filters: state.filters,
|
||||||
@ -327,9 +338,14 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
onClear: _dateEmpty ? null : onClearDateRange,
|
onClear: _dateEmpty ? null : onClearDateRange,
|
||||||
);
|
);
|
||||||
|
|
||||||
final resetButton = TextButton(
|
final theme = Theme.of(context);
|
||||||
|
final iconColor = theme.colorScheme.primary;
|
||||||
|
final resetButton = IconButton(
|
||||||
|
tooltip: 'Reset',
|
||||||
|
color: iconColor,
|
||||||
|
disabledColor: iconColor.withValues(alpha: 0.38),
|
||||||
onPressed: query.hasActiveFilter ? onReset : null,
|
onPressed: query.hasActiveFilter ? onReset : null,
|
||||||
child: const Text('Reset'),
|
icon: const Icon(Icons.restart_alt, size: 20),
|
||||||
);
|
);
|
||||||
|
|
||||||
return AppResponsiveFilterGrid(
|
return AppResponsiveFilterGrid(
|
||||||
@ -339,11 +355,8 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
actionDropdown,
|
actionDropdown,
|
||||||
performerDropdown,
|
performerDropdown,
|
||||||
dateField,
|
dateField,
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: resetButton,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
trailing: resetButton,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import '../../../../shared/widgets/app_empty_state.dart';
|
|||||||
import '../../../../shared/widgets/app_loading_view.dart';
|
import '../../../../shared/widgets/app_loading_view.dart';
|
||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||||
import '../../../../shared/widgets/can_permission.dart';
|
import '../../../../shared/widgets/can_permission.dart';
|
||||||
import '../../../../shared/widgets/app_table_action_icon.dart';
|
import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||||
@ -38,6 +39,7 @@ class GrnListScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _GrnListScreenState extends ConsumerState<GrnListScreen> {
|
class _GrnListScreenState extends ConsumerState<GrnListScreen> {
|
||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
|
bool _filtersExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -74,6 +76,27 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
|
|||||||
title: 'Goods Received Notes',
|
title: 'Goods Received Notes',
|
||||||
subtitle: 'Record and track purchase order receipts',
|
subtitle: 'Record and track purchase order receipts',
|
||||||
actions: [
|
actions: [
|
||||||
|
AppSearchFilterButton(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _filtersExpanded = !_filtersExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (canExport) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: state.isExporting ? null : _exportGrns,
|
||||||
|
icon: state.isExporting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.download_outlined, size: 18),
|
||||||
|
label: Text(state.isExporting ? 'Exporting...' : 'Export'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(width: 8),
|
||||||
CanPermission(
|
CanPermission(
|
||||||
module: 'grn',
|
module: 'grn',
|
||||||
action: PermissionAction.create,
|
action: PermissionAction.create,
|
||||||
@ -87,13 +110,11 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AppTableShell(
|
child: AppTableShell(
|
||||||
|
toolbarExpanded: _filtersExpanded,
|
||||||
toolbar: _FiltersBar(
|
toolbar: _FiltersBar(
|
||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
query: state.query,
|
query: state.query,
|
||||||
locationOptions: lookupsAsync.valueOrNull?.locations ?? const [],
|
locationOptions: lookupsAsync.valueOrNull?.locations ?? const [],
|
||||||
showExport: canExport,
|
|
||||||
isExporting: state.isExporting,
|
|
||||||
onExport: _exportGrns,
|
|
||||||
onSearch: ref.read(grnListProvider.notifier).setSearch,
|
onSearch: ref.read(grnListProvider.notifier).setSearch,
|
||||||
onStatusChanged:
|
onStatusChanged:
|
||||||
ref.read(grnListProvider.notifier).setStatusFilter,
|
ref.read(grnListProvider.notifier).setStatusFilter,
|
||||||
@ -197,9 +218,6 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
required this.onSearch,
|
required this.onSearch,
|
||||||
required this.onStatusChanged,
|
required this.onStatusChanged,
|
||||||
required this.onLocationChanged,
|
required this.onLocationChanged,
|
||||||
this.showExport = false,
|
|
||||||
this.isExporting = false,
|
|
||||||
this.onExport,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController searchController;
|
final TextEditingController searchController;
|
||||||
@ -208,9 +226,6 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
final ValueChanged<String> onSearch;
|
final ValueChanged<String> onSearch;
|
||||||
final ValueChanged<String?> onStatusChanged;
|
final ValueChanged<String?> onStatusChanged;
|
||||||
final ValueChanged<int?> onLocationChanged;
|
final ValueChanged<int?> onLocationChanged;
|
||||||
final bool showExport;
|
|
||||||
final bool isExporting;
|
|
||||||
final VoidCallback? onExport;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -263,19 +278,6 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
return AppResponsiveFilterBar(
|
return AppResponsiveFilterBar(
|
||||||
search: searchField,
|
search: searchField,
|
||||||
filters: filters,
|
filters: filters,
|
||||||
trailing: showExport
|
|
||||||
? OutlinedButton.icon(
|
|
||||||
onPressed: isExporting ? null : onExport,
|
|
||||||
icon: isExporting
|
|
||||||
? const SizedBox(
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.download_outlined, size: 18),
|
|
||||||
label: Text(isExporting ? 'Exporting...' : 'Export'),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import '../../../../shared/widgets/app_empty_state.dart';
|
|||||||
import '../../../../shared/widgets/app_loading_view.dart';
|
import '../../../../shared/widgets/app_loading_view.dart';
|
||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_search_export_bar.dart';
|
import '../../../../shared/widgets/app_search_export_bar.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_status_chip.dart';
|
import '../../../../shared/widgets/app_status_chip.dart';
|
||||||
import '../../../../shared/widgets/app_table_action_icon.dart';
|
import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||||
import '../../../../shared/widgets/app_table_shell.dart';
|
import '../../../../shared/widgets/app_table_shell.dart';
|
||||||
@ -35,6 +36,7 @@ class MasterListScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _MasterListScreenState extends ConsumerState<MasterListScreen> {
|
class _MasterListScreenState extends ConsumerState<MasterListScreen> {
|
||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
|
bool _filtersExpanded = false;
|
||||||
|
|
||||||
MasterDefinition get _definition {
|
MasterDefinition get _definition {
|
||||||
final def = masterDefinitionById(widget.masterId);
|
final def = masterDefinitionById(widget.masterId);
|
||||||
@ -173,6 +175,27 @@ class _MasterListScreenState extends ConsumerState<MasterListScreen> {
|
|||||||
title: def.title,
|
title: def.title,
|
||||||
subtitle: def.subtitle,
|
subtitle: def.subtitle,
|
||||||
actions: [
|
actions: [
|
||||||
|
AppSearchFilterButton(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _filtersExpanded = !_filtersExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (canExport) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: state.isExporting ? null : _exportRecords,
|
||||||
|
icon: state.isExporting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.download_outlined, size: 18),
|
||||||
|
label: Text(state.isExporting ? 'Exporting...' : 'Export'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(width: 8),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () => context.push(RouteConstants.masterData),
|
onPressed: () => context.push(RouteConstants.masterData),
|
||||||
icon: const Icon(Icons.grid_view_outlined),
|
icon: const Icon(Icons.grid_view_outlined),
|
||||||
@ -189,11 +212,11 @@ class _MasterListScreenState extends ConsumerState<MasterListScreen> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AppTableShell(
|
child: AppTableShell(
|
||||||
|
toolbarExpanded: _filtersExpanded,
|
||||||
toolbar: AppSearchExportBar(
|
toolbar: AppSearchExportBar(
|
||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
searchHint: _searchHint(def),
|
searchHint: _searchHint(def),
|
||||||
isExporting: state.isExporting,
|
showExport: false,
|
||||||
showExport: canExport,
|
|
||||||
onSearch: notifier.setSearch,
|
onSearch: notifier.setSearch,
|
||||||
onExport: _exportRecords,
|
onExport: _exportRecords,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import '../../../../shared/widgets/app_loading_view.dart';
|
|||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
||||||
import '../../../../shared/widgets/app_search_field.dart';
|
import '../../../../shared/widgets/app_search_field.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||||
import '../../../../shared/widgets/can_permission.dart';
|
import '../../../../shared/widgets/can_permission.dart';
|
||||||
import '../../../../shared/widgets/app_table_action_icon.dart';
|
import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||||
@ -46,6 +47,7 @@ class PurchaseOrderListScreen extends ConsumerStatefulWidget {
|
|||||||
class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScreen> {
|
class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScreen> {
|
||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
final Map<String, String> _knownStatuses = {};
|
final Map<String, String> _knownStatuses = {};
|
||||||
|
bool _filtersExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -122,7 +124,28 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
|||||||
? 'Purchase orders awaiting approval'
|
? 'Purchase orders awaiting approval'
|
||||||
: 'Create, approve and track procurement orders',
|
: 'Create, approve and track procurement orders',
|
||||||
actions: [
|
actions: [
|
||||||
if (!pendingOnly)
|
AppSearchFilterButton(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _filtersExpanded = !_filtersExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (canExport) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: state.isExporting ? null : _exportPurchaseOrders,
|
||||||
|
icon: state.isExporting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.download_outlined, size: 18),
|
||||||
|
label: Text(state.isExporting ? 'Exporting...' : 'Export'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (!pendingOnly) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
CanPermission(
|
CanPermission(
|
||||||
module: 'purchase_orders',
|
module: 'purchase_orders',
|
||||||
action: PermissionAction.create,
|
action: PermissionAction.create,
|
||||||
@ -134,17 +157,16 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AppTableShell(
|
child: AppTableShell(
|
||||||
|
toolbarExpanded: _filtersExpanded,
|
||||||
toolbar: _FiltersBar(
|
toolbar: _FiltersBar(
|
||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
query: state.query,
|
query: state.query,
|
||||||
statusOptions: _statusFilterOptions(state.orders),
|
statusOptions: _statusFilterOptions(state.orders),
|
||||||
showStatusFilter: !pendingOnly,
|
showStatusFilter: !pendingOnly,
|
||||||
showExport: canExport,
|
|
||||||
isExporting: state.isExporting,
|
|
||||||
onExport: _exportPurchaseOrders,
|
|
||||||
onSearch: pendingOnly
|
onSearch: pendingOnly
|
||||||
? ref
|
? ref
|
||||||
.read(
|
.read(
|
||||||
@ -361,9 +383,6 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
required this.onSearch,
|
required this.onSearch,
|
||||||
required this.onStatusChanged,
|
required this.onStatusChanged,
|
||||||
this.showStatusFilter = true,
|
this.showStatusFilter = true,
|
||||||
this.showExport = false,
|
|
||||||
this.isExporting = false,
|
|
||||||
this.onExport,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController searchController;
|
final TextEditingController searchController;
|
||||||
@ -372,9 +391,6 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
final ValueChanged<String> onSearch;
|
final ValueChanged<String> onSearch;
|
||||||
final ValueChanged<String?> onStatusChanged;
|
final ValueChanged<String?> onStatusChanged;
|
||||||
final bool showStatusFilter;
|
final bool showStatusFilter;
|
||||||
final bool showExport;
|
|
||||||
final bool isExporting;
|
|
||||||
final VoidCallback? onExport;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -399,19 +415,6 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
return AppResponsiveFilterBar(
|
return AppResponsiveFilterBar(
|
||||||
search: searchField,
|
search: searchField,
|
||||||
filters: filters,
|
filters: filters,
|
||||||
trailing: showExport
|
|
||||||
? OutlinedButton.icon(
|
|
||||||
onPressed: isExporting ? null : onExport,
|
|
||||||
icon: isExporting
|
|
||||||
? const SizedBox(
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.download_outlined, size: 18),
|
|
||||||
label: Text(isExporting ? 'Exporting...' : 'Export'),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,10 +18,10 @@ import '../../../../shared/widgets/app_dropdown.dart';
|
|||||||
import '../../../../shared/widgets/app_card.dart';
|
import '../../../../shared/widgets/app_card.dart';
|
||||||
import '../../../../shared/widgets/app_data_table.dart';
|
import '../../../../shared/widgets/app_data_table.dart';
|
||||||
import '../../../../shared/widgets/app_loading_view.dart';
|
import '../../../../shared/widgets/app_loading_view.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||||
import '../../../../shared/widgets/error_view.dart';
|
import '../../../../shared/widgets/error_view.dart';
|
||||||
import '../../../users/presentation/providers/users_provider.dart';
|
import '../../../users/presentation/providers/users_provider.dart';
|
||||||
import '../../domain/entities/rbac_entities.dart';
|
|
||||||
import '../providers/add_user_form_provider.dart';
|
import '../providers/add_user_form_provider.dart';
|
||||||
import '../providers/role_form_provider.dart';
|
import '../providers/role_form_provider.dart';
|
||||||
import '../providers/rbac_provider.dart';
|
import '../providers/rbac_provider.dart';
|
||||||
@ -51,6 +51,8 @@ class UsersRoleManagementScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _UsersRoleManagementScreenState
|
class _UsersRoleManagementScreenState
|
||||||
extends ConsumerState<UsersRoleManagementScreen> {
|
extends ConsumerState<UsersRoleManagementScreen> {
|
||||||
|
bool _userFiltersExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -233,6 +235,16 @@ class _UsersRoleManagementScreenState
|
|||||||
canViewUsers: canViewUsers,
|
canViewUsers: canViewUsers,
|
||||||
canViewRoles: canViewRoles,
|
canViewRoles: canViewRoles,
|
||||||
canEditRoles: canEditRoles,
|
canEditRoles: canEditRoles,
|
||||||
|
usersFiltersExpanded: _userFiltersExpanded,
|
||||||
|
onToggleUsersFilters: () => setState(
|
||||||
|
() => _userFiltersExpanded = !_userFiltersExpanded,
|
||||||
|
),
|
||||||
|
onSelectTab: (tab) {
|
||||||
|
if (tab != RbacTab.users && _userFiltersExpanded) {
|
||||||
|
setState(() => _userFiltersExpanded = false);
|
||||||
|
}
|
||||||
|
ref.read(rbacProvider.notifier).setTab(tab);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -246,6 +258,7 @@ class _UsersRoleManagementScreenState
|
|||||||
children: [
|
children: [
|
||||||
if (canViewUsers)
|
if (canViewUsers)
|
||||||
_UsersTab(
|
_UsersTab(
|
||||||
|
filtersExpanded: _userFiltersExpanded,
|
||||||
onAddUser: _openAddUser,
|
onAddUser: _openAddUser,
|
||||||
onEditUser: (user) => _openUserPanel(userId: user.id),
|
onEditUser: (user) => _openUserPanel(userId: user.id),
|
||||||
),
|
),
|
||||||
@ -360,6 +373,9 @@ class _TabBar extends ConsumerWidget {
|
|||||||
required this.canViewUsers,
|
required this.canViewUsers,
|
||||||
required this.canViewRoles,
|
required this.canViewRoles,
|
||||||
required this.canEditRoles,
|
required this.canEditRoles,
|
||||||
|
required this.usersFiltersExpanded,
|
||||||
|
required this.onToggleUsersFilters,
|
||||||
|
required this.onSelectTab,
|
||||||
});
|
});
|
||||||
|
|
||||||
final RbacState state;
|
final RbacState state;
|
||||||
@ -368,10 +384,22 @@ class _TabBar extends ConsumerWidget {
|
|||||||
final bool canViewUsers;
|
final bool canViewUsers;
|
||||||
final bool canViewRoles;
|
final bool canViewRoles;
|
||||||
final bool canEditRoles;
|
final bool canEditRoles;
|
||||||
|
final bool usersFiltersExpanded;
|
||||||
|
final VoidCallback onToggleUsersFilters;
|
||||||
|
final ValueChanged<RbacTab> onSelectTab;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return SingleChildScrollView(
|
final isUsersTab = state.selectedTab == RbacTab.users;
|
||||||
|
final usersState = ref.watch(usersListProvider).valueOrNull;
|
||||||
|
final canExport = ref.can('users', PermissionAction.export);
|
||||||
|
final isExporting = usersState?.isExporting ?? false;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@ -379,30 +407,81 @@ class _TabBar extends ConsumerWidget {
|
|||||||
_TabButton(
|
_TabButton(
|
||||||
label: 'Users ($userCount)',
|
label: 'Users ($userCount)',
|
||||||
icon: Icons.people_outline,
|
icon: Icons.people_outline,
|
||||||
selected: state.selectedTab == RbacTab.users,
|
selected: isUsersTab,
|
||||||
onTap: () => ref.read(rbacProvider.notifier).setTab(RbacTab.users),
|
onTap: () => onSelectTab(RbacTab.users),
|
||||||
),
|
),
|
||||||
if (canViewRoles)
|
if (canViewRoles)
|
||||||
_TabButton(
|
_TabButton(
|
||||||
label: 'Roles ($roleCount)',
|
label: 'Roles ($roleCount)',
|
||||||
icon: Icons.shield_outlined,
|
icon: Icons.shield_outlined,
|
||||||
selected: state.selectedTab == RbacTab.roles,
|
selected: state.selectedTab == RbacTab.roles,
|
||||||
onTap: () => ref.read(rbacProvider.notifier).setTab(RbacTab.roles),
|
onTap: () => onSelectTab(RbacTab.roles),
|
||||||
),
|
),
|
||||||
if (canEditRoles)
|
if (canEditRoles)
|
||||||
_TabButton(
|
_TabButton(
|
||||||
label: 'Permission Matrix',
|
label: 'Permission Matrix',
|
||||||
icon: Icons.vpn_key_outlined,
|
icon: Icons.vpn_key_outlined,
|
||||||
selected: state.selectedTab == RbacTab.permissions,
|
selected: state.selectedTab == RbacTab.permissions,
|
||||||
onTap: () =>
|
onTap: () => onSelectTab(RbacTab.permissions),
|
||||||
ref.read(rbacProvider.notifier).setTab(RbacTab.permissions),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isUsersTab) ...[
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
AppSearchFilterButton(
|
||||||
|
expanded: usersFiltersExpanded,
|
||||||
|
onPressed: onToggleUsersFilters,
|
||||||
|
),
|
||||||
|
if (canExport) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: isExporting
|
||||||
|
? null
|
||||||
|
: () => _exportUsersFromTabBar(context, ref),
|
||||||
|
icon: isExporting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.download_outlined, size: 18),
|
||||||
|
label: Text(isExporting ? 'Exporting...' : 'Export'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _exportUsersFromTabBar(BuildContext context, WidgetRef ref) async {
|
||||||
|
final file = await ref.read(usersListProvider.notifier).exportUsers();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
final error = ref.read(usersListProvider).valueOrNull?.actionError;
|
||||||
|
if (error != null) {
|
||||||
|
showAppToastFromSnackBar(context, SnackBar(content: Text(error)));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final saved = await downloadFile(
|
||||||
|
bytes: file.bytes,
|
||||||
|
fileName: file.fileName,
|
||||||
|
);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
showAppToastFromSnackBar(
|
||||||
|
context,
|
||||||
|
SnackBar(
|
||||||
|
content: Text(saved ? 'Downloaded ${file.fileName}' : 'Export cancelled'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class _TabButton extends StatelessWidget {
|
class _TabButton extends StatelessWidget {
|
||||||
const _TabButton({
|
const _TabButton({
|
||||||
required this.label,
|
required this.label,
|
||||||
@ -458,10 +537,12 @@ class _TabButton extends StatelessWidget {
|
|||||||
|
|
||||||
class _UsersTab extends ConsumerStatefulWidget {
|
class _UsersTab extends ConsumerStatefulWidget {
|
||||||
const _UsersTab({
|
const _UsersTab({
|
||||||
|
required this.filtersExpanded,
|
||||||
required this.onAddUser,
|
required this.onAddUser,
|
||||||
required this.onEditUser,
|
required this.onEditUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final bool filtersExpanded;
|
||||||
final VoidCallback onAddUser;
|
final VoidCallback onAddUser;
|
||||||
final void Function(ManagedUserModel user) onEditUser;
|
final void Function(ManagedUserModel user) onEditUser;
|
||||||
|
|
||||||
@ -674,7 +755,6 @@ class _UsersTabState extends ConsumerState<_UsersTab> {
|
|||||||
),
|
),
|
||||||
data: (usersState) {
|
data: (usersState) {
|
||||||
final filters = usersState.filters;
|
final filters = usersState.filters;
|
||||||
final canExport = ref.can('users', PermissionAction.export);
|
|
||||||
final canEditUser = ref.can('users', PermissionAction.update);
|
final canEditUser = ref.can('users', PermissionAction.update);
|
||||||
final canDeleteUser = ref.can('users', PermissionAction.delete);
|
final canDeleteUser = ref.can('users', PermissionAction.delete);
|
||||||
final roles = ['All Roles', ...?filters?.roles.map((r) => r.name)];
|
final roles = ['All Roles', ...?filters?.roles.map((r) => r.name)];
|
||||||
@ -714,7 +794,8 @@ class _UsersTabState extends ConsumerState<_UsersTab> {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
AppCollapsibleFilterPanel(
|
||||||
|
expanded: widget.filtersExpanded,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: _UsersFilterBar(
|
child: _UsersFilterBar(
|
||||||
roleFilter: roleFilter,
|
roleFilter: roleFilter,
|
||||||
@ -723,11 +804,12 @@ class _UsersTabState extends ConsumerState<_UsersTab> {
|
|||||||
roles: roles,
|
roles: roles,
|
||||||
departments: departments,
|
departments: departments,
|
||||||
statuses: statuses,
|
statuses: statuses,
|
||||||
isExporting: usersState.isExporting,
|
showExport: false,
|
||||||
showExport: canExport,
|
|
||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
onExport: _exportUsers,
|
onExport: _exportUsers,
|
||||||
onSearch: ref.read(usersListProvider.notifier).setSearch,
|
isExporting: usersState.isExporting,
|
||||||
|
onSearch: (value) =>
|
||||||
|
ref.read(usersListProvider.notifier).setSearch(value),
|
||||||
onRoleChanged: (value) {
|
onRoleChanged: (value) {
|
||||||
ref.read(usersListProvider.notifier).setRoleFilter(
|
ref.read(usersListProvider.notifier).setRoleFilter(
|
||||||
value == 'All Roles'
|
value == 'All Roles'
|
||||||
@ -751,7 +833,6 @@ class _UsersTabState extends ConsumerState<_UsersTab> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
if (usersState.users.isEmpty)
|
if (usersState.users.isEmpty)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import '../../../../shared/widgets/app_loading_view.dart';
|
|||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
||||||
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_table_shell.dart';
|
import '../../../../shared/widgets/app_table_shell.dart';
|
||||||
import '../../../../shared/widgets/error_view.dart';
|
import '../../../../shared/widgets/error_view.dart';
|
||||||
import '../../../../shared/widgets/page_header.dart';
|
import '../../../../shared/widgets/page_header.dart';
|
||||||
@ -39,6 +40,7 @@ class DepreciationReportScreen extends ConsumerStatefulWidget {
|
|||||||
class _DepreciationReportScreenState
|
class _DepreciationReportScreenState
|
||||||
extends ConsumerState<DepreciationReportScreen> {
|
extends ConsumerState<DepreciationReportScreen> {
|
||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
|
bool _filtersExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -149,7 +151,14 @@ class _DepreciationReportScreenState
|
|||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (canExport)
|
AppSearchFilterButton(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _filtersExpanded = !_filtersExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (canExport) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: state.isExporting ? null : _export,
|
onPressed: state.isExporting ? null : _export,
|
||||||
icon: state.isExporting
|
icon: state.isExporting
|
||||||
@ -164,11 +173,13 @@ class _DepreciationReportScreenState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
_SummaryStrip(summary: state.summary, asOfDate: state.asOfDate),
|
_SummaryStrip(summary: state.summary, asOfDate: state.asOfDate),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AppTableShell(
|
child: AppTableShell(
|
||||||
|
toolbarExpanded: _filtersExpanded,
|
||||||
toolbar: _FiltersBar(
|
toolbar: _FiltersBar(
|
||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
filters: state.filters,
|
filters: state.filters,
|
||||||
@ -500,25 +511,37 @@ class _FiltersBarState extends State<_FiltersBar> {
|
|||||||
onClear: purchaseEmpty ? null : widget.onClearPurchaseRange,
|
onClear: purchaseEmpty ? null : widget.onClearPurchaseRange,
|
||||||
);
|
);
|
||||||
|
|
||||||
final moreButton = TextButton.icon(
|
final moreTooltip = moreCount > 0
|
||||||
onPressed: () => setState(() => _moreOpen = !_moreOpen),
|
|
||||||
icon: Icon(
|
|
||||||
_moreOpen ? Icons.expand_less : Icons.tune_outlined,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
label: Text(
|
|
||||||
moreCount > 0
|
|
||||||
? (_moreOpen ? 'Less filters ($moreCount)' : 'More filters ($moreCount)')
|
? (_moreOpen ? 'Less filters ($moreCount)' : 'More filters ($moreCount)')
|
||||||
: (_moreOpen ? 'Less filters' : 'More filters'),
|
: (_moreOpen ? 'Less filters' : 'More filters');
|
||||||
|
|
||||||
|
final iconColor = theme.colorScheme.primary;
|
||||||
|
|
||||||
|
final moreButton = IconButton(
|
||||||
|
tooltip: moreTooltip,
|
||||||
|
color: iconColor,
|
||||||
|
onPressed: () => setState(() => _moreOpen = !_moreOpen),
|
||||||
|
icon: Badge(
|
||||||
|
isLabelVisible: moreCount > 0,
|
||||||
|
label: Text('$moreCount'),
|
||||||
|
child: Icon(
|
||||||
|
_moreOpen ? Icons.expand_less : Icons.tune_outlined,
|
||||||
|
size: 20,
|
||||||
|
color: iconColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final reset = TextButton(
|
final reset = IconButton(
|
||||||
|
tooltip: 'Reset',
|
||||||
|
color: iconColor,
|
||||||
|
disabledColor: iconColor.withValues(alpha: 0.38),
|
||||||
onPressed: query.hasActiveFilter ? widget.onReset : null,
|
onPressed: query.hasActiveFilter ? widget.onReset : null,
|
||||||
child: const Text('Reset'),
|
icon: const Icon(Icons.restart_alt, size: 20),
|
||||||
);
|
);
|
||||||
|
|
||||||
final actions = Row(
|
final actions = Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
moreButton,
|
moreButton,
|
||||||
reset,
|
reset,
|
||||||
@ -550,10 +573,7 @@ class _FiltersBarState extends State<_FiltersBar> {
|
|||||||
|
|
||||||
return AppResponsiveFilterGrid(
|
return AppResponsiveFilterGrid(
|
||||||
fields: [searchField, location, category, asOfField],
|
fields: [searchField, location, category, asOfField],
|
||||||
footer: Align(
|
trailing: actions,
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: actions,
|
|
||||||
),
|
|
||||||
extra: moreFilters,
|
extra: moreFilters,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import '../../../../shared/widgets/app_empty_state.dart';
|
|||||||
import '../../../../shared/widgets/app_loading_view.dart';
|
import '../../../../shared/widgets/app_loading_view.dart';
|
||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_search_field.dart';
|
import '../../../../shared/widgets/app_search_field.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_table_shell.dart';
|
import '../../../../shared/widgets/app_table_shell.dart';
|
||||||
import '../../../../shared/widgets/app_table_action_icon.dart';
|
import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||||
import '../../../../shared/widgets/error_view.dart';
|
import '../../../../shared/widgets/error_view.dart';
|
||||||
@ -26,6 +27,7 @@ class RoleListScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _RoleListScreenState extends ConsumerState<RoleListScreen> {
|
class _RoleListScreenState extends ConsumerState<RoleListScreen> {
|
||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
|
bool _filtersExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -51,12 +53,21 @@ class _RoleListScreenState extends ConsumerState<RoleListScreen> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const PageHeader(
|
PageHeader(
|
||||||
title: 'Roles',
|
title: 'Roles',
|
||||||
subtitle: 'Manage roles and permission assignments',
|
subtitle: 'Manage roles and permission assignments',
|
||||||
|
actions: [
|
||||||
|
AppSearchFilterButton(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _filtersExpanded = !_filtersExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AppTableShell(
|
child: AppTableShell(
|
||||||
|
toolbarExpanded: _filtersExpanded,
|
||||||
toolbar: SizedBox(
|
toolbar: SizedBox(
|
||||||
width: context.isMobile ? double.infinity : 320,
|
width: context.isMobile ? double.infinity : 320,
|
||||||
child: AppSearchField(
|
child: AppSearchField(
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import '../../../../shared/widgets/app_loading_view.dart';
|
|||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
||||||
import '../../../../shared/widgets/app_search_field.dart';
|
import '../../../../shared/widgets/app_search_field.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||||
import '../../../../shared/widgets/app_status_chip.dart';
|
import '../../../../shared/widgets/app_status_chip.dart';
|
||||||
import '../../../../shared/widgets/error_view.dart';
|
import '../../../../shared/widgets/error_view.dart';
|
||||||
@ -34,6 +35,7 @@ class UserListScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _UserListScreenState extends ConsumerState<UserListScreen> {
|
class _UserListScreenState extends ConsumerState<UserListScreen> {
|
||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
|
bool _filtersExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -60,6 +62,13 @@ class _UserListScreenState extends ConsumerState<UserListScreen> {
|
|||||||
title: 'Users',
|
title: 'Users',
|
||||||
subtitle: 'Manage employee accounts',
|
subtitle: 'Manage employee accounts',
|
||||||
actions: [
|
actions: [
|
||||||
|
AppSearchFilterButton(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _filtersExpanded = !_filtersExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () => context.push(RouteConstants.userAdd),
|
onPressed: () => context.push(RouteConstants.userAdd),
|
||||||
icon: const Icon(Icons.person_add),
|
icon: const Icon(Icons.person_add),
|
||||||
@ -73,6 +82,7 @@ class _UserListScreenState extends ConsumerState<UserListScreen> {
|
|||||||
],
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AppTableShell(
|
child: AppTableShell(
|
||||||
|
toolbarExpanded: _filtersExpanded,
|
||||||
toolbar: _FiltersBar(
|
toolbar: _FiltersBar(
|
||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
filters: state.filters,
|
filters: state.filters,
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import '../../../../shared/widgets/app_loading_view.dart';
|
|||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
import '../../../../shared/widgets/app_responsive_filter_bar.dart';
|
||||||
import '../../../../shared/widgets/app_search_field.dart';
|
import '../../../../shared/widgets/app_search_field.dart';
|
||||||
|
import '../../../../shared/widgets/app_search_filter_toggle.dart';
|
||||||
import '../../../../shared/widgets/app_status_chip.dart';
|
import '../../../../shared/widgets/app_status_chip.dart';
|
||||||
import '../../../../shared/widgets/app_table_action_icon.dart';
|
import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||||
import '../../../../shared/widgets/can_permission.dart';
|
import '../../../../shared/widgets/can_permission.dart';
|
||||||
@ -38,6 +39,7 @@ class VendorListScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _VendorListScreenState extends ConsumerState<VendorListScreen> {
|
class _VendorListScreenState extends ConsumerState<VendorListScreen> {
|
||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
|
bool _filtersExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -78,6 +80,27 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
|
|||||||
title: 'Vendors',
|
title: 'Vendors',
|
||||||
subtitle: 'Manage suppliers, service providers and vendor master data',
|
subtitle: 'Manage suppliers, service providers and vendor master data',
|
||||||
actions: [
|
actions: [
|
||||||
|
AppSearchFilterButton(
|
||||||
|
expanded: _filtersExpanded,
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _filtersExpanded = !_filtersExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (canExport) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: state.isExporting ? null : _exportVendors,
|
||||||
|
icon: state.isExporting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.download_outlined, size: 18),
|
||||||
|
label: Text(state.isExporting ? 'Exporting...' : 'Export'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(width: 8),
|
||||||
CanPermission(
|
CanPermission(
|
||||||
module: 'vendors',
|
module: 'vendors',
|
||||||
action: PermissionAction.create,
|
action: PermissionAction.create,
|
||||||
@ -91,12 +114,10 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AppTableShell(
|
child: AppTableShell(
|
||||||
|
toolbarExpanded: _filtersExpanded,
|
||||||
toolbar: _FiltersBar(
|
toolbar: _FiltersBar(
|
||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
query: state.query,
|
query: state.query,
|
||||||
showExport: canExport,
|
|
||||||
isExporting: state.isExporting,
|
|
||||||
onExport: _exportVendors,
|
|
||||||
onSearch: ref.read(vendorsListProvider.notifier).setSearch,
|
onSearch: ref.read(vendorsListProvider.notifier).setSearch,
|
||||||
onStatusChanged:
|
onStatusChanged:
|
||||||
ref.read(vendorsListProvider.notifier).setStatusFilter,
|
ref.read(vendorsListProvider.notifier).setStatusFilter,
|
||||||
@ -208,9 +229,6 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
required this.onSearch,
|
required this.onSearch,
|
||||||
required this.onStatusChanged,
|
required this.onStatusChanged,
|
||||||
required this.onVendorTypeChanged,
|
required this.onVendorTypeChanged,
|
||||||
this.showExport = false,
|
|
||||||
this.isExporting = false,
|
|
||||||
this.onExport,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController searchController;
|
final TextEditingController searchController;
|
||||||
@ -218,9 +236,6 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
final ValueChanged<String> onSearch;
|
final ValueChanged<String> onSearch;
|
||||||
final ValueChanged<String?> onStatusChanged;
|
final ValueChanged<String?> onStatusChanged;
|
||||||
final ValueChanged<String?> onVendorTypeChanged;
|
final ValueChanged<String?> onVendorTypeChanged;
|
||||||
final bool showExport;
|
|
||||||
final bool isExporting;
|
|
||||||
final VoidCallback? onExport;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -262,19 +277,6 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
return AppResponsiveFilterBar(
|
return AppResponsiveFilterBar(
|
||||||
search: searchField,
|
search: searchField,
|
||||||
filters: filters,
|
filters: filters,
|
||||||
trailing: showExport
|
|
||||||
? OutlinedButton.icon(
|
|
||||||
onPressed: isExporting ? null : onExport,
|
|
||||||
icon: isExporting
|
|
||||||
? const SizedBox(
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.download_outlined, size: 18),
|
|
||||||
label: Text(isExporting ? 'Exporting...' : 'Export'),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,6 +149,7 @@ class AppResponsiveFilterGrid extends StatelessWidget {
|
|||||||
this.runSpacing = 12,
|
this.runSpacing = 12,
|
||||||
this.minFieldWidth = 160,
|
this.minFieldWidth = 160,
|
||||||
this.maxColumns = 4,
|
this.maxColumns = 4,
|
||||||
|
this.trailing,
|
||||||
this.footer,
|
this.footer,
|
||||||
this.extra,
|
this.extra,
|
||||||
});
|
});
|
||||||
@ -158,6 +159,8 @@ class AppResponsiveFilterGrid extends StatelessWidget {
|
|||||||
final double runSpacing;
|
final double runSpacing;
|
||||||
final double minFieldWidth;
|
final double minFieldWidth;
|
||||||
final int maxColumns;
|
final int maxColumns;
|
||||||
|
/// Placed at the end of the primary filter row (wide layouts).
|
||||||
|
final Widget? trailing;
|
||||||
final Widget? footer;
|
final Widget? footer;
|
||||||
final Widget? extra;
|
final Widget? extra;
|
||||||
|
|
||||||
@ -169,6 +172,7 @@ class AppResponsiveFilterGrid extends StatelessWidget {
|
|||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final maxWidth = constraints.maxWidth;
|
final maxWidth = constraints.maxWidth;
|
||||||
|
final hasTrailing = trailing != null;
|
||||||
|
|
||||||
Widget content;
|
Widget content;
|
||||||
if (maxWidth >= _rowBreakpoint && fields.length <= maxColumns) {
|
if (maxWidth >= _rowBreakpoint && fields.length <= maxColumns) {
|
||||||
@ -179,6 +183,13 @@ class AppResponsiveFilterGrid extends StatelessWidget {
|
|||||||
if (i > 0) SizedBox(width: spacing),
|
if (i > 0) SizedBox(width: spacing),
|
||||||
Expanded(child: fields[i]),
|
Expanded(child: fields[i]),
|
||||||
],
|
],
|
||||||
|
if (hasTrailing) ...[
|
||||||
|
SizedBox(width: spacing),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: trailing!,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else if (maxWidth >= _stackBreakpoint) {
|
} else if (maxWidth >= _stackBreakpoint) {
|
||||||
@ -191,9 +202,15 @@ class AppResponsiveFilterGrid extends StatelessWidget {
|
|||||||
content = Wrap(
|
content = Wrap(
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
runSpacing: runSpacing,
|
runSpacing: runSpacing,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
for (final field in fields)
|
for (final field in fields)
|
||||||
SizedBox(width: itemWidth, child: field),
|
SizedBox(width: itemWidth, child: field),
|
||||||
|
if (hasTrailing)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: trailing!,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -204,6 +221,10 @@ class AppResponsiveFilterGrid extends StatelessWidget {
|
|||||||
if (i > 0) SizedBox(height: runSpacing),
|
if (i > 0) SizedBox(height: runSpacing),
|
||||||
fields[i],
|
fields[i],
|
||||||
],
|
],
|
||||||
|
if (hasTrailing) ...[
|
||||||
|
SizedBox(height: runSpacing),
|
||||||
|
Align(alignment: Alignment.centerRight, child: trailing!),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
110
lib/shared/widgets/app_search_filter_toggle.dart
Normal file
110
lib/shared/widgets/app_search_filter_toggle.dart
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Header action that toggles the collapsible search/filter panel.
|
||||||
|
class AppSearchFilterButton extends StatelessWidget {
|
||||||
|
const AppSearchFilterButton({
|
||||||
|
super.key,
|
||||||
|
required this.expanded,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool expanded;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final activeColor = theme.colorScheme.primary;
|
||||||
|
|
||||||
|
return OutlinedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
expanded ? activeColor.withValues(alpha: 0.08) : null,
|
||||||
|
foregroundColor:
|
||||||
|
expanded ? activeColor : theme.colorScheme.onSurface,
|
||||||
|
side: BorderSide(
|
||||||
|
color: expanded
|
||||||
|
? activeColor.withValues(alpha: 0.45)
|
||||||
|
: theme.colorScheme.outline.withValues(alpha: 0.35),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.search, size: 18, color: expanded ? activeColor : null),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Icon(
|
||||||
|
Icons.filter_list,
|
||||||
|
size: 18,
|
||||||
|
color: expanded ? activeColor : null,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text('Search & Filter'),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Icon(
|
||||||
|
expanded ? Icons.expand_less : Icons.expand_more,
|
||||||
|
size: 18,
|
||||||
|
color: expanded ? activeColor : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animates a search/filter panel open/closed, pushing content below.
|
||||||
|
class AppCollapsibleFilterPanel extends StatelessWidget {
|
||||||
|
const AppCollapsibleFilterPanel({
|
||||||
|
super.key,
|
||||||
|
required this.expanded,
|
||||||
|
required this.child,
|
||||||
|
this.padding = const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||||
|
this.showDivider = true,
|
||||||
|
this.duration = const Duration(milliseconds: 280),
|
||||||
|
this.curve = Curves.easeInOutCubic,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool expanded;
|
||||||
|
final Widget child;
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
final bool showDivider;
|
||||||
|
final Duration duration;
|
||||||
|
final Curve curve;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ClipRect(
|
||||||
|
child: AnimatedAlign(
|
||||||
|
duration: duration,
|
||||||
|
curve: curve,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
heightFactor: expanded ? 1.0 : 0.0,
|
||||||
|
child: Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: duration,
|
||||||
|
switchInCurve: curve,
|
||||||
|
switchOutCurve: curve,
|
||||||
|
transitionBuilder: (child, animation) => SizeTransition(
|
||||||
|
sizeFactor: animation,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: expanded && showDivider
|
||||||
|
? const Divider(key: ValueKey('filter-divider'), height: 1)
|
||||||
|
: const SizedBox.shrink(key: ValueKey('filter-divider-gone')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'app_card.dart';
|
import 'app_card.dart';
|
||||||
|
import 'app_search_filter_toggle.dart';
|
||||||
|
|
||||||
/// Card shell for list pages: toolbar, divider, full-width table body, footer.
|
/// Card shell for list pages: toolbar, divider, full-width table body, footer.
|
||||||
class AppTableShell extends StatelessWidget {
|
class AppTableShell extends StatelessWidget {
|
||||||
@ -9,12 +10,16 @@ class AppTableShell extends StatelessWidget {
|
|||||||
required this.toolbar,
|
required this.toolbar,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.footer,
|
this.footer,
|
||||||
|
this.toolbarExpanded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget toolbar;
|
final Widget toolbar;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Widget? footer;
|
final Widget? footer;
|
||||||
|
|
||||||
|
/// When false (default), the search/filter toolbar is collapsed.
|
||||||
|
final bool toolbarExpanded;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@ -32,11 +37,10 @@ class AppTableShell extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
AppCollapsibleFilterPanel(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
expanded: toolbarExpanded,
|
||||||
child: toolbar,
|
child: toolbar,
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
// Clip so scrolling rows cannot paint over the footer/pagination.
|
// Clip so scrolling rows cannot paint over the footer/pagination.
|
||||||
Expanded(child: ClipRect(child: child)),
|
Expanded(child: ClipRect(child: child)),
|
||||||
if (footer != null) ...[
|
if (footer != null) ...[
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user