improving usability by proposing tags
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -12,6 +12,8 @@ public interface AccountDb {
|
|||||||
|
|
||||||
Collection<Account> listAccounts(long userId);
|
Collection<Account> listAccounts(long userId);
|
||||||
|
|
||||||
|
Collection<String> listTags(long accountId, String source, String destination);
|
||||||
|
|
||||||
Account loadAccount(long accountId);
|
Account loadAccount(long accountId);
|
||||||
|
|
||||||
Transaction loadTransaction(long transactionId);
|
Transaction loadTransaction(long transactionId);
|
||||||
|
|||||||
@@ -156,6 +156,25 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
|||||||
return sendContent(ex, transaction);
|
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 {
|
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);
|
||||||
@@ -294,20 +313,17 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
|||||||
private boolean postSearchTags(long accountId, UmbrellaUser user, HttpExchange ex) throws IOException {
|
private boolean postSearchTags(long accountId, UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||||
LOG.log(WARNING,"Missing authorization check in AccountingModule.getAccount(…)!");
|
LOG.log(WARNING,"Missing authorization check in AccountingModule.getAccount(…)!");
|
||||||
var key = body(ex);
|
var key = body(ex);
|
||||||
var tags = accountDb.searchTagsContaining(key,accountId);
|
if (!key.trim().startsWith("{")) { // search tags that contain value of body
|
||||||
if (tags.size()<10) tags.addAll(tagService().search(key,user));
|
var tags = accountDb.searchTagsContaining(key, accountId);
|
||||||
return sendContent(ex,egalize(tags,key));
|
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);
|
|
||||||
}
|
}
|
||||||
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 {
|
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
|
@Override
|
||||||
public Account loadAccount(long accountId) {
|
public Account loadAccount(long accountId) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -29,8 +29,35 @@
|
|||||||
});
|
});
|
||||||
let router = useTinyRouter();
|
let router = useTinyRouter();
|
||||||
|
|
||||||
async function getTerminal(text,url){
|
async function dst_selected(destination){
|
||||||
var res = await post(url,text);
|
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){
|
if (res.ok){
|
||||||
yikes();
|
yikes();
|
||||||
const input = await res.json();
|
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) {
|
async function getPurposes(text) {
|
||||||
var url = api('accounting/purposes');
|
const url = api('accounting/purposes');
|
||||||
return await getTerminal(text,url);
|
return await getProposals(text,url);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSources(text){
|
async function getSources(text){
|
||||||
var url = api('accounting/sources');
|
const url = api('accounting/sources');
|
||||||
return await getTerminal(text,url);
|
return await getProposals(text,url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotoTags(purpose){
|
||||||
|
document.getElementById('new_tag_input');
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDisplay(object){
|
function mapDisplay(object){
|
||||||
@@ -131,15 +152,15 @@
|
|||||||
<Autocomplete bind:candidate={entry.source} getCandidates={getSources} id="source-input" />
|
<Autocomplete bind:candidate={entry.source} getCandidates={getSources} id="source-input" />
|
||||||
|
|
||||||
<span>{t('destination')}</span>
|
<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>{t('amount')}</span>
|
||||||
<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>
|
||||||
|
|
||||||
<span>{t('purpose')}</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>
|
<span>{t('tags')}</span>
|
||||||
<Tags getCandidates={getAccountTags} module={null} bind:tags={entry.tags} onEmptyCommit={save} />
|
<Tags getCandidates={getAccountTags} module={null} bind:tags={entry.tags} onEmptyCommit={save} />
|
||||||
|
|||||||
@@ -112,6 +112,6 @@
|
|||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
<span class="tag editor">
|
<span class="tag editor">
|
||||||
<Autocomplete {getCandidates} {onCommit} {onSelect} />
|
<Autocomplete {getCandidates} {onCommit} {onSelect} id="new_tag_input" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user