]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java
fix swipe-away in reworked new transaction activity
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / activity / NewTransactionModel.java
1 /*
2  * Copyright © 2019 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.ui.activity;
19
20 import androidx.annotation.NonNull;
21 import androidx.lifecycle.LiveData;
22 import androidx.lifecycle.MutableLiveData;
23 import androidx.lifecycle.ViewModel;
24
25 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
26 import net.ktnx.mobileledger.utils.Misc;
27
28 import org.jetbrains.annotations.NotNull;
29
30 import java.util.ArrayList;
31 import java.util.Calendar;
32 import java.util.Date;
33 import java.util.GregorianCalendar;
34 import java.util.Locale;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38 import static net.ktnx.mobileledger.utils.Logger.debug;
39 import static net.ktnx.mobileledger.utils.Misc.isZero;
40
41 public class NewTransactionModel extends ViewModel {
42     static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
43     static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
44     static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
45     private final Item header = new Item(this, null, "");
46     private final Item trailer = new Item(this);
47     private final ArrayList<Item> items = new ArrayList<>();
48     private final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
49     private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(null);
50     private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
51     public int getAccountCount() {
52         return items.size();
53     }
54     public Date getDate() {
55         return header.date.getValue();
56     }
57     public String getDescription() {
58         return header.description.getValue();
59     }
60     public LiveData<Boolean> isSubmittable() {
61         return this.isSubmittable;
62     }
63     void reset() {
64         header.date.setValue(null);
65         header.description.setValue(null);
66         items.clear();
67         items.add(new Item(this, new LedgerTransactionAccount("")));
68         items.add(new Item(this, new LedgerTransactionAccount("")));
69         focusedItem.setValue(0);
70     }
71     public void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
72                                    @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
73         this.focusedItem.observe(owner, observer);
74     }
75     public void stopObservingFocusedItem(
76             @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
77         this.focusedItem.removeObserver(observer);
78     }
79     public void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
80                                     @NonNull
81                                             androidx.lifecycle.Observer<? super Integer> observer) {
82         this.accountCount.observe(owner, observer);
83     }
84     public void stopObservingAccountCount(
85             @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
86         this.accountCount.removeObserver(observer);
87     }
88     public void setFocusedItem(int position) {
89         focusedItem.setValue(position);
90     }
91     public int addAccount(LedgerTransactionAccount acc) {
92         items.add(new Item(this, acc));
93         accountCount.setValue(getAccountCount());
94         return items.size();
95     }
96     boolean accountsInInitialState() {
97         for (Item item : items) {
98             LedgerTransactionAccount acc = item.getAccount();
99             if (acc.isAmountSet()) return false;
100             if (!acc.getAccountName()
101                     .trim()
102                     .isEmpty()) return false;
103         }
104
105         return true;
106     }
107     LedgerTransactionAccount getAccount(int index) {
108         return items.get(index)
109                     .getAccount();
110     }
111     public Item getItem(int index) {
112         if (index == 0) {
113             return header;
114         }
115         else if (index <= items.size()) return items.get(index - 1);
116         else return trailer;
117     }
118     // rules:
119     // 1) at least two account names
120     // 2) each amount must have account name
121     // 3) amounts must balance to 0, or
122     // 3a) there must be exactly one empty amount
123     // 4) empty accounts with empty amounts are ignored
124     // 5) a row with an empty account name or empty amount is guaranteed to exist
125     public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
126         int accounts = 0;
127         int accounts_with_values = 0;
128         int amounts = 0;
129         int amounts_with_accounts = 0;
130         int empty_rows = 0;
131         Item empty_amount = null;
132         boolean single_empty_amount = false;
133         boolean single_empty_amount_has_account = false;
134         float running_total = 0f;
135         final String descriptionText = getDescription();
136         final boolean have_description = ((descriptionText != null) && !descriptionText.isEmpty());
137
138         try {
139             for (int i = 0; i < this.items.size(); i++) {
140                 Item item = this.items.get(i);
141
142                 LedgerTransactionAccount acc = item.getAccount();
143                 String acc_name = acc.getAccountName()
144                                      .trim();
145                 if (!acc_name.isEmpty()) {
146                     accounts++;
147
148                     if (acc.isAmountSet()) {
149                         accounts_with_values++;
150                     }
151                 }
152                 else empty_rows++;
153
154                 if (!acc.isAmountSet()) {
155                     if (empty_amount == null) {
156                         empty_amount = item;
157                         single_empty_amount = true;
158                         single_empty_amount_has_account = !acc_name.isEmpty();
159                     }
160                     else if (!acc_name.isEmpty()) single_empty_amount = false;
161                 }
162                 else {
163                     amounts++;
164                     if (!acc_name.isEmpty()) amounts_with_accounts++;
165                     running_total += acc.getAmount();
166                 }
167             }
168
169             if ((empty_rows == 0) &&
170                 ((this.items.size() == accounts) || (this.items.size() == amounts)))
171             {
172                 adapter.addRow();
173             }
174
175             if (single_empty_amount) {
176                 empty_amount.setAmountHint(String.format(Locale.US, "%1.2f",
177                         Misc.isZero(running_total) ? 0f : -running_total));
178             }
179
180             debug("submittable", String.format(Locale.US,
181                     "%s, accounts=%d, accounts_with_values=%s, " +
182                     "amounts_with_accounts=%d, amounts=%d, running_total=%1.2f, " +
183                     "single_empty_with_acc=%s", have_description ? "description" : "NO description",
184                     accounts, accounts_with_values, amounts_with_accounts, amounts, running_total,
185                     (single_empty_amount && single_empty_amount_has_account) ? "true" : "false"));
186
187             if (have_description && (accounts >= 2) && (accounts_with_values >= (accounts - 1)) &&
188                 (amounts_with_accounts == amounts) &&
189                 (single_empty_amount && single_empty_amount_has_account || isZero(running_total)))
190             {
191                 debug("submittable", "YES");
192                 isSubmittable.setValue(true);
193             }
194             else {
195                 debug("submittable", "NO");
196                 isSubmittable.setValue(false);
197             }
198
199         }
200         catch (NumberFormatException e) {
201             debug("submittable", "NO (because of NumberFormatException)");
202             isSubmittable.setValue(false);
203         }
204         catch (Exception e) {
205             e.printStackTrace();
206             debug("submittable", "NO (because of an Exception)");
207             isSubmittable.setValue(false);
208         }
209     }
210     public void removeItem(int pos) {
211         items.remove(pos);
212         accountCount.setValue(getAccountCount());
213     }
214     public void sendCountNotifications() {
215         accountCount.setValue(getAccountCount());
216     }
217     enum ItemType {generalData, transactionRow, bottomFiller}
218
219     class Item extends Object {
220         private ItemType type;
221         private MutableLiveData<Date> date = new MutableLiveData<>();
222         private MutableLiveData<String> description = new MutableLiveData<>();
223         private LedgerTransactionAccount account;
224         private MutableLiveData<String> amountHint = new MutableLiveData<>();
225         private NewTransactionModel model;
226         private boolean editable = true;
227         public Item(NewTransactionModel model) {
228             this.model = model;
229             type = ItemType.bottomFiller;
230         }
231         public Item(NewTransactionModel model, Date date, String description) {
232             this.model = model;
233             this.type = ItemType.generalData;
234             this.date.setValue(date);
235             this.description.setValue(description);
236         }
237         public Item(NewTransactionModel model, LedgerTransactionAccount account) {
238             this.model = model;
239             this.type = ItemType.transactionRow;
240             this.account = account;
241         }
242         public NewTransactionModel getModel() {
243             return model;
244         }
245         public boolean isEditable() {
246             ensureType(ItemType.transactionRow);
247             return editable;
248         }
249         public void setEditable(boolean editable) {
250             ensureType(ItemType.transactionRow);
251             this.editable = editable;
252         }
253         public String getAmountHint() {
254             ensureType(ItemType.transactionRow);
255             return amountHint.getValue();
256         }
257         public void setAmountHint(String amountHint) {
258             ensureType(ItemType.transactionRow);
259             this.amountHint.setValue(amountHint);
260         }
261         public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
262                                       @NonNull
263                                               androidx.lifecycle.Observer<? super String> observer) {
264             this.amountHint.observe(owner, observer);
265         }
266         public void stopObservingAmountHint(
267                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
268             this.amountHint.removeObserver(observer);
269         }
270         public ItemType getType() {
271             return type;
272         }
273         public void ensureType(ItemType wantedType) {
274             if (type != wantedType) {
275                 throw new RuntimeException(
276                         String.format("Actual type (%d) differs from wanted (%s)", type,
277                                 wantedType));
278             }
279         }
280         public Date getDate() {
281             ensureType(ItemType.generalData);
282             return date.getValue();
283         }
284         public void setDate(Date date) {
285             ensureType(ItemType.generalData);
286             this.date.setValue(date);
287         }
288         public void setDate(String text) {
289             int year, month, day;
290             final Calendar c = GregorianCalendar.getInstance();
291             Matcher m = reYMD.matcher(text);
292             if (m.matches()) {
293                 year = Integer.parseInt(m.group(1));
294                 month = Integer.parseInt(m.group(2)) - 1;   // month is 0-based
295                 day = Integer.parseInt(m.group(3));
296             }
297             else {
298                 year = c.get(Calendar.YEAR);
299                 m = reMD.matcher(text);
300                 if (m.matches()) {
301                     month = Integer.parseInt(m.group(1)) - 1;
302                     day = Integer.parseInt(m.group(2));
303                 }
304                 else {
305                     month = c.get(Calendar.MONTH);
306                     m = reD.matcher(text);
307                     if (m.matches()) {
308                         day = Integer.parseInt(m.group(1));
309                     }
310                     else {
311                         day = c.get(Calendar.DAY_OF_MONTH);
312                     }
313                 }
314             }
315
316             c.set(year, month, day);
317
318             this.setDate(c.getTime());
319         }
320         public void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
321                                 @NonNull androidx.lifecycle.Observer<? super Date> observer) {
322             this.date.observe(owner, observer);
323         }
324         public void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> observer) {
325             this.date.removeObserver(observer);
326         }
327         public String getDescription() {
328             ensureType(ItemType.generalData);
329             return description.getValue();
330         }
331         public void setDescription(String description) {
332             ensureType(ItemType.generalData);
333             this.description.setValue(description);
334         }
335         public void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
336                                        @NonNull
337                                                androidx.lifecycle.Observer<? super String> observer) {
338             this.description.observe(owner, observer);
339         }
340         public void stopObservingDescription(
341                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
342             this.description.removeObserver(observer);
343         }
344         public LedgerTransactionAccount getAccount() {
345             ensureType(ItemType.transactionRow);
346             return account;
347         }
348         public void setAccountName(String name) {
349             account.setAccountName(name);
350         }
351         /**
352          * getFormattedDate()
353          *
354          * @return nicely formatted, shortest available date representation
355          */
356         public String getFormattedDate() {
357             if (date == null) return null;
358             Date time = date.getValue();
359             if (time == null) return null;
360
361             Calendar c = GregorianCalendar.getInstance();
362             c.setTime(time);
363             Calendar today = GregorianCalendar.getInstance();
364
365             final int myYear = c.get(Calendar.YEAR);
366             final int myMonth = c.get(Calendar.MONTH);
367             final int myDay = c.get(Calendar.DAY_OF_MONTH);
368
369             if (today.get(Calendar.YEAR) != myYear) {
370                 return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth, myDay);
371             }
372
373             if (today.get(Calendar.MONTH) != myMonth) {
374                 return String.format(Locale.US, "%d/%02d", myMonth, myDay);
375             }
376
377             return String.valueOf(myDay);
378         }
379     }
380 }