Merge branch 'accounting' into dev
This commit is contained in:
@@ -12,6 +12,8 @@ public interface AccountDb {
|
||||
|
||||
Collection<Account> listAccounts(long userId);
|
||||
|
||||
Collection<String> listTags(long accountId, String source, String destination);
|
||||
|
||||
Account loadAccount(long accountId);
|
||||
|
||||
Transaction loadTransaction(long transactionId);
|
||||
|
||||
@@ -156,6 +156,25 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
||||
return sendContent(ex, transaction);
|
||||
}
|
||||
|
||||
private List<String> egalize(Set<String> tags, String key) {
|
||||
var result = new HashSet<String>();
|
||||
var lower = key.toLowerCase();
|
||||
var len = key.length();
|
||||
for (var tag : tags) {
|
||||
if (tag.length() == key.length()) continue;
|
||||
result.add(tag.toLowerCase().startsWith(lower) ? key + tag.substring(len) : tag);
|
||||
}
|
||||
return result.stream().sorted(String.CASE_INSENSITIVE_ORDER).toList();
|
||||
}
|
||||
|
||||
private String extractParty(String field, JSONObject json){
|
||||
if (!json.has(field)) return null;
|
||||
if (!(json.get(field) instanceof JSONObject data)) return null;
|
||||
if (data.has(Field.ID)) return data.get(Field.ID).toString();
|
||||
if (data.has(Field.DISPLAY)) return data.get(Field.DISPLAY).toString();
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean getAccount(UmbrellaUser user, long accountId, HttpExchange ex) throws IOException {
|
||||
LOG.log(WARNING,"Missing authorization check in AccountingModule.getAccount(…)!");
|
||||
var account = accountDb.loadAccount(accountId);
|
||||
@@ -294,20 +313,17 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
||||
private boolean postSearchTags(long accountId, UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
LOG.log(WARNING,"Missing authorization check in AccountingModule.getAccount(…)!");
|
||||
var key = body(ex);
|
||||
var tags = accountDb.searchTagsContaining(key,accountId);
|
||||
if (tags.size()<10) tags.addAll(tagService().search(key,user));
|
||||
return sendContent(ex,egalize(tags,key));
|
||||
}
|
||||
|
||||
private List<String> egalize(Set<String> tags, String key) {
|
||||
var result = new HashSet<String>();
|
||||
var lower = key.toLowerCase();
|
||||
var len = key.length();
|
||||
for (var tag : tags) {
|
||||
if (tag.length() == key.length()) continue;
|
||||
result.add(tag.toLowerCase().startsWith(lower) ? key + tag.substring(len) : tag);
|
||||
if (!key.trim().startsWith("{")) { // search tags that contain value of body
|
||||
var tags = accountDb.searchTagsContaining(key, accountId);
|
||||
if (tags.size() < 10) tags.addAll(tagService().search(key, user));
|
||||
return sendContent(ex, egalize(tags, key));
|
||||
}
|
||||
return result.stream().sorted(String.CASE_INSENSITIVE_ORDER).toList();
|
||||
// search tags for account with specified source and destination
|
||||
var json = new JSONObject(key);
|
||||
var src = extractParty(Field.SOURCE, json);
|
||||
var dst = extractParty(Field.DESTINATION, json);
|
||||
var tags = accountDb.listTags(accountId,src,dst);
|
||||
return sendContent(ex,tags);
|
||||
}
|
||||
|
||||
private boolean postToAccount(long accountId, Path path, UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
|
||||
@@ -150,6 +150,45 @@ public class SqliteDb extends BaseDb implements AccountDb {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> listTags(long accountId, String source, String destination) {
|
||||
try {
|
||||
var rs = select(TRANSACTION_ID,Field.TAG)
|
||||
.from(TABLE_TRANSACTIONS)
|
||||
.leftJoin(ID,TABLE_TAGS_TRANSACTIONS, TRANSACTION_ID)
|
||||
.leftJoin(TAG_ID,TABLE_TAGS,ID)
|
||||
.where(ACCOUNT,equal(accountId))
|
||||
.where(SOURCE,equal(source))
|
||||
.where(DESTINATION,equal(destination))
|
||||
.sort(TRANSACTION_ID).exec(db);
|
||||
Set<String> set = null;
|
||||
Set<String> transactionTags = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
||||
Long lastTransaction = null;
|
||||
while (rs.next()){
|
||||
var currentTransaction = rs.getLong(TRANSACTION_ID);
|
||||
if (lastTransaction == null) { // first row
|
||||
transactionTags.add(rs.getString(TAG));
|
||||
lastTransaction = currentTransaction;
|
||||
} else if (lastTransaction == currentTransaction) {
|
||||
transactionTags.add(rs.getString(TAG));
|
||||
} else {
|
||||
if (set == null) {
|
||||
set = transactionTags;
|
||||
} else {
|
||||
set.retainAll(transactionTags);
|
||||
}
|
||||
transactionTags = new HashSet<>();
|
||||
transactionTags.add(rs.getString(TAG));
|
||||
lastTransaction = currentTransaction;
|
||||
}
|
||||
}
|
||||
rs.close();
|
||||
return set;
|
||||
} catch (SQLException e){
|
||||
throw failedToLoadMembers(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account loadAccount(long accountId) {
|
||||
try {
|
||||
|
||||
@@ -29,8 +29,35 @@
|
||||
});
|
||||
let router = useTinyRouter();
|
||||
|
||||
async function getTerminal(text,url){
|
||||
var res = await post(url,text);
|
||||
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();
|
||||
entry.tags = json;
|
||||
} else error(res);
|
||||
}
|
||||
|
||||
function focusOnEnter(ev,id){
|
||||
if (ev.key == 'Enter') 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();
|
||||
@@ -41,25 +68,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function getAccountTags(text){
|
||||
if (!text) return [];
|
||||
var url = api(`accounting/${entry.account.id}/tags`)
|
||||
return await getTerminal(text,url);
|
||||
}
|
||||
|
||||
async function getDestinations(text){
|
||||
var url = api('accounting/destinations');
|
||||
return await getTerminal(text,url);
|
||||
}
|
||||
|
||||
async function getPurposes(text) {
|
||||
var url = api('accounting/purposes');
|
||||
return await getTerminal(text,url);
|
||||
const url = api('accounting/purposes');
|
||||
return await getProposals(text,url);
|
||||
}
|
||||
|
||||
async function getSources(text){
|
||||
var url = api('accounting/sources');
|
||||
return await getTerminal(text,url);
|
||||
const url = api('accounting/sources');
|
||||
return await getProposals(text,url);
|
||||
}
|
||||
|
||||
function gotoTags(purpose){
|
||||
document.getElementById('new_tag_input');
|
||||
}
|
||||
|
||||
function mapDisplay(object){
|
||||
@@ -131,15 +152,15 @@
|
||||
<Autocomplete bind:candidate={entry.source} getCandidates={getSources} id="source-input" />
|
||||
|
||||
<span>{t('destination')}</span>
|
||||
<Autocomplete bind:candidate={entry.destination} getCandidates={getDestinations} />
|
||||
<Autocomplete bind:candidate={entry.destination} getCandidates={getDestinations} onSelect={dst_selected} />
|
||||
|
||||
<span>{t('amount')}</span>
|
||||
<span>
|
||||
<input type="number" bind:value={entry.amount} /> {entry.account.currency}
|
||||
<input type="number" bind:value={entry.amount} onkeyup={e => focusOnEnter(e,'purpose_input')} /> {entry.account.currency}
|
||||
</span>
|
||||
|
||||
<span>{t('purpose')}</span>
|
||||
<Autocomplete bind:candidate={entry.purpose} getCandidates={getPurposes} />
|
||||
<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} />
|
||||
|
||||
@@ -112,6 +112,6 @@
|
||||
</span>
|
||||
{/each}
|
||||
<span class="tag editor">
|
||||
<Autocomplete {getCandidates} {onCommit} {onSelect} />
|
||||
<Autocomplete {getCandidates} {onCommit} {onSelect} id="new_tag_input" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user