Line data Source code
1 : // Copyright 2025 The terCAD team. All rights reserved.
2 : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.
3 :
4 : import 'package:app_finance/_classes/controller/iterator_controller.dart';
5 : import 'package:app_finance/_classes/controller/focus_controller.dart';
6 : import 'package:app_finance/_classes/herald/app_design.dart';
7 : import 'package:app_finance/_classes/herald/app_locale.dart';
8 : import 'package:app_finance/_classes/storage/app_data.dart';
9 : import 'package:app_finance/_classes/structure/bill_app_data.dart';
10 : import 'package:app_finance/_configs/account_type.dart';
11 : import 'package:app_finance/_configs/screen_helper.dart';
12 : import 'package:app_finance/_configs/theme_helper.dart';
13 : import 'package:app_finance/_ext/build_context_ext.dart';
14 : import 'package:app_finance/design/form/date_range_input.dart';
15 : import 'package:app_finance/design/form/list_account_selector.dart';
16 : import 'package:app_finance/design/form/simple_input.dart';
17 : import 'package:app_finance/design/wrapper/input_wrapper.dart';
18 : import 'package:app_finance/design/wrapper/row_widget.dart';
19 : import 'package:app_finance/design/wrapper/single_scroll_wrapper.dart';
20 : import 'package:app_finance/design/wrapper/text_wrapper.dart';
21 : import 'package:app_finance/pages/bill/bill_page.dart';
22 : import 'package:material_ui/material_ui.dart';
23 : import 'package:flutter_currency_picker/flutter_currency_picker.dart';
24 :
25 : class BillSearchPage extends StatefulWidget {
26 1 : const BillSearchPage({super.key});
27 :
28 1 : @override
29 1 : BillViewPageState createState() => BillViewPageState();
30 : }
31 :
32 : class BillViewPageState extends BillPageState<BillSearchPage> {
33 : String? account;
34 : String? budget;
35 : String? type;
36 : Currency? currency;
37 : DateTime? startDate;
38 : DateTime? endDate;
39 : late TextEditingController description;
40 : late TextEditingController amountFrom;
41 : late TextEditingController amountTo;
42 : late FocusController focus;
43 1 : late List<ListAccountSelectorItem> accountList =
44 6 : state.getList(AppDataType.accounts).map((item) => ListAccountSelectorItem(item: item)).toList();
45 1 : late List<ListAccountSelectorItem> budgetList =
46 6 : state.getList(AppDataType.budgets).map((item) => ListAccountSelectorItem(item: item)).toList();
47 :
48 1 : @override
49 : void initState() {
50 2 : description = TextEditingController();
51 2 : amountFrom = TextEditingController();
52 2 : amountTo = TextEditingController();
53 2 : focus = FocusController();
54 1 : super.initState();
55 : }
56 :
57 1 : @override
58 : dispose() {
59 2 : description.dispose();
60 2 : amountFrom.dispose();
61 2 : amountTo.dispose();
62 2 : focus.dispose();
63 1 : super.dispose();
64 : }
65 :
66 1 : @override
67 2 : String getTitle() => AppLocale.labels.searchTooltip;
68 :
69 1 : @override
70 2 : String getButtonName() => AppLocale.labels.searchTooltip;
71 :
72 1 : @override
73 : Widget buildButton(BuildContext context, BoxConstraints constraints) {
74 1 : return FloatingActionButton(
75 : heroTag: 'bill_search_page',
76 3 : onPressed: () => setState(clearState),
77 1 : tooltip: getButtonName(),
78 : child: const Icon(Icons.search),
79 : );
80 : }
81 :
82 1 : bool getContentFilter(BillAppData item) {
83 6 : final descriptionMatch = item.title.toLowerCase().contains(description.text.toLowerCase());
84 4 : final accountMatch = account == null || item.account == account;
85 4 : final budgetMatch = budget == null || item.category == budget;
86 1 : final typeMatch = type == null || state.getByUuid(item.account)?.type == type;
87 1 : final currencyMatch = currency == null || item.currency == currency;
88 1 : final startDateMatch = startDate == null || item.createdAt.isAfter(startDate!);
89 1 : final endDateMatch = endDate == null || item.createdAt.isBefore(endDate!);
90 3 : final amountFromMatch = amountFrom.text.isEmpty || item.details >= double.tryParse(amountFrom.text)!;
91 3 : final amountToMatch = amountTo.text.isEmpty || item.details <= double.tryParse(amountTo.text)!;
92 : return !(descriptionMatch &&
93 : accountMatch &&
94 : budgetMatch &&
95 : typeMatch &&
96 : currencyMatch &&
97 : startDateMatch &&
98 : endDateMatch &&
99 : amountFromMatch &&
100 : amountToMatch);
101 : }
102 :
103 1 : @override
104 3 : InterfaceIterator getContentStream() => state.getStream<BillAppData>(AppDataType.bills, filter: getContentFilter);
105 :
106 1 : @override
107 : Widget addHeaderWidget() {
108 3 : final width = ScreenHelper.state().width - ThemeHelper.getIndent(4);
109 1 : final indent = ThemeHelper.getIndent(2);
110 1 : return SliverToBoxAdapter(
111 1 : child: SingleScrollWrapper(
112 1 : controller: focus,
113 1 : child: Column(
114 1 : crossAxisAlignment: AppDesign.getAlignment(),
115 1 : children: [
116 1 : InputWrapper.text(
117 2 : title: AppLocale.labels.description,
118 1 : controller: description,
119 2 : tooltip: AppLocale.labels.descriptionTooltip,
120 : ),
121 1 : RowWidget(
122 : indent: indent,
123 1 : maxWidth: width + indent,
124 : chunk: const [0.5, 0.5],
125 1 : children: [
126 1 : [
127 1 : InputWrapper.text(
128 2 : title: AppLocale.labels.amountFrom,
129 2 : tooltip: AppLocale.labels.billSetTooltip,
130 1 : controller: amountFrom,
131 : inputType: const TextInputType.numberWithOptions(decimal: true),
132 1 : formatter: [
133 1 : SimpleInputFormatter.filterDouble,
134 : ],
135 : ),
136 : ],
137 1 : [
138 1 : InputWrapper.text(
139 2 : title: AppLocale.labels.amountTo,
140 2 : tooltip: AppLocale.labels.billSetTooltip,
141 1 : controller: amountTo,
142 : inputType: const TextInputType.numberWithOptions(decimal: true),
143 1 : formatter: [
144 1 : SimpleInputFormatter.filterDouble,
145 : ],
146 : ),
147 : ],
148 : ],
149 : ),
150 1 : InputWrapper.select(
151 1 : value: type,
152 2 : title: AppLocale.labels.accountType,
153 2 : tooltip: AppLocale.labels.accountTypeTooltip,
154 1 : options: AccountType.getList(),
155 0 : onChange: (value) => setState(() {
156 0 : if (value != type) clearState();
157 0 : type = value;
158 : }),
159 : ),
160 1 : InputWrapper(
161 : type: NamedInputType.accountSelector,
162 4 : value: account != null ? state.getByUuid(account!) : null,
163 2 : title: AppLocale.labels.account,
164 2 : tooltip: AppLocale.labels.titleAccountTooltip,
165 1 : state: state,
166 1 : options: accountList,
167 3 : onChange: (value) => setState(() {
168 4 : if (value?.uuid != account) clearState();
169 2 : account = value?.uuid;
170 : }),
171 : width: width,
172 : ),
173 1 : InputWrapper(
174 : type: NamedInputType.budgetSelector,
175 4 : value: budget != null ? state.getByUuid(budget!) : null,
176 2 : title: AppLocale.labels.budget,
177 2 : tooltip: AppLocale.labels.titleBudgetTooltip,
178 1 : state: state,
179 1 : options: budgetList,
180 3 : onChange: (value) => setState(() {
181 4 : if (value?.uuid != budget) clearState();
182 2 : budget = value?.uuid;
183 : }),
184 : width: width,
185 : ),
186 1 : InputWrapper.currency(
187 1 : value: currency,
188 2 : title: AppLocale.labels.currency,
189 2 : tooltip: AppLocale.labels.currencyTooltip,
190 0 : onChange: (value) => setState(() {
191 0 : if (value != currency) clearState();
192 0 : currency = value;
193 : }),
194 : ),
195 1 : TextWrapper(
196 2 : AppLocale.labels.dateRange,
197 3 : style: context.textTheme.bodyLarge,
198 : ),
199 1 : DateRangeInput(
200 1 : valueFrom: startDate,
201 1 : value: endDate,
202 0 : setState: (start, end) => setState(() {
203 0 : startDate = start;
204 0 : endDate = end;
205 0 : clearState();
206 : }),
207 : ),
208 : ],
209 : ),
210 : ),
211 : );
212 : }
213 : }
|