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_design.dart';
5 : import 'package:app_finance/_classes/storage/app_data.dart';
6 : import 'package:app_finance/_classes/herald/app_locale.dart';
7 : import 'package:app_finance/_classes/storage/file_parser.dart';
8 : import 'package:app_finance/_classes/storage/file_picker.dart';
9 : import 'package:app_finance/_classes/storage/transaction_log.dart';
10 : import 'package:app_finance/_classes/controller/focus_controller.dart';
11 : import 'package:app_finance/design/form/list_selector_item.dart';
12 : import 'package:app_finance/_classes/structure/interface_app_data.dart';
13 : import 'package:app_finance/_classes/controller/date_format_helper.dart';
14 : import 'package:app_finance/_classes/storage/app_preferences.dart';
15 : import 'package:app_finance/_configs/theme_helper.dart';
16 : import 'package:app_finance/_ext/build_context_ext.dart';
17 : import 'package:app_finance/design/form/list_selector.dart';
18 : import 'package:app_finance/design/form/simple_input.dart';
19 : import 'package:app_finance/design/generic/loading_widget.dart';
20 : import 'package:app_finance/design/wrapper/input_wrapper.dart';
21 : import 'package:app_finance/pages/settings/widgets/recover_tab/date_time_helper_widget.dart';
22 : import 'package:app_finance/design/wrapper/single_scroll_wrapper.dart';
23 : import 'package:flutter/material.dart';
24 : import 'package:flutter_currency_picker/flutter_currency_picker.dart';
25 : import 'package:provider/provider.dart';
26 :
27 : class ImportTab extends StatefulWidget {
28 5 : const ImportTab({
29 : super.key,
30 : });
31 :
32 0 : @override
33 0 : ImportTabState createState() => ImportTabState();
34 : }
35 :
36 : class ImportTabState extends State<ImportTab> {
37 : late FocusController focus;
38 : late AppData state;
39 : List<List<dynamic>>? fileContent;
40 : StringBuffer errorMessage = StringBuffer();
41 : List<String> columnMap = [];
42 : bool isLoading = false;
43 : final dateFormat = TextEditingController(text: 'M/d/yyyy HH:mm');
44 :
45 0 : @override
46 : void initState() {
47 0 : focus = FocusController();
48 0 : super.initState();
49 : }
50 :
51 0 : @override
52 : void dispose() {
53 0 : focus.dispose();
54 0 : dateFormat.dispose();
55 0 : super.dispose();
56 : }
57 :
58 : late Map<String, String?> attrValue = {
59 : FileParser.attrAccountName: AppPreferences.get(AppPreferences.prefAccount),
60 : FileParser.attrCategoryName: AppPreferences.get(AppPreferences.prefBudget),
61 : FileParser.attrBillCurrency: AppPreferences.get(AppPreferences.prefCurrency),
62 : FileParser.attrBillType: AppLocale.labels.bill,
63 : };
64 :
65 0 : Future<void> pickFile(List<String> ext) async {
66 : try {
67 0 : setState(() => errorMessage.clear());
68 0 : final picker = FilePicker(ext);
69 0 : final content = await picker.pickFile();
70 0 : setState(() {
71 0 : fileContent = content;
72 0 : columnMap = picker.columnMap;
73 0 : if (columnMap.contains(FileParser.attrBillDate)) {
74 0 : int index = columnMap.indexOf(FileParser.attrBillDate);
75 0 : dateFormat.text = DateFormatHelper().detectFormat([fileContent!.last[index]], AppLocale.code);
76 : }
77 : });
78 : } catch (e) {
79 0 : setState(() => errorMessage.writeln(e.toString()));
80 : }
81 : }
82 :
83 0 : Future<void> parseFile() async {
84 0 : state.isLoading = true;
85 0 : fnSearch(AppDataType type, String value) => state.getList(type).where((e) => e.title == value).firstOrNull;
86 0 : fnAdd(InterfaceAppData item) {
87 0 : item = state.add(item);
88 0 : TransactionLog.save(item);
89 : return item;
90 : }
91 :
92 0 : final parser = FileParser(columnMap: columnMap, search: fnSearch, add: fnAdd);
93 0 : final def = {
94 0 : FileParser.attrAccountName: attrValue[FileParser.attrAccountName],
95 0 : FileParser.attrCategoryName: attrValue[FileParser.attrCategoryName],
96 0 : FileParser.attrBillCurrency: attrValue[FileParser.attrBillCurrency],
97 0 : FileParser.defDateFormat: dateFormat.text,
98 : };
99 0 : for (int i = 1; i < (fileContent?.length ?? 0); i++) {
100 : try {
101 0 : dynamic newItem = await parser.parseFileLine(fileContent![i], def);
102 0 : newItem = state.add(newItem, newItem.uuid);
103 0 : TransactionLog.save(newItem);
104 : } catch (e) {
105 0 : setState(() => errorMessage.writeln('[$i / ${fileContent?.length}] ${e.toString()}.'));
106 : }
107 : }
108 0 : await state.restate();
109 0 : setState(() => fileContent = null);
110 : }
111 :
112 0 : Future<void> wrapCall(Function callback) async {
113 0 : setState(() {
114 0 : isLoading = true;
115 0 : errorMessage.clear();
116 : });
117 0 : await callback();
118 0 : await Future.delayed(const Duration(seconds: 1));
119 0 : setState(() {
120 0 : isLoading = false;
121 0 : String isFinished = AppLocale.labels.processIsFinished;
122 0 : errorMessage.write(isFinished);
123 0 : if (errorMessage.toString() == isFinished) {
124 0 : Future.delayed(const Duration(seconds: 2), () => setState(() => errorMessage.clear()));
125 : }
126 : });
127 : }
128 :
129 0 : @override
130 : Widget build(BuildContext context) {
131 0 : final indent = ThemeHelper.getIndent(2);
132 0 : final width = ThemeHelper.getWidth(context, 12);
133 0 : final textTheme = context.textTheme;
134 0 : final ColorScheme colorScheme = context.colorScheme;
135 :
136 0 : return Consumer<AppData>(builder: (context, appState, _) {
137 0 : state = appState;
138 0 : return SingleScrollWrapper(
139 0 : controller: focus,
140 0 : child: Padding(
141 0 : padding: EdgeInsets.all(indent),
142 0 : child: Column(
143 0 : crossAxisAlignment: AppDesign.getAlignment(),
144 0 : children: [
145 : ThemeHelper.hIndent2x,
146 0 : if (errorMessage.toString() != '')
147 0 : Text(errorMessage.toString(), style: textTheme.headlineMedium?.copyWith(color: colorScheme.error)),
148 0 : if (isLoading) ...[
149 0 : SizedBox(height: indent * 6),
150 0 : LoadingWidget(isLoading: isLoading),
151 0 : ] else if (fileContent != null) ...[
152 0 : ...List<Widget>.generate(fileContent!.first.length, (index) {
153 0 : return Column(
154 0 : crossAxisAlignment: AppDesign.getAlignment(),
155 0 : children: [
156 : ThemeHelper.hIndent2x,
157 0 : Text(
158 0 : AppLocale.labels.columnMap(fileContent!.first[index]),
159 0 : style: textTheme.bodyLarge,
160 : ),
161 0 : ListSelector<ListSelectorItem>(
162 0 : options: FileParser.getMappingTypes(),
163 0 : value: ListSelectorItem(id: columnMap[index], name: ''),
164 0 : hintText: AppLocale.labels.columnMapTooltip(fileContent!.first[index]),
165 0 : setState: (value) => setState(() {
166 0 : columnMap[index] = value?.id ?? '';
167 0 : if (columnMap[index] == FileParser.attrBillDate) {
168 0 : dateFormat.text =
169 0 : DateFormatHelper().detectFormat([fileContent!.last[index]], AppLocale.code);
170 : }
171 : }),
172 : ),
173 : ],
174 : );
175 : }),
176 0 : const Divider(),
177 0 : if (!columnMap.contains(FileParser.attrAccountName)) ...[
178 : ThemeHelper.hIndent2x,
179 0 : InputWrapper(
180 : type: NamedInputType.accountSelector,
181 0 : title: AppLocale.labels.def('${AppLocale.labels.account}: ${AppLocale.labels.title}'),
182 0 : tooltip: AppLocale.labels.titleAccountTooltip,
183 0 : value: state.getByUuid(attrValue[FileParser.attrAccountName] ?? ''),
184 0 : onChange: (value) => setState(() => attrValue[FileParser.attrAccountName] = value?.id),
185 : width: width,
186 0 : state: state,
187 : ),
188 : ],
189 0 : if (!columnMap.contains(FileParser.attrCategoryName)) ...[
190 : ThemeHelper.hIndent2x,
191 0 : InputWrapper(
192 : type: NamedInputType.budgetSelector,
193 0 : title: AppLocale.labels.def('${AppLocale.labels.budget}: ${AppLocale.labels.title}'),
194 0 : tooltip: AppLocale.labels.titleBudgetTooltip,
195 0 : value: state.getByUuid(attrValue[FileParser.attrCategoryName] ?? ''),
196 0 : onChange: (value) => setState(() => attrValue[FileParser.attrCategoryName] = value?.id),
197 : width: width,
198 0 : state: state,
199 : ),
200 : ],
201 0 : if (!columnMap.contains(FileParser.attrBillType)) ...[
202 : ThemeHelper.hIndent2x,
203 0 : Text(
204 0 : AppLocale.labels.def('${AppLocale.labels.bill}: ${AppLocale.labels.billTypeTooltip}'),
205 0 : style: textTheme.bodyLarge,
206 : ),
207 0 : ListSelector<ListSelectorItem>(
208 0 : value: attrValue[FileParser.attrBillType] != null
209 0 : ? ListSelectorItem(id: attrValue[FileParser.attrBillType]!, name: '')
210 : : null,
211 0 : hintText: AppLocale.labels.billTypeTooltip,
212 0 : options: [
213 0 : ListSelectorItem(id: AppLocale.labels.bill, name: AppLocale.labels.bill),
214 0 : ListSelectorItem(id: AppLocale.labels.flowTypeInvoice, name: AppLocale.labels.flowTypeInvoice),
215 : ],
216 0 : setState: (value) => setState(() => attrValue[FileParser.attrBillType] = value?.id),
217 : ),
218 : ],
219 0 : if (!columnMap.contains(FileParser.attrBillCurrency)) ...[
220 : ThemeHelper.hIndent2x,
221 0 : InputWrapper.currency(
222 0 : title: AppLocale.labels.def('${AppLocale.labels.bill}: ${AppLocale.labels.currency}'),
223 0 : value: CurrencyProvider.find(attrValue[FileParser.attrBillCurrency]),
224 0 : onChange: (value) => setState(() => attrValue[FileParser.attrBillCurrency] = value?.id),
225 : ),
226 : ],
227 0 : Column(crossAxisAlignment: AppDesign.getAlignment(), children: [
228 : ThemeHelper.hIndent4x,
229 0 : Text(
230 0 : AppLocale.labels.dateFormat,
231 0 : style: textTheme.bodyLarge,
232 : ),
233 0 : SimpleInput(controller: dateFormat),
234 : const DateTimeHelperWidget(),
235 : ThemeHelper.hIndent4x,
236 0 : SizedBox(
237 : width: double.infinity,
238 0 : child: FloatingActionButton(
239 : heroTag: 'import_tab_parse',
240 0 : onPressed: () => wrapCall(parseFile),
241 0 : tooltip: AppLocale.labels.parseFile,
242 0 : child: Text(
243 0 : AppLocale.labels.parseFile,
244 : ),
245 : ),
246 : ),
247 : ]),
248 : ] else
249 0 : ...List<Widget>.generate(FilePicker.fileFormats.length * 2, (index) {
250 0 : if (index % 2 == 0) {
251 : return ThemeHelper.hIndent2x;
252 : } else {
253 0 : final format = FilePicker.fileFormats[index ~/ 2];
254 0 : return SizedBox(
255 : width: double.infinity,
256 0 : child: FloatingActionButton(
257 0 : heroTag: 'import_tab_pick_$format',
258 0 : onPressed: () => wrapCall(() => pickFile([format])),
259 0 : tooltip: AppLocale.labels.pickFile(format),
260 0 : child: Text(
261 0 : AppLocale.labels.pickFile(format),
262 : ),
263 : ),
264 : );
265 : }
266 : })
267 : ],
268 : ),
269 : ),
270 : );
271 : });
272 : }
273 : }
|