LCOV - code coverage report
Current view: top level - lib/pages/bill/widgets - expenses_tab.dart Coverage Total Hit
Test: lcov.info Lines: 95.1 % 162 154
Test Date: 2026-11-14 15:08:11 Functions: - 0 0

            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              : }
        

Generated by: LCOV version 2.0-1