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