From f6b854a227c9e36e411f6d9ffb833b5fa3e9553a Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 14 Apr 2026 21:44:13 +0200 Subject: [PATCH] implementd adding and removal of tags to/from transactions Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountDb.java | 2 + .../umbrella/accounting/AccountingModule.java | 44 +++++++++++++ .../umbrella/accounting/SqliteDb.java | 22 +++++-- .../umbrella/core/constants/Path.java | 1 + .../src/routes/accounting/transaction.svelte | 65 +++++++++++++++++-- frontend/src/urls.svelte.js | 14 ++-- 6 files changed, 134 insertions(+), 14 deletions(-) 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 0c96721f..feb2b953 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -8,6 +8,8 @@ import java.util.List; import java.util.Set; public interface AccountDb { + void dropTransactionTag(long transactionId, String tag); + Collection listAccounts(long userId); Account loadAccount(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 7573f8df..2ae25f6a 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -41,6 +41,30 @@ public class AccountingModule extends BaseHandler implements AccountingService { ModuleRegistry.add(this); } + @Override + public boolean doDelete(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + Optional token = SessionToken.from(ex).map(Token::of); + var user = userService().loadUser(token); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + return switch (head) { + case TRANSACTION -> { + try { + var transaction = accountDb.loadTransaction(Long.parseLong(path.pop())); + yield dropTransaction(transaction, user.get(), path, ex); + } catch (NumberFormatException ignored) { + yield super.doDelete(path,ex); + } + } + case null, default -> super.doDelete(path, ex); + }; + } catch (UmbrellaException e){ + return send(ex,e); + } + } + @Override public boolean doGet(Path path, HttpExchange ex) throws IOException { addCors(ex); @@ -114,6 +138,25 @@ public class AccountingModule extends BaseHandler implements AccountingService { } } + public boolean dropTransaction(Transaction transaction, UmbrellaUser user, Path path, HttpExchange ex) throws IOException { + var head = path.pop(); + return switch (head){ + case TAG -> dropTransactionTag(user,transaction,ex); + case null, default -> super.doDelete(path,ex); + }; + } + + private boolean dropTransactionTag(UmbrellaUser user, Transaction transaction, HttpExchange ex) throws IOException { + LOG.log(WARNING,"Missing permission check in AccountModule.dropTransactionTag!"); + var json = json(ex); + if (!json.has(Field.TAG)) throw missingField(Field.TAG); + var tag = json.getString(Field.TAG); + if (tag.isBlank()) throw invalidField(Field.TAG,"non-empty"); + accountDb.dropTransactionTag(transaction.id(),tag); + transaction.tags().remove(tag); + return sendContent(ex, transaction); + } + 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); @@ -164,6 +207,7 @@ public class AccountingModule extends BaseHandler implements AccountingService { if (json.has(Field.DESTINATION)) transaction.destination(IdOrString.of(json.getString(Field.DESTINATION))); if (json.has(Field.PURPOSE)) transaction.purpose(json.getString(Field.PURPOSE)); if (json.has(Field.SOURCE)) transaction.source(IdOrString.of(json.getString(Field.SOURCE))); + if (json.has(Field.TAG)) transaction.tags().add(json.getString(Field.TAG)); return sendContent(ex,accountDb.save(transaction)); } 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 7b657e92..6b865ca5 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -6,7 +6,7 @@ import static de.srsoftware.tools.jdbc.Condition.*; 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.constants.Field.ID; +import static de.srsoftware.umbrella.core.constants.Field.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.model.Translatable.t; import static java.text.MessageFormat.format; @@ -117,6 +117,20 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + @Override + public void dropTransactionTag(long transactionId, String tag) { + try { + var tagIds = new HashSet(); + var rs = select(ID).from(TABLE_TAGS).where(TAG, equal(tag)).exec(db); + while (rs.next()) tagIds.add(rs.getLong(1)); + rs.close(); + + delete().from(TABLE_TAGS_TRANSACTIONS).where(TRANSACTION_ID, equal(transactionId)).where(TAG_ID, in(tagIds.toArray())).execute(db); + } catch (SQLException e) { + throw failedToDropObject(tag); + } + } + @Override public HashSet listAccounts(long userId) { try { @@ -169,7 +183,7 @@ public class SqliteDb extends BaseDb implements AccountDb { public List loadTransactions(Account account) { try { var transactions = new HashMap(); - var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).sort(Field.TIMESTAMP).exec(db); + var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).exec(db); while (rs.next()) { var transaction = Transaction.of(rs); transactions.put(transaction.id(),transaction); @@ -182,7 +196,7 @@ public class SqliteDb extends BaseDb implements AccountDb { if (transaction != null) transaction.tags().add(rs.getString(Field.TAG)); } rs.close(); - return new ArrayList<>(transactions.values()); + return transactions.values().stream().sorted(Comparator.comparing(Transaction::date)).toList(); } catch (SQLException e) { throw failedToLoadMembers(account); } @@ -231,7 +245,7 @@ public class SqliteDb extends BaseDb implements AccountDb { } private Transaction saveTags(Transaction transaction) { - var remaining = new HashSet(transaction.tags()); + var remaining = new HashSet<>(transaction.tags()); var existingTags = new HashMap(); try { var rs = select(ALL).from(TABLE_TAGS).where(Field.TAG,in(transaction.tags().toArray())).exec(db); 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 132ed457..20664737 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 TAG = "tag"; public static final String TAGS = "tags"; public static final String TAGGED = "tagged"; public static final String TRANSACTION = "transaction"; diff --git a/frontend/src/routes/accounting/transaction.svelte b/frontend/src/routes/accounting/transaction.svelte index 60bbc068..5968308d 100644 --- a/frontend/src/routes/accounting/transaction.svelte +++ b/frontend/src/routes/accounting/transaction.svelte @@ -1,8 +1,57 @@