Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f43105d983 | |||
| fbe2380034 | |||
| 84bb908216 | |||
| f80bccfa0c | |||
| 372e371a8f | |||
| 51b2a497cf | |||
| 81b6090d88 | |||
| 1570aefc77 | |||
| 55cad3c854 | |||
| 747b2c1d48 |
@@ -41,6 +41,11 @@ jobs:
|
||||
docker push ${{ secrets.REGISTRY_PATH }}/umbrella:${{ gitea.ref_name }}
|
||||
docker push ${{ secrets.REGISTRY_PATH }}/umbrella:$TAG
|
||||
|
||||
- name: Restart umbrella.srsoftware.de
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: |
|
||||
curl -X POST -H "Authorization: Bearer ${{ secrets.ELDORADO_MAKE_BEARER }}" -d umbrella_25_start https://make.eldorado.srsoftware.de/launch
|
||||
|
||||
Clean-Registry:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ public interface AccountDb {
|
||||
|
||||
Collection<UmbrellaUser> getMembers(long accountId);
|
||||
|
||||
Optional<Transaction> lastTransaction(long accountId, String source, String destination, Double amount);
|
||||
Optional<Transaction> lastTransaction(long accountId, String source, String dest, double amount);
|
||||
|
||||
Collection<Account> listAccounts(long userId);
|
||||
|
||||
|
||||
@@ -311,11 +311,12 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
||||
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);
|
||||
String destination = dst.has(Field.ID) ? dst.get(Field.ID).toString() : dst.has(Field.DISPLAY) ? dst.get(Field.DISPLAY).toString() : null;
|
||||
Double amount = null;
|
||||
if (json.has(Field.AMOUNT) && json.get(Field.AMOUNT) instanceof Number amt) amount = amt.doubleValue();
|
||||
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, destination, amount);
|
||||
var transaction = accountDb.lastTransaction(accountId, source, dest, amount);
|
||||
return transaction.isPresent() ? sendContent(ex,transaction.get()) : notFound(ex);
|
||||
}
|
||||
|
||||
|
||||
@@ -170,42 +170,22 @@ public class SqliteDb extends BaseDb implements AccountDb {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Transaction> lastTransaction(long accountId, String source, String destination, Double amount) {
|
||||
public Optional<Transaction> lastTransaction(long accountId, String source, String dest, double amount) {
|
||||
try {
|
||||
var query = select(ALL).from(TABLE_TRANSACTIONS).where(ACCOUNT,equal(accountId));
|
||||
if (source != null) query = query.where(SOURCE,equal(source));
|
||||
if (destination != null) query = query.where(DESTINATION,equal(destination));
|
||||
if (amount != null) query = query.where(AMOUNT,equal(amount));
|
||||
var rs = query.sort(ID+" DESC").limit(1).exec(db);
|
||||
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 && 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){
|
||||
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));
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.db-journal
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -115,7 +115,7 @@
|
||||
|
||||
function select(index){
|
||||
candidate = candidates[index];
|
||||
disableDropdown();
|
||||
<disableDropdown></disableDropdown>();
|
||||
onSelect(candidate);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
|
||||
import EntryForm from './add_entry_new.svelte';
|
||||
import EntryForm from './add_entry.svelte';
|
||||
import Transaction from './transaction.svelte';
|
||||
|
||||
let { id } = $props();
|
||||
@@ -163,5 +163,5 @@
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<EntryForm {account} {onSave} {users} />
|
||||
<EntryForm {account} {onSave} />
|
||||
{/if}
|
||||
@@ -1,205 +0,0 @@
|
||||
<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')} /> {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>
|
||||
@@ -44,9 +44,9 @@
|
||||
</span>
|
||||
|
||||
<ul>
|
||||
{#each accounts.toSorted((a,b) => a.name.localeCompare(b.name)) as account (account.id)}
|
||||
{#each accounts as account (account.id)}
|
||||
<li>
|
||||
<a {onclick} href="/account/{account.id}">{account.name}</a>
|
||||
<a {onclick} href="/account/{account.id}">{account.name} ({account.id})</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
<legend>{t('state_open')}</legend>
|
||||
{#if sorted}
|
||||
{#each sorted as task}
|
||||
{#if task.status < 60 && task.status >= 20 && match(task)}
|
||||
{#if task.status == 20 && match(task)}
|
||||
<div href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} onmousedown={ontouchstart} onmouseup={ontouchend} >
|
||||
{task.name}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
import { api, drop, get, patch, post } from '../../urls.svelte.js';
|
||||
import { api, drop, patch, post } from '../../urls.svelte.js';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte.js';
|
||||
import { timetrack } from '../../user.svelte.js';
|
||||
@@ -41,18 +41,10 @@
|
||||
});
|
||||
|
||||
async function addTime(task_id){
|
||||
let url = api(`time/track_task/${task_id}`);
|
||||
let resp = await post(url,now()); // create new time or return time with assigned tasks
|
||||
const url = api(`time/track_task/${task_id}`);
|
||||
const resp = await post(url,now()); // create new time or return time with assigned tasks
|
||||
if (resp.ok) {
|
||||
const track = await resp.json();
|
||||
if (timetrack.running){
|
||||
url = api(`time/${timetrack.running.id}`);
|
||||
resp = await get(url);
|
||||
if (resp.ok){
|
||||
let previous = await resp.json();
|
||||
times[previous.id] = previous;
|
||||
}
|
||||
}
|
||||
timetrack.running = track;
|
||||
} else {
|
||||
error(resp);
|
||||
|
||||
@@ -79,15 +79,8 @@ public class TimeModule extends BaseHandler implements TimeService {
|
||||
if (user.isEmpty()) return unauthorized(ex);
|
||||
var head = path.pop();
|
||||
return switch (head) {
|
||||
case STARTED -> getStartedTime(user.get(),ex);
|
||||
case null -> super.doGet(path,ex);
|
||||
default -> {
|
||||
try {
|
||||
yield sendContent(ex,timeDb.load(Long.parseLong(head)));
|
||||
} catch (NumberFormatException ignored) {
|
||||
yield super.doGet(path,ex);
|
||||
}
|
||||
}
|
||||
case STARTED -> getStartedTime(user.get(),ex);
|
||||
case null, default -> super.doGet(path,ex);
|
||||
};
|
||||
} catch (UmbrellaException e){
|
||||
return send(ex,e);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"abort": "abbrechen",
|
||||
"account": "Konto",
|
||||
"accounting": "Konten",
|
||||
"account name": "Konto-Name",
|
||||
"accounts": "Konten",
|
||||
"actions": "Aktionen",
|
||||
"add_login_service": "Login-Service anlegen",
|
||||
@@ -76,7 +75,6 @@
|
||||
"CREATE_USERS": "Nutzer anlegen",
|
||||
"create_pdf": "PDF erzeugen",
|
||||
"created_with": "erzeugt mit {tool} von {producer}",
|
||||
"currency": "Währung",
|
||||
"customer": "Kunde",
|
||||
"customer_address": "Adresse",
|
||||
"customer_email": "Emailadresse des Kunden",
|
||||
@@ -164,7 +162,6 @@
|
||||
"files": "Dateien",
|
||||
"filter": "Filter",
|
||||
"filter by tags": "Nach Tags filtern",
|
||||
"first_transaction": "erste Transaktion",
|
||||
"footer": "Fuß-Text",
|
||||
"foreign_id": "externe Kennung",
|
||||
"forgot_pass" : "Password vergessen?",
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"abort": "abort",
|
||||
"account": "account",
|
||||
"accounting": "accounting",
|
||||
"account name": "Account name",
|
||||
"accounts": "accounts",
|
||||
"actions": "actions",
|
||||
"add_login_service": "add login service",
|
||||
@@ -76,7 +75,6 @@
|
||||
"CREATE_USERS": "create users",
|
||||
"create_pdf": "create PDF",
|
||||
"created_with": "created with {tool} by {producer}",
|
||||
"currency": "Currency",
|
||||
"customer": "customer",
|
||||
"customer_address": "address",
|
||||
"customer_email": "customer email address",
|
||||
@@ -164,7 +162,6 @@
|
||||
"files": "files",
|
||||
"filter": "filter",
|
||||
"filter by tags": "filter by tags",
|
||||
"first_transaction": "first transaction",
|
||||
"footer": "footer",
|
||||
"foreign_id": "external ID",
|
||||
"forgot_pass" : "forgot password?",
|
||||
@@ -462,11 +459,11 @@
|
||||
"wiki_pages": "wiki pages",
|
||||
|
||||
"year": "year",
|
||||
"You can view/edit this transaction at {base_url}/account/{id}": "You can view/edit this transaction at {base_url}/account/{id}",
|
||||
"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"
|
||||
|
||||
Reference in New Issue
Block a user