Line data Source code
1 : // Copyright 2023 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/delayed_call.dart';
5 : import 'package:app_finance/_classes/controller/exchange_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/structure/currency/exchange.dart';
9 : import 'package:app_finance/_classes/structure/bill_app_data.dart';
10 : import 'package:app_finance/_classes/controller/focus_controller.dart';
11 : import 'package:app_finance/_classes/storage/app_preferences.dart';
12 : import 'package:app_finance/_classes/storage/app_data.dart';
13 : import 'package:app_finance/_configs/account_type.dart';
14 : import 'package:app_finance/_configs/theme_helper.dart';
15 : import 'package:app_finance/_configs/screen_helper.dart';
16 : import 'package:app_finance/_ext/build_context_ext.dart';
17 : import 'package:app_finance/design/button/toolbar_button_widget.dart';
18 : import 'package:app_finance/design/wrapper/input_wrapper.dart';
19 : import 'package:app_finance/pages/_interfaces/interface_page_inject.dart';
20 : import 'package:app_finance/design/form/currency_exchange_input.dart';
21 : import 'package:app_finance/design/form/date_time_input.dart';
22 : import 'package:app_finance/design/button/full_sized_button_widget.dart';
23 : import 'package:app_finance/design/form/list_account_selector.dart';
24 : import 'package:app_finance/design/form/simple_input.dart';
25 : import 'package:app_finance/design/wrapper/row_widget.dart';
26 : import 'package:app_finance/design/wrapper/single_scroll_wrapper.dart';
27 : import 'package:material_ui/material_ui.dart';
28 : import 'package:flutter_currency_picker/flutter_currency_picker.dart';
29 : import 'package:flutter_solidart/flutter_solidart.dart';
30 :
31 : class ExpensesTab<T> extends StatefulWidget {
32 : final String? account;
33 : final String? budget;
34 : final Currency? currency;
35 : final double? bill;
36 : final String? description;
37 : final DateTime? createdAt;
38 : final AppData state;
39 : final bool isLeft;
40 : final FnPageCallback callback;
41 :
42 1 : const ExpensesTab({
43 : super.key,
44 : required this.state,
45 : required this.callback,
46 : this.account,
47 : this.budget,
48 : this.currency,
49 : this.bill,
50 : this.description,
51 : this.createdAt,
52 : this.isLeft = false,
53 : });
54 :
55 1 : @override
56 1 : ExpensesTabState createState() => ExpensesTabState();
57 : }
58 :
59 : class ExpensesTabState<T extends ExpensesTab> extends State<T> {
60 : late FocusController focus;
61 : String? account;
62 : Currency? accountCurrency;
63 : String? budget;
64 : Currency? budgetCurrency;
65 : Currency? currency;
66 : late TextEditingController bill;
67 : final billSignal = Signal<String>('');
68 : late TextEditingController description;
69 : final descriptionSignal = Signal<String>('');
70 : late ExchangeController exchange;
71 : DateTime? createdAt;
72 : bool hasErrors = false;
73 : bool isPushed = false;
74 3 : late List<ListAccountSelectorItem> accountList = widget.state
75 1 : .getList(AppDataType.accounts)
76 7 : .where((e) => ![AppAccountType.deposit.toString(), AppAccountType.credit.toString()].contains(e.type))
77 3 : .map((item) => ListAccountSelectorItem(item: item))
78 1 : .toList();
79 :
80 1 : @override
81 : void initState() {
82 2 : focus = FocusController();
83 1 : final accountId = AppPreferences.get(AppPreferences.prefAccount);
84 3 : final objAccount = widget.state.getByUuid(accountId ?? '');
85 1 : final budgetId = AppPreferences.get(AppPreferences.prefBudget);
86 3 : final objBudget = widget.state.getByUuid(budgetId ?? '');
87 3 : account = widget.account ?? objAccount?.uuid;
88 3 : budget = widget.budget ?? objBudget?.uuid;
89 3 : currency = widget.currency ?? objAccount?.currency ?? objBudget?.currency ?? Exchange.defaultCurrency;
90 4 : bill = TextEditingController(text: widget.bill != null ? widget.bill.toString() : '');
91 1 : final billTimer = DelayedCall(1000);
92 9 : bill.addListener(() => billTimer.run(() => billSignal.value = bill.text));
93 4 : description = TextEditingController(text: widget.description);
94 1 : final descriptionTimer = DelayedCall(1000);
95 9 : description.addListener(() => descriptionTimer.run(() => descriptionSignal.value = description.text));
96 3 : createdAt = widget.createdAt;
97 5 : accountCurrency = widget.state.getByUuid(account ?? '')?.currency;
98 5 : budgetCurrency = widget.state.getByUuid(budget ?? '')?.currency;
99 2 : exchange = ExchangeController(
100 1 : {},
101 2 : store: widget.state,
102 1 : targetController: bill,
103 1 : target: currency,
104 3 : source: [accountCurrency, budgetCurrency, Exchange.defaultCurrency],
105 : );
106 :
107 3 : widget.callback((
108 1 : buildButton: buildButton,
109 1 : buttonName: getButtonName(),
110 1 : title: getTitle(),
111 : ));
112 1 : super.initState();
113 : }
114 :
115 1 : @override
116 : dispose() {
117 1 : isPushed = false;
118 2 : description.dispose();
119 2 : exchange.dispose();
120 2 : bill.dispose();
121 2 : focus.dispose();
122 1 : super.dispose();
123 : }
124 :
125 3 : String getTitle() => AppLocale.labels.createBillHeader;
126 :
127 6 : bool hasFormErrors() => account == null || budget == null || bill.text.isEmpty;
128 :
129 1 : void updateStorage() {
130 2 : AppPreferences.set(AppPreferences.prefAccount, account ?? '');
131 2 : AppPreferences.set(AppPreferences.prefBudget, budget ?? '');
132 1 : if (currency != null) {
133 2 : CurrencyProvider.pin(currency!);
134 : }
135 2 : exchange.save();
136 4 : widget.state.add(getValues());
137 : }
138 :
139 2 : BillAppData getValues() => BillAppData(
140 1 : account: account ?? '',
141 1 : category: budget ?? '',
142 1 : currency: currency,
143 2 : title: description.text,
144 3 : details: double.tryParse(bill.text) ?? 0.0,
145 2 : createdAt: createdAt ?? DateTime.now(),
146 : );
147 :
148 3 : String getButtonName() => AppLocale.labels.createBillTooltip;
149 :
150 1 : Widget buildButton(BuildContext context, BoxConstraints constraints) {
151 1 : NavigatorState nav = Navigator.of(context);
152 1 : return FullSizedButtonWidget(
153 : constraints: constraints,
154 1 : controller: focus,
155 1 : onPressed: () {
156 1 : if (!mounted) {
157 : return;
158 : }
159 1 : if (hasFormErrors()) {
160 0 : setState(() => hasErrors = true);
161 : return;
162 : }
163 1 : updateStorage();
164 1 : nav.pop();
165 : },
166 1 : title: getButtonName(),
167 : icon: Icons.save,
168 : );
169 : }
170 :
171 1 : @override
172 : Widget build(BuildContext context) {
173 1 : final textTheme = context.textTheme;
174 1 : final indent = ThemeHelper.getIndent(2);
175 :
176 1 : return SingleScrollWrapper(
177 1 : controller: focus,
178 1 : child: Container(
179 1 : margin: EdgeInsets.fromLTRB(indent, indent, indent, 240),
180 2 : child: LayoutBuilder(builder: (context, constraints) {
181 1 : double width = constraints.maxWidth;
182 1 : final display = ScreenHelper.getInstance(context, constraints);
183 1 : return Column(
184 1 : crossAxisAlignment: AppDesign.getAlignment(),
185 1 : children: [
186 1 : InputWrapper(
187 : type: NamedInputType.accountSelector,
188 : isRequired: true,
189 5 : value: account != null ? widget.state.getByUuid(account!) : null,
190 2 : title: AppLocale.labels.account,
191 2 : tooltip: AppLocale.labels.titleAccountTooltip,
192 1 : showError: hasErrors && account == null,
193 2 : state: widget.state,
194 1 : options: accountList,
195 3 : onChange: (value) => setState(() {
196 2 : account = value?.uuid;
197 : if (value != null) {
198 2 : accountCurrency = value.currency;
199 2 : currency = accountCurrency;
200 : }
201 : }),
202 : width: width,
203 : ),
204 1 : RowWidget(
205 : indent: indent,
206 : maxWidth: width,
207 : chunk: const [125, null],
208 1 : children: [
209 1 : [
210 1 : InputWrapper.currency(
211 : type: NamedInputType.currencyShort,
212 1 : value: currency,
213 2 : title: AppLocale.labels.currency,
214 2 : tooltip: AppLocale.labels.currencyTooltip,
215 4 : onChange: (value) => setState(() => currency = value),
216 : ),
217 : ],
218 1 : [
219 1 : InputWrapper.text(
220 2 : title: AppLocale.labels.expense,
221 : isRequired: true,
222 1 : controller: bill,
223 1 : showError: hasErrors && bill.text.isEmpty,
224 2 : tooltip: AppLocale.labels.billSetTooltip,
225 : inputType: const TextInputType.numberWithOptions(decimal: true),
226 2 : formatter: [SimpleInputFormatter.filterDouble],
227 : ),
228 : ],
229 : ],
230 : ),
231 1 : CurrencyExchangeInput(
232 5 : key: ValueKey('expense${currency?.code}${accountCurrency?.code}${budgetCurrency?.code}'),
233 1 : width: width + indent,
234 : indent: indent,
235 1 : target: currency,
236 1 : controller: exchange,
237 3 : source: [accountCurrency, budgetCurrency, Exchange.defaultCurrency],
238 : ),
239 1 : InputWrapper.text(
240 2 : title: AppLocale.labels.description,
241 1 : controller: description,
242 2 : tooltip: AppLocale.labels.descriptionTooltip,
243 : ),
244 1 : InputWrapper(
245 : type: NamedInputType.budgetSelector,
246 : isRequired: true,
247 5 : value: budget != null ? widget.state.getByUuid(budget!) : null,
248 2 : title: AppLocale.labels.budget,
249 1 : showError: hasErrors && budget == null,
250 2 : tooltip: AppLocale.labels.titleBudgetTooltip,
251 2 : state: widget.state,
252 3 : onChange: (value) => setState(() {
253 2 : budget = value?.uuid;
254 : if (value != null) {
255 2 : budgetCurrency = value.currency;
256 1 : currency ??= budgetCurrency;
257 : }
258 : }),
259 : width: width,
260 : ),
261 2 : SignalBuilder(builder: (_, __) {
262 1 : final bill = BillAppData(
263 1 : account: account ?? '',
264 1 : category: budget ?? '',
265 1 : currency: currency,
266 2 : title: descriptionSignal.value,
267 3 : details: double.tryParse(billSignal.value) ?? 0.0,
268 : );
269 4 : final categories = widget.state.prediction.predict(bill);
270 1 : if (categories.isEmpty) return ThemeHelper.emptyBox;
271 2 : final scope = categories.map((e) {
272 3 : final item = widget.state.getByUuid(e);
273 1 : return ToolbarButtonWidget(
274 : isWide: true,
275 : maxWidth:
276 2 : display.isBottom ? width - indent * 2 : width / categories.length - indent * categories.length,
277 1 : tooltip: item?.title ?? '',
278 1 : color: item?.color ?? context.colorScheme.primary,
279 2 : borderColor: item?.color?.withValues(alpha: 0.5),
280 1 : icon: item?.icon ?? Icons.category,
281 0 : onPressed: () => setState(() => budget = item?.uuid),
282 : );
283 : });
284 : return display.isBottom
285 2 : ? Column(children: scope.toList())
286 0 : : RowWidget(
287 0 : alignment: AppDesign.getAlignment<MainAxisAlignment>(),
288 : indent: indent,
289 0 : maxWidth: constraints.maxWidth,
290 0 : chunk: scope.map((_) => null).toList(),
291 0 : children: scope.map((e) => [e]).toList(),
292 : );
293 : }),
294 : ThemeHelper.hIndent2x,
295 1 : Text(
296 2 : AppLocale.labels.expenseDateTime,
297 1 : style: textTheme.bodyLarge,
298 : ),
299 1 : DateTimeInput(
300 1 : width: width - indent,
301 2 : value: createdAt ?? DateTime.now(),
302 0 : setState: (value) => setState(() => createdAt = value),
303 : ),
304 : ThemeHelper.formEndBox,
305 : ],
306 : );
307 : }),
308 : ),
309 : );
310 : }
311 : }
|