diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java index feb2b953..ab7daf76 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -12,6 +12,8 @@ public interface AccountDb { Collection listAccounts(long userId); + Collection listTags(long accountId, String source, String destination); + Account loadAccount(long accountId); Transaction loadTransaction(long transactionId); diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java index 659f6b34..36c71d36 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -156,6 +156,25 @@ public class AccountingModule extends BaseHandler implements AccountingService { return sendContent(ex, transaction); } + private List egalize(Set tags, String key) { + var result = new HashSet(); + var lower = key.toLowerCase(); + var len = key.length(); + for (var tag : tags) { + if (tag.length() == key.length()) continue; + result.add(tag.toLowerCase().startsWith(lower) ? key + tag.substring(len) : tag); + } + return result.stream().sorted(String.CASE_INSENSITIVE_ORDER).toList(); + } + + private String extractParty(String field, JSONObject json){ + if (!json.has(field)) return null; + if (!(json.get(field) instanceof JSONObject data)) return null; + if (data.has(Field.ID)) return data.get(Field.ID).toString(); + if (data.has(Field.DISPLAY)) return data.get(Field.DISPLAY).toString(); + return null; + } + private boolean getAccount(UmbrellaUser user, long accountId, HttpExchange ex) throws IOException { LOG.log(WARNING,"Missing authorization check in AccountingModule.getAccount(…)!"); var account = accountDb.loadAccount(accountId); @@ -294,20 +313,17 @@ public class AccountingModule extends BaseHandler implements AccountingService { private boolean postSearchTags(long accountId, UmbrellaUser user, HttpExchange ex) throws IOException { LOG.log(WARNING,"Missing authorization check in AccountingModule.getAccount(…)!"); var key = body(ex); - var tags = accountDb.searchTagsContaining(key,accountId); - if (tags.size()<10) tags.addAll(tagService().search(key,user)); - return sendContent(ex,egalize(tags,key)); - } - - private List egalize(Set tags, String key) { - var result = new HashSet(); - var lower = key.toLowerCase(); - var len = key.length(); - for (var tag : tags) { - if (tag.length() == key.length()) continue; - result.add(tag.toLowerCase().startsWith(lower) ? key + tag.substring(len) : tag); + if (!key.trim().startsWith("{")) { // search tags that contain value of body + var tags = accountDb.searchTagsContaining(key, accountId); + if (tags.size() < 10) tags.addAll(tagService().search(key, user)); + return sendContent(ex, egalize(tags, key)); } - return result.stream().sorted(String.CASE_INSENSITIVE_ORDER).toList(); + // search tags for account with specified source and destination + var json = new JSONObject(key); + var src = extractParty(Field.SOURCE, json); + var dst = extractParty(Field.DESTINATION, json); + var tags = accountDb.listTags(accountId,src,dst); + return sendContent(ex,tags); } private boolean postToAccount(long accountId, Path path, UmbrellaUser user, HttpExchange ex) throws IOException { diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java index 69dc65b9..0ac23919 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -150,6 +150,45 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + @Override + public Collection listTags(long accountId, String source, String destination) { + try { + var rs = select(TRANSACTION_ID,Field.TAG) + .from(TABLE_TRANSACTIONS) + .leftJoin(ID,TABLE_TAGS_TRANSACTIONS, TRANSACTION_ID) + .leftJoin(TAG_ID,TABLE_TAGS,ID) + .where(ACCOUNT,equal(accountId)) + .where(SOURCE,equal(source)) + .where(DESTINATION,equal(destination)) + .sort(TRANSACTION_ID).exec(db); + Set set = null; + Set transactionTags = new TreeSet(String.CASE_INSENSITIVE_ORDER); + Long lastTransaction = null; + while (rs.next()){ + var currentTransaction = rs.getLong(TRANSACTION_ID); + if (lastTransaction == null) { // first row + transactionTags.add(rs.getString(TAG)); + lastTransaction = currentTransaction; + } else if (lastTransaction == currentTransaction) { + transactionTags.add(rs.getString(TAG)); + } else { + if (set == null) { + set = transactionTags; + } else { + set.retainAll(transactionTags); + } + transactionTags = new HashSet<>(); + transactionTags.add(rs.getString(TAG)); + lastTransaction = currentTransaction; + } + } + rs.close(); + return set; + } catch (SQLException e){ + throw failedToLoadMembers(accountId); + } + } + @Override public Account loadAccount(long accountId) { try { diff --git a/frontend/src/routes/accounting/add_entry.svelte b/frontend/src/routes/accounting/add_entry.svelte index e8666513..d452d37c 100644 --- a/frontend/src/routes/accounting/add_entry.svelte +++ b/frontend/src/routes/accounting/add_entry.svelte @@ -29,8 +29,35 @@ }); let router = useTinyRouter(); - async function getTerminal(text,url){ - var res = await post(url,text); + async function dst_selected(destination){ + destination = JSON.parse(JSON.stringify(destination)); + let source = JSON.parse(JSON.stringify(entry.source)); + const url = api(`accounting/${entry.account.id}/tags`) + const res = await post(url,{source,destination}); + if (res.ok) { + yikes(); + const json = await res.json(); + entry.tags = json; + } else error(res); + } + + function focusOnEnter(ev,id){ + if (ev.key == 'Enter') document.getElementById(id).focus(); + } + + async function getAccountTags(text){ + if (!text) return []; + const url = api(`accounting/${entry.account.id}/tags`) + return await getProposals(text,url); + } + + async function getDestinations(text){ + const url = api('accounting/destinations'); + return await getProposals(text,url); + } + + async function getProposals(text,url){ + const res = await post(url,text); if (res.ok){ yikes(); const input = await res.json(); @@ -41,25 +68,19 @@ } } - async function getAccountTags(text){ - if (!text) return []; - var url = api(`accounting/${entry.account.id}/tags`) - return await getTerminal(text,url); - } - - async function getDestinations(text){ - var url = api('accounting/destinations'); - return await getTerminal(text,url); - } async function getPurposes(text) { - var url = api('accounting/purposes'); - return await getTerminal(text,url); + const url = api('accounting/purposes'); + return await getProposals(text,url); } async function getSources(text){ - var url = api('accounting/sources'); - return await getTerminal(text,url); + const url = api('accounting/sources'); + return await getProposals(text,url); + } + + function gotoTags(purpose){ + document.getElementById('new_tag_input'); } function mapDisplay(object){ @@ -131,15 +152,15 @@ {t('destination')} - + {t('amount')} -  {entry.account.currency} + focusOnEnter(e,'purpose_input')} /> {entry.account.currency} {t('purpose')} - + {t('tags')} diff --git a/frontend/src/routes/tags/TagList.svelte b/frontend/src/routes/tags/TagList.svelte index a8183431..e8122394 100644 --- a/frontend/src/routes/tags/TagList.svelte +++ b/frontend/src/routes/tags/TagList.svelte @@ -112,6 +112,6 @@ {/each} - +