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 be9f40cd..88626ad1 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -9,6 +9,8 @@ import java.util.Optional; import java.util.Set; public interface AccountDb { + Transaction dropTransaction(Transaction transaction); + void dropTransactionTag(long transactionId, String tag); Optional lastTransaction(long accountId, String source, String dest, double amount); 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 209c2189..80c20cd3 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -6,13 +6,11 @@ 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.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField; import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus; import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE; -import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE; import static java.lang.System.Logger.Level.WARNING; import com.sun.net.httpserver.HttpExchange; @@ -26,13 +24,12 @@ 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 de.srsoftware.umbrella.messagebus.events.Event; +import de.srsoftware.umbrella.messagebus.events.TransactionEvent; import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; - -import de.srsoftware.umbrella.messagebus.events.Event; -import de.srsoftware.umbrella.messagebus.events.TransactionEvent; import org.json.JSONArray; import org.json.JSONObject; @@ -143,11 +140,18 @@ public class AccountingModule extends BaseHandler implements AccountingService { } } + public boolean dropTransaction(Transaction transaction, UmbrellaUser user, HttpExchange ex) throws IOException { + var dropped = accountDb.dropTransaction(transaction); + messageBus().dispatch(new TransactionEvent(user,dropped,Event.EventType.DELETE)); + return sendContent(ex,dropped); + } + 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); + case null -> dropTransaction(transaction,user,ex); + default -> super.doDelete(path,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 index 5d56a4c3..b9e62b58 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -10,8 +10,6 @@ import static de.srsoftware.umbrella.accounting.Constants.*; 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 de.srsoftware.umbrella.messagebus.MessageBus.messageBus; -import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE; import static java.text.MessageFormat.format; import de.srsoftware.tools.jdbc.Condition; @@ -21,8 +19,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 de.srsoftware.umbrella.messagebus.events.TransactionEvent; - import java.sql.Connection; import java.sql.SQLException; import java.time.ZoneOffset; @@ -121,6 +117,21 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + public Transaction dropTransaction(Transaction transaction){ + try { + db.setAutoCommit(false); + Query.delete().from(TABLE_TAGS_TRANSACTIONS).where(TRANSACTION_ID,equal(transaction.id())).execute(db); + Query.delete().from(TABLE_TRANSACTIONS).where(ID,equal(transaction.id())).execute(db); + db.setAutoCommit(true); + return transaction; + } catch (SQLException e){ + try { + db.rollback(); + } catch (SQLException ignored){}; + throw failedToDropObject(transaction); + } + } + @Override public void dropTransactionTag(long transactionId, String tag) { try { @@ -299,13 +310,9 @@ public class SqliteDb extends BaseDb implements AccountDb { } } else if (transaction.isDirty()) { try { - if (transaction.amount() == 0 || transaction.source().isEmpty() || transaction.destination().isEmpty()) { - delete().from(TABLE_TRANSACTIONS).where(Field.ID, equal(transaction.id())).where(ACCOUNT, equal(transaction.accountId())).execute(db); - } else { - 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(); - } + 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.clearDirtyState(); } catch (SQLException e) { throw failedToStoreObject(transaction); diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/TransactionEvent.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/TransactionEvent.java index ae0794db..9de64fef 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/TransactionEvent.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/TransactionEvent.java @@ -1,22 +1,19 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.messagebus.events; -import de.srsoftware.umbrella.core.ModuleRegistry; +import static de.srsoftware.umbrella.core.ModuleRegistry.accountingService; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.model.Translatable.t; + import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.constants.Module; import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.model.Transaction; import de.srsoftware.umbrella.core.model.Translatable; import de.srsoftware.umbrella.core.model.UmbrellaUser; -import de.srsoftware.umbrella.core.model.UnTranslatable; - import java.util.Collection; -import java.util.List; import java.util.Map; -import static de.srsoftware.umbrella.core.ModuleRegistry.accountingService; -import static de.srsoftware.umbrella.core.model.Translatable.t; -import static java.text.MessageFormat.format; - public class TransactionEvent extends Event { private Collection audience; @@ -38,17 +35,42 @@ public class TransactionEvent extends Event { @Override public Translatable describe() { + var user = initiator().name(); + var type = t(Text.TRANSACTION); + var entity = payload().purpose(); return switch (eventType()){ - case UPDATE -> new UnTranslatable(diff().orElse("")); - case null, default -> new UnTranslatable(payload().toString()); + case CREATE -> describeDetail(); + case DELETE -> describeDetail(); + case UPDATE -> describeUpdate(); + case null, default -> t("TODO"); // TODO }; } + private Translatable describeUpdate() { + var head = t("Changes in {type} '{entity}':\n\n{body}",Field.TYPE,t(Text.TRANSACTION),Field.ENTITY,oldData().get(PURPOSE),BODY,diff().orElse("")); + return t("{head}\n\n{link}","head",head,"link",link()); + } + + private Translatable describeDetail(){ + var tr = payload(); + var head = subject(); + + var message = "{head}:\n\n{source}: {source_name}\n{destination}: {dest_name}\n{amount}: {value}\n{purpose}: {purpose_val}\n\n{link}"; + return t(message,Field.HEAD, head, SOURCE,t(Text.SOURCE), "source_name",tr.source(), DESTINATION,t(Text.DESTINATION),"dest_name",tr.destination(), AMOUNT,t(Text.AMOUNT), VALUE,tr.amount(), PURPOSE,t(Text.PURPOSE),"purpose_val",tr.purpose(),"link",link()); + } + + private Translatable link() { + return t("You can view/edit this transaction at {base_url}/account/{id}", ID, payload().accountId()); + } + @Override public Translatable subject() { + var user = initiator().name(); + var entity = payload().purpose(); return switch (eventType()){ - case CREATE -> t("user_created_entity", Field.USER,initiator().name(), Field.ENTITY, t(Text.TRANSACTION)); - case UPDATE -> t("user_updated_entity", Field.USER,initiator().name(), Field.ENTITY, t(Text.TRANSACTION)); + case CREATE -> t("{user} added a new transaction: {entity}", USER,user, ENTITY, entity); + case DELETE -> t("The transaction '{entity}' has been deleted by {user}",Field.ENTITY, entity, USER, user); + case UPDATE -> t("{user} updated the transaction '{entity}'", USER,user, ENTITY, oldData().get(PURPOSE)); case null, default -> t("TODO"); // TODO }; } 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 886a97ae..98753e45 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,18 +1,17 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.api; +import static de.srsoftware.umbrella.core.Util.mapValues; + import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.model.Account; import de.srsoftware.umbrella.core.model.Transaction; import de.srsoftware.umbrella.core.model.UmbrellaUser; - import java.util.HashMap; import java.util.List; import java.util.Map; -import static de.srsoftware.umbrella.core.Util.mapValues; - public interface AccountingService { public record AccountData(Account account, List transactions, HashMap userMap) implements Mappable { public static AccountData of(Account account, List transactions, HashMap userMap) { 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 8f2be0cd..2e7fd581 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 @@ -7,6 +7,7 @@ package de.srsoftware.umbrella.core.constants; public class Text { public static final String ACCOUNT = "account"; public static final String ACCOUNTING = "accounting"; + public static final String AMOUNT = "amount"; public static final String BOOKMARK = "bookmark"; public static final String BOOKMARKS = "bookmarks"; @@ -21,6 +22,7 @@ public class Text { public static final String CUSTOMER = "customer"; public static final String CUSTOMER_SETTINGS = "customer settings"; + public static final String DESTINATION = "destination"; public static final String DOCUMENT = "document"; public static final String DOCUMENTS = "documents"; public static final String DOCUMENT_TYPE_ID = "document type id"; @@ -60,6 +62,7 @@ public class Text { public static final String PROJECT_WITH_ID = "project ({id})"; public static final String PROPERTIES = "properties"; public static final String PROPERTY = "property"; + public static final String PURPOSE = "purpose"; public static final String RECEIVER = "receiver"; public static final String RECEIVERS = "receivers"; @@ -69,6 +72,7 @@ public class Text { public static final String SERVICE_WITH_ID = "service ({id})"; public static final String SESSION = "session"; public static final String SETTINGS = "settings"; + public static final String SOURCE = "source"; public static final String STOCK = "stock"; public static final String STRING = "string"; 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 d95de3d6..bdc4432d 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,14 +1,13 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; +import static de.srsoftware.tools.Optionals.isSet; + import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.constants.Field; import java.util.HashMap; import java.util.Map; -import static de.srsoftware.tools.Optionals.isSet; -import static de.srsoftware.tools.Optionals.nullIfEmpty; - public class IdOrString implements Mappable { private final Long id; private final String value; diff --git a/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java b/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java index 2d1901b7..66e8b52b 100644 --- a/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java +++ b/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java @@ -16,6 +16,7 @@ import static de.srsoftware.umbrella.core.ModuleRegistry.*; import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE; import static de.srsoftware.umbrella.core.Util.mapValues; import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Field.AMOUNT; import static de.srsoftware.umbrella.core.constants.Field.COMPANY; import static de.srsoftware.umbrella.core.constants.Field.CUSTOMER; import static de.srsoftware.umbrella.core.constants.Field.DOCUMENT; diff --git a/documents/src/main/java/de/srsoftware/umbrella/documents/SqliteDb.java b/documents/src/main/java/de/srsoftware/umbrella/documents/SqliteDb.java index 2bd41e4b..6bfaa2cf 100644 --- a/documents/src/main/java/de/srsoftware/umbrella/documents/SqliteDb.java +++ b/documents/src/main/java/de/srsoftware/umbrella/documents/SqliteDb.java @@ -9,6 +9,7 @@ import static de.srsoftware.umbrella.core.Errors.*; import static de.srsoftware.umbrella.core.ModuleRegistry.translator; import static de.srsoftware.umbrella.core.constants.Constants.FALLBACK_LANG; import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Field.AMOUNT; import static de.srsoftware.umbrella.core.constants.Field.COMPANY; import static de.srsoftware.umbrella.core.constants.Field.CUSTOMER; import static de.srsoftware.umbrella.core.constants.Field.NUMBER; diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte index eac1340f..02167396 100644 --- a/frontend/src/routes/accounting/account.svelte +++ b/frontend/src/routes/accounting/account.svelte @@ -57,13 +57,9 @@ window.setTimeout(scrollToBottom,100); } - function handleEvent(evt,method){ - let event_data = JSON.parse(evt.data); - console.log({method, event_data}); - } - function handleDeleteEvent(evt){ - handleEvent(evt,'delete'); + const event_data = JSON.parse(evt.data); + transactions = transactions.filter(t => t.id != event_data.transaction.id); } function handleUpdateEvent(evt){ @@ -139,6 +135,7 @@ {t('other party')} {t('purpose')} {t('tags')} + @@ -160,7 +157,7 @@
{sums[0].toFixed(2)} {account.currency} - + diff --git a/frontend/src/routes/accounting/transaction.svelte b/frontend/src/routes/accounting/transaction.svelte index 467570ae..41dc5691 100644 --- a/frontend/src/routes/accounting/transaction.svelte +++ b/frontend/src/routes/accounting/transaction.svelte @@ -8,6 +8,16 @@ let { account, addToFilter = tag => {}, transaction, users } = $props(); let hidden = $state(false); + function deleteTransaction(ev){ + if (confirm(t('confirm_delete',{element:transaction.purpose}))){ + const url = api(`accounting/transaction/${transaction.id}`); + const res = drop(url); + if (res.ok){ + yikes(); + } else error(res); + } + } + async function dropTag(tag){ var url = api(`accounting/transaction/${transaction.id}/tag`) var res = await drop(url,{tag}); @@ -127,5 +137,8 @@ + + + {/if} diff --git a/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java index dfb424ea..582e794b 100644 --- a/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java +++ b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java @@ -7,6 +7,7 @@ import static de.srsoftware.umbrella.core.Util.mapValues; import static de.srsoftware.umbrella.core.constants.Field.*; import static de.srsoftware.umbrella.core.constants.Field.PERMISSION; import static de.srsoftware.umbrella.core.constants.Field.SETTINGS; +import static de.srsoftware.umbrella.core.constants.Field.SOURCE; import static de.srsoftware.umbrella.core.constants.Field.TAGS; import static de.srsoftware.umbrella.core.constants.Module.PROJECT; import static de.srsoftware.umbrella.core.constants.Path.*; diff --git a/translations/src/main/resources/de.json b/translations/src/main/resources/de.json index 00d803af..dfd27de6 100644 --- a/translations/src/main/resources/de.json +++ b/translations/src/main/resources/de.json @@ -397,6 +397,7 @@ "The task '{task}' has been created": "Die Aufgabe '{task}' wurde angelegt", "The task '{task}' has been deleted": "Die Aufgabe '{task}' wurde gelöscht", "The task '{task}' has been deleted by {user}": "Die Aufgabe '{task}' wurde von {user} gelöscht", + "The transaction '{entity}' has been deleted by {user}": "Der Umsatz „{entity}“ wurde von {user} gelöscht", "The wiki page '{name}' has been created": "Die Wiki-Seite „{name}“ wurde angelegt", "The wiki page '{name}' has been deleted": "Die Wiki-Seite „{name}“ wurde gelöscht.", "The wiki page '{page}' has been deleted by {user}": "Die Wiki-Seite „{page}“ wurde durch {user} gelöscht.", @@ -428,6 +429,8 @@ "upload_file": "Datei hochladen", "user": "Benutzer", "user ({id})": "Benutzer ({id})", + "{user} added a new transaction: {entity}": "{user} hat einen neuen Umsatz hinzugefügt: {entity}", + "{user} updated the transaction '{entity}'": "{user} hat den Umsatz „{entity}“ aktualisiert", "user_list": "Benutzer-Liste", "user_module" : "Umbrella User-Verwaltung", "user profile": "Benutzer-Profil", @@ -455,6 +458,7 @@ "wiki_pages": "Wiki-Seiten", "year": "Jahr", + "You can view/edit this transaction at {base_url}/account/{id}": "Du kannst diese transaktion unter {base_url}/account/{id} ansehen", "You can view/edit this project at {base_url}/project/{id}/view": "Du kannst dieses Projekt unter {base_url}/project/{id}/view ansehen/bearbeiten.", "You can view/edit this task at {base_url}/task/{id}/view": "Du kannst diese Aufgabe unter {base_url}/task/{id}/view ansehen/bearbeiten.", "You can view/edit this wiki page at {base_url}/wiki/{id}/view": "Du kannst diese Wiki-Seite unter {base_url}/wiki/{id}/view ansehen/bearbeiten.", diff --git a/translations/src/main/resources/en.json b/translations/src/main/resources/en.json index c302f645..d66f548f 100644 --- a/translations/src/main/resources/en.json +++ b/translations/src/main/resources/en.json @@ -397,6 +397,7 @@ "The task '{task}' has been created": "The task '{task}' has been created", "The task '{task}' has been deleted": "The task '{task}' has been deleted", "The task '{task}' has been deleted by {user}": "The task '{task}' has been deleted by {user}", + "The transaction '{entity}' has been deleted by {user}": "The transaction '{entity}' has been deleted by {user}", "The wiki page '{name}' has been created": "The wiki page '{name}' has been created", "The wiki page '{name}' has been deleted": "The wiki page '{name}' has been deleted", "The wiki page '{page}' has been deleted by {user}": "The wiki page '{page}' has been deleted by {user}", @@ -428,6 +429,8 @@ "upload_file": "upload file", "user": "user", "user ({id})": "user ({id})", + "{user} added a new transaction: {entity}": "{user} added a new transaction: {entity}", + "{user} updated the transaction '{entity}'": "{user} updated the transaction '{entity}'", "user_list": "user list", "user_module" : "Umbrella user management", "user profile": "User profile", @@ -455,10 +458,11 @@ "wiki_pages": "wiki pages", "year": "year", + "You have been added to the new project '{project}', created by {user}:\n\n{body}": "You have been added to the new project '{project}', created by {user}:\n\n{body}", + "You can view/edit this transaction at {base_url}/account/{id}": "Du kannst diese transaktion unter {base_url}/account/{id} ansehen", "You can view/edit this project at {base_url}/project/{id}/view": "You can view/edit this project at {base_url}/project/{id}/view", "You can view/edit this task at {base_url}/task/{id}/view": "You can view/edit this task at {base_url}/task/{id}/view", "You can view/edit this wiki page at {base_url}/wiki/{id}/view": "You can view/edit this wiki page at {base_url}/wiki/{id}/view", - "You have been added to the new project '{project}', created by {user}:\n\n{body}": "You have been added to the new project '{project}', created by {user}:\n\n{body}", "You may change your notification settings at {base_url}/message/settings": "You may change your notification settings at {base_url}/message/settings .", "Your token to create a new password" : "Your token to create a new password", "your_profile": "your profile"