X-Git-Url: https://git.ktnx.net/?a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fnet%2Fktnx%2Fmobileledger%2Fui%2Fnew_transaction%2FNewTransactionFragment.java;h=5edf252d72858ceccacef8663fe388e897a7810f;hb=b946450e2333ba9730631ddaacb1e7fdb7f32032;hp=d27de40a80503ed2e1b7c20fdb730ba383c5a9f8;hpb=1aa277a5209ad399fda1a687cf3de0c5192e6ec5;p=mobile-ledger.git diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java index d27de40a..5edf252d 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java @@ -18,8 +18,11 @@ package net.ktnx.mobileledger.ui.new_transaction; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; +import android.database.AbstractCursor; import android.os.Bundle; +import android.os.ParcelFormatException; import android.renderscript.RSInvalidStateException; import android.view.LayoutInflater; import android.view.Menu; @@ -34,26 +37,36 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.TemplateAccount; +import net.ktnx.mobileledger.db.TemplateHeader; import net.ktnx.mobileledger.json.API; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerTransaction; import net.ktnx.mobileledger.model.LedgerTransactionAccount; import net.ktnx.mobileledger.model.MobileLedgerProfile; -import net.ktnx.mobileledger.ui.QRScanAbleFragment; +import net.ktnx.mobileledger.ui.QRScanCapableFragment; +import net.ktnx.mobileledger.ui.templates.TemplatesActivity; import net.ktnx.mobileledger.utils.Logger; import net.ktnx.mobileledger.utils.Misc; import net.ktnx.mobileledger.utils.SimpleDate; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -66,7 +79,7 @@ import java.util.regex.Pattern; // TODO: offer to undo account remove-on-swipe -public class NewTransactionFragment extends QRScanAbleFragment { +public class NewTransactionFragment extends QRScanCapableFragment { private NewTransactionItemsAdapter listAdapter; private NewTransactionModel viewModel; private FloatingActionButton fab; @@ -76,55 +89,299 @@ public class NewTransactionFragment extends QRScanAbleFragment { // Required empty public constructor setHasOptionsMenu(true); } + private void startNewPatternActivity(String scanned) { + Intent intent = new Intent(requireContext(), TemplatesActivity.class); + Bundle args = new Bundle(); + args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned); + requireContext().startActivity(intent, args); + } + private void alertNoTemplateMatch(String scanned) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); + builder.setCancelable(true) + .setMessage(R.string.no_template_matches) + .setPositiveButton(R.string.add_button, + (dialog, which) -> startNewPatternActivity(scanned)) + .create() + .show(); + } protected void onQrScanned(String text) { Logger.debug("qr", String.format("Got QR scan result [%s]", text)); - Pattern p = - Pattern.compile("^(\\d+)\\*(\\d+)\\*(\\d+)-(\\d+)-(\\d+)\\*([:\\d]+)\\*([\\d.]+)$"); - Matcher m = p.matcher(text); - if (m.matches()) { - float amount = Float.parseFloat(m.group(7)); - viewModel.setDate( - new SimpleDate(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)), - Integer.parseInt(m.group(5)))); - - if (viewModel.accountsInInitialState()) { - { - NewTransactionModel.Item firstItem = viewModel.getItem(1); - if (firstItem == null) { - viewModel.addAccount(new LedgerTransactionAccount("разход:пазар")); - listAdapter.notifyItemInserted(viewModel.items.size() - 1); - } - else { - firstItem.setAccountName("разход:пазар"); - firstItem.getAccount() - .resetAmount(); - listAdapter.notifyItemChanged(1); - } + + if (Misc.emptyIsNull(text) == null) + return; + + LiveData> allTemplates = DB.get() + .getTemplateDAO() + .getTemplates(); + allTemplates.observe(getViewLifecycleOwner(), templateHeaders -> { + ArrayList matchingFallbackTemplates = new ArrayList<>(); + ArrayList matchingTemplates = new ArrayList<>(); + + for (TemplateHeader ph : templateHeaders) { + String patternSource = ph.getRegularExpression(); + if (Misc.emptyIsNull(patternSource) == null) + continue; + try { + Pattern pattern = Pattern.compile(patternSource); + Matcher matcher = pattern.matcher(text); + if (!matcher.matches()) + continue; + + Logger.debug("pattern", + String.format("Pattern '%s' [%s] matches '%s'", ph.getName(), + patternSource, text)); + if (ph.isFallback()) + matchingFallbackTemplates.add(ph); + else + matchingTemplates.add(ph); } - { - NewTransactionModel.Item secondItem = viewModel.getItem(2); - if (secondItem == null) { - viewModel.addAccount( - new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null)); - listAdapter.notifyItemInserted(viewModel.items.size() - 1); - } - else { - secondItem.setAccountName("актив:кеш:дам"); - secondItem.getAccount() - .setAmount(-amount); - listAdapter.notifyItemChanged(2); - } + catch (ParcelFormatException e) { + // ignored + Logger.debug("pattern", + String.format("Error compiling regular expression '%s'", patternSource), + e); } } - else { - viewModel.addAccount(new LedgerTransactionAccount("разход:пазар")); - viewModel.addAccount( - new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null)); - listAdapter.notifyItemRangeInserted(viewModel.items.size() - 1, 2); + + if (matchingTemplates.isEmpty()) + matchingTemplates = matchingFallbackTemplates; + + if (matchingTemplates.isEmpty()) + alertNoTemplateMatch(text); + else if (matchingTemplates.size() == 1) + applyTemplate(matchingTemplates.get(0), text); + else + chooseTemplate(matchingTemplates, text); + }); + } + private void chooseTemplate(ArrayList matchingTemplates, String matchedText) { + final String templateNameColumn = "name"; + AbstractCursor cursor = new AbstractCursor() { + @Override + public int getCount() { + return matchingTemplates.size(); + } + @Override + public String[] getColumnNames() { + return new String[]{"_id", templateNameColumn}; + } + @Override + public String getString(int column) { + if (column == 0) + return String.valueOf(getPosition()); + return matchingTemplates.get(getPosition()) + .getName(); + } + @Override + public short getShort(int column) { + if (column == 0) + return (short) getPosition(); + return -1; + } + @Override + public int getInt(int column) { + return getShort(column); + } + @Override + public long getLong(int column) { + return getShort(column); + } + @Override + public float getFloat(int column) { + return getShort(column); } + @Override + public double getDouble(int column) { + return getShort(column); + } + @Override + public boolean isNull(int column) { + return false; + } + @Override + public int getColumnCount() { + return 2; + } + }; + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); + builder.setCancelable(true) + .setTitle(R.string.choose_template_to_apply) + .setIcon(R.drawable.ic_baseline_auto_graph_24) + .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> { + applyTemplate(matchingTemplates.get(which), matchedText); + dialog.dismiss(); + }) + .create() + .show(); + } + private void applyTemplate(TemplateHeader patternHeader, String text) { + Pattern pattern = Pattern.compile(patternHeader.getRegularExpression()); + + Matcher m = pattern.matcher(text); + + if (!m.matches()) { + Snackbar.make(requireView(), R.string.pattern_does_not_match, + BaseTransientBottomBar.LENGTH_INDEFINITE) + .show(); + return; + } - listAdapter.checkTransactionSubmittable(); + SimpleDate transactionDate; + { + int day = extractIntFromMatches(m, patternHeader.getDateDayMatchGroup(), + patternHeader.getDateDay()); + int month = extractIntFromMatches(m, patternHeader.getDateMonthMatchGroup(), + patternHeader.getDateMonth()); + int year = extractIntFromMatches(m, patternHeader.getDateYearMatchGroup(), + patternHeader.getDateYear()); + + SimpleDate today = SimpleDate.today(); + if (year <= 0) + year = today.year; + if (month <= 0) + month = today.month; + if (day <= 0) + day = today.day; + + transactionDate = new SimpleDate(year, month, day); + + Logger.debug("pattern", "setting transaction date to " + transactionDate); } + + NewTransactionModel.Item head = viewModel.getItem(0); + head.ensureType(NewTransactionModel.ItemType.generalData); + final String transactionDescription = + extractStringFromMatches(m, patternHeader.getTransactionDescriptionMatchGroup(), + patternHeader.getTransactionDescription()); + head.setDescription(transactionDescription); + Logger.debug("pattern", "Setting transaction description to " + transactionDescription); + final String transactionComment = + extractStringFromMatches(m, patternHeader.getTransactionCommentMatchGroup(), + patternHeader.getTransactionComment()); + head.setTransactionComment(transactionComment); + Logger.debug("pattern", "Setting transaction comment to " + transactionComment); + head.setDate(transactionDate); + listAdapter.notifyItemChanged(0); + + DB.get() + .getTemplateDAO() + .getTemplateWithAccounts(patternHeader.getId()) + .observe(getViewLifecycleOwner(), entry -> { + int rowIndex = 0; + final boolean accountsInInitialState = viewModel.accountsInInitialState(); + for (TemplateAccount acc : entry.accounts) { + rowIndex++; + + String accountName = extractStringFromMatches(m, acc.getAccountNameMatchGroup(), + acc.getAccountName()); + String accountComment = + extractStringFromMatches(m, acc.getAccountCommentMatchGroup(), + acc.getAccountComment()); + Float amount = + extractFloatFromMatches(m, acc.getAmountMatchGroup(), acc.getAmount()); + if (amount != null && acc.getNegateAmount() != null && acc.getNegateAmount()) + amount = -amount; + + if (accountsInInitialState) { + NewTransactionModel.Item item = viewModel.getItem(rowIndex); + if (item == null) { + Logger.debug("pattern", String.format(Locale.US, + "Adding new account item [%s][c:%s][a:%s]", accountName, + accountComment, amount)); + final LedgerTransactionAccount ledgerAccount = + new LedgerTransactionAccount(accountName); + ledgerAccount.setComment(accountComment); + if (amount != null) + ledgerAccount.setAmount(amount); + // TODO currency + viewModel.addAccount(ledgerAccount); + listAdapter.notifyItemInserted(viewModel.items.size() - 1); + } + else { + Logger.debug("pattern", String.format(Locale.US, + "Stamping account item #%d [%s][c:%s][a:%s]", rowIndex, + accountName, accountComment, amount)); + + item.setAccountName(accountName); + item.setComment(accountComment); + if (amount != null) + item.getAccount() + .setAmount(amount); + + listAdapter.notifyItemChanged(rowIndex); + } + } + else { + final LedgerTransactionAccount transactionAccount = + new LedgerTransactionAccount(accountName); + transactionAccount.setComment(accountComment); + if (amount != null) + transactionAccount.setAmount(amount); + // TODO currency + Logger.debug("pattern", String.format(Locale.US, + "Adding trailing account item [%s][c:%s][a:%s]", accountName, + accountComment, amount)); + + viewModel.addAccount(transactionAccount); + listAdapter.notifyItemInserted(viewModel.items.size() - 1); + } + } + + listAdapter.checkTransactionSubmittable(); + }); + } + private int extractIntFromMatches(Matcher m, Integer group, Integer literal) { + if (literal != null) + return literal; + + if (group != null) { + int grp = group; + if (grp > 0 & grp <= m.groupCount()) + try { + return Integer.parseInt(m.group(grp)); + } + catch (NumberFormatException e) { + Snackbar.make(requireView(), + "Error extracting transaction date: " + e.getMessage(), + BaseTransientBottomBar.LENGTH_INDEFINITE) + .show(); + } + } + + return 0; + } + private String extractStringFromMatches(Matcher m, Integer group, String literal) { + if (literal != null) + return literal; + + if (group != null) { + int grp = group; + if (grp > 0 & grp <= m.groupCount()) + return m.group(grp); + } + + return null; + } + private Float extractFloatFromMatches(Matcher m, Integer group, Float literal) { + if (literal != null) + return literal; + + if (group != null) { + int grp = group; + if (grp > 0 & grp <= m.groupCount()) + try { + return Float.valueOf(m.group(grp)); + } + catch (NumberFormatException e) { + Snackbar.make(requireView(), + "Error extracting transaction amount: " + e.getMessage(), + BaseTransientBottomBar.LENGTH_INDEFINITE) + .show(); + } + } + + return null; } @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { @@ -212,7 +469,7 @@ public class NewTransactionFragment extends QRScanAbleFragment { }); // viewModel.checkTransactionSubmittable(listAdapter); - fab = activity.findViewById(R.id.fab); + fab = activity.findViewById(R.id.fabAdd); fab.setOnClickListener(v -> onFabPressed()); boolean keep = false;