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 ab7daf76..be9f40cd 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -5,11 +5,14 @@ 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.Optional; import java.util.Set; public interface AccountDb { void dropTransactionTag(long transactionId, String tag); + Optional lastTransaction(long accountId, String source, String dest, double amount); + Collection listAccounts(long userId); Collection listTags(long accountId, String source, String destination); 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 36c71d36..16c2ae65 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -119,7 +119,7 @@ public class AccountingModule extends BaseHandler implements AccountingService { var head = path.pop(); return switch (head) { case null -> postEntry(user.get(),ex); - case DESTINATIONS -> postSearchDestinations(user.get(),ex); + case DESTINATIONS -> postSearchDestinations(user.get(),ex); case PURPOSES -> postSearchPurposes(user.get(),ex); case SOURCES -> postSearchSources(user.get(),ex); default -> { @@ -295,6 +295,22 @@ public class AccountingModule extends BaseHandler implements AccountingService { return sendContent(ex,newAccount != null ? newAccount : transaction); } + public boolean postGetLastTransaction(long accountId, UmbrellaUser user, HttpExchange ex) throws IOException { + var json = json(ex); + if (!json.has(Field.SOURCE)) throw missingField(Field.SOURCE); + if (!(json.get(Field.SOURCE) instanceof JSONObject src)) throw invalidField(Field.SOURCE,JSON); + var source = src.get(src.has(Field.ID) ? Field.ID : Field.DISPLAY).toString(); + if (!json.has(Field.DESTINATION)) throw missingField(Field.DESTINATION); + if (!(json.get(Field.DESTINATION) instanceof JSONObject dst)) throw invalidField(Field.SOURCE,JSON); + var dest = dst.get(dst.has(Field.ID) ? Field.ID : Field.DISPLAY).toString(); + if (!json.has(Field.AMOUNT)) throw missingField(Field.AMOUNT); + if (!(json.get(Field.AMOUNT) instanceof Number amt)) throw invalidField(Field.AMOUNT,Text.NUMBER); + var amount = amt.doubleValue(); + + var transaction = accountDb.lastTransaction(accountId, source, dest, amount); + return transaction.isPresent() ? sendContent(ex,transaction.get()) : notFound(ex); + } + public boolean postSearchDestinations(UmbrellaUser user, HttpExchange ex) throws IOException { return sendContent(ex,searchOptions(user, Field.DESTINATION, body(ex))); } @@ -327,7 +343,9 @@ public class AccountingModule extends BaseHandler implements AccountingService { } private boolean postToAccount(long accountId, Path path, UmbrellaUser user, HttpExchange ex) throws IOException { - return switch (path.pop()) { + var head = path.pop(); + return switch (head) { + case PURPOSES -> postGetLastTransaction(accountId,user,ex); case TAGS -> postSearchTags(accountId,user,ex); case null, default -> super.doPost(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 a2582be1..a889eadd 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -2,6 +2,7 @@ package de.srsoftware.umbrella.accounting; import static de.srsoftware.tools.NotImplemented.notImplemented; +import static de.srsoftware.tools.Optionals.nullable; import static de.srsoftware.tools.jdbc.Condition.*; import static de.srsoftware.tools.jdbc.Query.*; import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; @@ -130,6 +131,29 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + @Override + public Optional lastTransaction(long accountId, String source, String dest, double amount) { + try { + var rs = select(ALL).from(TABLE_TRANSACTIONS) + .where(ACCOUNT,equal(accountId)).where(SOURCE,equal(source)).where(DESTINATION,equal(dest)).where(AMOUNT,equal(amount)) + .sort(ID+" DESC") + .limit(1) + .exec(db); + Transaction ta = null; + if (rs.next()) ta = Transaction.of(rs); + rs.close(); + if (ta != null){ + var tags = ta.tags(); + rs = select(TAG).from(TABLE_TAGS_TRANSACTIONS).leftJoin(TAG_ID,TABLE_TAGS,ID).where(TRANSACTION_ID,equal(ta.id())).exec(db); + while (rs.next()) tags.add(rs.getString(1)); + rs.close(); + } + return nullable(ta); + } catch (SQLException e) { + throw failedToSearchDb(t(Text.ACCOUNTING)); + } + } + @Override public HashSet listAccounts(long userId) { try { @@ -271,7 +295,7 @@ public class SqliteDb extends BaseDb implements AccountDb { } } else if (transaction.isDirty()) { try { - if (transaction.amount() == 0) { + 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) 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 37310fd3..d95de3d6 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 @@ -6,6 +6,9 @@ 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; @@ -52,6 +55,10 @@ public class IdOrString implements Mappable { return map; } + public boolean isEmpty() { + return id == null && !isSet(value); + } + @Override public String toString() { return value; @@ -60,4 +67,4 @@ public class IdOrString implements Mappable { public String value(){ return value; } - } \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/Components/Autocomplete.svelte b/frontend/src/Components/Autocomplete.svelte index df37a557..27631e63 100644 --- a/frontend/src/Components/Autocomplete.svelte +++ b/frontend/src/Components/Autocomplete.svelte @@ -17,6 +17,7 @@ let selected = $state(null); let candidates = $state([]); let timer = null; + let list_elem; async function dummyGetCandidates(text){ console.warn(`getCandidates(${text}) not overridden!`); @@ -58,9 +59,8 @@ } async function fetchCandidates(){ - candidates = await getCandidates(candidate.display); + candidates = candidate.display ? await getCandidates(candidate.display) : []; selected = null; - if (selected>candidates.length) selected = candidates.length; } async function onkeyup(ev){ @@ -69,12 +69,14 @@ ev.preventDefault(); selected = selected == null ? 0: selected +1; if (selected >= candidates.length) selected = 0; + scrollTo(selected); return false; } if (ev.key == 'ArrowUp'){ ev.preventDefault(); selected = selected == null ? candidates.length -1 : selected -1; if (selected < 0) selected = candidates.length -1; + scrollTo(selected); return false; } if (ev.key == 'Enter'|| ev.key == 'Tab'){ @@ -113,6 +115,11 @@ onSelect(candidate); } + + function scrollTo(index){ + let list_elements = list_elem.children; + if (list_elements) list_elements[index].scrollIntoView({block:'center'}); + } - + {#if candidates && candidates.length > 0} -
    +
      {#each candidates as candidate,i} -
    • selected = i} ondblclick={e => select(i)}>{candidate.display}
    • +
    • select(i)} ondblclick={e => select(i)}>{candidate.display}
    • {/each}
    {/if} diff --git a/frontend/src/Components/MarkdownEditor.svelte b/frontend/src/Components/MarkdownEditor.svelte index 9fe79866..e0e0ac43 100644 --- a/frontend/src/Components/MarkdownEditor.svelte +++ b/frontend/src/Components/MarkdownEditor.svelte @@ -155,5 +155,8 @@ {/if} {:else} + {#if !value.display} + + {/if} {/if} diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte index d7806b1c..721a4f96 100644 --- a/frontend/src/routes/accounting/account.svelte +++ b/frontend/src/routes/accounting/account.svelte @@ -77,6 +77,12 @@ .amount{ text-align: right } + + {#if account} + Umbrella – {account.name} + {/if} + + {#if filter.length > 0}
    {t('filter by tags')} diff --git a/frontend/src/routes/accounting/add_entry.svelte b/frontend/src/routes/accounting/add_entry.svelte index db7531e9..55190fdb 100644 --- a/frontend/src/routes/accounting/add_entry.svelte +++ b/frontend/src/routes/accounting/add_entry.svelte @@ -42,7 +42,10 @@ } function focusOnEnter(ev,id){ - if (ev.key == 'Enter') document.getElementById(id).focus(); + if (ev.key == 'Enter') { + proposePurpose(); + document.getElementById(id).focus(); + } } async function getAccountTags(text){ @@ -93,6 +96,20 @@ } } + async function proposePurpose(){ + const source = entry.source; + const destination = entry.destination; + const amount = entry.amount; + const url = api(`accounting/${account.id}/purposes`); + const res = await post(url,{source,destination,amount}); + if (res.ok) { + yikes(); + var lastTransaction = await res.json(); + entry.purpose = { display: lastTransaction.purpose}; + entry.tags = lastTransaction.tags; + } else error(res); + } + async function save(){ let data = { ...entry, @@ -169,4 +186,4 @@ -
    \ No newline at end of file + diff --git a/frontend/src/routes/accounting/index.svelte b/frontend/src/routes/accounting/index.svelte index e4184537..1d6888f1 100644 --- a/frontend/src/routes/accounting/index.svelte +++ b/frontend/src/routes/accounting/index.svelte @@ -32,6 +32,10 @@ onMount(load); + + Umbrella – {t('accounts')} + +
    diff --git a/frontend/src/routes/project/View.svelte b/frontend/src/routes/project/View.svelte index 3dd32b96..b734a6be 100644 --- a/frontend/src/routes/project/View.svelte +++ b/frontend/src/routes/project/View.svelte @@ -200,9 +200,10 @@ update({name:val})} />
    - + {t('options')}
    +
    {t('state')}
    diff --git a/translations/src/main/resources/de.json b/translations/src/main/resources/de.json index 6504c9a4..00d803af 100644 --- a/translations/src/main/resources/de.json +++ b/translations/src/main/resources/de.json @@ -192,6 +192,8 @@ "items": "Artikel", "join_objects" : "{objects} zusammenführen", + + "kanban": "Kanban", "key": "Suchbegriff", "language": "Sprache", @@ -307,6 +309,7 @@ "project ({id})": "Projekt ({id})", "Project '{project}' was edited": "Projekt '{project}' wurde bearbeitet", "projects": "Projekte", + "Projects": "Projekte", "properties": "Eigenschaften", "property": "Eigenschaft", "purpose": "Zweck", diff --git a/translations/src/main/resources/en.json b/translations/src/main/resources/en.json index a118b58d..c302f645 100644 --- a/translations/src/main/resources/en.json +++ b/translations/src/main/resources/en.json @@ -192,6 +192,8 @@ "items": "items", "join_objects" : "join {objects}", + + "kanban": "Kanban", "key": "search term", "language": "language", @@ -307,6 +309,7 @@ "project ({id})": "project ({id})", "Project '{project}' was edited": "Project '{project}' was edited", "projects": "projects", + "Projects": "projects", "properties": "properties", "property": "property", "purpose": "purpose", diff --git a/web/src/main/resources/web/css/bloodshed.css b/web/src/main/resources/web/css/bloodshed.css index 9af28530..ef977dcd 100644 --- a/web/src/main/resources/web/css/bloodshed.css +++ b/web/src/main/resources/web/css/bloodshed.css @@ -542,7 +542,8 @@ select.autocomplete{ } @media screen and (max-width: 600px) { - .grid2{ + .grid2, + .grid3{ display: grid; grid-template-columns: auto; } @@ -582,6 +583,10 @@ select.autocomplete{ #app nav.expanded .timetracking{ grid-column-end: span 2; } + + .autocomplete .suggestions > *{ + font-size: 1.5em; + } } fieldset.vcard{ diff --git a/web/src/main/resources/web/css/default.css b/web/src/main/resources/web/css/default.css index 65a92884..0f5c9bca 100644 --- a/web/src/main/resources/web/css/default.css +++ b/web/src/main/resources/web/css/default.css @@ -711,6 +711,10 @@ select.autocomplete{ #app nav.expanded .timetracking{ grid-column-end: span 2; } + + .autocomplete .suggestions > *{ + font-size: 1.5em; + } } fieldset.vcard{ diff --git a/web/src/main/resources/web/css/winter.css b/web/src/main/resources/web/css/winter.css index ee8c9285..a8b8d7b5 100644 --- a/web/src/main/resources/web/css/winter.css +++ b/web/src/main/resources/web/css/winter.css @@ -701,6 +701,10 @@ select.autocomplete{ #app nav.expanded .timetracking{ grid-column-end: span 2; } + + .autocomplete .suggestions > *{ + font-size: 1.5em; + } } fieldset.vcard{