implementd adding and removal of tags to/from transactions
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -8,6 +8,8 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public interface AccountDb {
|
public interface AccountDb {
|
||||||
|
void dropTransactionTag(long transactionId, String tag);
|
||||||
|
|
||||||
Collection<Account> listAccounts(long userId);
|
Collection<Account> listAccounts(long userId);
|
||||||
|
|
||||||
Account loadAccount(long accountId);
|
Account loadAccount(long accountId);
|
||||||
|
|||||||
@@ -41,6 +41,30 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
|||||||
ModuleRegistry.add(this);
|
ModuleRegistry.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doDelete(Path path, HttpExchange ex) throws IOException {
|
||||||
|
addCors(ex);
|
||||||
|
try {
|
||||||
|
Optional<Token> token = SessionToken.from(ex).map(Token::of);
|
||||||
|
var user = userService().loadUser(token);
|
||||||
|
if (user.isEmpty()) return unauthorized(ex);
|
||||||
|
var head = path.pop();
|
||||||
|
return switch (head) {
|
||||||
|
case TRANSACTION -> {
|
||||||
|
try {
|
||||||
|
var transaction = accountDb.loadTransaction(Long.parseLong(path.pop()));
|
||||||
|
yield dropTransaction(transaction, user.get(), path, ex);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
yield super.doDelete(path,ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case null, default -> super.doDelete(path, ex);
|
||||||
|
};
|
||||||
|
} catch (UmbrellaException e){
|
||||||
|
return send(ex,e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
||||||
addCors(ex);
|
addCors(ex);
|
||||||
@@ -114,6 +138,25 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean dropTransaction(Transaction transaction, UmbrellaUser user, Path path, HttpExchange ex) throws IOException {
|
||||||
|
var head = path.pop();
|
||||||
|
return switch (head){
|
||||||
|
case TAG -> dropTransactionTag(user,transaction,ex);
|
||||||
|
case null, default -> super.doDelete(path,ex);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean dropTransactionTag(UmbrellaUser user, Transaction transaction, HttpExchange ex) throws IOException {
|
||||||
|
LOG.log(WARNING,"Missing permission check in AccountModule.dropTransactionTag!");
|
||||||
|
var json = json(ex);
|
||||||
|
if (!json.has(Field.TAG)) throw missingField(Field.TAG);
|
||||||
|
var tag = json.getString(Field.TAG);
|
||||||
|
if (tag.isBlank()) throw invalidField(Field.TAG,"non-empty");
|
||||||
|
accountDb.dropTransactionTag(transaction.id(),tag);
|
||||||
|
transaction.tags().remove(tag);
|
||||||
|
return sendContent(ex, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean getAccount(UmbrellaUser user, long accountId, HttpExchange ex) throws IOException {
|
private boolean getAccount(UmbrellaUser user, long accountId, HttpExchange ex) throws IOException {
|
||||||
LOG.log(WARNING,"Missing authorization check in AccountingModule.getAccount(…)!");
|
LOG.log(WARNING,"Missing authorization check in AccountingModule.getAccount(…)!");
|
||||||
var account = accountDb.loadAccount(accountId);
|
var account = accountDb.loadAccount(accountId);
|
||||||
@@ -164,6 +207,7 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
|||||||
if (json.has(Field.DESTINATION)) transaction.destination(IdOrString.of(json.getString(Field.DESTINATION)));
|
if (json.has(Field.DESTINATION)) transaction.destination(IdOrString.of(json.getString(Field.DESTINATION)));
|
||||||
if (json.has(Field.PURPOSE)) transaction.purpose(json.getString(Field.PURPOSE));
|
if (json.has(Field.PURPOSE)) transaction.purpose(json.getString(Field.PURPOSE));
|
||||||
if (json.has(Field.SOURCE)) transaction.source(IdOrString.of(json.getString(Field.SOURCE)));
|
if (json.has(Field.SOURCE)) transaction.source(IdOrString.of(json.getString(Field.SOURCE)));
|
||||||
|
if (json.has(Field.TAG)) transaction.tags().add(json.getString(Field.TAG));
|
||||||
return sendContent(ex,accountDb.save(transaction));
|
return sendContent(ex,accountDb.save(transaction));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import static de.srsoftware.tools.jdbc.Condition.*;
|
|||||||
import static de.srsoftware.tools.jdbc.Query.*;
|
import static de.srsoftware.tools.jdbc.Query.*;
|
||||||
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
||||||
import static de.srsoftware.umbrella.accounting.Constants.*;
|
import static de.srsoftware.umbrella.accounting.Constants.*;
|
||||||
import static de.srsoftware.umbrella.core.constants.Field.ID;
|
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||||
import static de.srsoftware.umbrella.core.model.Translatable.t;
|
import static de.srsoftware.umbrella.core.model.Translatable.t;
|
||||||
import static java.text.MessageFormat.format;
|
import static java.text.MessageFormat.format;
|
||||||
@@ -117,6 +117,20 @@ public class SqliteDb extends BaseDb implements AccountDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dropTransactionTag(long transactionId, String tag) {
|
||||||
|
try {
|
||||||
|
var tagIds = new HashSet<Long>();
|
||||||
|
var rs = select(ID).from(TABLE_TAGS).where(TAG, equal(tag)).exec(db);
|
||||||
|
while (rs.next()) tagIds.add(rs.getLong(1));
|
||||||
|
rs.close();
|
||||||
|
|
||||||
|
delete().from(TABLE_TAGS_TRANSACTIONS).where(TRANSACTION_ID, equal(transactionId)).where(TAG_ID, in(tagIds.toArray())).execute(db);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw failedToDropObject(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HashSet<Account> listAccounts(long userId) {
|
public HashSet<Account> listAccounts(long userId) {
|
||||||
try {
|
try {
|
||||||
@@ -169,7 +183,7 @@ public class SqliteDb extends BaseDb implements AccountDb {
|
|||||||
public List<Transaction> loadTransactions(Account account) {
|
public List<Transaction> loadTransactions(Account account) {
|
||||||
try {
|
try {
|
||||||
var transactions = new HashMap<Long,Transaction>();
|
var transactions = new HashMap<Long,Transaction>();
|
||||||
var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).sort(Field.TIMESTAMP).exec(db);
|
var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).exec(db);
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
var transaction = Transaction.of(rs);
|
var transaction = Transaction.of(rs);
|
||||||
transactions.put(transaction.id(),transaction);
|
transactions.put(transaction.id(),transaction);
|
||||||
@@ -182,7 +196,7 @@ public class SqliteDb extends BaseDb implements AccountDb {
|
|||||||
if (transaction != null) transaction.tags().add(rs.getString(Field.TAG));
|
if (transaction != null) transaction.tags().add(rs.getString(Field.TAG));
|
||||||
}
|
}
|
||||||
rs.close();
|
rs.close();
|
||||||
return new ArrayList<>(transactions.values());
|
return transactions.values().stream().sorted(Comparator.comparing(Transaction::date)).toList();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw failedToLoadMembers(account);
|
throw failedToLoadMembers(account);
|
||||||
}
|
}
|
||||||
@@ -231,7 +245,7 @@ public class SqliteDb extends BaseDb implements AccountDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Transaction saveTags(Transaction transaction) {
|
private Transaction saveTags(Transaction transaction) {
|
||||||
var remaining = new HashSet<String>(transaction.tags());
|
var remaining = new HashSet<>(transaction.tags());
|
||||||
var existingTags = new HashMap<String,Long>();
|
var existingTags = new HashMap<String,Long>();
|
||||||
try {
|
try {
|
||||||
var rs = select(ALL).from(TABLE_TAGS).where(Field.TAG,in(transaction.tags().toArray())).exec(db);
|
var rs = select(ALL).from(TABLE_TAGS).where(Field.TAG,in(transaction.tags().toArray())).exec(db);
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public class Path {
|
|||||||
public static final String STATE = "state";
|
public static final String STATE = "state";
|
||||||
public static final String STOP = "stop";
|
public static final String STOP = "stop";
|
||||||
|
|
||||||
|
public static final String TAG = "tag";
|
||||||
public static final String TAGS = "tags";
|
public static final String TAGS = "tags";
|
||||||
public static final String TAGGED = "tagged";
|
public static final String TAGGED = "tagged";
|
||||||
public static final String TRANSACTION = "transaction";
|
public static final String TRANSACTION = "transaction";
|
||||||
|
|||||||
@@ -1,8 +1,57 @@
|
|||||||
<script>
|
<script>
|
||||||
import LineEditor from '../../Components/LineEditor.svelte';
|
import LineEditor from '../../Components/LineEditor.svelte';
|
||||||
import { api, patch } from '../../urls.svelte';
|
import Autocomplete from '../../Components/Autocomplete.svelte';
|
||||||
|
|
||||||
|
import { api, drop, patch, post } from '../../urls.svelte';
|
||||||
import { error, yikes } from '../../warn.svelte';
|
import { error, yikes } from '../../warn.svelte';
|
||||||
let { account, transaction, users } = $props();
|
let { account, addToFilter = tag => {}, transaction, users } = $props();
|
||||||
|
|
||||||
|
async function dropTag(tag){
|
||||||
|
var url = api(`accounting/transaction/${transaction.id}/tag`)
|
||||||
|
var res = await drop(url,{tag});
|
||||||
|
if (res.ok){
|
||||||
|
yikes();
|
||||||
|
transaction.tags = transaction.tags.filter(t => t != tag);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
error(res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCandidates(key){
|
||||||
|
var url = api(`accounting/${account.id}/tags`)
|
||||||
|
var res = await post(url,key);
|
||||||
|
if (res.ok){
|
||||||
|
yikes();
|
||||||
|
const input = await res.json();
|
||||||
|
return Object.values(input).map(mapDisplay);
|
||||||
|
} else {
|
||||||
|
error(res);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDisplay(object){
|
||||||
|
if (object.display){
|
||||||
|
return object;
|
||||||
|
} else if (object.name) {
|
||||||
|
return {...object, display: object.name};
|
||||||
|
} else {
|
||||||
|
return { display : object }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCommit(tag){
|
||||||
|
let url = api(`accounting/transaction/${transaction.id}`);
|
||||||
|
let res = await patch(url,{tag:tag.display});
|
||||||
|
if (res.ok) {
|
||||||
|
yikes();
|
||||||
|
transaction.tags.push(tag.display);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
error(res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function setAmount(amount){
|
async function setAmount(amount){
|
||||||
return await update({amount});
|
return await update({amount});
|
||||||
@@ -28,6 +77,7 @@
|
|||||||
let res = await patch(url,changes);
|
let res = await patch(url,changes);
|
||||||
if (res.ok){
|
if (res.ok){
|
||||||
yikes();
|
yikes();
|
||||||
|
for (let [k,v] of Object.entries(changes)) transaction[k]=v;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
error(res);
|
error(res);
|
||||||
@@ -60,7 +110,14 @@
|
|||||||
<td class="purpose">
|
<td class="purpose">
|
||||||
<LineEditor wrapper="span" editable="true" value={transaction.purpose} onSet={setPurpose} />
|
<LineEditor wrapper="span" editable="true" value={transaction.purpose} onSet={setPurpose} />
|
||||||
</td>
|
</td>
|
||||||
<td class="tags">
|
<td class="taglist">
|
||||||
{transaction.tags.join(', ')}
|
{#each transaction.tags as tag,i}
|
||||||
|
<span class="tag">
|
||||||
|
<span onclick={() => addToFilter(tag)}>{tag}</span> <button onclick={() => dropTag(tag)} class="symbol"></button>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
<span class="tag editor">
|
||||||
|
<Autocomplete {getCandidates} {onCommit} />
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ export function get(url){
|
|||||||
return fetch(url,{ credentials:'include' });
|
return fetch(url,{ credentials:'include' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function drop(url){
|
export function drop(url, payload){
|
||||||
return fetch(url,{
|
let data = {
|
||||||
credentials:'include',
|
credentials:'include',
|
||||||
method:'DELETE'
|
method:'DELETE'
|
||||||
});
|
};
|
||||||
|
if (payload) data['body'] = JSON.stringify(payload);
|
||||||
|
return fetch(url,data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function eventStream(createHandler,updateHandler,deleteHandler){
|
export function eventStream(createHandler,updateHandler,deleteHandler){
|
||||||
@@ -54,4 +56,4 @@ export function target(code){
|
|||||||
}
|
}
|
||||||
|
|
||||||
return altered;
|
return altered;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user