From a933ced8d80974261f3fd898f5ea7c1f64e47ced Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 3 Apr 2026 00:52:13 +0200 Subject: [PATCH] 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 @@