Merge branch 'main' into module/journal
This commit is contained in:
@@ -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<Transaction> lastTransaction(long accountId, String source, String dest, double amount);
|
||||
|
||||
Collection<Account> listAccounts(long userId);
|
||||
|
||||
Collection<String> listTags(long accountId, String source, String destination);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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<Transaction> 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<Account> 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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -129,17 +136,20 @@
|
||||
list-style: none;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
min-width: 400px;
|
||||
max-height: 200px;
|
||||
overflow: scroll;
|
||||
}
|
||||
.highlight { background: orange; color: black; }
|
||||
|
||||
</style>
|
||||
|
||||
<span>
|
||||
<span class="autocomplete">
|
||||
<input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} {id} />
|
||||
{#if candidates && candidates.length > 0}
|
||||
<ul>
|
||||
<ul bind:this={list_elem} class="suggestions">
|
||||
{#each candidates as candidate,i}
|
||||
<li class="option {selected==i?'highlight':''}" onclick={e => selected = i} ondblclick={e => select(i)}>{candidate.display}</li>
|
||||
<li class="option {selected==i?'highlight':''}" onclick={e => select(i)} ondblclick={e => select(i)}>{candidate.display}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
@@ -155,5 +155,8 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<Display classes={{editable}} markdown={value} {onclick} {oncontextmenu} title={t('right_click_to_edit')} wrapper={type} />
|
||||
{#if !value.display}
|
||||
<button onclick={oncontextmenu}>{t('add_object',{object:t('content')})}</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -77,6 +77,12 @@
|
||||
.amount{ text-align: right }
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
{#if account}
|
||||
<title>Umbrella – {account.name}</title>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
{#if filter.length > 0}
|
||||
<fieldset>
|
||||
<legend>{t('filter by tags')}</legend>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Umbrella – {t('accounts')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<fieldset>
|
||||
|
||||
<span></span>
|
||||
|
||||
@@ -200,9 +200,10 @@
|
||||
<LineEditor bind:value={project.name} editable={true} onSet={val => update({name:val})} />
|
||||
</div>
|
||||
<div>
|
||||
<button onclick={kanban}>{t('show_kanban')}</button>
|
||||
{t('options')}
|
||||
</div>
|
||||
<div>
|
||||
<button onclick={kanban}><span class="symbol"></span> {t('show_kanban')}</button>
|
||||
<button onclick={toggleSettings}><span class="symbol"></span> {t('settings')}</button>
|
||||
</div>
|
||||
<div>{t('state')}</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -711,6 +711,10 @@ select.autocomplete{
|
||||
#app nav.expanded .timetracking{
|
||||
grid-column-end: span 2;
|
||||
}
|
||||
|
||||
.autocomplete .suggestions > *{
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
fieldset.vcard{
|
||||
|
||||
@@ -701,6 +701,10 @@ select.autocomplete{
|
||||
#app nav.expanded .timetracking{
|
||||
grid-column-end: span 2;
|
||||
}
|
||||
|
||||
.autocomplete .suggestions > *{
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
fieldset.vcard{
|
||||
|
||||
Reference in New Issue
Block a user