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 b5415d07..1657ccd9 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -19,4 +19,6 @@ public interface AccountDb { Transaction save(Transaction transaction); Set searchField(long userId, String field, String key); + + Set searchTagsContaining(String key, long accountId); } 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 1a8dff2c..356c9624 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -4,6 +4,7 @@ package de.srsoftware.umbrella.accounting; import static de.srsoftware.tools.Optionals.nullIfEmpty; import static de.srsoftware.umbrella.accounting.Constants.CONFIG_DATABASE; import static de.srsoftware.umbrella.core.ConnectionProvider.connect; +import static de.srsoftware.umbrella.core.ModuleRegistry.tagService; import static de.srsoftware.umbrella.core.ModuleRegistry.userService; import static de.srsoftware.umbrella.core.Util.mapValues; import static de.srsoftware.umbrella.core.constants.Path.*; @@ -71,10 +72,18 @@ public class AccountingModule extends BaseHandler implements AccountingService { var head = path.pop(); return switch (head) { case null -> postEntry(user.get(),ex); - case SOURCES -> postSearchSources(user.get(),ex); case DESTINATIONS -> postSearchDestinations(user.get(),ex); case PURPOSES -> postSearchPurposes(user.get(),ex); - default -> super.doPost(path,ex); + case SOURCES -> postSearchSources(user.get(),ex); + default -> { + try { + var accountId = Long.parseLong(head); + yield postToAccount(accountId,path,user.get(),ex); + } catch (NumberFormatException ignored) { + yield super.doPost(path,ex); + } + + } }; } catch (UmbrellaException e){ return send(ex,e); @@ -82,6 +91,7 @@ public class AccountingModule extends BaseHandler implements AccountingService { } 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); var transactions = accountDb.loadTransactions(account); var userMap = new HashMap(); @@ -202,6 +212,21 @@ public class AccountingModule extends BaseHandler implements AccountingService { return sendContent(ex,searchOptions(user, Field.SOURCE, body(ex))); } + 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,tags); + } + + private boolean postToAccount(long accountId, Path path, UmbrellaUser user, HttpExchange ex) throws IOException { + return switch (path.pop()) { + case TAGS -> postSearchTags(accountId,user,ex); + case null, default -> super.doPost(path,ex); + }; + } + public ArrayList> searchOptions(UmbrellaUser user, String field, String key){ var users = userService().search(key); var destinations = accountDb.searchField(user.id(), field, key); 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 1eab87b5..7985b35f 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -7,6 +7,7 @@ import static de.srsoftware.tools.jdbc.Query.*; import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; import static de.srsoftware.umbrella.accounting.Constants.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; +import static de.srsoftware.umbrella.core.model.Translatable.t; import static java.text.MessageFormat.format; import de.srsoftware.tools.jdbc.Condition; @@ -20,10 +21,7 @@ import de.srsoftware.umbrella.core.model.Transaction; import java.sql.Connection; import java.sql.SQLException; import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; +import java.util.*; public class SqliteDb extends BaseDb implements AccountDb { public SqliteDb(Connection connection) { @@ -155,11 +153,21 @@ public class SqliteDb extends BaseDb implements AccountDb { @Override public List loadTransactions(Account account) { try { - var list = new ArrayList(); + var transactions = new HashMap(); var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).sort(Field.TIMESTAMP).exec(db); - while (rs.next()) list.add(Transaction.of(rs)); + while (rs.next()) { + var transaction = Transaction.of(rs); + transactions.put(transaction.id(),transaction); + } rs.close(); - return list; + var transactionIds = transactions.keySet().toArray(); + rs = select(ALL).from(TABLE_TAGS_TRANSACTIONS).leftJoin(Field.TAG_ID,TABLE_TAGS,Field.ID).where(Field.TRANSACTION_ID,in(transactionIds)).exec(db); + while (rs.next()) { + var transaction = transactions.get(rs.getLong(Field.TRANSACTION_ID)); + if (transaction != null) transaction.tags().add(rs.getString(Field.TAG)); + } + rs.close(); + return new ArrayList<>(transactions.values()); } catch (SQLException e) { throw failedToLoadMembers(account); } @@ -197,11 +205,10 @@ public class SqliteDb extends BaseDb implements AccountDb { } else { // TODO : implement update throw UmbrellaException.failedToStoreObject(transaction); } - saveTags(transaction); - return transaction; + return saveTags(transaction); } - private void saveTags(Transaction transaction) { + private Transaction saveTags(Transaction transaction) { var remaining = new HashSet(transaction.tags()); var existingTags = new HashMap(); try { @@ -228,6 +235,7 @@ public class SqliteDb extends BaseDb implements AccountDb { } catch (SQLException e){ throw failedToStoreObject(transaction); } + return transaction; } @Override @@ -244,4 +252,17 @@ public class SqliteDb extends BaseDb implements AccountDb { throw failedToReadFromTable(field,TABLE_TRANSACTIONS).causedBy(e); } } + + @Override + public Set searchTagsContaining(String key, long accountId) { + try { + var tags = new HashSet(); + var rs = select(ALL).from(TABLE_TRANSACTIONS).leftJoin(Field.ID,TABLE_TAGS_TRANSACTIONS,Field.TRANSACTION_ID).leftJoin(Field.TAG_ID,TABLE_TAGS,Field.ID).where(Field.TAG,like(format("%{0}%",key))).exec(db); + while (rs.next()) tags.add(rs.getString(Field.TAG)); + rs.close(); + return tags; + } catch (SQLException e){ + throw failedToSearchDb(t(Text.TAGS)); + } + } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/TagService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/TagService.java index 08a5b130..ed79e1e9 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/TagService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/TagService.java @@ -30,5 +30,7 @@ public interface TagService { String save(String module, long entityId, Collection userIds, String tag); + Collection search(String key, UmbrellaUser user); + void updateId(String module, Object oldId, Object newId); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java index 394ec2ba..04af091f 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java @@ -53,6 +53,7 @@ public class Path { public static final String STATE = "state"; public static final String STOP = "stop"; + public static final String TAGS = "tags"; public static final String TAGGED = "tagged"; public static final String TOKEN = "token"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/IdOrString.java b/core/src/main/java/de/srsoftware/umbrella/core/model/IdOrString.java index 75b68a21..37310fd3 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/IdOrString.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/IdOrString.java @@ -52,7 +52,12 @@ public class IdOrString implements Mappable { return map; } - public String value(){ + @Override + public String toString() { + return value; + } + + public String value(){ return value; } } \ No newline at end of file diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java index d5979b61..19805128 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java @@ -11,6 +11,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import static java.text.MessageFormat.format; + public record Transaction(long id, long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose, Set tags) implements Mappable { @@ -35,10 +37,16 @@ public record Transaction(long id, long accountId, LocalDateTime date, IdOrStrin Field.SOURCE, source.toMap(), Field.DESTINATION, destination.toMap(), Field.AMOUNT, amount, - Field.PURPOSE, purpose + Field.PURPOSE, purpose, + Field.TAGS, tags ); } + @Override + public String toString() { + return format("Transaction ({0} -[{1}]-> {2})",source,amount,destination); + } + public Transaction withId(long id) { return new Transaction(id, accountId, date, source, destination, amount, purpose, new HashSet<>(tags)); } diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte index f0a360fd..0e29a41e 100644 --- a/frontend/src/routes/accounting/account.svelte +++ b/frontend/src/routes/accounting/account.svelte @@ -62,6 +62,7 @@ {/each} {t('other party')} {t('purpose')} + {t('tags')} @@ -78,7 +79,7 @@ {/if} {/each} - + {#if !transaction.source.id} ← {transaction.source.value} {/if} @@ -86,7 +87,10 @@ → {transaction.destination.value} {/if} - {transaction.purpose} + {transaction.purpose} + + {transaction.tags.join(', ')} + {/each} @@ -109,5 +113,7 @@ +TODO: Bearbeiten von Umsätzen + {/if} \ No newline at end of file diff --git a/frontend/src/routes/accounting/add_entry.svelte b/frontend/src/routes/accounting/add_entry.svelte index c526981a..eab4357e 100644 --- a/frontend/src/routes/accounting/add_entry.svelte +++ b/frontend/src/routes/accounting/add_entry.svelte @@ -29,16 +29,6 @@ }); let router = useTinyRouter(); - function mapDisplay(object){ - if (object.display){ - return object; - } else if (object.name) { - return {...object, display: object.name}; - } else { - return { display : object } - } - } - async function getTerminal(text,url){ var res = await post(url,text); if (res.ok){ @@ -51,6 +41,11 @@ } } + async function getAccountTags(text){ + 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); @@ -66,6 +61,16 @@ return await getTerminal(text,url); } + function mapDisplay(object){ + if (object.display){ + return object; + } else if (object.name) { + return {...object, display: object.name}; + } else { + return { display : object } + } + } + async function save(){ let data = { ...entry, @@ -75,6 +80,10 @@ let res = await post(url, data); if (res.ok) { yikes(); + if (new_account){ + router.navigate('/accounting'); + return; + } entry.tags = []; onSave(); document.getElementById('date-input').focus(); @@ -93,8 +102,6 @@ } -TODO: Tagging von Umsätzen -
{#if new_account} {t('create_new_object',{object:t('account')})} @@ -134,7 +141,7 @@ {t('tags')} - + diff --git a/frontend/src/routes/tags/TagList.svelte b/frontend/src/routes/tags/TagList.svelte index a9b0d9b7..103fde82 100644 --- a/frontend/src/routes/tags/TagList.svelte +++ b/frontend/src/routes/tags/TagList.svelte @@ -11,6 +11,7 @@ let { id = null, + getCandidates = getCandidateTags, module, tags = $bindable([]), user_list = [], @@ -64,7 +65,7 @@ return false; } - async function getCandidates(input){ + async function getCandidateTags(input){ if (!input || input.length <3) return []; const url = api(`tags/search/${encodeURI(input)}`); const res = await get(url); diff --git a/tags/src/main/java/de/srsoftware/umbrella/tags/TagModule.java b/tags/src/main/java/de/srsoftware/umbrella/tags/TagModule.java index cf985419..5503a997 100644 --- a/tags/src/main/java/de/srsoftware/umbrella/tags/TagModule.java +++ b/tags/src/main/java/de/srsoftware/umbrella/tags/TagModule.java @@ -153,8 +153,13 @@ public class TagModule extends BaseHandler implements TagService { return tag; } + @Override + public Collection search(String key, UmbrellaUser user) { + return tagDb.search(key, user); + } + private boolean searchTags(HttpExchange ex, String head, UmbrellaUser user) throws IOException { - return sendContent(ex, tagDb.search(head, user)); + return sendContent(ex, search(head, user)); } @Override