LCOV - code coverage report
Current view: top level - lib/components - component_summary.dart Coverage Total Hit
Test: lcov.info Lines: 0.0 % 131 0
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/herald/app_locale.dart';
       5              : import 'package:app_finance/_classes/herald/app_sorting.dart';
       6              : import 'package:app_finance/_classes/herald/app_start_of_month.dart';
       7              : import 'package:app_finance/_classes/storage/app_data.dart';
       8              : import 'package:app_finance/_classes/structure/account_app_data.dart';
       9              : import 'package:app_finance/_classes/structure/bill_app_data.dart';
      10              : import 'package:app_finance/_classes/structure/budget_app_data.dart';
      11              : import 'package:app_finance/_classes/structure/currency/exchange.dart';
      12              : import 'package:app_finance/_classes/structure/invoice_app_data.dart';
      13              : import 'package:app_finance/_configs/theme_helper.dart';
      14              : import 'package:app_finance/_ext/build_context_ext.dart';
      15              : import 'package:app_finance/_ext/date_time_ext.dart';
      16              : import 'package:app_finance/components/_core/component_data.dart';
      17              : import 'package:app_finance/design/generic/text_widget.dart';
      18              : import 'package:app_finance/design/wrapper/table_widget.dart';
      19              : import 'package:app_finance/design/wrapper/text_wrapper.dart';
      20              : import 'package:material_ui/material_ui.dart';
      21              : import 'package:flutter_currency_picker/flutter_currency_picker.dart';
      22              : import 'package:provider/provider.dart';
      23              : 
      24              : class ComponentSummary extends StatelessWidget {
      25              :   final ComponentData data;
      26              : 
      27            0 :   const ComponentSummary(this.data, {super.key});
      28              : 
      29            0 :   List<List<Widget>> _generateSummaryTable(AppData appState, BuildContext context) {
      30            0 :     final exchange = Exchange(store: appState);
      31            0 :     final displayCurrency = exchange.getDefaultCurrency() ?? Exchange.defaultCurrency;
      32              : 
      33            0 :     final DateTime startingDay = DateTime.now().getStartingDay(AppStartOfMonth.get());
      34            0 :     final monthStarts = <DateTime>[
      35            0 :       startingDay.getPreviousMonth(5),
      36            0 :       startingDay.getPreviousMonth(4),
      37            0 :       startingDay.getPreviousMonth(3),
      38            0 :       startingDay.getPreviousMonth(2),
      39            0 :       startingDay.getPreviousMonth(1),
      40              :       startingDay,
      41              :     ];
      42            0 :     final monthEnds = monthStarts.map((d) => d.getNextMonth()).toList();
      43              : 
      44            0 :     final sorting = Provider.of<AppSorting>(context, listen: true);
      45            0 :     final budgets = appState.getList(AppDataType.budgets).cast<BudgetAppData>();
      46            0 :     budgets.sort(sorting.getSortFunction(context));
      47              :     final bills = appState
      48            0 :         .getStream(AppDataType.bills, boundary: startingDay.getPreviousMonth(6).millisecondsSinceEpoch + 0.0)
      49            0 :         .toList()
      50            0 :         .cast<BillAppData>();
      51            0 :     final accounts = appState.getList(AppDataType.accounts).cast<AccountAppData>().where((a) => !a.hidden).toList();
      52            0 :     accounts.sort(sorting.getSortFunction(context));
      53              :     final invoices =
      54            0 :         appState.getList(AppDataType.invoice).cast<InvoiceAppData>().where((i) => i.accountFrom == null).toList();
      55              : 
      56            0 :     String formatAmount(double value, Currency? currency) {
      57              :       if (currency == null) {
      58            0 :         return value.toStringAsFixed(2);
      59              :       }
      60            0 :       return value.toCurrency(currency: currency, withPattern: false);
      61              :     }
      62              : 
      63            0 :     double sumForBudgetAndMonth(BudgetAppData budget, int monthIndex) {
      64            0 :       final start = monthStarts[monthIndex];
      65            0 :       final end = monthEnds[monthIndex];
      66              : 
      67              :       double total = 0.0;
      68            0 :       for (final bill in bills) {
      69            0 :         if (bill.category != budget.uuid) {
      70              :           continue;
      71              :         }
      72            0 :         final createdAt = bill.createdAt;
      73            0 :         if (createdAt.isBefore(start) || !createdAt.isBefore(end)) {
      74              :           continue;
      75              :         }
      76              : 
      77            0 :         final amount = (bill.details as num?)?.toDouble() ?? 0.0;
      78            0 :         total += exchange.reform(amount, bill.currency, displayCurrency);
      79              :       }
      80              :       return total;
      81              :     }
      82              : 
      83            0 :     String formatAmountForBudget(BudgetAppData budget, double value) => formatAmount(value, displayCurrency);
      84              : 
      85            0 :     double sumBillsForMonth(int monthIndex) {
      86            0 :       final start = monthStarts[monthIndex];
      87            0 :       final end = monthEnds[monthIndex];
      88              : 
      89              :       double total = 0.0;
      90            0 :       for (final bill in bills) {
      91            0 :         final createdAt = bill.createdAt;
      92            0 :         if (createdAt.isBefore(start) || !createdAt.isBefore(end)) {
      93              :           continue;
      94              :         }
      95            0 :         final amount = (bill.details as num?)?.toDouble() ?? 0.0;
      96            0 :         total += exchange.reform(amount, bill.currency, displayCurrency);
      97              :       }
      98              :       return total;
      99              :     }
     100              : 
     101            0 :     double sumIncomeForMonth(int monthIndex) {
     102            0 :       final start = monthStarts[monthIndex];
     103            0 :       final end = monthEnds[monthIndex];
     104              : 
     105              :       double total = 0.0;
     106            0 :       for (final invoice in invoices) {
     107            0 :         final createdAt = invoice.createdAt;
     108            0 :         if (createdAt.isBefore(start) || !createdAt.isBefore(end)) {
     109              :           continue;
     110              :         }
     111            0 :         final amount = (invoice.details as num?)?.toDouble() ?? 0.0;
     112            0 :         total += exchange.reform(amount, invoice.currency, displayCurrency);
     113              :       }
     114              :       return total;
     115              :     }
     116              : 
     117            0 :     double sumIncomeForAccountAndMonth(AccountAppData account, int monthIndex) {
     118            0 :       final start = monthStarts[monthIndex];
     119            0 :       final end = monthEnds[monthIndex];
     120              : 
     121              :       double total = 0.0;
     122            0 :       for (final invoice in invoices) {
     123            0 :         if (invoice.account != account.uuid) {
     124              :           continue;
     125              :         }
     126            0 :         final createdAt = invoice.createdAt;
     127            0 :         if (createdAt.isBefore(start) || !createdAt.isBefore(end)) {
     128              :           continue;
     129              :         }
     130            0 :         final amount = (invoice.details as num?)?.toDouble() ?? 0.0;
     131            0 :         total += exchange.reform(amount, invoice.currency, displayCurrency);
     132              :       }
     133              :       return total;
     134              :     }
     135              : 
     136            0 :     final rows = <List<Widget>>[];
     137              : 
     138              :     // Header
     139            0 :     rows.add([
     140            0 :       TextWidget(' '),
     141            0 :       for (final m in monthStarts)
     142            0 :         Align(
     143              :           alignment: Alignment.centerRight,
     144            0 :           child: TextWidget(m.fullMonthYear(), style: TextStyle(fontWeight: FontWeight.bold)),
     145              :         ),
     146            0 :       Align(
     147              :         alignment: Alignment.centerRight,
     148            0 :         child: TextWidget(AppLocale.labels.summary, style: TextStyle(fontWeight: FontWeight.bold)),
     149              :       ),
     150              :     ]);
     151            0 :     rows.add([
     152            0 :       TextWidget(AppLocale.labels.budgetHeadline, style: TextStyle(fontWeight: FontWeight.bold)),
     153            0 :       for (final _ in monthStarts) TextWidget(' '),
     154            0 :       TextWidget(' '),
     155              :     ]);
     156              : 
     157              :     // Budget category rows
     158            0 :     for (final budget in budgets) {
     159            0 :       final monthTotals = List<double>.generate(6, (i) => sumForBudgetAndMonth(budget, i));
     160            0 :       final total = monthTotals.fold<double>(0.0, (a, b) => a + b);
     161            0 :       rows.add([
     162            0 :         TextWidget('- ${budget.title}'),
     163            0 :         for (final v in monthTotals)
     164            0 :           Align(
     165              :             alignment: Alignment.centerRight,
     166            0 :             child: TextWidget(formatAmountForBudget(budget, v)),
     167              :           ),
     168            0 :         Align(
     169              :           alignment: Alignment.centerRight,
     170            0 :           child: TextWidget(formatAmountForBudget(budget, total)),
     171              :         ),
     172              :       ]);
     173              :     }
     174              : 
     175            0 :     List<Widget> separatorRow() => List<Widget>.generate(8, (_) => const TextWidget(' '));
     176              : 
     177              :     // Header
     178            0 :     rows.add([
     179            0 :       TextWidget(AppLocale.labels.accountHeadline, style: TextStyle(fontWeight: FontWeight.bold)),
     180            0 :       for (final _ in monthStarts) TextWidget(' '),
     181            0 :       TextWidget(' '),
     182              :     ]);
     183              : 
     184              :     // Accounts with invoices
     185            0 :     final accountsWithInvoices = accounts.where((a) => invoices.any((i) => i.account == a.uuid)).toList();
     186            0 :     for (final account in accountsWithInvoices) {
     187            0 :       final monthTotals = List<double>.generate(6, (i) => sumIncomeForAccountAndMonth(account, i));
     188            0 :       final total = monthTotals.fold<double>(0.0, (a, b) => a + b);
     189            0 :       rows.add([
     190            0 :         TextWidget('- ${account.title}'),
     191            0 :         for (final v in monthTotals)
     192            0 :           Align(
     193              :             alignment: Alignment.centerRight,
     194            0 :             child: TextWidget(formatAmount(v, displayCurrency)),
     195              :           ),
     196            0 :         Align(
     197              :           alignment: Alignment.centerRight,
     198            0 :           child: TextWidget(formatAmount(total, displayCurrency)),
     199              :         ),
     200              :       ]);
     201              :     }
     202              : 
     203              :     // Income
     204              : 
     205              :     // Totals: Income - Bills
     206            0 :     rows.add(separatorRow());
     207            0 :     final incomeMonthTotals = List<double>.generate(6, (i) => sumIncomeForMonth(i));
     208            0 :     final billMonthTotals = List<double>.generate(6, (i) => sumBillsForMonth(i));
     209            0 :     final netMonthTotals = List<double>.generate(6, (i) => incomeMonthTotals[i] - billMonthTotals[i]);
     210            0 :     final netTotal = netMonthTotals.fold<double>(0.0, (a, b) => a + b);
     211            0 :     rows.add([
     212            0 :       TextWidget(AppLocale.labels.total, style: TextStyle(fontWeight: FontWeight.bold)),
     213            0 :       for (final v in netMonthTotals)
     214            0 :         Align(
     215              :           alignment: Alignment.centerRight,
     216            0 :           child: TextWidget(
     217            0 :             formatAmount(v, displayCurrency),
     218            0 :             style: TextStyle(fontWeight: FontWeight.bold),
     219              :           ),
     220              :         ),
     221            0 :       Align(
     222              :         alignment: Alignment.centerRight,
     223            0 :         child: TextWidget(formatAmount(netTotal, displayCurrency), style: TextStyle(fontWeight: FontWeight.bold)),
     224              :       ),
     225              :     ]);
     226              : 
     227              :     return rows;
     228              :   }
     229              : 
     230            0 :   @override
     231              :   Widget build(BuildContext context) {
     232            0 :     double indent = ThemeHelper.getIndent(0.5);
     233            0 :     return Consumer<AppData>(builder: (context, appState, _) {
     234            0 :       return LayoutBuilder(builder: (context, constraints) {
     235            0 :         return Flex(
     236              :           direction: Axis.horizontal,
     237            0 :           children: [
     238            0 :             Expanded(
     239            0 :               child: SingleChildScrollView(
     240              :                 scrollDirection: Axis.vertical,
     241            0 :                 child: TableWidget(
     242            0 :                   shadowColor: context.colorScheme.onSurface.withValues(alpha: 0.1),
     243            0 :                   width: constraints.maxWidth - indent * 4,
     244              :                   chunk: const [200, null, null, null, null, null, null, 100],
     245            0 :                   data: _generateSummaryTable(appState, context),
     246              :                 ),
     247              :               ),
     248              :             ),
     249              :           ],
     250              :         );
     251              :       });
     252              :     });
     253              :   }
     254              : }
     255              : 
     256              : class ComponentSummaryForm extends StatefulWidget {
     257              :   final ComponentData data;
     258              :   final Function adjust;
     259              : 
     260            0 :   const ComponentSummaryForm(this.data, {super.key, required this.adjust});
     261              : 
     262            0 :   @override
     263            0 :   ComponentSummaryFormState createState() => ComponentSummaryFormState();
     264              : }
     265              : 
     266              : class ComponentSummaryFormState extends State<ComponentSummaryForm> {
     267            0 :   @override
     268              :   Widget build(BuildContext context) {
     269            0 :     return Container(
     270            0 :       color: context.colorScheme.surface,
     271            0 :       padding: EdgeInsets.all(ThemeHelper.getIndent(4)),
     272              :       height: double.infinity,
     273            0 :       child: TextWrapper(AppLocale.labels.summary),
     274              :     );
     275              :   }
     276              : }
        

Generated by: LCOV version 2.0-1