working on autocomplete fields
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.accounting;
|
||||
|
||||
import de.srsoftware.umbrella.core.model.Account;
|
||||
import de.srsoftware.umbrella.core.model.Transaction;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface AccountDb {
|
||||
Collection<Account> listAccounts(long userId);
|
||||
@@ -16,4 +17,6 @@ public interface AccountDb {
|
||||
Account save(Account account);
|
||||
|
||||
Transaction save(Transaction transaction);
|
||||
|
||||
Set<String> searchField(long userId, String field, String key);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.accounting;
|
||||
|
||||
import static de.srsoftware.tools.Optionals.nullIfEmpty;
|
||||
import static de.srsoftware.umbrella.accounting.Constants.CONFIG_DATABASE;
|
||||
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
|
||||
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
|
||||
import static de.srsoftware.umbrella.core.Util.mapValues;
|
||||
import static de.srsoftware.umbrella.core.constants.Path.*;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.configuration.Configuration;
|
||||
import de.srsoftware.tools.Path;
|
||||
@@ -11,26 +21,14 @@ import de.srsoftware.umbrella.core.constants.Field;
|
||||
import de.srsoftware.umbrella.core.constants.Text;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.*;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import static de.srsoftware.umbrella.core.constants.Path.SOURCES;
|
||||
import static de.srsoftware.umbrella.core.constants.Path.DESTINATIONS;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.srsoftware.tools.Optionals.nullIfEmpty;
|
||||
import static de.srsoftware.umbrella.accounting.Constants.CONFIG_DATABASE;
|
||||
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
|
||||
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
|
||||
import static de.srsoftware.umbrella.core.Util.mapValues;
|
||||
import static de.srsoftware.umbrella.core.constants.Path.JSON;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class AccountingModule extends BaseHandler implements AccountingService {
|
||||
private final AccountDb accountDb;
|
||||
@@ -68,14 +66,14 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
||||
public boolean doPost(Path path, HttpExchange ex) throws IOException {
|
||||
addCors(ex);
|
||||
try {
|
||||
Optional<Token> token = SessionToken.from(ex).map(Token::of);
|
||||
var user = userService().loadUser(token);
|
||||
var user = userService().refreshSession(ex);
|
||||
if (user.isEmpty()) return unauthorized(ex);
|
||||
var head = path.pop();
|
||||
return switch (head) {
|
||||
case null -> postEntry(user.get(),ex);
|
||||
case SOURCES -> postSearchSources(user.get(),ex);
|
||||
case DESTINATIONS -> postSearchDestinations(user.get(),ex);
|
||||
case PURPOSES -> postSearchPurposes(user.get(),ex);
|
||||
default -> super.doPost(path,ex);
|
||||
};
|
||||
} catch (UmbrellaException e){
|
||||
@@ -114,6 +112,15 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
||||
|
||||
|
||||
|
||||
private static boolean noNumbers(String s){
|
||||
try {
|
||||
Long.parseLong(s);
|
||||
return false;
|
||||
} catch (NumberFormatException e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean postEntry(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
var json = json(ex);
|
||||
if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT);
|
||||
@@ -176,16 +183,26 @@ public class AccountingModule extends BaseHandler implements AccountingService {
|
||||
}
|
||||
|
||||
public boolean postSearchDestinations(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
var key = body(ex);
|
||||
var users = userService().search(key);
|
||||
// TODO: search known transactions for possible destinations
|
||||
return sendContent(ex,mapValues(users));
|
||||
return sendContent(ex,searchOptions(user, Field.DESTINATION, body(ex)));
|
||||
}
|
||||
|
||||
public boolean postSearchSources(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
public boolean postSearchPurposes(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
var key = body(ex);
|
||||
var purposes = accountDb.searchField(user.id(),Field.DESCRIPTION,key);
|
||||
return sendContent(ex,purposes);
|
||||
}
|
||||
|
||||
|
||||
public boolean postSearchSources(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
return sendContent(ex,searchOptions(user, Field.SOURCE, body(ex)));
|
||||
}
|
||||
|
||||
public ArrayList<Map<String,?>> searchOptions(UmbrellaUser user, String field, String key){
|
||||
var users = userService().search(key);
|
||||
// TODO: search known transactions for possible sources
|
||||
return sendContent(ex,mapValues(users));
|
||||
var destinations = accountDb.searchField(user.id(), field, key);
|
||||
var optionList = new ArrayList<Map<String,?>>();
|
||||
users.values().stream().map(UmbrellaUser::toMap).forEach(optionList::add);
|
||||
destinations.stream().filter(AccountingModule::noNumbers).map(s -> Map.of(Field.DISPLAY,s)).forEach(optionList::add);
|
||||
return optionList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.accounting;
|
||||
|
||||
public class Constants {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.accounting;
|
||||
|
||||
import static de.srsoftware.tools.NotImplemented.notImplemented;
|
||||
import static de.srsoftware.tools.jdbc.Condition.*;
|
||||
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
||||
import static de.srsoftware.tools.jdbc.Query.select;
|
||||
import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS;
|
||||
import static de.srsoftware.umbrella.accounting.Constants.TABLE_TRANSACTIONS;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import de.srsoftware.tools.jdbc.Condition;
|
||||
import de.srsoftware.tools.jdbc.Query;
|
||||
import de.srsoftware.umbrella.core.BaseDb;
|
||||
@@ -7,7 +17,6 @@ import de.srsoftware.umbrella.core.constants.Field;
|
||||
import de.srsoftware.umbrella.core.constants.Text;
|
||||
import de.srsoftware.umbrella.core.model.Account;
|
||||
import de.srsoftware.umbrella.core.model.Transaction;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.time.ZoneOffset;
|
||||
@@ -15,15 +24,6 @@ import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static de.srsoftware.tools.NotImplemented.notImplemented;
|
||||
import static de.srsoftware.tools.jdbc.Condition.equal;
|
||||
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
||||
import static de.srsoftware.tools.jdbc.Query.select;
|
||||
import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS;
|
||||
import static de.srsoftware.umbrella.accounting.Constants.TABLE_TRANSACTIONS;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
public class SqliteDb extends BaseDb implements AccountDb {
|
||||
public SqliteDb(Connection connection) {
|
||||
super(connection);
|
||||
@@ -154,4 +154,19 @@ public class SqliteDb extends BaseDb implements AccountDb {
|
||||
throw failedToStoreObject(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashSet<String> searchField(long userId, String field , String key) {
|
||||
var accounts = listAccounts(userId);
|
||||
var accountIds = accounts.stream().map(Account::id).toArray();
|
||||
var destinations = new HashSet<String>();
|
||||
try {
|
||||
var rs = Query.select("DISTINCT "+field).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,in(accountIds)).where(field,like("%"+key+"%")).exec(db);
|
||||
while (rs.next()) destinations.add(rs.getString(1));
|
||||
rs.close();
|
||||
return destinations;
|
||||
} catch (SQLException e) {
|
||||
throw failedToReadFromTable(field,TABLE_TRANSACTIONS).causedBy(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import de.srsoftware.umbrella.company.CompanyModule;
|
||||
import de.srsoftware.umbrella.contact.ContactModule;
|
||||
import de.srsoftware.umbrella.core.SettingsService;
|
||||
import de.srsoftware.umbrella.core.Util;
|
||||
import de.srsoftware.umbrella.core.api.AccountingService;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.documents.DocumentApi;
|
||||
import de.srsoftware.umbrella.files.FileModule;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core.api;
|
||||
|
||||
public interface AccountingService {
|
||||
|
||||
@@ -4,7 +4,6 @@ package de.srsoftware.umbrella.core.api;
|
||||
|
||||
import de.srsoftware.umbrella.core.model.DbLocation;
|
||||
import de.srsoftware.umbrella.core.model.Item;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface StockService {
|
||||
|
||||
@@ -39,6 +39,7 @@ public class Path {
|
||||
public static final String PROJECT = "project";
|
||||
public static final String PROPERTIES = "properties";
|
||||
public static final String PROPERTY = "property";
|
||||
public static final String PURPOSES = "purposes";
|
||||
|
||||
public static final String READ = "read";
|
||||
public static final String REDIRECT = "redirect";
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core.model;
|
||||
|
||||
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
|
||||
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
|
||||
|
||||
public record Account(long id, String name, String currency, long ownerId) implements Mappable {
|
||||
public static Account of(ResultSet rs) throws SQLException {
|
||||
var id = rs.getLong(Field.ID);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core.model;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core.model;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public record Transaction(long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose) implements Mappable {
|
||||
|
||||
@@ -5,7 +5,6 @@ package de.srsoftware.umbrella.core.model;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import de.srsoftware.umbrella.core.api.NamedThing;
|
||||
import de.srsoftware.umbrella.core.api.Owner;
|
||||
import de.srsoftware.umbrella.core.constants.Module;
|
||||
import java.util.HashMap;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
|
||||
let {
|
||||
id = null,
|
||||
autofocus = false,
|
||||
getCandidates = dummyGetCandidates,
|
||||
onCommit = dummyOnCommit,
|
||||
@@ -108,7 +109,7 @@
|
||||
</style>
|
||||
|
||||
<span>
|
||||
<input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} />
|
||||
<input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} {id} />
|
||||
{#if candidates && candidates.length > 0}
|
||||
<select bind:value={selected} {ondblclick} multiple tabindex="-1">
|
||||
{#each candidates as candidate,i}
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
|
||||
let { id } = $props();
|
||||
let account = $state(null);
|
||||
let transactions = [];
|
||||
let transactions = $state([]);
|
||||
let users = {};
|
||||
|
||||
let sums = {};
|
||||
|
||||
async function load(){
|
||||
let url = api(`accounting/${id}`);
|
||||
let res = await get(url);
|
||||
@@ -20,10 +22,13 @@
|
||||
transactions = json.transactions;
|
||||
users = json.user_list;
|
||||
account = json.account;
|
||||
console.log(users);
|
||||
} else error(res);
|
||||
}
|
||||
|
||||
function onSave(){
|
||||
load();
|
||||
}
|
||||
|
||||
onMount(load);
|
||||
</script>
|
||||
{#if account}
|
||||
@@ -47,27 +52,47 @@
|
||||
{#each Object.entries(users) as [id,user]}
|
||||
<td>
|
||||
{#if id == transaction.source.id}
|
||||
{sums[id] = -transaction.amount + (sums[id]?sums[id]:0)}
|
||||
{-transaction.amount} {account.currency}
|
||||
{/if}
|
||||
{#if id == transaction.destination.id}
|
||||
{sums[id] = transaction.amount + (sums[id]?sums[id]:0)}
|
||||
{transaction.amount} {account.currency}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
<td>
|
||||
{#if !transaction.source.id}
|
||||
{transaction.source.value}
|
||||
{sums[0] = -transaction.amount + (sums[0]?sums[0]:0)}
|
||||
← {transaction.source.value}
|
||||
{/if}
|
||||
{#if !transaction.destination.id}
|
||||
{transaction.destination.value}
|
||||
{sums[0] = transaction.amount + (sums[0]?sums[0]:0)}
|
||||
→ {transaction.destination.value}
|
||||
{/if}
|
||||
</td>
|
||||
<td>{transaction.purpose}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
<tr>
|
||||
<td>
|
||||
<br/>
|
||||
{t('sums')}
|
||||
</td>
|
||||
{#each Object.entries(users) as [id,user]}
|
||||
<th>
|
||||
{user.name}<br/>
|
||||
{sums[id]} {account.currency}
|
||||
</th>
|
||||
{/each}
|
||||
<td>
|
||||
<br/>
|
||||
{sums[0]} {account.currency}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<EntryForm {account} />
|
||||
<EntryForm {account} {onSave} />
|
||||
{/if}
|
||||
@@ -12,7 +12,7 @@
|
||||
name : '',
|
||||
currency : ''
|
||||
};
|
||||
let { account = defaultAccount, new_account = false } = $props();
|
||||
let { account = defaultAccount, new_account = false, onSave = () => {} } = $props();
|
||||
|
||||
let entry = $state({
|
||||
account,
|
||||
@@ -27,12 +27,22 @@
|
||||
});
|
||||
let router = useTinyRouter();
|
||||
|
||||
function mapDisplay(object){
|
||||
if (object.display){
|
||||
return object;
|
||||
} else if (object.name) {
|
||||
return {...object, display: object.name};
|
||||
} else {
|
||||
return { display : object }
|
||||
}
|
||||
}
|
||||
|
||||
async function getTerminal(text,url){
|
||||
var res = await post(url,text);
|
||||
if (res.ok){
|
||||
yikes();
|
||||
const input = await res.json();
|
||||
return Object.values(input).map(user => { return {...user, display: user.name}});
|
||||
return Object.values(input).map(mapDisplay);
|
||||
} else {
|
||||
error(res);
|
||||
return {};
|
||||
@@ -44,6 +54,11 @@
|
||||
return await getTerminal(text,url);
|
||||
}
|
||||
|
||||
async function getPurposes(text) {
|
||||
var url = api('accounting/purposes');
|
||||
return await getTerminal(text,url);
|
||||
}
|
||||
|
||||
async function getSources(text){
|
||||
var url = api('accounting/sources');
|
||||
return await getTerminal(text,url);
|
||||
@@ -58,7 +73,8 @@
|
||||
let res = await post(url, data);
|
||||
if (res.ok) {
|
||||
yikes();
|
||||
router.navigate('/accounting');
|
||||
onSave();
|
||||
document.getElementById('date-input').focus();
|
||||
} else error(res);
|
||||
}
|
||||
</script>
|
||||
@@ -74,6 +90,8 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="warn">TODO: Tagging von Umsätzen</span>
|
||||
|
||||
<fieldset class="grid2">
|
||||
{#if new_account}
|
||||
<legend>{t('create_new_object',{object:t('account')})}</legend>
|
||||
@@ -95,11 +113,11 @@
|
||||
|
||||
<span>{t('date')}</span>
|
||||
<span>
|
||||
<input type="date" value={entry.date} />
|
||||
<input type="date" bind:value={entry.date} id="date-input" />
|
||||
</span>
|
||||
|
||||
<span>{t('source')}</span>
|
||||
<Autocomplete bind:candidate={entry.source} getCandidates={getSources} />
|
||||
<Autocomplete bind:candidate={entry.source} getCandidates={getSources} id="source-input" />
|
||||
|
||||
<span>{t('destination')}</span>
|
||||
<Autocomplete bind:candidate={entry.destination} getCandidates={getDestinations} />
|
||||
@@ -110,10 +128,10 @@
|
||||
</span>
|
||||
|
||||
<span>{t('purpose')}</span>
|
||||
<Autocomplete bind:candidate={entry.purpose} />
|
||||
<Autocomplete bind:candidate={entry.purpose} getCandidates={getPurposes} />
|
||||
|
||||
<span></span>
|
||||
<span>
|
||||
<button onclick={save}>{t('save')}</button>
|
||||
</span>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
Reference in New Issue
Block a user