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 'dart:collection';
5 : import 'package:app_finance/_classes/controller/iterator_controller.dart';
6 : import 'package:app_finance/_classes/herald/app_budget_positive.dart';
7 : import 'package:app_finance/_classes/herald/app_start_of_month.dart';
8 : import 'package:app_finance/_classes/herald/app_sync.dart';
9 : import 'package:app_finance/_classes/math/budget_prediction.dart';
10 : import 'package:app_finance/_classes/math/goal_recalculation.dart';
11 : import 'package:app_finance/_classes/math/invoice_recalculation.dart';
12 : import 'package:app_finance/_classes/storage/history_data.dart';
13 : import 'package:app_finance/_classes/structure/account_app_data.dart';
14 : import 'package:app_finance/_classes/structure/bill_app_data.dart';
15 : import 'package:app_finance/_classes/math/bill_recalculation.dart';
16 : import 'package:app_finance/_classes/structure/budget_app_data.dart';
17 : import 'package:app_finance/_classes/math/budget_recalculation.dart';
18 : import 'package:app_finance/_classes/structure/currency_app_data.dart';
19 : import 'package:app_finance/_classes/structure/currency/exchange.dart';
20 : import 'package:app_finance/_classes/structure/goal_app_data.dart';
21 : import 'package:app_finance/_classes/structure/interface_app_data.dart';
22 : import 'package:app_finance/_classes/structure/invoice_app_data.dart';
23 : import 'package:app_finance/_classes/structure/payment_app_data.dart';
24 : import 'package:app_finance/_classes/structure/summary_app_data.dart';
25 : import 'package:app_finance/_classes/math/total_recalculation.dart';
26 : import 'package:app_finance/_classes/storage/transaction_log.dart';
27 : import 'package:app_finance/_ext/iterable_ext.dart';
28 : import 'package:material_ui/material_ui.dart';
29 : import 'package:uuid/uuid.dart';
30 :
31 : enum AppDataType {
32 : goals,
33 : bills,
34 : accounts,
35 : budgets,
36 : currencies,
37 : invoice,
38 : payments,
39 : }
40 :
41 : typedef AppDataGetter = ({
42 : List<dynamic> list,
43 : double total,
44 : InterfaceIterator stream,
45 : });
46 :
47 : class AppData extends ChangeNotifier {
48 : final AppSync appSync;
49 : bool isLoading = false;
50 : final _hashTable = HashMap<String, dynamic>();
51 : final _data = <AppDataType, SummaryAppData>{};
52 : final prediction = BudgetPrediction();
53 :
54 12 : AppData(this.appSync) : super() {
55 6 : isLoading = true;
56 6 : int startingDay = AppStartOfMonth.get();
57 12 : for (var key in AppDataType.values) {
58 18 : _data[key] = SummaryAppData(startingDay: startingDay);
59 : }
60 12 : Exchange(store: this).getDefaultCurrency();
61 18 : TransactionLog.load(this).then((_) async => await restate()).then((_) => appSync.follow(AppData, _stream));
62 : }
63 :
64 6 : @override
65 : dispose() {
66 6 : super.dispose();
67 12 : appSync.unfollow(AppData);
68 : }
69 :
70 0 : void _stream(String value) {
71 : try {
72 0 : TransactionLog.add(this, value, true, true);
73 : } catch (e) {
74 : //...
75 : }
76 : }
77 :
78 0 : Future<void> flush() async {
79 0 : isLoading = true;
80 0 : int startingDay = AppStartOfMonth.get();
81 0 : _hashTable.clear();
82 0 : _data.updateAll((key, value) => SummaryAppData(startingDay: startingDay));
83 0 : await TransactionLog.load(this);
84 0 : await restate();
85 : }
86 :
87 2 : Future<void> restate() async {
88 2 : await updateTotals(AppDataType.values);
89 2 : isLoading = false;
90 2 : notifyListeners();
91 : }
92 :
93 2 : void _set(AppDataType property, dynamic value) {
94 6 : _hashTable[value.uuid] = value;
95 10 : _data[property]?.add(value.uuid, updatedAt: value.createdAt);
96 2 : if (!isLoading) {
97 2 : TransactionLog.save(value);
98 6 : appSync.send(value.toStream());
99 2 : _notify();
100 : }
101 : }
102 :
103 2 : void _notify() {
104 2 : if (!isLoading) {
105 8 : WidgetsBinding.instance.addPostFrameCallback((_) => notifyListeners());
106 : }
107 : }
108 :
109 2 : dynamic add(InterfaceAppData value, [String? uuid]) {
110 4 : value.uuid = uuid ?? const Uuid().v4();
111 2 : _update(null, value);
112 4 : return getByUuid(value.uuid!);
113 : }
114 :
115 0 : void update(String uuid, InterfaceAppData value, [bool createIfMissing = false]) {
116 0 : var initial = getByUuid(uuid, false);
117 : if (initial != null || createIfMissing) {
118 0 : _update(initial, value);
119 : }
120 : }
121 :
122 2 : Future<void> updateTotals(List<AppDataType> scope) async {
123 2 : final accountTotal = getTotal(AppDataType.accounts);
124 2 : final exchange = Exchange(store: this);
125 2 : int isBudgetPositive = AppBudgetPositive.get();
126 4 : final rec = TotalRecalculation(exchange: exchange, isBudgetPositive: isBudgetPositive != 0);
127 4 : for (AppDataType type in scope) {
128 8 : await rec.updateTotal(type, _data[type], _hashTable);
129 : }
130 2 : if (scope.contains(AppDataType.accounts)) {
131 6 : rec.updateGoals(getList(AppDataType.goals, false), accountTotal, getTotal(AppDataType.accounts));
132 : }
133 2 : _notify();
134 : }
135 :
136 2 : void _update(InterfaceAppData? initial, InterfaceAppData change) {
137 8 : if (change.getType() != AppDataType.budgets && change.getType() != AppDataType.payments) {
138 6 : HistoryData.addLog(change.uuid, change, initial?.details ?? 0.0, change.details);
139 : }
140 2 : switch (change.getType()) {
141 2 : case AppDataType.accounts:
142 2 : _updateAccount(initial as AccountAppData?, change as AccountAppData);
143 : break;
144 2 : case AppDataType.bills:
145 2 : (change as BillAppData).setState(this);
146 2 : _updateBill(initial as BillAppData?, change);
147 : break;
148 2 : case AppDataType.budgets:
149 2 : (change as BudgetAppData).setState(this);
150 2 : _updateBudget(initial as BudgetAppData?, change);
151 6 : HistoryData.addLog(change.uuid, change, initial?.amountLimit ?? 0.0, change.amountLimit);
152 : break;
153 2 : case AppDataType.goals:
154 0 : _updateGoal(initial as GoalAppData?, change as GoalAppData);
155 : break;
156 2 : case AppDataType.currencies:
157 2 : _updateCurrency(initial as CurrencyAppData?, change as CurrencyAppData);
158 : break;
159 2 : case AppDataType.invoice:
160 2 : (change as InvoiceAppData).setState(this);
161 2 : _updateInvoice(initial as InvoiceAppData?, change);
162 : break;
163 0 : case AppDataType.payments:
164 0 : _updatePayment(initial as PaymentAppData?, change as PaymentAppData);
165 : break;
166 : }
167 : }
168 :
169 2 : void _updateInvoice(InvoiceAppData? initial, InvoiceAppData change) {
170 2 : _set(AppDataType.invoice, change);
171 4 : AccountAppData? currAccount = getByUuid(change.account, false);
172 : AccountAppData? prevAccount;
173 : if (initial != null) {
174 0 : prevAccount = getByUuid(initial.account, false);
175 : if (prevAccount != null) {
176 0 : _data[AppDataType.accounts]?.add(initial.account);
177 : }
178 : }
179 : if (currAccount != null) {
180 6 : final rec = InvoiceRecalculation(change, initial)..exchange = Exchange(store: this);
181 2 : rec.updateAccount(currAccount, prevAccount);
182 8 : _data[AppDataType.accounts]?.add(change.account);
183 2 : if (change.accountFrom != null) {
184 0 : rec.updateAccount(getByUuid(change.accountFrom!, false),
185 0 : initial != null && initial.accountFrom != null ? getByUuid(initial.accountFrom!, false) : null, true);
186 0 : _data[AppDataType.accounts]?.add(change.accountFrom!);
187 : }
188 : }
189 2 : if (!isLoading) {
190 2 : updateTotals([AppDataType.accounts]);
191 : }
192 : }
193 :
194 2 : void _updateAccount(AccountAppData? initial, AccountAppData change) {
195 2 : _set(AppDataType.accounts, change);
196 2 : if (!isLoading) {
197 4 : updateTotals([AppDataType.accounts]);
198 : }
199 : }
200 :
201 2 : void _updateBill(BillAppData? initial, BillAppData change) {
202 4 : AccountAppData? currAccount = getByUuid(change.account, false);
203 : AccountAppData? prevAccount;
204 4 : BudgetAppData? currBudget = getByUuid(change.category, false);
205 : BudgetAppData? prevBudget;
206 2 : final exchange = Exchange(store: this);
207 8 : change.exchangeAccount = exchange.reform(1.0, change.currency, currAccount?.currency);
208 8 : change.exchangeCategory = exchange.reform(1.0, change.currency, currBudget?.currency);
209 : if (initial != null) {
210 0 : prevAccount = getByUuid(initial.account, false);
211 : if (prevAccount != null) {
212 0 : _data[AppDataType.accounts]?.add(initial.account);
213 : }
214 0 : prevBudget = getByUuid(initial.category, false);
215 : if (prevBudget != null) {
216 0 : _data[AppDataType.budgets]?.add(initial.category);
217 : }
218 : }
219 4 : prediction.add(change);
220 4 : final rec = BillRecalculation(change: change, initial: initial)..exchange = exchange;
221 : if (currAccount != null) {
222 2 : rec.updateAccount(currAccount, prevAccount);
223 8 : _data[AppDataType.accounts]?.add(change.account);
224 : }
225 : if (currBudget != null) {
226 2 : rec.updateBudget(currBudget, prevBudget);
227 8 : _data[AppDataType.budgets]?.add(change.category);
228 : }
229 2 : _set(AppDataType.bills, change);
230 0 : if (initial?.childOf != null) {
231 0 : BillAppData? parent = getByUuid(initial!.childOf!, false);
232 : if (parent != null) {
233 0 : parent.details -= change.details - (initial.details as double);
234 0 : update(initial.childOf!, parent);
235 : }
236 : }
237 2 : if (!isLoading) {
238 2 : updateTotals([AppDataType.bills, AppDataType.accounts, AppDataType.budgets]);
239 : }
240 : }
241 :
242 2 : void _updateBudget(BudgetAppData? initial, BudgetAppData change) {
243 2 : BudgetRecalculation(change: change, initial: initial)
244 4 : ..exchange = Exchange(store: this)
245 2 : ..updateBudget();
246 2 : _set(AppDataType.budgets, change);
247 2 : if (!isLoading) {
248 4 : updateTotals([AppDataType.budgets]);
249 : }
250 : }
251 :
252 0 : void _updateGoal(GoalAppData? initial, GoalAppData change) {
253 0 : GoalRecalculation(change: change, initial: initial)
254 0 : ..exchange = Exchange(store: this)
255 0 : ..updateGoal();
256 0 : _set(AppDataType.goals, change);
257 0 : if (!isLoading) {
258 0 : updateTotals([AppDataType.goals]);
259 : }
260 : }
261 :
262 2 : void _updateCurrency(CurrencyAppData? initial, CurrencyAppData change) {
263 2 : _set(AppDataType.currencies, change);
264 2 : if (!isLoading) {
265 1 : updateTotals(AppDataType.values);
266 : }
267 : }
268 :
269 0 : void _updatePayment(PaymentAppData? initial, PaymentAppData change) {
270 0 : _set(AppDataType.payments, change);
271 : }
272 :
273 6 : AppDataGetter get(AppDataType property) {
274 : return (
275 6 : list: getList(property),
276 6 : total: getTotal(property),
277 6 : stream: getStream<InterfaceAppData>(property),
278 : );
279 : }
280 :
281 6 : List<dynamic> getList(AppDataType property, [bool isClone = true]) {
282 18 : return (_data[property]?.list ?? [])
283 10 : .map((uuid) => getByUuid(uuid, isClone))
284 10 : .where((element) => !element.hidden)
285 6 : .toList();
286 : }
287 :
288 6 : InterfaceIterator getStream<M extends InterfaceAppData>(AppDataType property,
289 : {bool inverse = true, double? boundary, Function? filter}) {
290 12 : if (_data[property] == null) {
291 0 : return IteratorController<num, dynamic, M>(SplayTreeMap<num, dynamic>(), transform: getByUuid);
292 : }
293 24 : return _data[property]!.origin.toStream<M>(
294 : inverse,
295 6 : transform: getByUuid,
296 : boundary: boundary,
297 0 : filter: (M v) => v.hidden || filter?.call(v) == true,
298 : );
299 : }
300 :
301 1 : List<dynamic> getActualList(AppDataType property, [bool isClone = true]) {
302 3 : return (_data[property]?.listActual ?? [])
303 1 : .map((uuid) => getByUuid(uuid, isClone))
304 1 : .where((element) => !element.hidden)
305 1 : .toList();
306 : }
307 :
308 6 : double getTotal(AppDataType property) {
309 18 : return _data[property]?.total ?? 0.0;
310 : }
311 :
312 4 : dynamic getByUuid(String uuid, [bool isClone = true]) {
313 4 : if (uuid == '') return null;
314 10 : var obj = isClone ? _hashTable[uuid]?.clone() : _hashTable[uuid];
315 6 : if (obj is BillAppData || obj is BudgetAppData || obj is InvoiceAppData) {
316 2 : obj.setState(this);
317 : }
318 : return obj;
319 : }
320 : }
|