Compare commits

..

17 Commits

Author SHA1 Message Date
StephanRichter 38430feca0 extended documentation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 73f74a0929 extended documentation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 0a3f33c70d working on task description
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter e7c8039249 working on demodata
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter cc0dd85814 adding demodata
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 33bff55811 working on demodata
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 182060134e extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 64be02d863 extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 70ea315fa7 extending demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 889599dc40 started creating demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter ac0b61dca0 Merge branch 'main' into dev
Build Docker Image / Clean-Registry (push) Failing after 12m21s
Build Docker Image / Docker-Build (push) Failing after 16m34s
2026-06-17 15:07:52 +02:00
StephanRichter 07492d34de Merge branch 'bugfix/pipeline' into dev
Build Docker Image / Docker-Build (push) Successful in 2m9s
Build Docker Image / Clean-Registry (push) Failing after 10m20s
2026-06-17 09:18:17 +02:00
StephanRichter 9e4158ad19 Merge branch 'bugfix/timecomplete' into dev
Build Docker Image / Clean-Registry (push) Successful in 6s
Build Docker Image / Docker-Build (push) Failing after 14m27s
2026-06-17 09:15:08 +02:00
StephanRichter b81d518a2b Merge branch 'main' into dev
Build Docker Image / Docker-Build (push) Successful in 2m30s
Build Docker Image / Clean-Registry (push) Failing after 12m3s
2026-06-16 08:32:36 +02:00
StephanRichter e17fdbc619 Merge branch 'main' into dev
Build Docker Image / Docker-Build (push) Successful in 2m21s
Build Docker Image / Clean-Registry (push) Successful in 5s
2026-06-16 08:26:56 +02:00
StephanRichter 433ea6ddd3 Merge branch 'main' into dev
Build Docker Image / Clean-Registry (push) Successful in 5s
Build Docker Image / Docker-Build (push) Failing after 14m25s
2026-06-08 10:53:23 +02:00
StephanRichter 6249cdb7b9 re-implemented new transaction form
Build Docker Image / Docker-Build (push) Failing after 5m19s
Build Docker Image / Clean-Registry (push) Failing after 11m53s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-30 22:19:29 +02:00
35 changed files with 321 additions and 33 deletions
@@ -16,7 +16,7 @@ public interface AccountDb {
Collection<UmbrellaUser> getMembers(long accountId); Collection<UmbrellaUser> getMembers(long accountId);
Optional<Transaction> lastTransaction(long accountId, String source, String dest, double amount); Optional<Transaction> lastTransaction(long accountId, String source, String destination, Double amount);
Collection<Account> listAccounts(long userId); Collection<Account> listAccounts(long userId);
@@ -311,12 +311,11 @@ public class AccountingModule extends BaseHandler implements AccountingService {
var source = src.get(src.has(Field.ID) ? Field.ID : Field.DISPLAY).toString(); var source = src.get(src.has(Field.ID) ? Field.ID : Field.DISPLAY).toString();
if (!json.has(Field.DESTINATION)) throw missingField(Field.DESTINATION); if (!json.has(Field.DESTINATION)) throw missingField(Field.DESTINATION);
if (!(json.get(Field.DESTINATION) instanceof JSONObject dst)) throw invalidField(Field.SOURCE,JSON); 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(); String destination = dst.has(Field.ID) ? dst.get(Field.ID).toString() : dst.has(Field.DISPLAY) ? dst.get(Field.DISPLAY).toString() : null;
if (!json.has(Field.AMOUNT)) throw missingField(Field.AMOUNT); Double amount = null;
if (!(json.get(Field.AMOUNT) instanceof Number amt)) throw invalidField(Field.AMOUNT,Text.NUMBER); if (json.has(Field.AMOUNT) && json.get(Field.AMOUNT) instanceof Number amt) amount = amt.doubleValue();
var amount = amt.doubleValue();
var transaction = accountDb.lastTransaction(accountId, source, dest, amount); var transaction = accountDb.lastTransaction(accountId, source, destination, amount);
return transaction.isPresent() ? sendContent(ex,transaction.get()) : notFound(ex); return transaction.isPresent() ? sendContent(ex,transaction.get()) : notFound(ex);
} }
@@ -170,22 +170,42 @@ public class SqliteDb extends BaseDb implements AccountDb {
} }
@Override @Override
public Optional<Transaction> lastTransaction(long accountId, String source, String dest, double amount) { public Optional<Transaction> lastTransaction(long accountId, String source, String destination, Double amount) {
try { try {
var rs = select(ALL).from(TABLE_TRANSACTIONS) var query = select(ALL).from(TABLE_TRANSACTIONS).where(ACCOUNT,equal(accountId));
.where(ACCOUNT,equal(accountId)).where(SOURCE,equal(source)).where(DESTINATION,equal(dest)).where(AMOUNT,equal(amount)) if (source != null) query = query.where(SOURCE,equal(source));
.sort(ID+" DESC") if (destination != null) query = query.where(DESTINATION,equal(destination));
.limit(1) if (amount != null) query = query.where(AMOUNT,equal(amount));
.exec(db); var rs = query.sort(ID+" DESC").limit(1).exec(db);
Transaction ta = null; Transaction ta = null;
if (rs.next()) ta = Transaction.of(rs); if (rs.next()) ta = Transaction.of(rs);
rs.close(); rs.close();
if (ta == null && amount != null) { // try to search by amount, ignore source and dest
rs = select(ALL).from(TABLE_TRANSACTIONS).where(ACCOUNT, equal(accountId)).where(AMOUNT, equal(amount))
.sort(ID + " DESC").limit(1).exec(db);
if (rs.next()) ta = Transaction.of(rs);
rs.close();
}
if (ta == null && source != null && destination != null) { // try to search by amount, ignore source and dest
rs = select(ALL).from(TABLE_TRANSACTIONS)
.where(SOURCE,equal(source))
.where(DESTINATION,equal(destination))
.where(ACCOUNT, equal(accountId))
.sort(ID + " DESC").limit(1).exec(db);
if (rs.next()) ta = Transaction.of(rs);
rs.close();
}
if (ta != null){ if (ta != null){
var tags = ta.tags(); 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); 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)); while (rs.next()) tags.add(rs.getString(1));
rs.close(); rs.close();
} }
return nullable(ta); return nullable(ta);
} catch (SQLException e) { } catch (SQLException e) {
throw failedToSearchDb(t(Text.ACCOUNTING)); throw failedToSearchDb(t(Text.ACCOUNTING));
@@ -14,7 +14,6 @@ import de.srsoftware.umbrella.bookmarks.BookmarkApi;
import de.srsoftware.umbrella.company.CompanyModule; import de.srsoftware.umbrella.company.CompanyModule;
import de.srsoftware.umbrella.contact.ContactModule; import de.srsoftware.umbrella.contact.ContactModule;
import de.srsoftware.umbrella.core.SettingsService; import de.srsoftware.umbrella.core.SettingsService;
import de.srsoftware.umbrella.core.Util;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.documents.DocumentApi; import de.srsoftware.umbrella.documents.DocumentApi;
import de.srsoftware.umbrella.files.FileModule; import de.srsoftware.umbrella.files.FileModule;
@@ -34,7 +33,6 @@ import de.srsoftware.umbrella.translations.Translations;
import de.srsoftware.umbrella.user.UserModule; import de.srsoftware.umbrella.user.UserModule;
import de.srsoftware.umbrella.web.WebHandler; import de.srsoftware.umbrella.web.WebHandler;
import de.srsoftware.umbrella.wiki.WikiModule; import de.srsoftware.umbrella.wiki.WikiModule;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -15,7 +15,6 @@ import static java.lang.System.Logger.Level.*;
import static java.lang.System.Logger.Level.WARNING; import static java.lang.System.Logger.Level.WARNING;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import com.xrbpowered.jparsedown.JParsedown;
import de.srsoftware.tools.Mappable; import de.srsoftware.tools.Mappable;
import de.srsoftware.tools.Query; import de.srsoftware.tools.Query;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
@@ -32,7 +31,6 @@ import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern;
import org.json.JSONObject; import org.json.JSONObject;
public class Util { public class Util {
+1
View File
@@ -0,0 +1 @@
*.db-journal
Binary file not shown.
Binary file not shown.
Binary file not shown.
+74
View File
@@ -0,0 +1,74 @@
{
"umbrella": {
"base_url": "http://127.0.0.1:5173",
"logging": {
"rootLevel": "INFO"
},
"http": {
"port": 8080
},
"threads": 16,
"modules": {
"accounting": {
"database": "demodata/accounting.db"
},
"bookmark": {
"database": "demodata/bookmark.db"
},
"company": {
"database": "demodata/company.db"
},
"contact": {
"database": "demodata/contacts.db"
},
"document": {
"database": "demodata/documents.db",
"templates": "demodata/templates"
},
"files": {
"database": "demodata/files.db",
"base_dir": "demodata/filestore"
},
"journal": {
"database": "demodata/journal.db"
},
"message": {
"database": "demodata/message.db",
"smtp": {
"from": "umbrella@example.com",
"host": "none",
"pass": "none",
"port": 587,
"user": "none"
}
},
"notes": {
"database": "demodata/notes.db"
},
"poll": {
"database": "demodata/poll.db"
},
"project": {
"database": "demodata/projects.db"
},
"stock": {
"database": "demodata/stock.db"
},
"tags": {
"database": "demodata/tags.db"
},
"task": {
"database": "demodata/tasks.db"
},
"time": {
"database": "demodata/times.db"
},
"user": {
"database": "demodata/users.db"
},
"wiki": {
"database": "demodata/wiki.db"
}
}
}
}
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -115,7 +115,7 @@
function select(index){ function select(index){
candidate = candidates[index]; candidate = candidates[index];
disableDropdown(); <disableDropdown></disableDropdown>();
onSelect(candidate); onSelect(candidate);
} }
@@ -4,7 +4,7 @@
import { error, yikes } from '../../warn.svelte'; import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte'; import { t } from '../../translations.svelte';
import EntryForm from './add_entry.svelte'; import EntryForm from './add_entry_new.svelte';
import Transaction from './transaction.svelte'; import Transaction from './transaction.svelte';
let { id } = $props(); let { id } = $props();
@@ -163,5 +163,5 @@
</table> </table>
</fieldset> </fieldset>
<EntryForm {account} {onSave} /> <EntryForm {account} {onSave} {users} />
{/if} {/if}
@@ -0,0 +1,205 @@
<script>
import { useTinyRouter } from 'svelte-tiny-router';
import { t } from '../../translations.svelte';
import { api, post } from '../../urls.svelte';
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,
name : '',
currency : ''
};
let { account = defaultAccount, new_account = false, onSave = () => {}, users } = $props();
let entry = $state({
account,
date : new Date().toISOString().substring(0, 10),
source : {
display: user.name,
id: user.id
},
destination : {},
amount : 0.0,
purpose : {},
tags : []
});
let router = useTinyRouter();
async function dst_selected(destination){
destination = JSON.parse(JSON.stringify(destination));
let source = JSON.parse(JSON.stringify(entry.source));
const url = api(`accounting/${entry.account.id}/tags`)
const res = await post(url,{source,destination});
if (res.ok) {
yikes();
const json = await res.json();
await proposePurpose();
entry.tags = json;
} else error(res);
}
function focusOnEnter(ev,id){
if (ev.key == 'Enter') {
proposePurpose();
document.getElementById(id).focus();
}
}
async function getAccountTags(text){
if (!text) return [];
const url = api(`accounting/${entry.account.id}/tags`)
return await getProposals(text,url);
}
async function getDestinations(text){
const url = api('accounting/destinations');
return await getProposals(text,url);
}
async function getProposals(text,url){
const res = await post(url,text);
if (res.ok){
yikes();
const input = await res.json();
return Object.values(input).map(mapDisplay);
} else {
error(res);
return {};
}
}
async function getPurposes(text) {
const url = api('accounting/purposes');
return await getProposals(text,url);
}
async function getSources(text){
const url = api('accounting/sources');
return await getProposals(text,url);
}
function gotoTags(purpose){
document.getElementById('new_tag_input');
}
function mapDisplay(object){
if (object.display){
return object;
} else if (object.name) {
return {...object, display: object.name};
} else {
return { display : object }
}
}
async function proposePurpose(){
console.log('proposePurpose()');
const amount = entry.amount;
const source = entry.source;
const destination = entry.destination;
const url = api(`accounting/${account.id}/purposes`);
const res = await post(url,{source,destination,amount});
if (res.ok) {
yikes();
var lastTransaction = await res.json();
console.log({lastTransaction,users:JSON.parse(JSON.stringify(users))});
entry.purpose = { display: lastTransaction.purpose};
entry.tags = lastTransaction.tags;
if (lastTransaction.source.value){
if (users[lastTransaction.source.value]){
let user = users[lastTransaction.source.value];
entry.source = { id : +lastTransaction.source.value, display : user.name };
} else entry.source = { display: lastTransaction.source.value };
}
if (lastTransaction.destination.value){
if (users[lastTransaction.destination.value]){
let user = users[lastTransaction.destination.value];
entry.destination = { id : +lastTransaction.destination.value, display : user.name };
} else entry.destination = { display: lastTransaction.destination.value };
}
} else error(res);
}
async function save(){
let data = {
...entry,
purpose: entry.purpose.display
}
let url = api('accounting');
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();
} else error(res);
}
</script>
<style>
hr{
grid-column: 1 / -1;
margin: 0.5rem 0;
border: 0;
height: 1px;
align-self: center;
background: red;
}
</style>
<fieldset class="grid2 new_transaction">
{#if new_account}
<legend>{t('create_new_object',{object:t('account')})}</legend>
<span style="display:none"></span>
<span>{t('account name')}</span>
<span>
<input type="text" bind:value={entry.account.name} />
</span>
<span>{t('currency')}</span>
<span>
<input type="text" bind:value={entry.account.currency} />
</span>
<hr/>
<span style="grid-column-end: span 2">{t('first transaction')}</span>
{:else}
<legend>{t('add_object',{object:t('transaction')})}</legend>
<span style="display:none"></span>
{/if}
<span>{t('date')}</span>
<span>
<input type="date" bind:value={entry.date} id="date-input" />
</span>
<span>{t('amount')}</span>
<span>
<input type="number" bind:value={entry.amount} onkeyup={e => focusOnEnter(e,'source-input')} />&nbsp;{entry.account.currency}
</span>
<span>{t('source')}</span>
<Autocomplete bind:candidate={entry.source} getCandidates={getSources} id="source-input" />
<span>{t('destination')}</span>
<Autocomplete bind:candidate={entry.destination} getCandidates={getDestinations} onSelect={dst_selected} />
<span>{t('purpose')}</span>
<Autocomplete bind:candidate={entry.purpose} getCandidates={getPurposes} onCommit={gotoTags} id="purpose_input" />
<span>{t('tags')}</span>
<Tags getCandidates={getAccountTags} module={null} bind:tags={entry.tags} onEmptyCommit={save} />
<span></span>
<span>
<button onclick={save}>{t('save')}</button>
</span>
</fieldset>
@@ -13,8 +13,6 @@ import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.messagebus.events.Event; import de.srsoftware.umbrella.messagebus.events.Event;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
public class SqliteDb extends BaseDb implements JournalDb{ public class SqliteDb extends BaseDb implements JournalDb{
public SqliteDb(Connection connection) { public SqliteDb(Connection connection) {
@@ -35,14 +33,13 @@ public class SqliteDb extends BaseDb implements JournalDb{
var sql = """ var sql = """
CREATE TABLE IF NOT EXISTS {0} ( CREATE TABLE IF NOT EXISTS {0} (
{1} INTEGER PRIMARY KEY, {1} INTEGER PRIMARY KEY,
{2} LONG NOT NULL, {2} INTEGER,
{3} INTEGER, {3} VARCHAR(255) NOT NULL,
{4} VARCHAR(255) NOT NULL, {4} VARCHAR(16) NOT NULL,
{5} VARCHAR(16) NOT NULL, {5} TEXT
{6} TEXT
); );
"""; """;
sql = format(sql,TABLE_JOURNAL,ID,TIMESTAMP,USER_ID,MODULE,ACTION,DESCRIPTION); sql = format(sql,TABLE_JOURNAL,ID,USER_ID,MODULE,ACTION,DESCRIPTION);
try { try {
db.prepareStatement(sql).execute(); db.prepareStatement(sql).execute();
} catch (SQLException e) { } catch (SQLException e) {
@@ -53,9 +50,8 @@ public class SqliteDb extends BaseDb implements JournalDb{
@Override @Override
public void logEvent(Event<?> event) { public void logEvent(Event<?> event) {
try { try {
var timestamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); insertInto(TABLE_JOURNAL,USER_ID,MODULE,ACTION,DESCRIPTION)
insertInto(TABLE_JOURNAL,TIMESTAMP,USER_ID,MODULE,ACTION,DESCRIPTION) .values(event.initiator().id(), event.module(), event.eventType(), event.describe())
.values(timestamp,event.initiator().id(), event.module(), event.eventType(), event.describe())
.execute(db).close(); .execute(db).close();
} catch (SQLException e) { } catch (SQLException e) {
throw databaseException(ERROR_WRITE_EVENT,event.eventType(),event.initiator().name()); throw databaseException(ERROR_WRITE_EVENT,event.eventType(),event.initiator().name());
@@ -14,11 +14,8 @@ import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Path; import de.srsoftware.tools.Path;
import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry; import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.Util;
import de.srsoftware.umbrella.core.api.MarkdownService; import de.srsoftware.umbrella.core.api.MarkdownService;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;