Skip to content

Commit fef3afb

Browse files
committed
Allow setting allocation date only no category
1 parent 8b81743 commit fef3afb

6 files changed

Lines changed: 180 additions & 31 deletions

File tree

lib/components/menus/category_selection_menu.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,60 @@ class CategorySelectionFormFieldV2 extends StatelessWidget {
140140
);
141141
}
142142
}
143+
144+
/// Doesn't wrap with FormField which is annoying and hard to see state
145+
class CategorySelectionDropDownV2 extends StatelessWidget {
146+
const CategorySelectionDropDownV2({
147+
super.key,
148+
required this.categories,
149+
this.selected,
150+
this.onSelected,
151+
this.borderRadius,
152+
this.height,
153+
this.superAsNone = false,
154+
this.nullText,
155+
this.hasError = false,
156+
});
157+
158+
final List<Category?> categories;
159+
final Category? selected;
160+
final Function(Category?)? onSelected;
161+
final BorderRadius? borderRadius;
162+
final double? height;
163+
final bool superAsNone;
164+
final String? nullText;
165+
final bool hasError;
166+
167+
@override
168+
Widget build(BuildContext context) {
169+
return SizedBox(
170+
height: height,
171+
child: BorderedDropdownSelector<Category?>(
172+
selected: selected,
173+
items: categories,
174+
subItems: (item) {
175+
if (item != null && item.subCats.isNotEmpty) {
176+
return item.subCats;
177+
}
178+
return null;
179+
},
180+
borderRadius: borderRadius,
181+
hasError: hasError,
182+
builder: (context, cat) => categoryMenuBuilder(
183+
context,
184+
cat,
185+
indentSubcats: false,
186+
superAsNone: superAsNone,
187+
nullText: nullText,
188+
),
189+
selectedBuilder: (context, cat) => categoryMenuBuilder(
190+
context,
191+
cat,
192+
superAsNone: superAsNone,
193+
selected: true,
194+
nullText: nullText,
195+
),
196+
),
197+
);
198+
}
199+
}

lib/components/menus/dropdown_selector.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,52 @@ class _DropdownSelectorState<T> extends State<DropdownSelector<T>> {
127127
}
128128
}
129129

130+
class BorderedDropdownSelector<T> extends StatelessWidget {
131+
const BorderedDropdownSelector({
132+
super.key,
133+
required this.selected,
134+
required this.items,
135+
this.subItems,
136+
this.borderRadius,
137+
this.hasError = false,
138+
required this.builder,
139+
this.selectedBuilder,
140+
this.onSelected,
141+
});
142+
143+
final T selected;
144+
final List<T> items; // this should be nullable because FormField uses T? too
145+
final Iterable<T>? Function(T item)? subItems;
146+
final BorderRadius? borderRadius;
147+
final bool hasError;
148+
final Widget Function(BuildContext, T) builder;
149+
final Widget Function(BuildContext, T)? selectedBuilder;
150+
final Function(T)? onSelected;
151+
152+
@override
153+
Widget build(BuildContext context) {
154+
return Container(
155+
decoration: BoxDecoration(
156+
borderRadius: borderRadius ?? BorderRadius.circular(4),
157+
border: Border.all(
158+
color: (hasError)
159+
? Theme.of(context).colorScheme.error
160+
: Theme.of(context).colorScheme.outline,
161+
),
162+
),
163+
child: DropdownSelector<T>(
164+
selected: selected,
165+
items: items,
166+
subItems: subItems,
167+
builder: builder,
168+
selectedBuilder: selectedBuilder,
169+
borderRadius: borderRadius ?? BorderRadius.circular(4),
170+
onSelected: onSelected,
171+
),
172+
);
173+
}
174+
}
175+
130176
class LibraDropdownFormField<T> extends StatelessWidget {
131177
const LibraDropdownFormField({
132178
super.key,

lib/tabs/transactionDetails/allocation_editor.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class AllocationEditor extends StatelessWidget {
7474
categories: categories,
7575
onSave: (it) => state.updatedAllocation.category = it,
7676
borderRadius: BorderRadius.circular(4),
77+
validator: (cat) => null, // can be empty if only allocating by date
7778
),
7879
),
7980
],
@@ -88,6 +89,15 @@ class AllocationEditor extends StatelessWidget {
8889
onSave: state.saveAllocation,
8990
onCancel: state.clearFocus,
9091
),
92+
const SizedBox(height: 10),
93+
if (state.allocationErrorMessage != null)
94+
ConstrainedBox(
95+
constraints: const BoxConstraints(maxWidth: 500),
96+
child: Text(
97+
state.allocationErrorMessage!,
98+
style: TextStyle(color: Theme.of(context).colorScheme.error),
99+
),
100+
),
91101
],
92102
);
93103
}

