From 5dcd8be93a814f0e7b79f7192b02df34116b2310 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Wed, 1 Apr 2026 18:28:02 +0200 Subject: [PATCH 01/21] preparing for new accounting module --- accounting/build.gradle.kts | 5 +++ .../umbrella/accounting/AccountingModule.java | 8 +++++ .../umbrella/core/SettingsService.java | 1 + .../umbrella/core/api/AccountingService.java | 4 +++ .../umbrella/core/constants/Module.java | 31 ++++++++++--------- .../umbrella/core/constants/Text.java | 6 ++-- frontend/src/App.svelte | 2 ++ frontend/src/routes/accounting/index.svelte | 1 + settings.gradle.kts | 4 +-- 9 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 accounting/build.gradle.kts create mode 100644 accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java create mode 100644 core/src/main/java/de/srsoftware/umbrella/core/api/AccountingService.java create mode 100644 frontend/src/routes/accounting/index.svelte diff --git a/accounting/build.gradle.kts b/accounting/build.gradle.kts new file mode 100644 index 00000000..efceaa32 --- /dev/null +++ b/accounting/build.gradle.kts @@ -0,0 +1,5 @@ +description = "Umbrella : Accounting" + +dependencies{ + implementation(project(":core")) +} \ No newline at end of file diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java new file mode 100644 index 00000000..1d520ae2 --- /dev/null +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -0,0 +1,8 @@ +package de.srsoftware.umbrella.accounting; + +import de.srsoftware.umbrella.core.BaseHandler; +import de.srsoftware.umbrella.core.api.AccountingService; + +public class AccountingModule extends BaseHandler implements AccountingService { + +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/SettingsService.java b/core/src/main/java/de/srsoftware/umbrella/core/SettingsService.java index 924519e9..9a87797a 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/SettingsService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/SettingsService.java @@ -85,6 +85,7 @@ public class SettingsService extends BaseHandler { entries.add(MenuEntry.of(13,Module.STOCK)); entries.add(MenuEntry.of(14,Module.MESSAGE, MESSAGES)); entries.add(MenuEntry.of(15,Module.POLL,Text.POLLS)); + entries.add(MenuEntry.of(16,Module.ACCOUNTING,Text.ACCOUNTING)); for (var i=0; i{@html messages.warning} {/if} + diff --git a/frontend/src/routes/accounting/index.svelte b/frontend/src/routes/accounting/index.svelte new file mode 100644 index 00000000..4b993eec --- /dev/null +++ b/frontend/src/routes/accounting/index.svelte @@ -0,0 +1 @@ +

Accounts

\ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9b27edfe..1ba16a67 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,6 @@ rootProject.name = "Umbrella25" +include("accounting") include("backend") include("bookmark") include("bus") @@ -22,5 +23,4 @@ include("time") include("translations") include("user") include("web") -include("wiki") - +include("wiki") \ No newline at end of file From 71f91f0f615262147d9d611530df3a4bcd39d4c4 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Wed, 1 Apr 2026 23:26:04 +0200 Subject: [PATCH 02/21] working on entry form Signed-off-by: Stephan Richter --- frontend/src/App.svelte | 2 + frontend/src/Components/Autocomplete.svelte | 4 +- .../src/routes/accounting/add_entry.svelte | 99 +++++++++++++++++++ frontend/src/routes/accounting/index.svelte | 17 +++- frontend/src/routes/accounting/new.svelte | 5 + 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 frontend/src/routes/accounting/add_entry.svelte create mode 100644 frontend/src/routes/accounting/new.svelte diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 69ac224e..443b88da 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -26,6 +26,7 @@ import Messages from "./routes/message/Messages.svelte"; import MsgSettings from "./routes/message/Settings.svelte"; import Menu from "./Components/Menu.svelte"; + import NewAccount from "./routes/accounting/new.svelte"; import NewPage from "./routes/wiki/AddPage.svelte"; import Notes from "./routes/notes/Index.svelte"; import PollList from "./routes/poll/Index.svelte"; @@ -90,6 +91,7 @@ {/if} + diff --git a/frontend/src/Components/Autocomplete.svelte b/frontend/src/Components/Autocomplete.svelte index 35a02c88..76bd89ca 100644 --- a/frontend/src/Components/Autocomplete.svelte +++ b/frontend/src/Components/Autocomplete.svelte @@ -52,7 +52,6 @@ candidate = candidates[idx]; candidates = []; selected = []; - console.log(candidate); onSelect(candidate); } @@ -93,6 +92,7 @@ return false; } + candidate = { display : candidate.display }; candidates = await getCandidates(candidate.display); if (selected>candidates.length) selected = candidates.length; return false; @@ -103,7 +103,7 @@ span { position : relative } select { position : absolute; top: 30px; left: 3px; } - select { background: black; color: orange; border: 1px solid orange; border-radius: 5px; } + select { background: black; color: orange; border: 1px solid orange; border-radius: 5px; z-index: 50; } option:checked { background: orange; color: black; } diff --git a/frontend/src/routes/accounting/add_entry.svelte b/frontend/src/routes/accounting/add_entry.svelte new file mode 100644 index 00000000..d468ea40 --- /dev/null +++ b/frontend/src/routes/accounting/add_entry.svelte @@ -0,0 +1,99 @@ + + + + +
+ {#if new_account} + {t('create_new_object',{object:t('account')})} + {t('account name')} + + + {t('currency')} + +
+ {:else} + {t('add_object',{object:t('entry')})} + {/if} + + {t('date')} + + + {t('source')} + + + {t('destination')} + + + {t('amount')} + +  {entry.account.currency} + + + {t('purpose')} + + + + + + +
+
+
+{JSON.stringify(entry,null,2)}
+
+
+ +
+
+{JSON.stringify(user,null,2)}
+
+
diff --git a/frontend/src/routes/accounting/index.svelte b/frontend/src/routes/accounting/index.svelte index 4b993eec..517d60bd 100644 --- a/frontend/src/routes/accounting/index.svelte +++ b/frontend/src/routes/accounting/index.svelte @@ -1 +1,16 @@ -

Accounts

\ No newline at end of file + + +
+ {t('accounts')} + + +
\ No newline at end of file diff --git a/frontend/src/routes/accounting/new.svelte b/frontend/src/routes/accounting/new.svelte new file mode 100644 index 00000000..5f4df92f --- /dev/null +++ b/frontend/src/routes/accounting/new.svelte @@ -0,0 +1,5 @@ + + + From 62e7b322ceb3a90f6c05aca60ed5d9854e3fac76 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Wed, 1 Apr 2026 23:51:09 +0200 Subject: [PATCH 03/21] preparing to save data for accouting Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountDb.java | 7 ++ .../umbrella/accounting/AccountingModule.java | 77 +++++++++++++++++++ .../umbrella/accounting/SqliteDb.java | 12 +++ .../umbrella/core/constants/Field.java | 1 + .../umbrella/core/model/Account.java | 4 + 5 files changed, 101 insertions(+) create mode 100644 accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java create mode 100644 accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java create mode 100644 core/src/main/java/de/srsoftware/umbrella/core/model/Account.java diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java new file mode 100644 index 00000000..9b813ec9 --- /dev/null +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -0,0 +1,7 @@ +package de.srsoftware.umbrella.accounting; + +import de.srsoftware.umbrella.core.model.Account; + +public interface AccountDb { + Account save(Account account); +} 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 1d520ae2..11f7ec24 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -1,8 +1,85 @@ package de.srsoftware.umbrella.accounting; +import com.sun.net.httpserver.HttpExchange; +import de.srsoftware.configuration.Configuration; +import de.srsoftware.tools.Path; +import de.srsoftware.tools.SessionToken; import de.srsoftware.umbrella.core.BaseHandler; +import de.srsoftware.umbrella.core.ModuleRegistry; import de.srsoftware.umbrella.core.api.AccountingService; +import de.srsoftware.umbrella.core.constants.Field; +import de.srsoftware.umbrella.core.constants.Text; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Account; +import de.srsoftware.umbrella.core.model.Token; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Optional; + +import static de.srsoftware.tools.Optionals.nullIfEmpty; +import static de.srsoftware.umbrella.core.ConnectionProvider.connect; +import static de.srsoftware.umbrella.core.constants.Path.JSON; +import static de.srsoftware.umbrella.core.constants.Path.SEARCH; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; public class AccountingModule extends BaseHandler implements AccountingService { + public static final String CONFIG_DATABASE = "umbrella.modules.accounting.database"; + private final AccountDb accountDb; + public AccountingModule(Configuration config) throws UmbrellaException { + super(); + var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE)); + accountDb = new SqliteDb(connect(dbFile)); + ModuleRegistry.add(this); + } + + @Override + public boolean doPost(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + Optional token = SessionToken.from(ex).map(Token::of); + var user = ModuleRegistry.userService().loadUser(token); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + return switch (head) { + case null -> postEntry(user.get(),ex); + default -> super.doPost(path,ex); + }; + } catch (NumberFormatException e){ + return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id"); + } catch (UmbrellaException e){ + return send(ex,e); + } + } + + private boolean postEntry(UmbrellaUser user, HttpExchange ex) throws IOException { + var json = json(ex); + if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT); + if (!(json.get(Field.ACCOUNT) instanceof JSONObject acc)) throw invalidField(Field.ACCOUNT,JSON); + // TODO more tests + + Long accountId = null; + if (acc.has(Field.ID)) { + if (!(acc.get(Field.ID) instanceof Number accId)) throw invalidField(String.join(".",Field.ACCOUNT,Field.ID), Text.NUMBER); + if (accId.longValue() != 0) accountId = accId.longValue(); + } + if (accountId == null){ + if (!acc.has(Field.NAME)) throw missingField(String.join(".",Field.ACCOUNT,Field.NAME)); + var accountName = acc.getString(Field.NAME); + if (accountName.isBlank()) throw invalidField(String.join(".",Field.ACCOUNT,Field.NAME),Text.STRING); + + var currency = acc.has(Field.CURRENCY) ? nullIfEmpty(acc.getString(Field.CURRENCY)) : null; + + var account = accountDb.save(new Account(0, accountName, currency)); + accountId = account.id(); + } + + // TODO: save entry + + return notFound(ex); + } } diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java new file mode 100644 index 00000000..03f57c8d --- /dev/null +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -0,0 +1,12 @@ +package de.srsoftware.umbrella.accounting; + +import de.srsoftware.umbrella.core.BaseDb; +import de.srsoftware.umbrella.core.model.Account; + +import java.sql.Connection; + +public class SqliteDb extends BaseDb implements AccountDb { + public SqliteDb(Connection connection) { + super(connection); + } +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java index 430e524a..2222cb8b 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java @@ -3,6 +3,7 @@ package de.srsoftware.umbrella.core.constants; public class Field { public static final String ACTION = "action"; + public static final String ACCOUNT = "account"; public static final String ADDRESS = "address"; public static final String ALLOWED_STATES = "allowed_states"; public static final String AMOUNT = "amount"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java new file mode 100644 index 00000000..f62c4b28 --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java @@ -0,0 +1,4 @@ +package de.srsoftware.umbrella.core.model; + +public record Account(long id, String name, String currency) { +} From 4fd9bd6c8fdf5d4465dbc34ae7c100eb1af496da Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Thu, 2 Apr 2026 00:35:07 +0200 Subject: [PATCH 04/21] implemented creation of new account Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountingModule.java | 4 +- .../umbrella/accounting/Constants.java | 8 ++ .../umbrella/accounting/SqliteDb.java | 76 +++++++++++++++++++ backend/build.gradle.kts | 2 + .../umbrella/backend/Application.java | 3 + .../umbrella/core/ModuleRegistry.java | 36 ++++----- .../umbrella/core/constants/Field.java | 1 + .../umbrella/core/model/Account.java | 5 +- 8 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java 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 11f7ec24..262061e5 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.Optional; 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.constants.Path.JSON; import static de.srsoftware.umbrella.core.constants.Path.SEARCH; @@ -27,7 +28,6 @@ import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFi import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; public class AccountingModule extends BaseHandler implements AccountingService { - public static final String CONFIG_DATABASE = "umbrella.modules.accounting.database"; private final AccountDb accountDb; public AccountingModule(Configuration config) throws UmbrellaException { @@ -74,7 +74,7 @@ public class AccountingModule extends BaseHandler implements AccountingService { var currency = acc.has(Field.CURRENCY) ? nullIfEmpty(acc.getString(Field.CURRENCY)) : null; - var account = accountDb.save(new Account(0, accountName, currency)); + var account = accountDb.save(new Account(0, accountName, currency, user.id())); accountId = account.id(); } diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java new file mode 100644 index 00000000..9031166c --- /dev/null +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java @@ -0,0 +1,8 @@ +package de.srsoftware.umbrella.accounting; + +public class Constants { + public static final String CONFIG_DATABASE = "umbrella.modules.accounting.database"; + + public static final String TABLE_ACCOUNTS = "accounts"; + public static final String TABLE_TRANSACTIONS = "transactions"; +} 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 03f57c8d..ad144e19 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -1,12 +1,88 @@ package de.srsoftware.umbrella.accounting; +import de.srsoftware.tools.jdbc.Query; import de.srsoftware.umbrella.core.BaseDb; +import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.model.Account; import java.sql.Connection; +import java.sql.SQLException; + +import static de.srsoftware.tools.NotImplemented.notImplemented; +import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS; +import static de.srsoftware.umbrella.accounting.Constants.TABLE_TRANSACTIONS; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.failedToCreateTable; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.failedToStoreObject; +import static java.text.MessageFormat.format; public class SqliteDb extends BaseDb implements AccountDb { public SqliteDb(Connection connection) { super(connection); } + + @Override + protected int createTables() { + var version = createSettingsTable(); + switch (version){ + case 0: + createAccountsTable(); + createTransactionsTable(); + } + return setCurrentVersion(1); + } + + private void createAccountsTable() { + var sql = """ + CREATE TABLE IF NOT EXISTS {0} ( + {1} INTEGER PRIMARY KEY, + {2} VARCHAR(255) NOT NULL, + {3} LONG NOT NULL, + {4} CURRENCY VARCHAR(16) + );"""; + + try { + sql = format(sql,TABLE_ACCOUNTS, Field.ID, Field.NAME, Field.OWNER, Field.CURRENCY); + var stmt = db.prepareStatement(sql); + stmt.execute(); + stmt.close(); + } catch (SQLException e) { + throw failedToCreateTable(TABLE_ACCOUNTS).causedBy(e); + } + } + + private void createTransactionsTable() { + var sql = """ + CREATE TABLE IF NOT EXISTS {0} ( + {1} LONG NOT NULL, + {2} LONG NOT NULL, + {3} VARCHAR(255) NOT NULL, + {4} VARCHAR(255) NOT NULL, + {5} DOUBLE NOT NULL, + {6} TEXT + );"""; + try { + sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION); + var stmt = db.prepareStatement(sql); + stmt.execute(); + stmt.close(); + } catch (SQLException e) { + throw failedToCreateTable(TABLE_TRANSACTIONS).causedBy(e); + } + } + + @Override + public Account save(Account account) { + if (account.id() == 0) try { // new account + var rs = Query.insertInto(TABLE_ACCOUNTS,Field.NAME, Field.CURRENCY, Field.OWNER).values(account.name(),account.currency(),account.ownerId()).execute(db).getGeneratedKeys(); + Long newId = null; + if (rs.next()) newId = rs.getLong(1); + rs.close(); + if (newId == null) throw failedToStoreObject(account); + return account.withId(newId); + } catch (SQLException e) { + throw failedToStoreObject(account).causedBy(e); + } else { + throw notImplemented(this,"save(account)"); + } + } } diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 94839ead..fc93936e 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -11,6 +11,7 @@ application{ } dependencies{ + implementation(project(":accounting")); implementation(project(":bookmark")); implementation(project(":bus")); implementation(project(":company")) @@ -47,6 +48,7 @@ tasks.jar { .map(::zipTree) // OR .map { zipTree(it) } from(dependencies) dependsOn( + ":accounting:jar", ":bookmark:jar", ":bus:jar", ":company:jar", diff --git a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java index 42e76b2a..edd2e11b 100644 --- a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java +++ b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java @@ -9,11 +9,13 @@ import static java.lang.System.Logger.Level.INFO; import com.sun.net.httpserver.HttpServer; import de.srsoftware.configuration.JsonConfig; import de.srsoftware.tools.ColorLogger; +import de.srsoftware.umbrella.accounting.AccountingModule; import de.srsoftware.umbrella.bookmarks.BookmarkApi; import de.srsoftware.umbrella.company.CompanyModule; import de.srsoftware.umbrella.contact.ContactModule; import de.srsoftware.umbrella.core.SettingsService; import de.srsoftware.umbrella.core.Util; +import de.srsoftware.umbrella.core.api.AccountingService; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.documents.DocumentApi; import de.srsoftware.umbrella.files.FileModule; @@ -92,6 +94,7 @@ public class Application { new WikiModule(config).bindPath("/api/wiki").on(server); new FileModule(config).bindPath("/api/files").on(server); new SettingsService(config).bindPath("/api/settings").on(server); + new AccountingModule(config).bindPath("/api/accounting").on(server); } catch (Exception e) { LOG.log(ERROR,"Startup failed",e); System.exit(-1); diff --git a/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java b/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java index dff11922..5ecb60ee 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java @@ -5,6 +5,7 @@ package de.srsoftware.umbrella.core; import de.srsoftware.umbrella.core.api.*; public class ModuleRegistry { + private AccountingService accountingService; private BookmarkService bookmarkService; private CompanyService companyService; private ContactService contactService; @@ -29,23 +30,24 @@ public class ModuleRegistry { public static void add(Object service) { switch (service) { - case BookmarkService bs: singleton.bookmarkService = bs; break; - case CompanyService cs: singleton.companyService = cs; break; - case ContactService cs: singleton.contactService = cs; break; - case DocumentService ds: singleton.documentService = ds; break; - case FileService fs: singleton.fileService = fs; break; - case StockService is: singleton.stockService = is; break; - case MarkdownService ms: singleton.markdownService = ms; break; - case NoteService ns: singleton.noteService = ns; break; - case PollService ps: singleton.pollService = ps; break; - case PostBox pb: singleton.postBox = pb; break; - case ProjectService ps: singleton.projectService = ps; break; - case TagService ts: singleton.tagService = ts; break; - case TaskService ts: singleton.taskService = ts; break; - case TimeService ts: singleton.timeService = ts; break; - case Translator tr: singleton.translator = tr; break; - case UserService us: singleton.userService = us; break; - case WikiService ws: singleton.wikiService = ws; break; + case AccountingService as: singleton.accountingService = as; break; + case BookmarkService bs: singleton.bookmarkService = bs; break; + case CompanyService cs: singleton.companyService = cs; break; + case ContactService cs: singleton.contactService = cs; break; + case DocumentService ds: singleton.documentService = ds; break; + case FileService fs: singleton.fileService = fs; break; + case StockService is: singleton.stockService = is; break; + case MarkdownService ms: singleton.markdownService = ms; break; + case NoteService ns: singleton.noteService = ns; break; + case PollService ps: singleton.pollService = ps; break; + case PostBox pb: singleton.postBox = pb; break; + case ProjectService ps: singleton.projectService = ps; break; + case TagService ts: singleton.tagService = ts; break; + case TaskService ts: singleton.taskService = ts; break; + case TimeService ts: singleton.timeService = ts; break; + case Translator tr: singleton.translator = tr; break; + case UserService us: singleton.userService = us; break; + case WikiService ws: singleton.wikiService = ws; break; case null: break; default: System.getLogger(ModuleRegistry.class.getSimpleName()).log(System.Logger.Level.WARNING,"Trying to add untracked class {0}",service.getClass().getSimpleName()); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java index 2222cb8b..485a151b 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java @@ -44,6 +44,7 @@ public class Field { public static final String DELIVERY = "delivery"; public static final String DELIVERY_DATE = "delivery_date"; public static final String DESCRIPTION = "description"; + public static final String DESTINATION = "destination"; public static final String DOCUMENT = "document"; public static final String DOCUMENT_ID = "document_id"; public static final String DOC_TYPE_ID = "document_type_id"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java index f62c4b28..1abb8365 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java @@ -1,4 +1,7 @@ package de.srsoftware.umbrella.core.model; -public record Account(long id, String name, String currency) { +public record Account(long id, String name, String currency, long ownerId) { + public Account withId(long newId) { + return new Account(newId,name,currency,ownerId); + } } From 4c7a660fa782138e1497274311ebdf21181922a8 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Thu, 2 Apr 2026 13:17:14 +0200 Subject: [PATCH 05/21] implemented storing of new transaction Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountDb.java | 2 + .../umbrella/accounting/AccountingModule.java | 53 ++++++++++++++++--- .../umbrella/accounting/SqliteDb.java | 15 ++++++ .../umbrella/core/constants/Field.java | 2 + .../umbrella/core/model/Account.java | 20 ++++++- .../umbrella/core/model/Transaction.java | 38 +++++++++++++ frontend/src/routes/accounting/index.svelte | 2 + 7 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java 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 9b813ec9..550e2c97 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -1,7 +1,9 @@ package de.srsoftware.umbrella.accounting; import de.srsoftware.umbrella.core.model.Account; +import de.srsoftware.umbrella.core.model.Transaction; public interface AccountDb { Account save(Account account); + Transaction save(Transaction transaction); } 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 262061e5..c5c809ca 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -12,15 +12,20 @@ import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Account; import de.srsoftware.umbrella.core.model.Token; +import de.srsoftware.umbrella.core.model.Transaction; import de.srsoftware.umbrella.core.model.UmbrellaUser; import org.json.JSONObject; import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Optional; 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.userService; import static de.srsoftware.umbrella.core.constants.Path.JSON; import static de.srsoftware.umbrella.core.constants.Path.SEARCH; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; @@ -42,7 +47,7 @@ public class AccountingModule extends BaseHandler implements AccountingService { addCors(ex); try { Optional token = SessionToken.from(ex).map(Token::of); - var user = ModuleRegistry.userService().loadUser(token); + var user = userService().loadUser(token); if (user.isEmpty()) return unauthorized(ex); var head = path.pop(); return switch (head) { @@ -60,13 +65,49 @@ public class AccountingModule extends BaseHandler implements AccountingService { var json = json(ex); if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT); if (!(json.get(Field.ACCOUNT) instanceof JSONObject acc)) throw invalidField(Field.ACCOUNT,JSON); - // TODO more tests + + if (!json.has(Field.DATE)) throw missingField(Field.DATE); + var date = LocalDate.parse(json.getString(Field.DATE)); + var dateTime = LocalDateTime.now().withYear(date.getYear()).withMonth(date.getMonthValue()).withDayOfMonth(date.getDayOfMonth()); + if (!json.has(Field.AMOUNT)) throw missingField(Field.AMOUNT); + if (!(json.get(Field.AMOUNT) instanceof Number amount)) throw invalidField(Field.AMOUNT,Text.NUMBER); + + var purpose = json.has(Field.PURPOSE) ? json.getString(Field.PURPOSE) : null; + + if (!json.has(Field.SOURCE)) throw missingField(Field.SOURCE); + if (!(json.get(Field.SOURCE) instanceof JSONObject sourceData)) throw invalidField(Field.SOURCE,JSON); + if (!json.has(Field.DESTINATION)) throw missingField(Field.DESTINATION); + if (!(json.get(Field.DESTINATION) instanceof JSONObject destinationData)) throw invalidField(Field.DESTINATION,JSON); + + String source = null, destination = null; + + if (sourceData.has(Field.USER_ID)) { + if (!(sourceData.get(Field.USER_ID) instanceof Number uid)) throw invalidField(String.join(".",Field.SOURCE,Field.USER_ID),Text.NUMBER); + var u = userService().loadUser(uid.longValue()); + source = ""+u.id(); + } else { + if (!sourceData.has(Field.DISPLAY)) throw missingField(String.join(".",Field.SOURCE,Field.DISPLAY)); + source = sourceData.getString(Field.DISPLAY); + if (source.isBlank()) throw invalidField(String.join(".",Field.SOURCE,Field.DISPLAY),Text.STRING); + } + + if (destinationData.has(Field.USER_ID)) { + if (!(destinationData.get(Field.USER_ID) instanceof Number uid)) throw invalidField(String.join(".",Field.DESTINATION,Field.USER_ID),Text.NUMBER); + var u = userService().loadUser(uid.longValue()); + destination = ""+u; + } else { + if (!destinationData.has(Field.DISPLAY)) throw missingField(String.join(".",Field.DESTINATION,Field.DISPLAY)); + destination = destinationData.getString(Field.DISPLAY); + if (destination.isBlank()) throw invalidField(String.join(".",Field.DESTINATION,Field.DISPLAY),Text.STRING); + } + Long accountId = null; if (acc.has(Field.ID)) { if (!(acc.get(Field.ID) instanceof Number accId)) throw invalidField(String.join(".",Field.ACCOUNT,Field.ID), Text.NUMBER); if (accId.longValue() != 0) accountId = accId.longValue(); } + Account newAccount = null; if (accountId == null){ if (!acc.has(Field.NAME)) throw missingField(String.join(".",Field.ACCOUNT,Field.NAME)); var accountName = acc.getString(Field.NAME); @@ -74,12 +115,12 @@ public class AccountingModule extends BaseHandler implements AccountingService { var currency = acc.has(Field.CURRENCY) ? nullIfEmpty(acc.getString(Field.CURRENCY)) : null; - var account = accountDb.save(new Account(0, accountName, currency, user.id())); - accountId = account.id(); + newAccount = accountDb.save(new Account(0, accountName, currency, user.id())); + accountId = newAccount.id(); } - // TODO: save entry + var transaction = accountDb.save(new Transaction(accountId,dateTime,source,destination,amount.doubleValue(),purpose)); - return notFound(ex); + return sendContent(ex,newAccount != null ? newAccount : 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 ad144e19..46dbd4d3 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -4,9 +4,11 @@ import de.srsoftware.tools.jdbc.Query; import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.model.Account; +import de.srsoftware.umbrella.core.model.Transaction; import java.sql.Connection; import java.sql.SQLException; +import java.time.ZoneOffset; import static de.srsoftware.tools.NotImplemented.notImplemented; import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS; @@ -85,4 +87,17 @@ public class SqliteDb extends BaseDb implements AccountDb { throw notImplemented(this,"save(account)"); } } + + @Override + public Transaction save(Transaction transaction) { + try { + var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); + Query.replaceInto(TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION) + .values(transaction.accountId(),timestamp,transaction.source(),transaction.destination(),transaction.amount(),transaction.purpose()) + .execute(db).close(); + return transaction; + } catch (SQLException e) { + throw failedToStoreObject(transaction); + } + } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java index 485a151b..53600c1d 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java @@ -45,6 +45,7 @@ public class Field { public static final String DELIVERY_DATE = "delivery_date"; public static final String DESCRIPTION = "description"; public static final String DESTINATION = "destination"; + public static final String DISPLAY = "display"; public static final String DOCUMENT = "document"; public static final String DOCUMENT_ID = "document_id"; public static final String DOC_TYPE_ID = "document_type_id"; @@ -129,6 +130,7 @@ public class Field { public static final String PROJECT = "project"; public static final String PROJECT_ID = "project_id"; public static final String PROPERTIES = "properties"; + public static final String PURPOSE = "purpose"; public static final String RECEIVERS = "receivers"; public static final String REDIRECT = "redirect"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java index 1abb8365..a3f19152 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java @@ -1,7 +1,25 @@ package de.srsoftware.umbrella.core.model; -public record Account(long id, String name, String currency, long ownerId) { +import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.constants.Field; + +import java.util.Map; + +import static de.srsoftware.umbrella.core.ModuleRegistry.userService; + +public record Account(long id, String name, String currency, long ownerId) implements Mappable { public Account withId(long newId) { return new Account(newId,name,currency,ownerId); } + + @Override + public Map toMap() { + var owner = userService().loadUser(ownerId); + return Map.of( + Field.ID, id, + Field.NAME, name, + Field.CURRENCY, currency, + Field.OWNER, owner.toMap() + ); + } } 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 new file mode 100644 index 00000000..f2cd50af --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java @@ -0,0 +1,38 @@ +package de.srsoftware.umbrella.core.model; + +import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.constants.Field; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Map; + +import static de.srsoftware.umbrella.core.ModuleRegistry.userService; + +public record Transaction(long accountId, LocalDateTime date, String source, String destination, double amount, String purpose) implements Mappable { + @Override + public Map toMap() { + var source = this.source; + try { + var userId = Long.parseLong(source); + var user = userService().loadUser(userId); + source = user.name(); + } catch (NumberFormatException ignored) {} + + var destination = this.destination; + try { + var userId = Long.parseLong(destination); + var user = userService().loadUser(userId); + destination = user.name(); + } catch (NumberFormatException ignored) {} + + return Map.of( + Field.ID, accountId, + Field.DATE, date.toLocalDate(), + Field.SOURCE, source, + Field.DESTINATION, destination, + Field.AMOUNT, amount, + Field.PURPOSE, purpose + ); + } +} diff --git a/frontend/src/routes/accounting/index.svelte b/frontend/src/routes/accounting/index.svelte index 517d60bd..08882cde 100644 --- a/frontend/src/routes/accounting/index.svelte +++ b/frontend/src/routes/accounting/index.svelte @@ -7,6 +7,8 @@ function newAccount(){ router.navigate('/accounting/new'); } + +
From bba5fd36b4d1dba3a9cc0fda812f201aa2d201af Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Thu, 2 Apr 2026 13:39:35 +0200 Subject: [PATCH 06/21] implemented loading of accounts Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountDb.java | 4 +++ .../umbrella/accounting/AccountingModule.java | 25 +++++++++++++++++ .../umbrella/accounting/SqliteDb.java | 28 +++++++++++++++++-- .../umbrella/core/model/Account.java | 14 ++++++++-- frontend/src/routes/accounting/index.svelte | 16 ++++++++++- 5 files changed, 82 insertions(+), 5 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 550e2c97..8dab5dce 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -3,7 +3,11 @@ package de.srsoftware.umbrella.accounting; import de.srsoftware.umbrella.core.model.Account; import de.srsoftware.umbrella.core.model.Transaction; +import java.util.Collection; + public interface AccountDb { + Collection listAccounts(long userId); Account save(Account account); + Transaction save(Transaction transaction); } 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 c5c809ca..2b3d9552 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -42,6 +42,25 @@ public class AccountingModule extends BaseHandler implements AccountingService { ModuleRegistry.add(this); } + @Override + public boolean doGet(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 null -> getAccounts(user.get(),ex); + default -> super.doGet(path,ex); + }; + } catch (NumberFormatException e){ + return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id"); + } catch (UmbrellaException e){ + return send(ex,e); + } + } + @Override public boolean doPost(Path path, HttpExchange ex) throws IOException { addCors(ex); @@ -61,6 +80,12 @@ public class AccountingModule extends BaseHandler implements AccountingService { } } + private boolean getAccounts(UmbrellaUser user, HttpExchange ex) throws IOException { + return sendContent(ex,accountDb.listAccounts(user.id())); + } + + + private boolean postEntry(UmbrellaUser user, HttpExchange ex) throws IOException { var json = json(ex); if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT); 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 46dbd4d3..a1b805c7 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -1,20 +1,24 @@ package de.srsoftware.umbrella.accounting; +import de.srsoftware.tools.jdbc.Condition; import de.srsoftware.tools.jdbc.Query; import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.constants.Field; +import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.model.Account; import de.srsoftware.umbrella.core.model.Transaction; import java.sql.Connection; import java.sql.SQLException; import java.time.ZoneOffset; +import java.util.HashSet; import static de.srsoftware.tools.NotImplemented.notImplemented; +import static de.srsoftware.tools.jdbc.Condition.equal; +import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS; import static de.srsoftware.umbrella.accounting.Constants.TABLE_TRANSACTIONS; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.failedToCreateTable; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.failedToStoreObject; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static java.text.MessageFormat.format; public class SqliteDb extends BaseDb implements AccountDb { @@ -72,6 +76,26 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + @Override + public HashSet listAccounts(long userId) { + try { + var accountIds = new HashSet(); + var rs = Query.select("DISTINCT " + Field.ACCOUNT).from(TABLE_TRANSACTIONS).where(Field.SOURCE, equal(userId)).exec(db); + while (rs.next()) accountIds.add(rs.getLong(1)); + rs.close(); + rs = Query.select("DISTINCT " + Field.ACCOUNT).from(TABLE_TRANSACTIONS).where(Field.DESTINATION, equal(userId)).exec(db); + while (rs.next()) accountIds.add(rs.getLong(1)); + rs.close(); + var accounts = new HashSet(); + rs = Query.select(ALL).from(TABLE_ACCOUNTS).where(Field.ID, Condition.in(accountIds.toArray())).exec(db); + while (rs.next()) accounts.add(Account.of(rs)); + rs.close(); + return accounts; + } catch (SQLException e){ + throw failedToLoadObject(Text.ACCOUNTING).causedBy(e); + } + } + @Override public Account save(Account account) { if (account.id() == 0) try { // new account diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java index a3f19152..4a520312 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java @@ -3,13 +3,19 @@ package de.srsoftware.umbrella.core.model; import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.constants.Field; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.Map; import static de.srsoftware.umbrella.core.ModuleRegistry.userService; public record Account(long id, String name, String currency, long ownerId) implements Mappable { - public Account withId(long newId) { - return new Account(newId,name,currency,ownerId); + public static Account of(ResultSet rs) throws SQLException { + var id = rs.getLong(Field.ID); + var name = rs.getString(Field.NAME); + var owner = rs.getLong(Field.OWNER); + var currency = rs.getString(Field.CURRENCY); + return new Account(id,name,currency,owner); } @Override @@ -22,4 +28,8 @@ public record Account(long id, String name, String currency, long ownerId) imple Field.OWNER, owner.toMap() ); } + + public Account withId(long newId) { + return new Account(newId,name,currency,ownerId); + } } diff --git a/frontend/src/routes/accounting/index.svelte b/frontend/src/routes/accounting/index.svelte index 08882cde..6c9d140c 100644 --- a/frontend/src/routes/accounting/index.svelte +++ b/frontend/src/routes/accounting/index.svelte @@ -1,14 +1,28 @@
From 246bb887b079aa27e5420f9a261c1d11c12f834a Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Thu, 2 Apr 2026 13:57:08 +0200 Subject: [PATCH 07/21] implemented account list Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountingModule.java | 3 ++- frontend/src/App.svelte | 2 ++ frontend/src/routes/accounting/account.svelte | 5 +++++ frontend/src/routes/accounting/index.svelte | 20 ++++++++++++++++++- .../srsoftware/umbrella/user/UserModule.java | 3 +-- 5 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 frontend/src/routes/accounting/account.svelte 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 2b3d9552..4966563e 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -26,6 +26,7 @@ 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.userService; +import static de.srsoftware.umbrella.core.Util.mapValues; import static de.srsoftware.umbrella.core.constants.Path.JSON; import static de.srsoftware.umbrella.core.constants.Path.SEARCH; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; @@ -81,7 +82,7 @@ public class AccountingModule extends BaseHandler implements AccountingService { } private boolean getAccounts(UmbrellaUser user, HttpExchange ex) throws IOException { - return sendContent(ex,accountDb.listAccounts(user.id())); + return sendContent(ex,accountDb.listAccounts(user.id()).stream().map(Account::toMap)); } diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 443b88da..a84253ab 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -5,6 +5,7 @@ import { loadTranslation } from './translations.svelte'; import { checkUser, user } from './user.svelte'; + import Account from "./routes/accounting/account.svelte"; import Accounts from "./routes/accounting/index.svelte"; import AddDoc from "./routes/document/Add.svelte"; import AddTask from "./routes/task/Add.svelte"; @@ -90,6 +91,7 @@ {@html messages.warning} {/if} + diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte new file mode 100644 index 00000000..93839853 --- /dev/null +++ b/frontend/src/routes/accounting/account.svelte @@ -0,0 +1,5 @@ + +

Account {id}

\ No newline at end of file diff --git a/frontend/src/routes/accounting/index.svelte b/frontend/src/routes/accounting/index.svelte index 6c9d140c..494f9f74 100644 --- a/frontend/src/routes/accounting/index.svelte +++ b/frontend/src/routes/accounting/index.svelte @@ -22,11 +22,29 @@ router.navigate('/accounting/new'); } + function onclick(e){ + e.preventDefault(); + let href = e.target.getAttribute('href'); + if (href) router.navigate(href); + return false; + } + onMount(load);
{t('accounts')} - + + + + + +
\ No newline at end of file diff --git a/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java b/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java index 14ced958..e836000d 100644 --- a/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java +++ b/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java @@ -412,8 +412,7 @@ public class UserModule extends BaseHandler implements UserService { private boolean getUserList(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException { if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(LIST_USERS))) throw forbidden("You are not allowed to list users!"); - var list = users.list(0, null,null).values().stream().map(UmbrellaUser::toMap).toList(); - return sendContent(ex,list); + return sendContent(ex,users.list(0, null,null).values().stream().map(UmbrellaUser::toMap)); } private boolean impersonate(HttpExchange ex, Long targetId) throws IOException, UmbrellaException { From 087c2ef95e91b65789990ff66743248e761aad42 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Thu, 2 Apr 2026 18:14:19 +0200 Subject: [PATCH 08/21] working on loading of account data Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountDb.java | 6 +++ .../umbrella/accounting/AccountingModule.java | 43 ++++++++++++++-- .../umbrella/accounting/SqliteDb.java | 36 ++++++++++++-- .../umbrella/core/constants/Field.java | 1 + .../umbrella/core/constants/Text.java | 1 + .../umbrella/core/model/Transaction.java | 16 +++++- frontend/src/routes/accounting/account.svelte | 49 ++++++++++++++++++- frontend/src/routes/accounting/index.svelte | 2 +- 8 files changed, 142 insertions(+), 12 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 8dab5dce..f181a3f8 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -4,9 +4,15 @@ import de.srsoftware.umbrella.core.model.Account; import de.srsoftware.umbrella.core.model.Transaction; import java.util.Collection; +import java.util.List; public interface AccountDb { Collection listAccounts(long userId); + + Account loadAccount(long accountId); + + List loadTransactions(Account account); + Account save(Account account); Transaction save(Transaction transaction); 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 4966563e..a57edfd3 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -20,6 +20,8 @@ import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import static de.srsoftware.tools.Optionals.nullIfEmpty; @@ -53,10 +55,13 @@ public class AccountingModule extends BaseHandler implements AccountingService { var head = path.pop(); return switch (head) { case null -> getAccounts(user.get(),ex); - default -> super.doGet(path,ex); + default -> { + try { + yield getAccount(user.get(),Long.parseLong(head),ex); + } catch (NumberFormatException ignored) {} + yield super.doGet(path,ex); + } }; - } catch (NumberFormatException e){ - return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id"); } catch (UmbrellaException e){ return send(ex,e); } @@ -74,13 +79,41 @@ public class AccountingModule extends BaseHandler implements AccountingService { case null -> postEntry(user.get(),ex); default -> super.doPost(path,ex); }; - } catch (NumberFormatException e){ - return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id"); } catch (UmbrellaException e){ return send(ex,e); } } + private boolean getAccount(UmbrellaUser user, long accountId, HttpExchange ex) throws IOException { + var account = accountDb.loadAccount(accountId); + var transactions = accountDb.loadTransactions(account); + var userMap = new HashMap(); + var foundRequestingUser = false; + for (var i=0; i listAccounts(long userId) { try { var accountIds = new HashSet(); - var rs = Query.select("DISTINCT " + Field.ACCOUNT).from(TABLE_TRANSACTIONS).where(Field.SOURCE, equal(userId)).exec(db); + var rs = select("DISTINCT " + Field.ACCOUNT).from(TABLE_TRANSACTIONS).where(Field.SOURCE, equal(userId)).exec(db); while (rs.next()) accountIds.add(rs.getLong(1)); rs.close(); - rs = Query.select("DISTINCT " + Field.ACCOUNT).from(TABLE_TRANSACTIONS).where(Field.DESTINATION, equal(userId)).exec(db); + rs = select("DISTINCT " + Field.ACCOUNT).from(TABLE_TRANSACTIONS).where(Field.DESTINATION, equal(userId)).exec(db); while (rs.next()) accountIds.add(rs.getLong(1)); rs.close(); var accounts = new HashSet(); - rs = Query.select(ALL).from(TABLE_ACCOUNTS).where(Field.ID, Condition.in(accountIds.toArray())).exec(db); + rs = select(ALL).from(TABLE_ACCOUNTS).where(Field.ID, Condition.in(accountIds.toArray())).exec(db); while (rs.next()) accounts.add(Account.of(rs)); rs.close(); return accounts; @@ -96,6 +99,33 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + @Override + public Account loadAccount(long accountId) { + try { + var rs = select(ALL).from(TABLE_ACCOUNTS).where(Field.ID,equal(accountId)).exec(db); + Account account = null; + if (rs.next()) account = Account.of(rs); + rs.close(); + if (account==null) throw failedToLoadObject(Text.ACCOUNT,accountId); + return account; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public List loadTransactions(Account account) { + try { + var list = new ArrayList(); + var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).exec(db); + while (rs.next()) list.add(Transaction.of(rs)); + rs.close(); + return list; + } catch (SQLException e) { + throw failedToLoadMembers(account); + } + } + @Override public Account save(Account account) { if (account.id() == 0) try { // new account diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java index 53600c1d..77550b1c 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java @@ -175,6 +175,7 @@ public class Field { public static final String TO = "to"; public static final String TOKEN = "token"; public static final String TOTAL_PRIO = "total_prio"; + public static final String TRANSACTIONS = "transactions"; public static final String TYPE = "type"; public static final String TYPE_ID = "type_id"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java index 8a4d3ab4..5665ef6f 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java @@ -5,6 +5,7 @@ package de.srsoftware.umbrella.core.constants; * This is a collection of messages that appear throughout the project */ public class Text { + public static final String ACCOUNT = "account"; public static final String ACCOUNTING = "accounting"; public static final String BOOKMARK = "bookmark"; 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 f2cd50af..dcbcca55 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 @@ -3,13 +3,27 @@ package de.srsoftware.umbrella.core.model; import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.constants.Field; +import java.sql.ResultSet; +import java.sql.SQLException; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Map; import static de.srsoftware.umbrella.core.ModuleRegistry.userService; public record Transaction(long accountId, LocalDateTime date, String source, String destination, double amount, String purpose) implements Mappable { + public static Transaction of(ResultSet rs) throws SQLException { + var accountId = rs.getLong(Field.ACCOUNT); + var timestamp = rs.getLong(Field.TIMESTAMP); + var date = LocalDateTime.ofEpochSecond(timestamp,0, ZoneOffset.UTC); + var source = rs.getString(Field.SOURCE); + var destination = rs.getString(Field.DESTINATION); + var amount = rs.getDouble(Field.AMOUNT); + var purpose = rs.getString(Field.DESCRIPTION); + return new Transaction(accountId,date,source,destination,amount,purpose); + } + @Override public Map toMap() { var source = this.source; @@ -27,7 +41,7 @@ public record Transaction(long accountId, LocalDateTime date, String source, Str } catch (NumberFormatException ignored) {} return Map.of( - Field.ID, accountId, + Field.ACCOUNT, accountId, Field.DATE, date.toLocalDate(), Field.SOURCE, source, Field.DESTINATION, destination, diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte index 93839853..8d0d6010 100644 --- a/frontend/src/routes/accounting/account.svelte +++ b/frontend/src/routes/accounting/account.svelte @@ -1,5 +1,50 @@ -

Account {id}

\ No newline at end of file +{#if account} +
+ {account.name} + + + + + + + + + + + + {#each transactions as transaction, i} + + + + + + + + {/each} + +
{t('date')}{t('source')}{t('destination')}{t('amount')}{t('purpose')}
{transaction.date}{transaction.source}{transaction.destination}{transaction.amount} {account.currency}{transaction.purpose}
+
+{/if} \ No newline at end of file diff --git a/frontend/src/routes/accounting/index.svelte b/frontend/src/routes/accounting/index.svelte index 494f9f74..e69022eb 100644 --- a/frontend/src/routes/accounting/index.svelte +++ b/frontend/src/routes/accounting/index.svelte @@ -43,7 +43,7 @@ From eddd4d9b5157b60b42873ceed31d6aa65e2d3a0f Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 3 Apr 2026 00:52:13 +0200 Subject: [PATCH 09/21] first working version where transactions can be stored Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountingModule.java | 70 ++++++++----------- .../umbrella/accounting/SqliteDb.java | 2 +- .../umbrella/core/model/IdOrString.java | 58 +++++++++++++++ .../umbrella/core/model/Transaction.java | 28 ++------ .../umbrella/core/model/UmbrellaUser.java | 1 + frontend/src/routes/accounting/account.svelte | 43 +++++++++--- .../src/routes/accounting/add_entry.svelte | 32 ++++++--- frontend/src/routes/company/Editor.svelte | 8 +-- frontend/src/urls.svelte.js | 2 +- 9 files changed, 155 insertions(+), 89 deletions(-) create mode 100644 core/src/main/java/de/srsoftware/umbrella/core/model/IdOrString.java 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 a57edfd3..0fe66115 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -10,10 +10,7 @@ import de.srsoftware.umbrella.core.api.AccountingService; import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; -import de.srsoftware.umbrella.core.model.Account; -import de.srsoftware.umbrella.core.model.Token; -import de.srsoftware.umbrella.core.model.Transaction; -import de.srsoftware.umbrella.core.model.UmbrellaUser; +import de.srsoftware.umbrella.core.model.*; import org.json.JSONObject; import java.io.IOException; @@ -30,10 +27,8 @@ import static de.srsoftware.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.ModuleRegistry.userService; import static de.srsoftware.umbrella.core.Util.mapValues; import static de.srsoftware.umbrella.core.constants.Path.JSON; -import static de.srsoftware.umbrella.core.constants.Path.SEARCH; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField; -import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; public class AccountingModule extends BaseHandler implements AccountingService { private final AccountDb accountDb; @@ -85,32 +80,27 @@ public class AccountingModule extends BaseHandler implements AccountingService { } private boolean getAccount(UmbrellaUser user, long accountId, HttpExchange ex) throws IOException { - var account = accountDb.loadAccount(accountId); + var account = accountDb.loadAccount(accountId); var transactions = accountDb.loadTransactions(account); - var userMap = new HashMap(); - var foundRequestingUser = false; - for (var i=0; i(); + + for (var transaction : transactions){ + var source = transaction.source(); + if (source.isId()) { + var userId = source.id(); + if (!userMap.containsKey(userId)) userMap.put(source.id(),userService().loadUser(userId)); + } + var destination = transaction.destination(); + if (destination.isId()) { + var userId = destination.id(); + if (!userMap.containsKey(userId)) userMap.put(destination.id(),userService().loadUser(userId)); + } } + return sendContent(ex, Map.of( Field.ACCOUNT,account.toMap(), - Field.TRANSACTIONS,transactions.stream().map(Transaction::toMap).toList() + Field.TRANSACTIONS,transactions.stream().map(Transaction::toMap).toList(), + Field.USER_LIST,mapValues(userMap) )); } @@ -138,26 +128,24 @@ public class AccountingModule extends BaseHandler implements AccountingService { if (!json.has(Field.DESTINATION)) throw missingField(Field.DESTINATION); if (!(json.get(Field.DESTINATION) instanceof JSONObject destinationData)) throw invalidField(Field.DESTINATION,JSON); - String source = null, destination = null; + IdOrString source = null, destination = null; - if (sourceData.has(Field.USER_ID)) { - if (!(sourceData.get(Field.USER_ID) instanceof Number uid)) throw invalidField(String.join(".",Field.SOURCE,Field.USER_ID),Text.NUMBER); - var u = userService().loadUser(uid.longValue()); - source = ""+u.id(); + if (sourceData.has(Field.ID)) { + if (!(sourceData.get(Field.ID) instanceof Number uid)) throw invalidField(String.join(".",Field.SOURCE,Field.ID),Text.NUMBER); + source = IdOrString.of(userService().loadUser(uid.longValue())); } else { if (!sourceData.has(Field.DISPLAY)) throw missingField(String.join(".",Field.SOURCE,Field.DISPLAY)); - source = sourceData.getString(Field.DISPLAY); - if (source.isBlank()) throw invalidField(String.join(".",Field.SOURCE,Field.DISPLAY),Text.STRING); + source = IdOrString.of(sourceData.getString(Field.DISPLAY)); + if (source.value().isBlank()) throw invalidField(String.join(".",Field.SOURCE,Field.DISPLAY),Text.STRING); } - if (destinationData.has(Field.USER_ID)) { - if (!(destinationData.get(Field.USER_ID) instanceof Number uid)) throw invalidField(String.join(".",Field.DESTINATION,Field.USER_ID),Text.NUMBER); - var u = userService().loadUser(uid.longValue()); - destination = ""+u; + if (destinationData.has(Field.ID)) { + if (!(destinationData.get(Field.ID) instanceof Number uid)) throw invalidField(String.join(".",Field.DESTINATION,Field.ID),Text.NUMBER); + destination = IdOrString.of(userService().loadUser(uid.longValue())); } else { if (!destinationData.has(Field.DISPLAY)) throw missingField(String.join(".",Field.DESTINATION,Field.DISPLAY)); - destination = destinationData.getString(Field.DISPLAY); - if (destination.isBlank()) throw invalidField(String.join(".",Field.DESTINATION,Field.DISPLAY),Text.STRING); + destination = IdOrString.of(destinationData.getString(Field.DISPLAY)); + if (destination.value().isBlank()) throw invalidField(String.join(".",Field.DESTINATION,Field.DISPLAY),Text.STRING); } 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 4e1b5e2d..61d7fdc2 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -147,7 +147,7 @@ public class SqliteDb extends BaseDb implements AccountDb { try { var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); Query.replaceInto(TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION) - .values(transaction.accountId(),timestamp,transaction.source(),transaction.destination(),transaction.amount(),transaction.purpose()) + .values(transaction.accountId(),timestamp,transaction.source().value(),transaction.destination().value(),transaction.amount(),transaction.purpose()) .execute(db).close(); return transaction; } catch (SQLException e) { 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 new file mode 100644 index 00000000..463e7318 --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/IdOrString.java @@ -0,0 +1,58 @@ +package de.srsoftware.umbrella.core.model; + +import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.constants.Field; + +import java.util.HashMap; +import java.util.Map; + +public class IdOrString implements Mappable { + private final Long id; + private final String value; + + public IdOrString(String val){ + this.value = val; + this.id = parseOrNull(val); + } + + public IdOrString(long id){ + this.value = ""+id; + this.id = id; + } + + public boolean isId(){ + return id != null; + } + + public static IdOrString of(String val){ + return new IdOrString(val); + } + + public static IdOrString of(UmbrellaUser user) { + return new IdOrString(user.id()); + } + + private static Long parseOrNull(String val) { + try { + return Long.parseLong(val); + } catch (NumberFormatException e) { + return null; + } + } + + public long id(){ + return id; + } + + @Override + public Map toMap() { + var map = new HashMap(); + map.put(Field.VALUE,value); + if (isId()) map.put(Field.ID, id); + return map; + } + + 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 dcbcca55..f8d700bf 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 @@ -5,20 +5,20 @@ import de.srsoftware.umbrella.core.constants.Field; import java.sql.ResultSet; import java.sql.SQLException; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.HashMap; import java.util.Map; -import static de.srsoftware.umbrella.core.ModuleRegistry.userService; +public record Transaction(long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose) implements Mappable { + -public record Transaction(long accountId, LocalDateTime date, String source, String destination, double amount, String purpose) implements Mappable { public static Transaction of(ResultSet rs) throws SQLException { var accountId = rs.getLong(Field.ACCOUNT); var timestamp = rs.getLong(Field.TIMESTAMP); var date = LocalDateTime.ofEpochSecond(timestamp,0, ZoneOffset.UTC); - var source = rs.getString(Field.SOURCE); - var destination = rs.getString(Field.DESTINATION); + var source = IdOrString.of(rs.getString(Field.SOURCE)); + var destination = IdOrString.of(rs.getString(Field.DESTINATION)); var amount = rs.getDouble(Field.AMOUNT); var purpose = rs.getString(Field.DESCRIPTION); return new Transaction(accountId,date,source,destination,amount,purpose); @@ -26,25 +26,11 @@ public record Transaction(long accountId, LocalDateTime date, String source, Str @Override public Map toMap() { - var source = this.source; - try { - var userId = Long.parseLong(source); - var user = userService().loadUser(userId); - source = user.name(); - } catch (NumberFormatException ignored) {} - - var destination = this.destination; - try { - var userId = Long.parseLong(destination); - var user = userService().loadUser(userId); - destination = user.name(); - } catch (NumberFormatException ignored) {} - return Map.of( Field.ACCOUNT, accountId, Field.DATE, date.toLocalDate(), - Field.SOURCE, source, - Field.DESTINATION, destination, + Field.SOURCE, source.toMap(), + Field.DESTINATION, destination.toMap(), Field.AMOUNT, amount, Field.PURPOSE, purpose ); diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java b/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java index f72adc98..ba1b1fd1 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java @@ -5,6 +5,7 @@ package de.srsoftware.umbrella.core.model; import static de.srsoftware.umbrella.core.constants.Field.*; import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.api.NamedThing; import de.srsoftware.umbrella.core.api.Owner; import de.srsoftware.umbrella.core.constants.Module; import java.util.HashMap; diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte index 8d0d6010..ab9169aa 100644 --- a/frontend/src/routes/accounting/account.svelte +++ b/frontend/src/routes/accounting/account.svelte @@ -4,18 +4,23 @@ import { error, yikes } from '../../warn.svelte'; import { t } from '../../translations.svelte'; - let { id } = $props(); - let account = $state(null); + import EntryForm from './add_entry.svelte'; + + let { id } = $props(); + let account = $state(null); let transactions = []; + let users = {}; async function load(){ let url = api(`accounting/${id}`); let res = await get(url); if (res.ok) { yikes(); - let json = await res.json(); + let json = await res.json(); transactions = json.transactions; - account = json.account; + users = json.user_list; + account = json.account; + console.log(users); } else error(res); } @@ -28,9 +33,10 @@ {t('date')} - {t('source')} - {t('destination')} - {t('amount')} + {#each Object.entries(users) as [id,user]} + {user.name} + {/each} + {t('other party')} {t('purpose')} @@ -38,13 +44,30 @@ {#each transactions as transaction, i} {transaction.date} - {transaction.source} - {transaction.destination} - {transaction.amount} {account.currency} + {#each Object.entries(users) as [id,user]} + + {#if id == transaction.source.id} + {-transaction.amount} {account.currency} + {/if} + {#if id == transaction.destination.id} + {transaction.amount} {account.currency} + {/if} + + {/each} + + {#if !transaction.source.id} + {transaction.source.value} + {/if} + {#if !transaction.destination.id} + {transaction.destination.value} + {/if} + {transaction.purpose} {/each}
+ + {/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 d468ea40..a45317ff 100644 --- a/frontend/src/routes/accounting/add_entry.svelte +++ b/frontend/src/routes/accounting/add_entry.svelte @@ -7,18 +7,19 @@ import { user } from '../../user.svelte'; import Autocomplete from '../../Components/Autocomplete.svelte'; - let { new_account = false } = $props(); + let defaultAccount = { + id : 0, + name : '', + currency : '' + }; + let { account = defaultAccount, new_account = false } = $props(); let entry = $state({ - account : { - id : 0, - name : '', - currency : '' - }, + account, date : new Date().toISOString().substring(0, 10), source : { display: user.name, - user_id: user.id + id: user.id }, destination : {}, amount : 0.0, @@ -26,6 +27,19 @@ }); let router = useTinyRouter(); + async function getUsers(text){ + var url = api('user/search'); + var res = await post(url,text); + if (res.ok){ + yikes(); + const input = await res.json(); + return Object.values(input).map(user => { return {...user, display: user.name}}); + } else { + error(res); + return {}; + } + } + async function save(){ let data = { ...entry, @@ -68,10 +82,10 @@ {t('source')} - + {t('destination')} - + {t('amount')} diff --git a/frontend/src/routes/company/Editor.svelte b/frontend/src/routes/company/Editor.svelte index 8579fdba..7d128d6f 100644 --- a/frontend/src/routes/company/Editor.svelte +++ b/frontend/src/routes/company/Editor.svelte @@ -1,5 +1,5 @@
- {t('accounts')} @@ -47,4 +46,6 @@ {/each} + {t('accounts')} +
\ No newline at end of file diff --git a/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java b/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java index e836000d..2e895e95 100644 --- a/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java +++ b/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java @@ -555,7 +555,7 @@ public class UserModule extends BaseHandler implements UserService { var requestingUser = loadUser(ex); if (!(requestingUser.isPresent() && requestingUser.get() instanceof DbUser dbUser)) return unauthorized(ex); var key = body(ex); - return sendContent(ex,mapValues(users.search(key))); + return sendContent(ex,mapValues(search(key))); } @Override @@ -576,6 +576,11 @@ public class UserModule extends BaseHandler implements UserService { return score; } + @Override + public Map search(String key) { + return users.search(key); + } + private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException { var id = user.id(); var name = json.has(NAME) && json.get(NAME) instanceof String s && !s.isBlank() ? s : user.name(); From 58f5b251dae7c84accc1bd0f94f67c9a12b3b969 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Mon, 6 Apr 2026 22:12:35 +0200 Subject: [PATCH 11/21] working on autocomplete fields Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountDb.java | 5 +- .../umbrella/accounting/AccountingModule.java | 63 ++++++++++++------- .../umbrella/accounting/Constants.java | 1 + .../umbrella/accounting/SqliteDb.java | 35 ++++++++--- .../umbrella/backend/Application.java | 1 - .../umbrella/core/api/AccountingService.java | 1 + .../umbrella/core/api/StockService.java | 1 - .../umbrella/core/constants/Path.java | 1 + .../umbrella/core/model/Account.java | 6 +- .../umbrella/core/model/IdOrString.java | 2 +- .../umbrella/core/model/Transaction.java | 3 +- .../umbrella/core/model/UmbrellaUser.java | 1 - frontend/src/Components/Autocomplete.svelte | 3 +- frontend/src/routes/accounting/account.svelte | 35 +++++++++-- .../src/routes/accounting/add_entry.svelte | 32 +++++++--- 15 files changed, 134 insertions(+), 56 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 f181a3f8..b5415d07 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -1,10 +1,11 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.accounting; import de.srsoftware.umbrella.core.model.Account; import de.srsoftware.umbrella.core.model.Transaction; - import java.util.Collection; import java.util.List; +import java.util.Set; public interface AccountDb { Collection listAccounts(long userId); @@ -16,4 +17,6 @@ public interface AccountDb { Account save(Account account); Transaction save(Transaction transaction); + + Set searchField(long userId, String field, String key); } 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 b0b115eb..21c5a350 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -1,5 +1,15 @@ +/* © SRSoftware 2025 */ 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.userService; +import static de.srsoftware.umbrella.core.Util.mapValues; +import static de.srsoftware.umbrella.core.constants.Path.*; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField; + import com.sun.net.httpserver.HttpExchange; import de.srsoftware.configuration.Configuration; import de.srsoftware.tools.Path; @@ -11,26 +21,14 @@ import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.*; -import org.json.JSONObject; - -import static de.srsoftware.umbrella.core.constants.Path.SOURCES; -import static de.srsoftware.umbrella.core.constants.Path.DESTINATIONS; - import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Optional; - -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.userService; -import static de.srsoftware.umbrella.core.Util.mapValues; -import static de.srsoftware.umbrella.core.constants.Path.JSON; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField; +import org.json.JSONObject; public class AccountingModule extends BaseHandler implements AccountingService { private final AccountDb accountDb; @@ -68,14 +66,14 @@ public class AccountingModule extends BaseHandler implements AccountingService { public boolean doPost(Path path, HttpExchange ex) throws IOException { addCors(ex); try { - Optional token = SessionToken.from(ex).map(Token::of); - var user = userService().loadUser(token); + var user = userService().refreshSession(ex); if (user.isEmpty()) return unauthorized(ex); 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); }; } catch (UmbrellaException e){ @@ -114,6 +112,15 @@ public class AccountingModule extends BaseHandler implements AccountingService { + private static boolean noNumbers(String s){ + try { + Long.parseLong(s); + return false; + } catch (NumberFormatException e) { + return true; + } + } + private boolean postEntry(UmbrellaUser user, HttpExchange ex) throws IOException { var json = json(ex); if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT); @@ -176,16 +183,26 @@ public class AccountingModule extends BaseHandler implements AccountingService { } public boolean postSearchDestinations(UmbrellaUser user, HttpExchange ex) throws IOException { - var key = body(ex); - var users = userService().search(key); - // TODO: search known transactions for possible destinations - return sendContent(ex,mapValues(users)); + return sendContent(ex,searchOptions(user, Field.DESTINATION, body(ex))); } - public boolean postSearchSources(UmbrellaUser user, HttpExchange ex) throws IOException { + public boolean postSearchPurposes(UmbrellaUser user, HttpExchange ex) throws IOException { var key = body(ex); + var purposes = accountDb.searchField(user.id(),Field.DESCRIPTION,key); + return sendContent(ex,purposes); + } + + + public boolean postSearchSources(UmbrellaUser user, HttpExchange ex) throws IOException { + return sendContent(ex,searchOptions(user, Field.SOURCE, body(ex))); + } + + public ArrayList> searchOptions(UmbrellaUser user, String field, String key){ var users = userService().search(key); - // TODO: search known transactions for possible sources - return sendContent(ex,mapValues(users)); + var destinations = accountDb.searchField(user.id(), field, key); + var optionList = new ArrayList>(); + users.values().stream().map(UmbrellaUser::toMap).forEach(optionList::add); + destinations.stream().filter(AccountingModule::noNumbers).map(s -> Map.of(Field.DISPLAY,s)).forEach(optionList::add); + return optionList; } } diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java index 9031166c..1bc7de26 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java @@ -1,3 +1,4 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.accounting; public class Constants { 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 61d7fdc2..f94be490 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -1,5 +1,15 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.accounting; +import static de.srsoftware.tools.NotImplemented.notImplemented; +import static de.srsoftware.tools.jdbc.Condition.*; +import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; +import static de.srsoftware.tools.jdbc.Query.select; +import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS; +import static de.srsoftware.umbrella.accounting.Constants.TABLE_TRANSACTIONS; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; +import static java.text.MessageFormat.format; + import de.srsoftware.tools.jdbc.Condition; import de.srsoftware.tools.jdbc.Query; import de.srsoftware.umbrella.core.BaseDb; @@ -7,7 +17,6 @@ import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.model.Account; import de.srsoftware.umbrella.core.model.Transaction; - import java.sql.Connection; import java.sql.SQLException; import java.time.ZoneOffset; @@ -15,15 +24,6 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import static de.srsoftware.tools.NotImplemented.notImplemented; -import static de.srsoftware.tools.jdbc.Condition.equal; -import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; -import static de.srsoftware.tools.jdbc.Query.select; -import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS; -import static de.srsoftware.umbrella.accounting.Constants.TABLE_TRANSACTIONS; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; -import static java.text.MessageFormat.format; - public class SqliteDb extends BaseDb implements AccountDb { public SqliteDb(Connection connection) { super(connection); @@ -154,4 +154,19 @@ public class SqliteDb extends BaseDb implements AccountDb { throw failedToStoreObject(transaction); } } + + @Override + public HashSet searchField(long userId, String field , String key) { + var accounts = listAccounts(userId); + var accountIds = accounts.stream().map(Account::id).toArray(); + var destinations = new HashSet(); + try { + var rs = Query.select("DISTINCT "+field).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,in(accountIds)).where(field,like("%"+key+"%")).exec(db); + while (rs.next()) destinations.add(rs.getString(1)); + rs.close(); + return destinations; + } catch (SQLException e) { + throw failedToReadFromTable(field,TABLE_TRANSACTIONS).causedBy(e); + } + } } diff --git a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java index edd2e11b..23d87a8a 100644 --- a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java +++ b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java @@ -15,7 +15,6 @@ import de.srsoftware.umbrella.company.CompanyModule; import de.srsoftware.umbrella.contact.ContactModule; import de.srsoftware.umbrella.core.SettingsService; import de.srsoftware.umbrella.core.Util; -import de.srsoftware.umbrella.core.api.AccountingService; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.documents.DocumentApi; import de.srsoftware.umbrella.files.FileModule; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/AccountingService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/AccountingService.java index fbac2aca..4cbc8c58 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/AccountingService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/AccountingService.java @@ -1,3 +1,4 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.api; public interface AccountingService { diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/StockService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/StockService.java index f96bf9ab..3407a88f 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/StockService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/StockService.java @@ -4,7 +4,6 @@ package de.srsoftware.umbrella.core.api; import de.srsoftware.umbrella.core.model.DbLocation; import de.srsoftware.umbrella.core.model.Item; - import java.util.Collection; public interface StockService { 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 399c84c2..394ec2ba 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 @@ -39,6 +39,7 @@ public class Path { public static final String PROJECT = "project"; public static final String PROPERTIES = "properties"; public static final String PROPERTY = "property"; + public static final String PURPOSES = "purposes"; public static final String READ = "read"; public static final String REDIRECT = "redirect"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java index 4a520312..d2610d60 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java @@ -1,14 +1,14 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; +import static de.srsoftware.umbrella.core.ModuleRegistry.userService; + import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.constants.Field; - import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; -import static de.srsoftware.umbrella.core.ModuleRegistry.userService; - public record Account(long id, String name, String currency, long ownerId) implements Mappable { public static Account of(ResultSet rs) throws SQLException { var id = rs.getLong(Field.ID); 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 463e7318..75b68a21 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 @@ -1,8 +1,8 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.constants.Field; - import java.util.HashMap; import java.util.Map; 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 f8d700bf..49524300 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 @@ -1,13 +1,12 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.constants.Field; - import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.HashMap; import java.util.Map; public record Transaction(long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose) implements Mappable { diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java b/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java index ba1b1fd1..f72adc98 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java @@ -5,7 +5,6 @@ package de.srsoftware.umbrella.core.model; import static de.srsoftware.umbrella.core.constants.Field.*; import de.srsoftware.tools.Mappable; -import de.srsoftware.umbrella.core.api.NamedThing; import de.srsoftware.umbrella.core.api.Owner; import de.srsoftware.umbrella.core.constants.Module; import java.util.HashMap; diff --git a/frontend/src/Components/Autocomplete.svelte b/frontend/src/Components/Autocomplete.svelte index 76bd89ca..37c0f8ef 100644 --- a/frontend/src/Components/Autocomplete.svelte +++ b/frontend/src/Components/Autocomplete.svelte @@ -4,6 +4,7 @@ let { + id = null, autofocus = false, getCandidates = dummyGetCandidates, onCommit = dummyOnCommit, @@ -108,7 +109,7 @@ - + {#if candidates && candidates.length > 0} + {t('source')} - + {t('destination')} @@ -110,10 +128,10 @@
{t('purpose')} - + -
+ \ No newline at end of file From d6714224bb3cd92a4c3a93f3a1a841449f565bb9 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Mon, 6 Apr 2026 22:59:51 +0200 Subject: [PATCH 12/21] adding sums Signed-off-by: Stephan Richter --- frontend/src/routes/accounting/account.svelte | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte index b30224f2..f0a360fd 100644 --- a/frontend/src/routes/accounting/account.svelte +++ b/frontend/src/routes/accounting/account.svelte @@ -13,6 +13,19 @@ let sums = {}; + function calcSums(){ + sums[0] = 0; + for (let user of Object.values(users)) sums[user.id] = 0; + for (let transaction of transactions) { + for (let user of Object.values(users)){ + if (user.id == transaction.destination.id) sums[user.id] += transaction.amount; + if (user.id == transaction.source.id) sums[user.id] -= transaction.amount; + } + if (!transaction.destination.id) sums[0] += transaction.amount; + if (!transaction.source.id) sums[0] -= transaction.amount; + } + } + async function load(){ let url = api(`accounting/${id}`); let res = await get(url); @@ -22,6 +35,7 @@ transactions = json.transactions; users = json.user_list; account = json.account; + calcSums(); } else error(res); } @@ -31,6 +45,11 @@ onMount(load); + + + {#if account}
{account.name} @@ -50,24 +69,20 @@ {transaction.date} {#each Object.entries(users) as [id,user]} - + {#if id == transaction.source.id} - {sums[id] = -transaction.amount + (sums[id]?sums[id]:0)} - {-transaction.amount} {account.currency} + {(-transaction.amount).toFixed(2)} {account.currency} {/if} {#if id == transaction.destination.id} - {sums[id] = transaction.amount + (sums[id]?sums[id]:0)} - {transaction.amount} {account.currency} + {(+transaction.amount).toFixed(2)} {account.currency} {/if} {/each} {#if !transaction.source.id} - {sums[0] = -transaction.amount + (sums[0]?sums[0]:0)} ← {transaction.source.value} {/if} {#if !transaction.destination.id} - {sums[0] = transaction.amount + (sums[0]?sums[0]:0)} → {transaction.destination.value} {/if} @@ -80,14 +95,14 @@ {t('sums')} {#each Object.entries(users) as [id,user]} - + {user.name}
- {sums[id]} {account.currency} + {sums[id].toFixed(2)} {account.currency} {/each} - +
- {sums[0]} {account.currency} + {sums[0].toFixed(2)} {account.currency} From b4b3173cc79d815246a29755d1deb06efbc1b85c Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Mon, 6 Apr 2026 23:15:36 +0200 Subject: [PATCH 13/21] preparing tagging of transactions Signed-off-by: Stephan Richter --- .../srsoftware/umbrella/accounting/AccountingModule.java | 5 +++++ .../java/de/srsoftware/umbrella/accounting/SqliteDb.java | 2 +- frontend/src/routes/accounting/add_entry.svelte | 8 +++++++- 3 files changed, 13 insertions(+), 2 deletions(-) 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 21c5a350..8345d494 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -9,6 +9,7 @@ import static de.srsoftware.umbrella.core.Util.mapValues; import static de.srsoftware.umbrella.core.constants.Path.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField; +import static java.lang.System.Logger.Level.WARNING; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.configuration.Configuration; @@ -28,6 +29,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Optional; + +import org.json.JSONArray; import org.json.JSONObject; public class AccountingModule extends BaseHandler implements AccountingService { @@ -179,6 +182,8 @@ public class AccountingModule extends BaseHandler implements AccountingService { var transaction = accountDb.save(new Transaction(accountId,dateTime,source,destination,amount.doubleValue(),purpose)); + var tags = json.has(Field.TAGS) && json.get(Field.TAGS) instanceof JSONArray t ? t : null; + if (tags != null) LOG.log(WARNING, "Tagging transactions not implemented!"); return sendContent(ex,newAccount != null ? newAccount : 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 f94be490..227c083a 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -117,7 +117,7 @@ public class SqliteDb extends BaseDb implements AccountDb { public List loadTransactions(Account account) { try { var list = new ArrayList(); - var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).exec(db); + 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)); rs.close(); return list; diff --git a/frontend/src/routes/accounting/add_entry.svelte b/frontend/src/routes/accounting/add_entry.svelte index 582ad190..c526981a 100644 --- a/frontend/src/routes/accounting/add_entry.svelte +++ b/frontend/src/routes/accounting/add_entry.svelte @@ -6,6 +6,7 @@ import { error, yikes } from '../../warn.svelte'; import { user } from '../../user.svelte'; import Autocomplete from '../../Components/Autocomplete.svelte'; + import Tags from '../tags/TagList.svelte'; let defaultAccount = { id : 0, @@ -23,7 +24,8 @@ }, destination : {}, amount : 0.0, - purpose : {} + purpose : {}, + tags : [] }); let router = useTinyRouter(); @@ -73,6 +75,7 @@ let res = await post(url, data); if (res.ok) { yikes(); + entry.tags = []; onSave(); document.getElementById('date-input').focus(); } else error(res); @@ -130,6 +133,9 @@ {t('purpose')} + {t('tags')} + + From 85efb0ec02033a55bb70801d6591d42229ac67aa Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Thu, 9 Apr 2026 09:09:17 +0200 Subject: [PATCH 14/21] preparing storage of tags Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountingModule.java | 14 +-- .../umbrella/accounting/Constants.java | 2 + .../umbrella/accounting/SqliteDb.java | 95 +++++++++++++++++-- .../umbrella/core/constants/Field.java | 2 + .../umbrella/core/model/Transaction.java | 12 ++- 5 files changed, 106 insertions(+), 19 deletions(-) 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 8345d494..1a8dff2c 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -25,10 +25,7 @@ import de.srsoftware.umbrella.core.model.*; import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.util.*; import org.json.JSONArray; import org.json.JSONObject; @@ -180,10 +177,13 @@ public class AccountingModule extends BaseHandler implements AccountingService { accountId = newAccount.id(); } - var transaction = accountDb.save(new Transaction(accountId,dateTime,source,destination,amount.doubleValue(),purpose)); + var tagList = json.has(Field.TAGS) && json.get(Field.TAGS) instanceof JSONArray t ? t : List.of(); + var tags = new HashSet(); + tagList.forEach(e -> tags.add(e.toString())); + + + var transaction = accountDb.save(new Transaction(0,accountId,dateTime,source,destination,amount.doubleValue(),purpose,tags)); - var tags = json.has(Field.TAGS) && json.get(Field.TAGS) instanceof JSONArray t ? t : null; - if (tags != null) LOG.log(WARNING, "Tagging transactions not implemented!"); return sendContent(ex,newAccount != null ? newAccount : transaction); } diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java index 1bc7de26..eb281211 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java @@ -5,5 +5,7 @@ public class Constants { public static final String CONFIG_DATABASE = "umbrella.modules.accounting.database"; public static final String TABLE_ACCOUNTS = "accounts"; + public static final String TABLE_TAGS = "tag"; + public static final String TABLE_TAGS_TRANSACTIONS = "tags_transactions"; public static final String TABLE_TRANSACTIONS = "transactions"; } 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 227c083a..1eab87b5 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -3,10 +3,9 @@ package de.srsoftware.umbrella.accounting; import static de.srsoftware.tools.NotImplemented.notImplemented; 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.tools.jdbc.Query.select; -import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS; -import static de.srsoftware.umbrella.accounting.Constants.TABLE_TRANSACTIONS; +import static de.srsoftware.umbrella.accounting.Constants.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static java.text.MessageFormat.format; @@ -15,12 +14,14 @@ import de.srsoftware.tools.jdbc.Query; import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.constants.Text; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Account; 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; @@ -36,6 +37,8 @@ public class SqliteDb extends BaseDb implements AccountDb { case 0: createAccountsTable(); createTransactionsTable(); + createTagsTable(); + createTagsTransactionsTable(); } return setCurrentVersion(1); } @@ -59,9 +62,45 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + private void createTagsTable(){ + var sql = """ + CREATE TABLE IF NOT EXISTS {0} ( + {1} INTEGER PRIMARY KEY, + {2} VARCHAR(32) UNIQUE NOT NULL + ); + """; + try { + sql = format(sql,TABLE_TAGS,Field.ID,Field.TAG); + var stmt = db.prepareStatement(sql); + stmt.execute(); + stmt.close(); + } catch (SQLException e) { + throw failedToCreateTable(TABLE_TAGS).causedBy(e); + } + } + + private void createTagsTransactionsTable(){ + var sql = """ + CREATE TABLE IF NOT EXISTS {0} ( + {1} LONG NOT NULL, + {2} LONG NOT NULL, + PRIMARY KEY({1}, {2}) + ); + """; + try { + sql = format(sql,TABLE_TAGS_TRANSACTIONS,Field.TRANSACTION_ID,Field.TAG_ID); + var stmt = db.prepareStatement(sql); + stmt.execute(); + stmt.close(); + } catch (SQLException e) { + throw failedToCreateTable(TABLE_TAGS).causedBy(e); + } + } + private void createTransactionsTable() { var sql = """ CREATE TABLE IF NOT EXISTS {0} ( + {7} INTEGER PRIMARY KEY, {1} LONG NOT NULL, {2} LONG NOT NULL, {3} VARCHAR(255) NOT NULL, @@ -70,7 +109,7 @@ public class SqliteDb extends BaseDb implements AccountDb { {6} TEXT );"""; try { - sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION); + sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION, Field.ID); var stmt = db.prepareStatement(sql); stmt.execute(); stmt.close(); @@ -144,13 +183,49 @@ public class SqliteDb extends BaseDb implements AccountDb { @Override public Transaction save(Transaction transaction) { + if (transaction.id() == 0) { + try { + var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); + var rs = Query.insertInto(TABLE_TRANSACTIONS, Field.ACCOUNT, Field.TIMESTAMP, Field.SOURCE, Field.DESTINATION, Field.AMOUNT, Field.DESCRIPTION) + .values(transaction.accountId(), timestamp, transaction.source().value(), transaction.destination().value(), transaction.amount(), transaction.purpose()) + .execute(db).getGeneratedKeys(); + if (rs.next()) transaction = transaction.withId(rs.getLong(1)); + rs.close(); + } catch (SQLException e) { + throw failedToStoreObject(transaction); + } + } else { // TODO : implement update + throw UmbrellaException.failedToStoreObject(transaction); + } + saveTags(transaction); + return transaction; + } + + private void saveTags(Transaction transaction) { + var remaining = new HashSet(transaction.tags()); + var existingTags = new HashMap(); try { - var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); - Query.replaceInto(TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION) - .values(transaction.accountId(),timestamp,transaction.source().value(),transaction.destination().value(),transaction.amount(),transaction.purpose()) - .execute(db).close(); - return transaction; - } catch (SQLException e) { + var rs = select(ALL).from(TABLE_TAGS).where(Field.TAG,in(transaction.tags().toArray())).exec(db); + while (rs.next()) existingTags.put(rs.getString(Field.TAG), rs.getLong(Field.ID)); + rs.close(); + } catch (SQLException e){ + throw failedToLoadMembers(transaction); + } + remaining.removeAll(existingTags.keySet()); + for (var tag : remaining) { + try { + var rs = insertInto(TABLE_TAGS, Field.TAG).values(tag).execute(db).getGeneratedKeys(); + if (rs.next()) existingTags.put(tag,rs.getLong(1)); + rs.close(); + } catch (SQLException e){ + throw failedToStoreObject(tag); + } + } + try { + var query = replaceInto(TABLE_TAGS_TRANSACTIONS, Field.TRANSACTION_ID, Field.TAG_ID); + for (var tag_id : existingTags.values()) query.values(transaction.id(), tag_id); + query.execute(db).close(); + } catch (SQLException e){ throw failedToStoreObject(transaction); } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java index 77550b1c..a8ca1d2d 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java @@ -157,6 +157,7 @@ public class Field { public static final String TAG = "tag"; public static final String TAGS = "tags"; public static final String TAG_COLORS = "tag_colors"; + public static final String TAG_ID = "tag_id"; public static final String TASK = "task"; public static final String TASK_IDS = "task_ids"; public static final String TASKS = "tasks"; @@ -175,6 +176,7 @@ public class Field { public static final String TO = "to"; public static final String TOKEN = "token"; public static final String TOTAL_PRIO = "total_prio"; + public static final String TRANSACTION_ID = "transaction_id"; public static final String TRANSACTIONS = "transactions"; public static final String TYPE = "type"; public static final String TYPE_ID = "type_id"; 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 49524300..d5979b61 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 @@ -7,9 +7,11 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.HashSet; import java.util.Map; +import java.util.Set; -public record Transaction(long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose) implements Mappable { +public record Transaction(long id, long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose, Set tags) implements Mappable { public static Transaction of(ResultSet rs) throws SQLException { @@ -20,12 +22,14 @@ public record Transaction(long accountId, LocalDateTime date, IdOrString source, var destination = IdOrString.of(rs.getString(Field.DESTINATION)); var amount = rs.getDouble(Field.AMOUNT); var purpose = rs.getString(Field.DESCRIPTION); - return new Transaction(accountId,date,source,destination,amount,purpose); + var id = rs.getLong(Field.ID); + return new Transaction(id, accountId, date, source, destination, amount, purpose, new HashSet<>()); } @Override public Map toMap() { return Map.of( + Field.ID, id, Field.ACCOUNT, accountId, Field.DATE, date.toLocalDate(), Field.SOURCE, source.toMap(), @@ -34,4 +38,8 @@ public record Transaction(long accountId, LocalDateTime date, IdOrString source, Field.PURPOSE, purpose ); } + + public Transaction withId(long id) { + return new Transaction(id, accountId, date, source, destination, amount, purpose, new HashSet<>(tags)); + } } From ec3add70c6c42ef7e64195ff8d8922837b0f0631 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 10 Apr 2026 00:02:54 +0200 Subject: [PATCH 15/21] working on tag handling in accounting module Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountDb.java | 2 + .../umbrella/accounting/AccountingModule.java | 29 ++++++++++++- .../umbrella/accounting/SqliteDb.java | 41 ++++++++++++++----- .../umbrella/core/api/TagService.java | 2 + .../umbrella/core/constants/Path.java | 1 + .../umbrella/core/model/IdOrString.java | 7 +++- .../umbrella/core/model/Transaction.java | 10 ++++- frontend/src/routes/accounting/account.svelte | 10 ++++- .../src/routes/accounting/add_entry.svelte | 33 +++++++++------ frontend/src/routes/tags/TagList.svelte | 3 +- .../srsoftware/umbrella/tags/TagModule.java | 7 +++- 11 files changed, 114 insertions(+), 31 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 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 From 90a7c5dd18b7f3554dc2ba5610307bcc7fc57568 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 10 Apr 2026 09:23:26 +0200 Subject: [PATCH 16/21] started to implement updates on transactions Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountDb.java | 2 + .../umbrella/accounting/AccountingModule.java | 36 +++++++++++ .../umbrella/accounting/SqliteDb.java | 44 +++++++++---- .../umbrella/core/constants/Path.java | 7 ++- .../umbrella/core/constants/Text.java | 1 + .../umbrella/core/model/Transaction.java | 62 ++++++++++++++++++- frontend/src/Components/LineEditor.svelte | 9 +-- frontend/src/routes/accounting/account.svelte | 27 +------- .../src/routes/accounting/transaction.svelte | 49 +++++++++++++++ frontend/src/routes/contact/Address.svelte | 14 ++--- frontend/src/routes/contact/Email.svelte | 2 +- frontend/src/routes/contact/ExtraField.svelte | 2 +- frontend/src/routes/contact/FN.svelte | 2 +- frontend/src/routes/contact/Name.svelte | 10 +-- frontend/src/routes/contact/Number.svelte | 2 +- frontend/src/routes/contact/Org.svelte | 2 +- frontend/src/routes/contact/URL.svelte | 2 +- frontend/src/routes/stock/Index.svelte | 2 +- frontend/src/routes/stock/ItemProps.svelte | 4 +- frontend/src/routes/task/ListTask.svelte | 2 +- frontend/src/routes/wiki/View.svelte | 2 +- 21 files changed, 215 insertions(+), 68 deletions(-) create mode 100644 frontend/src/routes/accounting/transaction.svelte 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 1657ccd9..0c96721f 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 { Account loadAccount(long accountId); + Transaction loadTransaction(long transactionId); + List loadTransactions(Account account); Account save(Account account); 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 356c9624..b8315a55 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -63,6 +63,30 @@ public class AccountingModule extends BaseHandler implements AccountingService { } } + @Override + public boolean doPatch(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + var user = userService().refreshSession(ex); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + return switch (head) { + case TRANSACTION -> { + try { + var tid = Long.parseLong(path.pop()); + yield patchTransaction(user.get(),tid,ex); + } catch (NumberFormatException ignored) { + yield super.doPatch(path,ex); + } + + } + case null, default -> super.doPatch(path, ex); + }; + } catch (UmbrellaException e){ + return send(ex,e); + } + } + @Override public boolean doPost(Path path, HttpExchange ex) throws IOException { addCors(ex); @@ -131,6 +155,18 @@ public class AccountingModule extends BaseHandler implements AccountingService { } } + private boolean patchTransaction(UmbrellaUser user, long transactionId, HttpExchange ex) throws IOException { + var transaction = accountDb.loadTransaction(transactionId); + LOG.log(WARNING,"Missing permission check in patchTransaction(…)!"); + var json = json(ex); + if (json.has(Field.DATE)){ + var date = LocalDate.parse(json.getString(Field.DATE)); + transaction.date(date); + accountDb.save(transaction); + } + return sendContent(ex,transaction); + } + private boolean postEntry(UmbrellaUser user, HttpExchange ex) throws IOException { var json = json(ex); if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT); 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 7985b35f..a719f910 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -6,6 +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.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.model.Translatable.t; import static java.text.MessageFormat.format; @@ -51,7 +52,7 @@ public class SqliteDb extends BaseDb implements AccountDb { );"""; try { - sql = format(sql,TABLE_ACCOUNTS, Field.ID, Field.NAME, Field.OWNER, Field.CURRENCY); + sql = format(sql,TABLE_ACCOUNTS, ID, Field.NAME, Field.OWNER, Field.CURRENCY); var stmt = db.prepareStatement(sql); stmt.execute(); stmt.close(); @@ -68,7 +69,7 @@ public class SqliteDb extends BaseDb implements AccountDb { ); """; try { - sql = format(sql,TABLE_TAGS,Field.ID,Field.TAG); + sql = format(sql,TABLE_TAGS, ID,Field.TAG); var stmt = db.prepareStatement(sql); stmt.execute(); stmt.close(); @@ -107,7 +108,7 @@ public class SqliteDb extends BaseDb implements AccountDb { {6} TEXT );"""; try { - sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION, Field.ID); + sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION, ID); var stmt = db.prepareStatement(sql); stmt.execute(); stmt.close(); @@ -127,7 +128,7 @@ public class SqliteDb extends BaseDb implements AccountDb { while (rs.next()) accountIds.add(rs.getLong(1)); rs.close(); var accounts = new HashSet(); - rs = select(ALL).from(TABLE_ACCOUNTS).where(Field.ID, Condition.in(accountIds.toArray())).exec(db); + rs = select(ALL).from(TABLE_ACCOUNTS).where(ID, Condition.in(accountIds.toArray())).exec(db); while (rs.next()) accounts.add(Account.of(rs)); rs.close(); return accounts; @@ -139,7 +140,7 @@ public class SqliteDb extends BaseDb implements AccountDb { @Override public Account loadAccount(long accountId) { try { - var rs = select(ALL).from(TABLE_ACCOUNTS).where(Field.ID,equal(accountId)).exec(db); + var rs = select(ALL).from(TABLE_ACCOUNTS).where(ID,equal(accountId)).exec(db); Account account = null; if (rs.next()) account = Account.of(rs); rs.close(); @@ -150,6 +151,20 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + @Override + public Transaction loadTransaction(long transactionId) { + try { + Transaction transaction = null; + var rs = select(ALL).from(TABLE_TRANSACTIONS).where(ID,equal(transactionId)).exec(db); + if (rs.next()) transaction = Transaction.of(rs); + rs.close(); + if (transaction != null) return transaction; + throw failedToLoadObject(Text.TRANSACTION,transactionId); + } catch (SQLException e) { + throw failedToLoadObject(Text.TRANSACTION); + } + } + @Override public List loadTransactions(Account account) { try { @@ -161,7 +176,7 @@ public class SqliteDb extends BaseDb implements AccountDb { } rs.close(); 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); + rs = select(ALL).from(TABLE_TAGS_TRANSACTIONS).leftJoin(Field.TAG_ID,TABLE_TAGS, 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)); @@ -191,9 +206,9 @@ public class SqliteDb extends BaseDb implements AccountDb { @Override public Transaction save(Transaction transaction) { + var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); if (transaction.id() == 0) { try { - var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); var rs = Query.insertInto(TABLE_TRANSACTIONS, Field.ACCOUNT, Field.TIMESTAMP, Field.SOURCE, Field.DESTINATION, Field.AMOUNT, Field.DESCRIPTION) .values(transaction.accountId(), timestamp, transaction.source().value(), transaction.destination().value(), transaction.amount(), transaction.purpose()) .execute(db).getGeneratedKeys(); @@ -202,8 +217,15 @@ public class SqliteDb extends BaseDb implements AccountDb { } catch (SQLException e) { throw failedToStoreObject(transaction); } - } else { // TODO : implement update - throw UmbrellaException.failedToStoreObject(transaction); + } else { + try { + Query.replaceInto(TABLE_TRANSACTIONS, Field.ID, Field.ACCOUNT, Field.TIMESTAMP, Field.SOURCE, Field.DESTINATION, Field.AMOUNT, Field.DESCRIPTION) + .values(transaction.id(), transaction.accountId(), timestamp, transaction.source().value(), transaction.destination().value(), transaction.amount(), transaction.purpose()) + .execute(db).close(); + return transaction; + } catch (SQLException e) { + throw failedToStoreObject(transaction); + } } return saveTags(transaction); } @@ -213,7 +235,7 @@ public class SqliteDb extends BaseDb implements AccountDb { var existingTags = new HashMap(); try { var rs = select(ALL).from(TABLE_TAGS).where(Field.TAG,in(transaction.tags().toArray())).exec(db); - while (rs.next()) existingTags.put(rs.getString(Field.TAG), rs.getLong(Field.ID)); + while (rs.next()) existingTags.put(rs.getString(Field.TAG), rs.getLong(ID)); rs.close(); } catch (SQLException e){ throw failedToLoadMembers(transaction); @@ -257,7 +279,7 @@ public class SqliteDb extends BaseDb implements AccountDb { 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); + var rs = select(ALL).from(TABLE_TRANSACTIONS).leftJoin(ID,TABLE_TAGS_TRANSACTIONS,Field.TRANSACTION_ID).leftJoin(Field.TAG_ID,TABLE_TAGS, ID).where(Field.TAG,like(format("%{0}%",key))).exec(db); while (rs.next()) tags.add(rs.getString(Field.TAG)); rs.close(); return tags; 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 04af091f..132ed457 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,9 +53,10 @@ 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"; + public static final String TAGS = "tags"; + public static final String TAGGED = "tagged"; + public static final String TRANSACTION = "transaction"; + public static final String TOKEN = "token"; public static final String USER = "user"; public static final String USES = "uses"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java index 5665ef6f..8f2be0cd 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java @@ -78,6 +78,7 @@ public class Text { public static final String TASKS = "tasks"; public static final String TIMETRACKING = "timetracking"; public static final String TIME_WITH_ID = "time ({id})"; + public static final String TRANSACTION = "transaction"; public static final String TYPE = "type"; public static final String UNIT = "unit"; 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 19805128..71a1cd38 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 @@ -5,6 +5,7 @@ import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.constants.Field; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.HashSet; @@ -13,8 +14,53 @@ 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 { +public class Transaction implements Mappable { + private long accountId, id; + private LocalDateTime date; + private IdOrString source, destination; + private double amount; + private String purpose; + private Set tags; + public Transaction(long id, long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose, Set tags){ + this.id = id; + this.accountId = accountId; + this.date = date; + this.source = source; + this.destination = destination; + this.amount = amount; + this.purpose = purpose; + this.tags = tags == null ? new HashSet<>() : tags; + } + + public long accountId(){ + return accountId; + } + + public double amount(){ + return amount; + } + + public LocalDateTime date(){ + return date; + } + + public Transaction date(LocalDateTime newVal){ + date = newVal; + return this; + } + + public void date(LocalDate date) { + this.date = this.date.withYear(date.getYear()).withMonth(date.getMonthValue()).withDayOfMonth(date.getDayOfMonth()); + } + + public IdOrString destination(){ + return destination; + } + + public long id(){ + return id; + } public static Transaction of(ResultSet rs) throws SQLException { var accountId = rs.getLong(Field.ACCOUNT); @@ -25,7 +71,19 @@ public record Transaction(long id, long accountId, LocalDateTime date, IdOrStrin var amount = rs.getDouble(Field.AMOUNT); var purpose = rs.getString(Field.DESCRIPTION); var id = rs.getLong(Field.ID); - return new Transaction(id, accountId, date, source, destination, amount, purpose, new HashSet<>()); + return new Transaction(id, accountId, date, source, destination, amount, purpose, null); + } + + public String purpose(){ + return purpose; + } + + public IdOrString source(){ + return source; + } + + public Set tags(){ + return tags; } @Override diff --git a/frontend/src/Components/LineEditor.svelte b/frontend/src/Components/LineEditor.svelte index 16f23da5..21676323 100644 --- a/frontend/src/Components/LineEditor.svelte +++ b/frontend/src/Components/LineEditor.svelte @@ -9,8 +9,9 @@ onclick = evt => { evt.preventDefault(); startEdit(); return false }, onSet = newVal => {return true;}, title = t('click_to_edit'), - type = 'div', - value = $bindable(null) + type = 'text', + value = $bindable(null), + wrapper = 'div' } = $props(); let editing = $state(simple); @@ -110,7 +111,7 @@ {#if editable && editing} - + {:else} -{value} +{value} {/if} diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte index 0e29a41e..f79061ce 100644 --- a/frontend/src/routes/accounting/account.svelte +++ b/frontend/src/routes/accounting/account.svelte @@ -5,6 +5,7 @@ import { t } from '../../translations.svelte'; import EntryForm from './add_entry.svelte'; + import Transaction from './transaction.svelte'; let { id } = $props(); let account = $state(null); @@ -67,31 +68,7 @@ {#each transactions as transaction, i} - - {transaction.date} - {#each Object.entries(users) as [id,user]} - - {#if id == transaction.source.id} - {(-transaction.amount).toFixed(2)} {account.currency} - {/if} - {#if id == transaction.destination.id} - {(+transaction.amount).toFixed(2)} {account.currency} - {/if} - - {/each} - - {#if !transaction.source.id} - ← {transaction.source.value} - {/if} - {#if !transaction.destination.id} - → {transaction.destination.value} - {/if} - - {transaction.purpose} - - {transaction.tags.join(', ')} - - + {/each} diff --git a/frontend/src/routes/accounting/transaction.svelte b/frontend/src/routes/accounting/transaction.svelte new file mode 100644 index 00000000..bb165bfc --- /dev/null +++ b/frontend/src/routes/accounting/transaction.svelte @@ -0,0 +1,49 @@ + + + + + + + {#each Object.entries(users) as [id,user]} + + {#if id == transaction.source.id} + {(-transaction.amount).toFixed(2)} {account.currency} + {/if} + {#if id == transaction.destination.id} + {(+transaction.amount).toFixed(2)} {account.currency} + {/if} + + {/each} + + {#if !transaction.source.id} + ← {transaction.source.value} + {/if} + {#if !transaction.destination.id} + → {transaction.destination.value} + {/if} + + {transaction.purpose} + + {transaction.tags.join(', ')} + + diff --git a/frontend/src/routes/contact/Address.svelte b/frontend/src/routes/contact/Address.svelte index 9fa4576b..25e65c49 100644 --- a/frontend/src/routes/contact/Address.svelte +++ b/frontend/src/routes/contact/Address.svelte @@ -36,11 +36,11 @@ - onSet(address.box,newVal)} title={t('post_box')} /> - onSet(address.ext,newVal)} title={t('extended_address')} /> - onSet(address.street,newVal)} title={t('street')} /> - onSet(address.post_code,newVal)} title={t('post_code')} /> - onSet(address.locality,newVal)} title={t('locality')} /> - onSet(address.region,newVal)} title={t('region')} /> - onSet(address.country,newVal)} title={t('country')} /> + onSet(address.box,newVal)} title={t('post_box')} /> + onSet(address.ext,newVal)} title={t('extended_address')} /> + onSet(address.street,newVal)} title={t('street')} /> + onSet(address.post_code,newVal)} title={t('post_code')} /> + onSet(address.locality,newVal)} title={t('locality')} /> + onSet(address.region,newVal)} title={t('region')} /> + onSet(address.country,newVal)} title={t('country')} /> \ No newline at end of file diff --git a/frontend/src/routes/contact/Email.svelte b/frontend/src/routes/contact/Email.svelte index 33d4e798..1a4eee6c 100644 --- a/frontend/src/routes/contact/Email.svelte +++ b/frontend/src/routes/contact/Email.svelte @@ -33,5 +33,5 @@ {#if value} -
+
{/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/ExtraField.svelte b/frontend/src/routes/contact/ExtraField.svelte index 88b9a833..66a9f5f0 100644 --- a/frontend/src/routes/contact/ExtraField.svelte +++ b/frontend/src/routes/contact/ExtraField.svelte @@ -19,7 +19,7 @@ {#if field.value.includes('\\n')} {:else} - + {/if} {/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/FN.svelte b/frontend/src/routes/contact/FN.svelte index 76a9aba1..b29ad55f 100644 --- a/frontend/src/routes/contact/FN.svelte +++ b/frontend/src/routes/contact/FN.svelte @@ -14,5 +14,5 @@ {#if value} - + {/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/Name.svelte b/frontend/src/routes/contact/Name.svelte index 9aa18932..9ef85cb2 100644 --- a/frontend/src/routes/contact/Name.svelte +++ b/frontend/src/routes/contact/Name.svelte @@ -16,18 +16,18 @@
{#if n.prefix} - onSet(n.prefix,newVal)} title={t('name_prefix')} /> + onSet(n.prefix,newVal)} title={t('name_prefix')} /> {/if} {#if n.given} - onSet(n.given,newVal)} title={t('given_name')} /> + onSet(n.given,newVal)} title={t('given_name')} /> {/if} {#if n.additional} - onSet(n.additional,newVal)} title={t('additional_name')} /> + onSet(n.additional,newVal)} title={t('additional_name')} /> {/if} {#if n.family} - onSet(n.family,newVal)} title={t('family_name')} /> + onSet(n.family,newVal)} title={t('family_name')} /> {/if} {#if n.suffix} - onSet(n.suffix,newVal)} title={t('')} /> + onSet(n.suffix,newVal)} title={t('')} /> {/if}
\ No newline at end of file diff --git a/frontend/src/routes/contact/Number.svelte b/frontend/src/routes/contact/Number.svelte index 53114e49..fd86475a 100644 --- a/frontend/src/routes/contact/Number.svelte +++ b/frontend/src/routes/contact/Number.svelte @@ -39,5 +39,5 @@ -
+
{/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/Org.svelte b/frontend/src/routes/contact/Org.svelte index 04b0285f..5b7c52e3 100644 --- a/frontend/src/routes/contact/Org.svelte +++ b/frontend/src/routes/contact/Org.svelte @@ -14,5 +14,5 @@ {#if value} - + {/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/URL.svelte b/frontend/src/routes/contact/URL.svelte index 4c392218..b47cedab 100644 --- a/frontend/src/routes/contact/URL.svelte +++ b/frontend/src/routes/contact/URL.svelte @@ -14,5 +14,5 @@ {#if value} - + {/if} \ No newline at end of file diff --git a/frontend/src/routes/stock/Index.svelte b/frontend/src/routes/stock/Index.svelte index 7ab1c641..754f1bc9 100644 --- a/frontend/src/routes/stock/Index.svelte +++ b/frontend/src/routes/stock/Index.svelte @@ -263,7 +263,7 @@
{#if location}

- patchLocation(location,'name',newName)} /> + patchLocation(location,'name',newName)} /> {#if location.parent_location_id} diff --git a/frontend/src/routes/stock/ItemProps.svelte b/frontend/src/routes/stock/ItemProps.svelte index c936c3ba..2fe1eeb0 100644 --- a/frontend/src/routes/stock/ItemProps.svelte +++ b/frontend/src/routes/stock/ItemProps.svelte @@ -68,7 +68,7 @@ {#if item} - update('name',v)} /> + update('name',v)} />
{@html item.description.rendered} @@ -80,7 +80,7 @@ {t('ID')} - update('code',v)} /> + update('code',v)} /> {#each item.properties.toSorted(byName) as prop} diff --git a/frontend/src/routes/task/ListTask.svelte b/frontend/src/routes/task/ListTask.svelte index 5173f323..cb74453b 100644 --- a/frontend/src/routes/task/ListTask.svelte +++ b/frontend/src/routes/task/ListTask.svelte @@ -156,7 +156,7 @@ {#if !deleted}
  • e.preventDefault()} {ondragstart} class="task {states[task.status]?.toLowerCase()}"> - + {#if task.est_time} ({+task.est_time} h) {/if} diff --git a/frontend/src/routes/wiki/View.svelte b/frontend/src/routes/wiki/View.svelte index c06b385b..8423d8fb 100644 --- a/frontend/src/routes/wiki/View.svelte +++ b/frontend/src/routes/wiki/View.svelte @@ -165,7 +165,7 @@ {/each}
  • - patchTitle(t)} /> + patchTitle(t)} /> {#if page.version != page.versions[0]} {t('not_recent_version')} {/if} From 0c6e5850d2e462e8c2cfbe46bf76ca762e4cdb48 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 10 Apr 2026 09:30:41 +0200 Subject: [PATCH 17/21] working on update of transactions Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountingModule.java | 8 +++---- .../umbrella/accounting/SqliteDb.java | 4 ++-- .../umbrella/core/model/Transaction.java | 21 +++++++++++++++++-- .../src/routes/accounting/transaction.svelte | 12 ++++++++--- 4 files changed, 33 insertions(+), 12 deletions(-) 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 b8315a55..57a52f44 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -159,11 +159,9 @@ public class AccountingModule extends BaseHandler implements AccountingService { var transaction = accountDb.loadTransaction(transactionId); LOG.log(WARNING,"Missing permission check in patchTransaction(…)!"); var json = json(ex); - if (json.has(Field.DATE)){ - var date = LocalDate.parse(json.getString(Field.DATE)); - transaction.date(date); - accountDb.save(transaction); - } + if (json.has(Field.DATE)) transaction.date(LocalDate.parse(json.getString(Field.DATE))); + if (json.has(Field.PURPOSE)) transaction.purpose(json.getString(Field.PURPOSE)); + accountDb.save(transaction); return sendContent(ex,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 a719f910..7b657e92 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -217,12 +217,12 @@ public class SqliteDb extends BaseDb implements AccountDb { } catch (SQLException e) { throw failedToStoreObject(transaction); } - } else { + } else if (transaction.isDirty()) { try { Query.replaceInto(TABLE_TRANSACTIONS, Field.ID, Field.ACCOUNT, Field.TIMESTAMP, Field.SOURCE, Field.DESTINATION, Field.AMOUNT, Field.DESCRIPTION) .values(transaction.id(), transaction.accountId(), timestamp, transaction.source().value(), transaction.destination().value(), transaction.amount(), transaction.purpose()) .execute(db).close(); - return transaction; + return transaction.clearDirtyState(); } catch (SQLException e) { throw failedToStoreObject(transaction); } 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 71a1cd38..28dde06f 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 @@ -21,6 +21,7 @@ public class Transaction implements Mappable { private double amount; private String purpose; private Set tags; + private HashSet dirtyFields = new HashSet<>(); public Transaction(long id, long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose, Set tags){ this.id = id; @@ -41,17 +42,23 @@ public class Transaction implements Mappable { return amount; } + public Transaction clearDirtyState(){ + dirtyFields.clear(); + return this; + } + public LocalDateTime date(){ return date; } public Transaction date(LocalDateTime newVal){ date = newVal; + dirtyFields.add(Field.DATE); return this; } - public void date(LocalDate date) { - this.date = this.date.withYear(date.getYear()).withMonth(date.getMonthValue()).withDayOfMonth(date.getDayOfMonth()); + public Transaction date(LocalDate date) { + return date(this.date.withYear(date.getYear()).withMonth(date.getMonthValue()).withDayOfMonth(date.getDayOfMonth())); } public IdOrString destination(){ @@ -62,6 +69,10 @@ public class Transaction implements Mappable { return id; } + public boolean isDirty(){ + return !dirtyFields.isEmpty(); + } + public static Transaction of(ResultSet rs) throws SQLException { var accountId = rs.getLong(Field.ACCOUNT); var timestamp = rs.getLong(Field.TIMESTAMP); @@ -78,6 +89,12 @@ public class Transaction implements Mappable { return purpose; } + public Transaction purpose(String newVal){ + purpose = newVal; + dirtyFields.add(Field.PURPOSE); + return this; + } + public IdOrString source(){ return source; } diff --git a/frontend/src/routes/accounting/transaction.svelte b/frontend/src/routes/accounting/transaction.svelte index bb165bfc..7173b002 100644 --- a/frontend/src/routes/accounting/transaction.svelte +++ b/frontend/src/routes/accounting/transaction.svelte @@ -15,8 +15,12 @@ return false; } - async function setDate(newDate){ - return await update({date:newDate}); + async function setDate(date){ + return await update({date}); + } + + async function setPurpose(purpose){ + return await update({purpose}); } @@ -42,7 +46,9 @@ → {transaction.destination.value} {/if} - {transaction.purpose} + + + {transaction.tags.join(', ')} From 9d9e2ed50b1c3891565ff398a56e6837f82cd31e Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 10 Apr 2026 16:08:03 +0200 Subject: [PATCH 18/21] extended possibilities to edit transaction Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountingModule.java | 6 ++-- .../umbrella/core/model/Transaction.java | 18 ++++++++++ .../src/routes/accounting/transaction.svelte | 35 ++++++++++++------- 3 files changed, 45 insertions(+), 14 deletions(-) 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 57a52f44..7573f8df 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -159,10 +159,12 @@ public class AccountingModule extends BaseHandler implements AccountingService { var transaction = accountDb.loadTransaction(transactionId); LOG.log(WARNING,"Missing permission check in patchTransaction(…)!"); var json = json(ex); + if (json.has(Field.AMOUNT)) transaction.amount(json.getDouble(Field.AMOUNT)); if (json.has(Field.DATE)) transaction.date(LocalDate.parse(json.getString(Field.DATE))); + if (json.has(Field.DESTINATION)) transaction.destination(IdOrString.of(json.getString(Field.DESTINATION))); if (json.has(Field.PURPOSE)) transaction.purpose(json.getString(Field.PURPOSE)); - accountDb.save(transaction); - return sendContent(ex,transaction); + if (json.has(Field.SOURCE)) transaction.source(IdOrString.of(json.getString(Field.SOURCE))); + return sendContent(ex,accountDb.save(transaction)); } private boolean postEntry(UmbrellaUser user, HttpExchange ex) throws IOException { 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 28dde06f..37095118 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 @@ -42,6 +42,12 @@ public class Transaction implements Mappable { return amount; } + public Transaction amount(double newVal){ + amount = newVal; + dirtyFields.add(Field.AMOUNT); + return this; + } + public Transaction clearDirtyState(){ dirtyFields.clear(); return this; @@ -65,6 +71,12 @@ public class Transaction implements Mappable { return destination; } + public Transaction destination(IdOrString newVal){ + destination = newVal; + dirtyFields.add(Field.DESTINATION); + return this; + } + public long id(){ return id; } @@ -99,6 +111,12 @@ public class Transaction implements Mappable { return source; } + public Transaction source(IdOrString newVal){ + source = newVal; + dirtyFields.add(Field.SOURCE); + return this; + } + public Set tags(){ return tags; } diff --git a/frontend/src/routes/accounting/transaction.svelte b/frontend/src/routes/accounting/transaction.svelte index 7173b002..60bbc068 100644 --- a/frontend/src/routes/accounting/transaction.svelte +++ b/frontend/src/routes/accounting/transaction.svelte @@ -4,6 +4,25 @@ import { error, yikes } from '../../warn.svelte'; let { account, transaction, users } = $props(); + async function setAmount(amount){ + return await update({amount}); + } + async function setDate(date){ + return await update({date}); + } + + async function setDestination(destination){ + return await update({destination}); + } + + async function setPurpose(purpose){ + return await update({purpose}); + } + + async function setSource(source){ + return await update({source}); + } + async function update(changes){ let url = api('accounting/transaction/'+transaction.id); let res = await patch(url,changes); @@ -14,14 +33,6 @@ error(res); return false; } - - async function setDate(date){ - return await update({date}); - } - - async function setPurpose(purpose){ - return await update({purpose}); - } @@ -31,19 +42,19 @@ {#each Object.entries(users) as [id,user]} {#if id == transaction.source.id} - {(-transaction.amount).toFixed(2)} {account.currency} + - {account.currency} {/if} {#if id == transaction.destination.id} - {(+transaction.amount).toFixed(2)} {account.currency} +  {account.currency} {/if} {/each} {#if !transaction.source.id} - ← {transaction.source.value} + ← {/if} {#if !transaction.destination.id} - → {transaction.destination.value} + → {/if} From f6b854a227c9e36e411f6d9ffb833b5fa3e9553a Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 14 Apr 2026 21:44:13 +0200 Subject: [PATCH 19/21] 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 @@ +{#if !hidden} @@ -92,10 +98,10 @@ {#each Object.entries(users) as [id,user]} {#if id == transaction.source.id} - - {account.currency} + - {account.currency} {/if} {#if id == transaction.destination.id} -  {account.currency} +  {account.currency} {/if} {/each} @@ -121,3 +127,4 @@ +{/if} \ No newline at end of file From b5e18fd5ca5c7211278d55c111a8036930777f76 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 14 Apr 2026 22:49:06 +0200 Subject: [PATCH 21/21] minor improvements Signed-off-by: Stephan Richter --- .../de/srsoftware/umbrella/accounting/AccountingModule.java | 5 ++++- frontend/src/routes/accounting/account.svelte | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) 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 c55feffb..216cc61d 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -304,7 +304,10 @@ public class AccountingModule extends BaseHandler implements AccountingService { var result = new HashSet(); var lower = key.toLowerCase(); var len = key.length(); - for (var tag : tags) result.add(tag.toLowerCase().startsWith(lower) ? key + tag.substring(len) : tag); + 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(); } diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte index 8a2f141d..35a4ff9e 100644 --- a/frontend/src/routes/accounting/account.svelte +++ b/frontend/src/routes/accounting/account.svelte @@ -115,7 +115,5 @@

    -TODO: Bearbeiten von Umsätzen - {/if} \ No newline at end of file