Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd32779f9f | ||
|
|
1b2cef5bd8 | ||
|
|
de0d9c1797 | ||
|
|
db66097253 | ||
|
|
92faa364c9 | ||
|
|
66bd0e3741 | ||
|
|
c61a2b14b6 | ||
|
|
57bce84e8e | ||
|
|
6e9d32f105 | ||
|
|
6d9e368e43 | ||
|
|
b5b361514d | ||
|
|
167365d0b3 | ||
|
|
95bd48df82 | ||
|
|
06ee1adf27 |
@@ -16,7 +16,7 @@ public interface AccountDb {
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -311,12 +311,11 @@ 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);
|
||||
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();
|
||||
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 transaction = accountDb.lastTransaction(accountId, source, dest, amount);
|
||||
var transaction = accountDb.lastTransaction(accountId, source, destination, amount);
|
||||
return transaction.isPresent() ? sendContent(ex,transaction.get()) : notFound(ex);
|
||||
}
|
||||
|
||||
|
||||
@@ -170,22 +170,42 @@ public class SqliteDb extends BaseDb implements AccountDb {
|
||||
}
|
||||
|
||||
@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 {
|
||||
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);
|
||||
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);
|
||||
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.svelte';
|
||||
import EntryForm from './add_entry_new.svelte';
|
||||
import Transaction from './transaction.svelte';
|
||||
|
||||
let { id } = $props();
|
||||
@@ -173,5 +173,5 @@
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<EntryForm {account} {onSave} />
|
||||
<EntryForm {account} {onSave} {users} />
|
||||
{/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')} /> {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>
|
||||
@@ -63,9 +63,13 @@
|
||||
if (resp.ok){
|
||||
parent_task = await resp.json();
|
||||
task.parent_task_id = +parent_task_id;
|
||||
for (let [uid, member] of Object.entries(parent_task.members)){
|
||||
if (['OWNER','ASSIGNEE'].includes(member.permission.name)) member.permission = { name: 'EDIT', code: 2 };
|
||||
task.members[uid] = member;
|
||||
}
|
||||
project_id = +parent_task.project_id;
|
||||
yikes();
|
||||
project = null; // TODO
|
||||
project = null;
|
||||
} else error(resp);
|
||||
}
|
||||
|
||||
@@ -75,11 +79,17 @@
|
||||
if (resp.ok){
|
||||
project = await resp.json();
|
||||
task.project_id = +project_id;
|
||||
if (Object.keys(task.members).length < 1) {
|
||||
for (let [uid, member] of Object.entries(project.members)){
|
||||
if (['OWNER','ASSIGNEE'].includes(member.permission.name)) member.permission = { name: 'EDIT', code: 2 };
|
||||
task.members[uid] = member;
|
||||
}
|
||||
}
|
||||
if (assignee && project.members[assignee]){
|
||||
task.members[assignee] = project.members[assignee];
|
||||
task.members[assignee].permission = { name : "ASSIGNEE", code : 3 }
|
||||
}
|
||||
if (task.taks.length < 1) task.tags = project.tags;
|
||||
if (task.tags.length < 1) task.tags = project.tags;
|
||||
yikes();
|
||||
} else {
|
||||
error(resp);
|
||||
@@ -124,6 +134,10 @@
|
||||
extendedSettings = !extendedSettings;
|
||||
}
|
||||
|
||||
function updatePermission(uid,perm){
|
||||
task.members[uid].permission = perm;
|
||||
}
|
||||
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
@@ -161,7 +175,7 @@
|
||||
{#if extendedSettings}
|
||||
<div>{t('members')}</div>
|
||||
<div>
|
||||
<PermissionEditor members={task.members} {addMember} {dropMember} {getCandidates} />
|
||||
<PermissionEditor members={task.members} {addMember} {dropMember} {getCandidates} {updatePermission} />
|
||||
</div>
|
||||
|
||||
<div>{t('estimated_time')}</div>
|
||||
|
||||
@@ -386,40 +386,33 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
long projectId = pid.longValue();
|
||||
var project = projectService().load(projectId);
|
||||
projectService().loadMembers(List.of(project));
|
||||
var members = project.members();
|
||||
var member = members.get(user.id());
|
||||
var parentMembers = project.members();
|
||||
var member = parentMembers.get(user.id());
|
||||
if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not allowed to create new tasks in this project");
|
||||
|
||||
var parentTask = json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number par ? taskService().load(Set.of(par.longValue())).get(par.longValue()) : null;
|
||||
if (parentTask != null) {
|
||||
taskService().loadMembers(parentTask);
|
||||
members = parentTask.members();
|
||||
member = members.get(user.id());
|
||||
parentMembers = parentTask.members();
|
||||
member = parentMembers.get(user.id());
|
||||
if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not allowed to add sub-stasks to {object}", OBJECT, parentTask.name());
|
||||
}
|
||||
|
||||
var newMembers = new HashMap<Long, Permission>();
|
||||
for (var mem : members.values()) { // Assign members from project or parent task
|
||||
var permission = mem.permission() == OWNER ? EDIT : mem.permission();
|
||||
newMembers.put(mem.user().id(), permission);
|
||||
}
|
||||
|
||||
if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject mems) {
|
||||
// check of assignee has been set by client
|
||||
for (var k : mems.keySet()) {
|
||||
try {
|
||||
var userId = Long.parseLong(k);
|
||||
var permName = mems.getJSONObject(k).getJSONObject(PERMISSION).getString(NAME);
|
||||
if (Permission.valueOf(permName) == ASSIGNEE) newMembers.put(userId, ASSIGNEE);
|
||||
} catch (Exception ignored) {
|
||||
LOG.log(WARNING, "Failed to parse {0}", mems.get(k));
|
||||
}
|
||||
if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject members) {
|
||||
for (var key : members.keySet()){
|
||||
var code = members.getJSONObject(key).getJSONObject(PERMISSION).getInt(CODE);
|
||||
var perm = Permission.of(code);
|
||||
if (perm == OWNER) perm = EDIT;
|
||||
var userId = Long.parseLong(key);
|
||||
if (!parentMembers.containsKey(userId)) throw forbidden("{user} is not a member of {path}",Field.USER,userService().loadUser(userId).name(),Field.PATH, parentTask == null ? project.name() : parentTask.name());
|
||||
newMembers.put(userId,perm);
|
||||
}
|
||||
}
|
||||
|
||||
// set ownership to current user
|
||||
newMembers.put(user.id(), OWNER);
|
||||
|
||||
|
||||
json.put(MEMBERS, Map.of()); // reset member map for task-to-be-created
|
||||
Task task = Task.of(json);
|
||||
if (parentTask != null && parentTask.dueDate() != null && task.dueDate() == null) task.dueDate(parentTask.dueDate());
|
||||
|
||||
Reference in New Issue
Block a user