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