Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f43105d983 | |||
| fbe2380034 | |||
| 84bb908216 | |||
| f80bccfa0c | |||
| 372e371a8f | |||
| 51b2a497cf | |||
| 81b6090d88 | |||
| 1570aefc77 | |||
| 55cad3c854 | |||
| 747b2c1d48 |
@@ -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 destination, Double amount);
|
Optional<Transaction> lastTransaction(long accountId, String source, String dest, double amount);
|
||||||
|
|
||||||
Collection<Account> listAccounts(long userId);
|
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();
|
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);
|
||||||
String destination = dst.has(Field.ID) ? dst.get(Field.ID).toString() : dst.has(Field.DISPLAY) ? dst.get(Field.DISPLAY).toString() : null;
|
var dest = dst.get(dst.has(Field.ID) ? Field.ID : Field.DISPLAY).toString();
|
||||||
Double amount = null;
|
if (!json.has(Field.AMOUNT)) throw missingField(Field.AMOUNT);
|
||||||
if (json.has(Field.AMOUNT) && json.get(Field.AMOUNT) instanceof Number amt) amount = amt.doubleValue();
|
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);
|
return transaction.isPresent() ? sendContent(ex,transaction.get()) : notFound(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,42 +170,22 @@ public class SqliteDb extends BaseDb implements AccountDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 {
|
try {
|
||||||
var query = select(ALL).from(TABLE_TRANSACTIONS).where(ACCOUNT,equal(accountId));
|
var rs = select(ALL).from(TABLE_TRANSACTIONS)
|
||||||
if (source != null) query = query.where(SOURCE,equal(source));
|
.where(ACCOUNT,equal(accountId)).where(SOURCE,equal(source)).where(DESTINATION,equal(dest)).where(AMOUNT,equal(amount))
|
||||||
if (destination != null) query = query.where(DESTINATION,equal(destination));
|
.sort(ID+" DESC")
|
||||||
if (amount != null) query = query.where(AMOUNT,equal(amount));
|
.limit(1)
|
||||||
var rs = query.sort(ID+" DESC").limit(1).exec(db);
|
.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));
|
||||||
|
|||||||
@@ -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){
|
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_new.svelte';
|
import EntryForm from './add_entry.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} {users} />
|
<EntryForm {account} {onSave} />
|
||||||
{/if}
|
{/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>
|
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
<legend>{t('state_open')}</legend>
|
<legend>{t('state_open')}</legend>
|
||||||
{#if sorted}
|
{#if sorted}
|
||||||
{#each sorted as task}
|
{#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} >
|
<div href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} onmousedown={ontouchstart} onmouseup={ontouchend} >
|
||||||
{task.name}
|
{task.name}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user