lib/tabs/transactionDetails/transaction_details_editor.dart

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,6 @@ class TransactionDetailsEditor extends StatelessWidget {
3838
final state = context.watch<TransactionDetailsState>();
3939
var categories = context.watch<LibraAppState>().categories.parentCategories(state.expenseType);
4040
categories += [Category.ignore, Category.other];
41-
// the transaction constructor will convert Category.empty into the correct super category
42-
// however we must manually convert the initial category to [Category.empty] so that there isn't
43-
// a duplicate.
44-
Category? initialCategory() {
45-
if (state.seed == null) return null;
46-
if (state.seed!.category == Category.income || state.seed!.category == Category.expense) {
47-
return Category.empty;
48-
}
49-
return state.seed!.category;
50-
}
5141

5242
/// WARNING!
5343
/// Form rebuilds every FormField descendant on every change of one of the fields (i.e. it calls
@@ -123,11 +113,11 @@ class TransactionDetailsEditor extends StatelessWidget {
123113
labelRow(
124114
context,
125115
'', // not used
126-
CategorySelectionFormFieldV2(
116+
CategorySelectionDropDownV2(
127117
height: 35,
128-
initial: initialCategory(),
118+
selected: state.category,
129119
categories: categories,
130-
onSave: (it) => state.category = it,
120+
onSelected: state.onCategorySelected,
131121
),
132122
labelCustom: const _CategoryLabel(),
133123
),

lib/tabs/transactionDetails/transaction_details_state.dart

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,13 @@ class TransactionDetailsState extends ChangeNotifier {
7676
String? name;
7777
DateTime? date;
7878
int? value;
79-
Category? category;
8079
String? note;
8180

8281
/// Reimbursement editor
8382
int reimbursementValue = 0;
8483

8584
/// Allocation editor
86-
final Allocation updatedAllocation = Allocation(name: '', category: null, value: 0);
85+
Allocation updatedAllocation = Allocation(name: '', category: null, value: 0);
8786

8887
//---------------------------------------------------------------------------------------------
8988
// These variables are the UI state for the relevant fields. They are used to finalize the output
@@ -94,7 +93,9 @@ class TransactionDetailsState extends ChangeNotifier {
9493
/// filter state of the reimbursement targets. It is set on each [onValueChanged].
9594
ExpenseFilterType expenseType = ExpenseFilterType.all;
9695
TransactionDetailActiveFocus focus = TransactionDetailActiveFocus.none;
96+
Category? category;
9797
String? errorMessage;
98+
String? allocationErrorMessage;
9899
String? reimbursementError;
99100
bool saveAsRule = false;
100101

@@ -117,10 +118,24 @@ class TransactionDetailsState extends ChangeNotifier {
117118
tags.insertAll(0, seed?.tags ?? const []);
118119
allocations.insertAll(0, seed?.allocations ?? const []);
119120
reimbursements.insertAll(0, seed?.reimbursements ?? const []);
121+
122+
category = _initialCategory();
123+
120124
notifyListeners();
121125
}
122126
}
123127

128+
// the transaction constructor will convert Category.empty into the correct super category
129+
// however we must manually convert the initial category to [Category.empty] so that there isn't
130+
// a duplicate.
131+
Category? _initialCategory() {
132+
if (seed == null) return null;
133+
if (seed!.category == Category.income || seed!.category == Category.expense) {
134+
return Category.empty;
135+
}
136+
return seed!.category;
137+
}
138+
124139
void replaceSeed(Transaction? t) {
125140
seed = t;
126141
reset();
@@ -131,6 +146,7 @@ class TransactionDetailsState extends ChangeNotifier {
131146
/// Reset manual state elements
132147
saveAsRule = false;
133148
errorMessage = null;
149+
allocationErrorMessage = null;
134150
reimbursementError = null;
135151
reimburseTarget = null;
136152
tags.clear();
@@ -320,22 +336,42 @@ class TransactionDetailsState extends ChangeNotifier {
320336
}
321337
}
322338

339+
String? _validateAllocation() {
340+
if (!(allocationFormKey.currentState?.validate() ?? false)) {
341+
return '';
342+
}
343+
allocationFormKey.currentState?.save();
344+
345+
if (updatedAllocation.category == null) {
346+
if (updatedAllocation.timestamp != null && category != null) {
347+
updatedAllocation.category = category;
348+
} else {
349+
return 'Category must be selected';
350+
}
351+
}
352+
return null;
353+
}
354+
323355
/// Save a single allocation from the allocation editor in [allocations]
324356
void saveAllocation() {
325-
if (allocationFormKey.currentState?.validate() ?? false) {
326-
allocationFormKey.currentState?.save();
327-
if (focusedAllocation == null) {
328-
allocations.add(updatedAllocation.copy());
329-
} else {
330-
for (int i = 0; i < allocations.length; i++) {
331-
if (allocations[i] == focusedAllocation) {
332-
allocations[i] = updatedAllocation.copy(key: allocations[i].key);
333-
break;
334-
}
357+
String? errorMsg = _validateAllocation();
358+
if (errorMsg != null) {
359+
allocationErrorMessage = errorMsg;
360+
notifyListeners();
361+
return;
362+
}
363+
364+
if (focusedAllocation == null) {
365+
allocations.add(updatedAllocation.copy());
366+
} else {
367+
for (int i = 0; i < allocations.length; i++) {
368+
if (allocations[i] == focusedAllocation) {
369+
allocations[i] = updatedAllocation.copy(key: allocations[i].key);
370+
break;
335371
}
336372
}
337-
clearFocus();
338373
}
374+
clearFocus();
339375
}
340376

341377
/// Validates the form for the reimbursement editor only. Returns an error message, or null on
@@ -417,16 +453,19 @@ class TransactionDetailsState extends ChangeNotifier {
417453
//---------------------------------------------------------------------------------------------
418454
void clearFocus() {
419455
focusedAllocation = null;
456+
updatedAllocation = Allocation(name: '', category: null, value: 0);
457+
allocationErrorMessage = null;
420458
focusedReimbursement = null;
421459
reimburseTarget = null;
422460
focus = TransactionDetailActiveFocus.none;
423461
notifyListeners();
424462
}
425463

426464
void focusAllocation(Allocation? alloc) {
427-
if (focus == TransactionDetailActiveFocus.reimbursement) {
428-
focusedReimbursement = null;
465+
if (focus == TransactionDetailActiveFocus.allocation) {
466+
return;
429467
}
468+
clearFocus();
430469
focusedAllocation = alloc;
431470
focus = TransactionDetailActiveFocus.allocation;
432471
resetAllocation();
@@ -436,9 +475,10 @@ class TransactionDetailsState extends ChangeNotifier {
436475
}
437476

438477
void focusReimbursement(Reimbursement? it) {
439-
if (focus == TransactionDetailActiveFocus.allocation) {
440-
focusedAllocation = null;
478+
if (focus == TransactionDetailActiveFocus.reimbursement) {
479+
return;
441480
}
481+
clearFocus();
442482
focusedReimbursement = it;
443483
reimburseTarget = it?.target;
444484
focus = TransactionDetailActiveFocus.reimbursement;
@@ -469,6 +509,13 @@ class TransactionDetailsState extends ChangeNotifier {
469509
}
470510
}
471511

512+
void onCategorySelected(Category? cat) {
513+
if (category != cat) {
514+
category = cat;
515+
notifyListeners();
516+
}
517+
}
518+
472519
void onTagChanged(Tag tag, bool? selected) {
473520
if (selected == true) {
474521
tags.add(tag);

lib/todo.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
## Refactor
1717
- Undo make AccountState an independent ChangeNotifier; huge hassle, need to watch the state everywhere you use an account. Alternative is to always fetch the account object from the AccountState instead of making it linked in other objects, to force the watch.
18-
- Remove MutableAllocation
1918

2019

2120
## Low priority features

0 commit comments

Comments
 (0)