Compare commits
14 Commits
bugfix/wik
...
accounting
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d840b9f8a | |||
| a933ced8d8 | |||
| d4aaa24aaa | |||
| a5d5d5872d | |||
| 44fb120891 | |||
| c0ab71d71d | |||
| 677d6c9797 | |||
| 153584a031 | |||
| 15f8116430 | |||
| bde0901302 | |||
| 9f5e1e0853 | |||
| ed26f6e46f | |||
| 55dfea65b0 | |||
| c04dfe225c |
5
accounting/build.gradle.kts
Normal file
5
accounting/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
description = "Umbrella : Accounting"
|
||||||
|
|
||||||
|
dependencies{
|
||||||
|
implementation(project(":core"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public interface AccountDb {
|
||||||
|
Collection<Account> listAccounts(long userId);
|
||||||
|
|
||||||
|
Account loadAccount(long accountId);
|
||||||
|
|
||||||
|
List<Transaction> loadTransactions(Account account);
|
||||||
|
|
||||||
|
Account save(Account account);
|
||||||
|
|
||||||
|
Transaction save(Transaction transaction);
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package de.srsoftware.umbrella.accounting;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import de.srsoftware.configuration.Configuration;
|
||||||
|
import de.srsoftware.tools.Path;
|
||||||
|
import de.srsoftware.tools.SessionToken;
|
||||||
|
import de.srsoftware.umbrella.core.BaseHandler;
|
||||||
|
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||||
|
import de.srsoftware.umbrella.core.api.AccountingService;
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
public class AccountingModule extends BaseHandler implements AccountingService {
|
||||||
|
private final AccountDb accountDb;
|
||||||
|
|
||||||
|
public AccountingModule(Configuration config) throws UmbrellaException {
|
||||||
|
super();
|
||||||
|
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE));
|
||||||
|
accountDb = new SqliteDb(connect(dbFile));
|
||||||
|
ModuleRegistry.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doGet(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 null -> getAccounts(user.get(),ex);
|
||||||
|
default -> {
|
||||||
|
try {
|
||||||
|
yield getAccount(user.get(),Long.parseLong(head),ex);
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
yield super.doGet(path,ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (UmbrellaException e){
|
||||||
|
return send(ex,e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
default -> super.doPost(path,ex);
|
||||||
|
};
|
||||||
|
} catch (UmbrellaException e){
|
||||||
|
return send(ex,e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean getAccount(UmbrellaUser user, long accountId, HttpExchange ex) throws IOException {
|
||||||
|
var account = accountDb.loadAccount(accountId);
|
||||||
|
var transactions = accountDb.loadTransactions(account);
|
||||||
|
var userMap = new HashMap<Long,UmbrellaUser>();
|
||||||
|
|
||||||
|
for (var transaction : transactions){
|
||||||
|
var source = transaction.source();
|
||||||
|
if (source.isId()) {
|
||||||
|
var userId = source.id();
|
||||||
|
if (!userMap.containsKey(userId)) userMap.put(source.id(),userService().loadUser(userId));
|
||||||
|
}
|
||||||
|
var destination = transaction.destination();
|
||||||
|
if (destination.isId()) {
|
||||||
|
var userId = destination.id();
|
||||||
|
if (!userMap.containsKey(userId)) userMap.put(destination.id(),userService().loadUser(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendContent(ex, Map.of(
|
||||||
|
Field.ACCOUNT,account.toMap(),
|
||||||
|
Field.TRANSACTIONS,transactions.stream().map(Transaction::toMap).toList(),
|
||||||
|
Field.USER_LIST,mapValues(userMap)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean getAccounts(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||||
|
return sendContent(ex,accountDb.listAccounts(user.id()).stream().map(Account::toMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private boolean postEntry(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||||
|
var json = json(ex);
|
||||||
|
if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT);
|
||||||
|
if (!(json.get(Field.ACCOUNT) instanceof JSONObject acc)) throw invalidField(Field.ACCOUNT,JSON);
|
||||||
|
|
||||||
|
if (!json.has(Field.DATE)) throw missingField(Field.DATE);
|
||||||
|
var date = LocalDate.parse(json.getString(Field.DATE));
|
||||||
|
var dateTime = LocalDateTime.now().withYear(date.getYear()).withMonth(date.getMonthValue()).withDayOfMonth(date.getDayOfMonth());
|
||||||
|
if (!json.has(Field.AMOUNT)) throw missingField(Field.AMOUNT);
|
||||||
|
if (!(json.get(Field.AMOUNT) instanceof Number amount)) throw invalidField(Field.AMOUNT,Text.NUMBER);
|
||||||
|
|
||||||
|
var purpose = json.has(Field.PURPOSE) ? json.getString(Field.PURPOSE) : null;
|
||||||
|
|
||||||
|
if (!json.has(Field.SOURCE)) throw missingField(Field.SOURCE);
|
||||||
|
if (!(json.get(Field.SOURCE) instanceof JSONObject sourceData)) throw invalidField(Field.SOURCE,JSON);
|
||||||
|
if (!json.has(Field.DESTINATION)) throw missingField(Field.DESTINATION);
|
||||||
|
if (!(json.get(Field.DESTINATION) instanceof JSONObject destinationData)) throw invalidField(Field.DESTINATION,JSON);
|
||||||
|
|
||||||
|
IdOrString source = null, destination = null;
|
||||||
|
|
||||||
|
if (sourceData.has(Field.ID)) {
|
||||||
|
if (!(sourceData.get(Field.ID) instanceof Number uid)) throw invalidField(String.join(".",Field.SOURCE,Field.ID),Text.NUMBER);
|
||||||
|
source = IdOrString.of(userService().loadUser(uid.longValue()));
|
||||||
|
} else {
|
||||||
|
if (!sourceData.has(Field.DISPLAY)) throw missingField(String.join(".",Field.SOURCE,Field.DISPLAY));
|
||||||
|
source = IdOrString.of(sourceData.getString(Field.DISPLAY));
|
||||||
|
if (source.value().isBlank()) throw invalidField(String.join(".",Field.SOURCE,Field.DISPLAY),Text.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationData.has(Field.ID)) {
|
||||||
|
if (!(destinationData.get(Field.ID) instanceof Number uid)) throw invalidField(String.join(".",Field.DESTINATION,Field.ID),Text.NUMBER);
|
||||||
|
destination = IdOrString.of(userService().loadUser(uid.longValue()));
|
||||||
|
} else {
|
||||||
|
if (!destinationData.has(Field.DISPLAY)) throw missingField(String.join(".",Field.DESTINATION,Field.DISPLAY));
|
||||||
|
destination = IdOrString.of(destinationData.getString(Field.DISPLAY));
|
||||||
|
if (destination.value().isBlank()) throw invalidField(String.join(".",Field.DESTINATION,Field.DISPLAY),Text.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Long accountId = null;
|
||||||
|
if (acc.has(Field.ID)) {
|
||||||
|
if (!(acc.get(Field.ID) instanceof Number accId)) throw invalidField(String.join(".",Field.ACCOUNT,Field.ID), Text.NUMBER);
|
||||||
|
if (accId.longValue() != 0) accountId = accId.longValue();
|
||||||
|
}
|
||||||
|
Account newAccount = null;
|
||||||
|
if (accountId == null){
|
||||||
|
if (!acc.has(Field.NAME)) throw missingField(String.join(".",Field.ACCOUNT,Field.NAME));
|
||||||
|
var accountName = acc.getString(Field.NAME);
|
||||||
|
if (accountName.isBlank()) throw invalidField(String.join(".",Field.ACCOUNT,Field.NAME),Text.STRING);
|
||||||
|
|
||||||
|
var currency = acc.has(Field.CURRENCY) ? nullIfEmpty(acc.getString(Field.CURRENCY)) : null;
|
||||||
|
|
||||||
|
newAccount = accountDb.save(new Account(0, accountName, currency, user.id()));
|
||||||
|
accountId = newAccount.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
var transaction = accountDb.save(new Transaction(accountId,dateTime,source,destination,amount.doubleValue(),purpose));
|
||||||
|
|
||||||
|
return sendContent(ex,newAccount != null ? newAccount : transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean postSearchSources(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||||
|
var key = body(ex);
|
||||||
|
var users = userService().search(key);
|
||||||
|
// TODO: search known transactions for possible sources
|
||||||
|
return sendContent(ex,mapValues(users));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.srsoftware.umbrella.accounting;
|
||||||
|
|
||||||
|
public class Constants {
|
||||||
|
public static final String CONFIG_DATABASE = "umbrella.modules.accounting.database";
|
||||||
|
|
||||||
|
public static final String TABLE_ACCOUNTS = "accounts";
|
||||||
|
public static final String TABLE_TRANSACTIONS = "transactions";
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package de.srsoftware.umbrella.accounting;
|
||||||
|
|
||||||
|
import de.srsoftware.tools.jdbc.Condition;
|
||||||
|
import de.srsoftware.tools.jdbc.Query;
|
||||||
|
import de.srsoftware.umbrella.core.BaseDb;
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int createTables() {
|
||||||
|
var version = createSettingsTable();
|
||||||
|
switch (version){
|
||||||
|
case 0:
|
||||||
|
createAccountsTable();
|
||||||
|
createTransactionsTable();
|
||||||
|
}
|
||||||
|
return setCurrentVersion(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createAccountsTable() {
|
||||||
|
var sql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS {0} (
|
||||||
|
{1} INTEGER PRIMARY KEY,
|
||||||
|
{2} VARCHAR(255) NOT NULL,
|
||||||
|
{3} LONG NOT NULL,
|
||||||
|
{4} CURRENCY VARCHAR(16)
|
||||||
|
);""";
|
||||||
|
|
||||||
|
try {
|
||||||
|
sql = format(sql,TABLE_ACCOUNTS, Field.ID, Field.NAME, Field.OWNER, Field.CURRENCY);
|
||||||
|
var stmt = db.prepareStatement(sql);
|
||||||
|
stmt.execute();
|
||||||
|
stmt.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw failedToCreateTable(TABLE_ACCOUNTS).causedBy(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTransactionsTable() {
|
||||||
|
var sql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS {0} (
|
||||||
|
{1} LONG NOT NULL,
|
||||||
|
{2} LONG NOT NULL,
|
||||||
|
{3} VARCHAR(255) NOT NULL,
|
||||||
|
{4} VARCHAR(255) NOT NULL,
|
||||||
|
{5} DOUBLE NOT NULL,
|
||||||
|
{6} TEXT
|
||||||
|
);""";
|
||||||
|
try {
|
||||||
|
sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION);
|
||||||
|
var stmt = db.prepareStatement(sql);
|
||||||
|
stmt.execute();
|
||||||
|
stmt.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw failedToCreateTable(TABLE_TRANSACTIONS).causedBy(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HashSet<Account> listAccounts(long userId) {
|
||||||
|
try {
|
||||||
|
var accountIds = new HashSet<Long>();
|
||||||
|
var rs = select("DISTINCT " + Field.ACCOUNT).from(TABLE_TRANSACTIONS).where(Field.SOURCE, equal(userId)).exec(db);
|
||||||
|
while (rs.next()) accountIds.add(rs.getLong(1));
|
||||||
|
rs.close();
|
||||||
|
rs = select("DISTINCT " + Field.ACCOUNT).from(TABLE_TRANSACTIONS).where(Field.DESTINATION, equal(userId)).exec(db);
|
||||||
|
while (rs.next()) accountIds.add(rs.getLong(1));
|
||||||
|
rs.close();
|
||||||
|
var accounts = new HashSet<Account>();
|
||||||
|
rs = select(ALL).from(TABLE_ACCOUNTS).where(Field.ID, Condition.in(accountIds.toArray())).exec(db);
|
||||||
|
while (rs.next()) accounts.add(Account.of(rs));
|
||||||
|
rs.close();
|
||||||
|
return accounts;
|
||||||
|
} catch (SQLException e){
|
||||||
|
throw failedToLoadObject(Text.ACCOUNTING).causedBy(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account loadAccount(long accountId) {
|
||||||
|
try {
|
||||||
|
var rs = select(ALL).from(TABLE_ACCOUNTS).where(Field.ID,equal(accountId)).exec(db);
|
||||||
|
Account account = null;
|
||||||
|
if (rs.next()) account = Account.of(rs);
|
||||||
|
rs.close();
|
||||||
|
if (account==null) throw failedToLoadObject(Text.ACCOUNT,accountId);
|
||||||
|
return account;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Transaction> loadTransactions(Account account) {
|
||||||
|
try {
|
||||||
|
var list = new ArrayList<Transaction>();
|
||||||
|
var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).exec(db);
|
||||||
|
while (rs.next()) list.add(Transaction.of(rs));
|
||||||
|
rs.close();
|
||||||
|
return list;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw failedToLoadMembers(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account save(Account account) {
|
||||||
|
if (account.id() == 0) try { // new account
|
||||||
|
var rs = Query.insertInto(TABLE_ACCOUNTS,Field.NAME, Field.CURRENCY, Field.OWNER).values(account.name(),account.currency(),account.ownerId()).execute(db).getGeneratedKeys();
|
||||||
|
Long newId = null;
|
||||||
|
if (rs.next()) newId = rs.getLong(1);
|
||||||
|
rs.close();
|
||||||
|
if (newId == null) throw failedToStoreObject(account);
|
||||||
|
return account.withId(newId);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw failedToStoreObject(account).causedBy(e);
|
||||||
|
} else {
|
||||||
|
throw notImplemented(this,"save(account)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Transaction save(Transaction transaction) {
|
||||||
|
try {
|
||||||
|
var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC);
|
||||||
|
Query.replaceInto(TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION)
|
||||||
|
.values(transaction.accountId(),timestamp,transaction.source().value(),transaction.destination().value(),transaction.amount(),transaction.purpose())
|
||||||
|
.execute(db).close();
|
||||||
|
return transaction;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw failedToStoreObject(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ application{
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies{
|
dependencies{
|
||||||
|
implementation(project(":accounting"));
|
||||||
implementation(project(":bookmark"));
|
implementation(project(":bookmark"));
|
||||||
implementation(project(":bus"));
|
implementation(project(":bus"));
|
||||||
implementation(project(":company"))
|
implementation(project(":company"))
|
||||||
@@ -47,6 +48,7 @@ tasks.jar {
|
|||||||
.map(::zipTree) // OR .map { zipTree(it) }
|
.map(::zipTree) // OR .map { zipTree(it) }
|
||||||
from(dependencies)
|
from(dependencies)
|
||||||
dependsOn(
|
dependsOn(
|
||||||
|
":accounting:jar",
|
||||||
":bookmark:jar",
|
":bookmark:jar",
|
||||||
":bus:jar",
|
":bus:jar",
|
||||||
":company:jar",
|
":company:jar",
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import static java.lang.System.Logger.Level.INFO;
|
|||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import de.srsoftware.configuration.JsonConfig;
|
import de.srsoftware.configuration.JsonConfig;
|
||||||
import de.srsoftware.tools.ColorLogger;
|
import de.srsoftware.tools.ColorLogger;
|
||||||
|
import de.srsoftware.umbrella.accounting.AccountingModule;
|
||||||
import de.srsoftware.umbrella.bookmarks.BookmarkApi;
|
import de.srsoftware.umbrella.bookmarks.BookmarkApi;
|
||||||
import de.srsoftware.umbrella.company.CompanyModule;
|
import de.srsoftware.umbrella.company.CompanyModule;
|
||||||
import de.srsoftware.umbrella.contact.ContactModule;
|
import de.srsoftware.umbrella.contact.ContactModule;
|
||||||
import de.srsoftware.umbrella.core.SettingsService;
|
import de.srsoftware.umbrella.core.SettingsService;
|
||||||
import de.srsoftware.umbrella.core.Util;
|
import de.srsoftware.umbrella.core.Util;
|
||||||
|
import de.srsoftware.umbrella.core.api.AccountingService;
|
||||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||||
import de.srsoftware.umbrella.documents.DocumentApi;
|
import de.srsoftware.umbrella.documents.DocumentApi;
|
||||||
import de.srsoftware.umbrella.files.FileModule;
|
import de.srsoftware.umbrella.files.FileModule;
|
||||||
@@ -92,6 +94,7 @@ public class Application {
|
|||||||
new WikiModule(config).bindPath("/api/wiki").on(server);
|
new WikiModule(config).bindPath("/api/wiki").on(server);
|
||||||
new FileModule(config).bindPath("/api/files").on(server);
|
new FileModule(config).bindPath("/api/files").on(server);
|
||||||
new SettingsService(config).bindPath("/api/settings").on(server);
|
new SettingsService(config).bindPath("/api/settings").on(server);
|
||||||
|
new AccountingModule(config).bindPath("/api/accounting").on(server);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.log(ERROR,"Startup failed",e);
|
LOG.log(ERROR,"Startup failed",e);
|
||||||
System.exit(-1);
|
System.exit(-1);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package de.srsoftware.umbrella.core;
|
|||||||
import de.srsoftware.umbrella.core.api.*;
|
import de.srsoftware.umbrella.core.api.*;
|
||||||
|
|
||||||
public class ModuleRegistry {
|
public class ModuleRegistry {
|
||||||
|
private AccountingService accountingService;
|
||||||
private BookmarkService bookmarkService;
|
private BookmarkService bookmarkService;
|
||||||
private CompanyService companyService;
|
private CompanyService companyService;
|
||||||
private ContactService contactService;
|
private ContactService contactService;
|
||||||
@@ -29,6 +30,7 @@ public class ModuleRegistry {
|
|||||||
|
|
||||||
public static void add(Object service) {
|
public static void add(Object service) {
|
||||||
switch (service) {
|
switch (service) {
|
||||||
|
case AccountingService as: singleton.accountingService = as; break;
|
||||||
case BookmarkService bs: singleton.bookmarkService = bs; break;
|
case BookmarkService bs: singleton.bookmarkService = bs; break;
|
||||||
case CompanyService cs: singleton.companyService = cs; break;
|
case CompanyService cs: singleton.companyService = cs; break;
|
||||||
case ContactService cs: singleton.contactService = cs; break;
|
case ContactService cs: singleton.contactService = cs; break;
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ public class SettingsService extends BaseHandler {
|
|||||||
entries.add(MenuEntry.of(13,Module.STOCK));
|
entries.add(MenuEntry.of(13,Module.STOCK));
|
||||||
entries.add(MenuEntry.of(14,Module.MESSAGE, MESSAGES));
|
entries.add(MenuEntry.of(14,Module.MESSAGE, MESSAGES));
|
||||||
entries.add(MenuEntry.of(15,Module.POLL,Text.POLLS));
|
entries.add(MenuEntry.of(15,Module.POLL,Text.POLLS));
|
||||||
|
entries.add(MenuEntry.of(16,Module.ACCOUNTING,Text.ACCOUNTING));
|
||||||
|
|
||||||
for (var i=0; i<entries.size(); i++){
|
for (var i=0; i<entries.size(); i++){
|
||||||
var entry = entries.get(i);
|
var entry = entries.get(i);
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package de.srsoftware.umbrella.core.api;
|
||||||
|
|
||||||
|
public interface AccountingService {
|
||||||
|
}
|
||||||
@@ -22,4 +22,5 @@ public interface UserService {
|
|||||||
Optional<UmbrellaUser> loadUser(Optional<Token> sessionToken) throws UmbrellaException;
|
Optional<UmbrellaUser> loadUser(Optional<Token> sessionToken) throws UmbrellaException;
|
||||||
Optional<UmbrellaUser> loadUser(HttpExchange ex) throws UmbrellaException;
|
Optional<UmbrellaUser> loadUser(HttpExchange ex) throws UmbrellaException;
|
||||||
Optional<UmbrellaUser> refreshSession(HttpExchange ex);
|
Optional<UmbrellaUser> refreshSession(HttpExchange ex);
|
||||||
|
Map<Long, ? extends UmbrellaUser> search(String key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.core.constants;
|
|||||||
|
|
||||||
public class Field {
|
public class Field {
|
||||||
public static final String ACTION = "action";
|
public static final String ACTION = "action";
|
||||||
|
public static final String ACCOUNT = "account";
|
||||||
public static final String ADDRESS = "address";
|
public static final String ADDRESS = "address";
|
||||||
public static final String ALLOWED_STATES = "allowed_states";
|
public static final String ALLOWED_STATES = "allowed_states";
|
||||||
public static final String AMOUNT = "amount";
|
public static final String AMOUNT = "amount";
|
||||||
@@ -43,6 +44,8 @@ public class Field {
|
|||||||
public static final String DELIVERY = "delivery";
|
public static final String DELIVERY = "delivery";
|
||||||
public static final String DELIVERY_DATE = "delivery_date";
|
public static final String DELIVERY_DATE = "delivery_date";
|
||||||
public static final String DESCRIPTION = "description";
|
public static final String DESCRIPTION = "description";
|
||||||
|
public static final String DESTINATION = "destination";
|
||||||
|
public static final String DISPLAY = "display";
|
||||||
public static final String DOCUMENT = "document";
|
public static final String DOCUMENT = "document";
|
||||||
public static final String DOCUMENT_ID = "document_id";
|
public static final String DOCUMENT_ID = "document_id";
|
||||||
public static final String DOC_TYPE_ID = "document_type_id";
|
public static final String DOC_TYPE_ID = "document_type_id";
|
||||||
@@ -127,6 +130,7 @@ public class Field {
|
|||||||
public static final String PROJECT = "project";
|
public static final String PROJECT = "project";
|
||||||
public static final String PROJECT_ID = "project_id";
|
public static final String PROJECT_ID = "project_id";
|
||||||
public static final String PROPERTIES = "properties";
|
public static final String PROPERTIES = "properties";
|
||||||
|
public static final String PURPOSE = "purpose";
|
||||||
|
|
||||||
public static final String RECEIVERS = "receivers";
|
public static final String RECEIVERS = "receivers";
|
||||||
public static final String REDIRECT = "redirect";
|
public static final String REDIRECT = "redirect";
|
||||||
@@ -171,6 +175,7 @@ public class Field {
|
|||||||
public static final String TO = "to";
|
public static final String TO = "to";
|
||||||
public static final String TOKEN = "token";
|
public static final String TOKEN = "token";
|
||||||
public static final String TOTAL_PRIO = "total_prio";
|
public static final String TOTAL_PRIO = "total_prio";
|
||||||
|
public static final String TRANSACTIONS = "transactions";
|
||||||
public static final String TYPE = "type";
|
public static final String TYPE = "type";
|
||||||
public static final String TYPE_ID = "type_id";
|
public static final String TYPE_ID = "type_id";
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package de.srsoftware.umbrella.core.constants;
|
package de.srsoftware.umbrella.core.constants;
|
||||||
|
|
||||||
public class Module {
|
public class Module {
|
||||||
|
public static final String ACCOUNTING = "accounting";
|
||||||
public static final String BOOKMARK = "bookmark";
|
public static final String BOOKMARK = "bookmark";
|
||||||
public static final String COMPANY = "company";
|
public static final String COMPANY = "company";
|
||||||
public static final String CONTACT = "contact";
|
public static final String CONTACT = "contact";
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public class Path {
|
|||||||
public static final String COMPANY = "company";
|
public static final String COMPANY = "company";
|
||||||
public static final String CONNECTED = "connected";
|
public static final String CONNECTED = "connected";
|
||||||
|
|
||||||
|
public static final String DESTINATIONS = "destinations";
|
||||||
|
|
||||||
public static final String EVALUATE = "evaluate";
|
public static final String EVALUATE = "evaluate";
|
||||||
|
|
||||||
public static final String ITEM = "item";
|
public static final String ITEM = "item";
|
||||||
@@ -44,6 +46,7 @@ public class Path {
|
|||||||
public static final String SEARCH = "search";
|
public static final String SEARCH = "search";
|
||||||
public static final String SELECT = "select";
|
public static final String SELECT = "select";
|
||||||
public static final String SETTINGS = "settings";
|
public static final String SETTINGS = "settings";
|
||||||
|
public static final String SOURCES = "sources";
|
||||||
public static final String STATES = "states";
|
public static final String STATES = "states";
|
||||||
public static final String STARTED = "started";
|
public static final String STARTED = "started";
|
||||||
public static final String STATE = "state";
|
public static final String STATE = "state";
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ package de.srsoftware.umbrella.core.constants;
|
|||||||
* This is a collection of messages that appear throughout the project
|
* This is a collection of messages that appear throughout the project
|
||||||
*/
|
*/
|
||||||
public class Text {
|
public class Text {
|
||||||
|
public static final String ACCOUNT = "account";
|
||||||
|
public static final String ACCOUNTING = "accounting";
|
||||||
|
|
||||||
public static final String BOOKMARK = "bookmark";
|
public static final String BOOKMARK = "bookmark";
|
||||||
public static final String BOOKMARKS = "bookmarks";
|
public static final String BOOKMARKS = "bookmarks";
|
||||||
public static final String BOOLEAN = "Boolean";
|
public static final String BOOLEAN = "Boolean";
|
||||||
@@ -47,8 +50,8 @@ public class Text {
|
|||||||
public static final String NOTE_WITH_ID = "note ({id})";
|
public static final String NOTE_WITH_ID = "note ({id})";
|
||||||
public static final String NUMBER = "number";
|
public static final String NUMBER = "number";
|
||||||
|
|
||||||
public static final Object OPTION = "option"
|
public static final Object OPTION = "option";
|
||||||
;
|
|
||||||
public static final String PATH = "path";
|
public static final String PATH = "path";
|
||||||
public static final String PERMISSION = "permission";
|
public static final String PERMISSION = "permission";
|
||||||
public static final String POLL = "poll";
|
public static final String POLL = "poll";
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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.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);
|
||||||
|
var name = rs.getString(Field.NAME);
|
||||||
|
var owner = rs.getLong(Field.OWNER);
|
||||||
|
var currency = rs.getString(Field.CURRENCY);
|
||||||
|
return new Account(id,name,currency,owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> toMap() {
|
||||||
|
var owner = userService().loadUser(ownerId);
|
||||||
|
return Map.of(
|
||||||
|
Field.ID, id,
|
||||||
|
Field.NAME, name,
|
||||||
|
Field.CURRENCY, currency,
|
||||||
|
Field.OWNER, owner.toMap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account withId(long newId) {
|
||||||
|
return new Account(newId,name,currency,ownerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public class IdOrString implements Mappable {
|
||||||
|
private final Long id;
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
public IdOrString(String val){
|
||||||
|
this.value = val;
|
||||||
|
this.id = parseOrNull(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IdOrString(long id){
|
||||||
|
this.value = ""+id;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isId(){
|
||||||
|
return id != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IdOrString of(String val){
|
||||||
|
return new IdOrString(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IdOrString of(UmbrellaUser user) {
|
||||||
|
return new IdOrString(user.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long parseOrNull(String val) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(val);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long id(){
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> toMap() {
|
||||||
|
var map = new HashMap<String,Object>();
|
||||||
|
map.put(Field.VALUE,value);
|
||||||
|
if (isId()) map.put(Field.ID, id);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String value(){
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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 {
|
||||||
|
|
||||||
|
|
||||||
|
public static Transaction of(ResultSet rs) throws SQLException {
|
||||||
|
var accountId = rs.getLong(Field.ACCOUNT);
|
||||||
|
var timestamp = rs.getLong(Field.TIMESTAMP);
|
||||||
|
var date = LocalDateTime.ofEpochSecond(timestamp,0, ZoneOffset.UTC);
|
||||||
|
var source = IdOrString.of(rs.getString(Field.SOURCE));
|
||||||
|
var destination = IdOrString.of(rs.getString(Field.DESTINATION));
|
||||||
|
var amount = rs.getDouble(Field.AMOUNT);
|
||||||
|
var purpose = rs.getString(Field.DESCRIPTION);
|
||||||
|
return new Transaction(accountId,date,source,destination,amount,purpose);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> toMap() {
|
||||||
|
return Map.of(
|
||||||
|
Field.ACCOUNT, accountId,
|
||||||
|
Field.DATE, date.toLocalDate(),
|
||||||
|
Field.SOURCE, source.toMap(),
|
||||||
|
Field.DESTINATION, destination.toMap(),
|
||||||
|
Field.AMOUNT, amount,
|
||||||
|
Field.PURPOSE, purpose
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package de.srsoftware.umbrella.core.model;
|
|||||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||||
|
|
||||||
import de.srsoftware.tools.Mappable;
|
import de.srsoftware.tools.Mappable;
|
||||||
|
import de.srsoftware.umbrella.core.api.NamedThing;
|
||||||
import de.srsoftware.umbrella.core.api.Owner;
|
import de.srsoftware.umbrella.core.api.Owner;
|
||||||
import de.srsoftware.umbrella.core.constants.Module;
|
import de.srsoftware.umbrella.core.constants.Module;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
import { loadTranslation } from './translations.svelte';
|
import { loadTranslation } from './translations.svelte';
|
||||||
import { checkUser, user } from './user.svelte';
|
import { checkUser, user } from './user.svelte';
|
||||||
|
|
||||||
|
import Account from "./routes/accounting/account.svelte";
|
||||||
|
import Accounts from "./routes/accounting/index.svelte";
|
||||||
import AddDoc from "./routes/document/Add.svelte";
|
import AddDoc from "./routes/document/Add.svelte";
|
||||||
import AddTask from "./routes/task/Add.svelte";
|
import AddTask from "./routes/task/Add.svelte";
|
||||||
import Bookmark from "./routes/bookmark/View.svelte";
|
import Bookmark from "./routes/bookmark/View.svelte";
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
import Messages from "./routes/message/Messages.svelte";
|
import Messages from "./routes/message/Messages.svelte";
|
||||||
import MsgSettings from "./routes/message/Settings.svelte";
|
import MsgSettings from "./routes/message/Settings.svelte";
|
||||||
import Menu from "./Components/Menu.svelte";
|
import Menu from "./Components/Menu.svelte";
|
||||||
|
import NewAccount from "./routes/accounting/new.svelte";
|
||||||
import NewPage from "./routes/wiki/AddPage.svelte";
|
import NewPage from "./routes/wiki/AddPage.svelte";
|
||||||
import Notes from "./routes/notes/Index.svelte";
|
import Notes from "./routes/notes/Index.svelte";
|
||||||
import PollList from "./routes/poll/Index.svelte";
|
import PollList from "./routes/poll/Index.svelte";
|
||||||
@@ -88,6 +91,9 @@
|
|||||||
<span class="warn">{@html messages.warning}</span>
|
<span class="warn">{@html messages.warning}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<Route path="/" component={User} />
|
<Route path="/" component={User} />
|
||||||
|
<Route path="/account/:id" component={Account} />
|
||||||
|
<Route path="/accounting" component={Accounts} />
|
||||||
|
<Route path="/accounting/new" component={NewAccount} />
|
||||||
<Route path="/bookmark" component={Bookmarks} />
|
<Route path="/bookmark" component={Bookmarks} />
|
||||||
<Route path="/bookmark/:id/view" component={Bookmark} />
|
<Route path="/bookmark/:id/view" component={Bookmark} />
|
||||||
<Route path="/calc" component={Spreadsheet} />
|
<Route path="/calc" component={Spreadsheet} />
|
||||||
|
|||||||
@@ -52,7 +52,6 @@
|
|||||||
candidate = candidates[idx];
|
candidate = candidates[idx];
|
||||||
candidates = [];
|
candidates = [];
|
||||||
selected = [];
|
selected = [];
|
||||||
console.log(candidate);
|
|
||||||
onSelect(candidate);
|
onSelect(candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +92,7 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
candidate = { display : candidate.display };
|
||||||
candidates = await getCandidates(candidate.display);
|
candidates = await getCandidates(candidate.display);
|
||||||
if (selected>candidates.length) selected = candidates.length;
|
if (selected>candidates.length) selected = candidates.length;
|
||||||
return false;
|
return false;
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
span { position : relative }
|
span { position : relative }
|
||||||
select { position : absolute; top: 30px; left: 3px; }
|
select { position : absolute; top: 30px; left: 3px; }
|
||||||
|
|
||||||
select { background: black; color: orange; border: 1px solid orange; border-radius: 5px; }
|
select { background: black; color: orange; border: 1px solid orange; border-radius: 5px; z-index: 50; }
|
||||||
option:checked { background: orange; color: black; }
|
option:checked { background: orange; color: black; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,15 @@
|
|||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { t } from '../translations.svelte';
|
import { t } from '../translations.svelte';
|
||||||
|
|
||||||
let { classes='markdown', markdown=$bindable({source:'',rendered:''}), onclick = null, oncontextmenu = null, title='', wrapper = 'div' } = $props();
|
let {
|
||||||
|
classes='markdown',
|
||||||
|
markdown=$bindable({source:'',rendered:''}),
|
||||||
|
onclick = null,
|
||||||
|
oncontextmenu = e => {},
|
||||||
|
sheet = null,
|
||||||
|
title='',
|
||||||
|
wrapper = 'div'
|
||||||
|
} = $props();
|
||||||
let jspreadsheet = null;
|
let jspreadsheet = null;
|
||||||
const regex = /@startsheet[\s\S]*?@endsheet/g;
|
const regex = /@startsheet[\s\S]*?@endsheet/g;
|
||||||
const number = /^[0-9.-]+$/
|
const number = /^[0-9.-]+$/
|
||||||
@@ -31,16 +39,16 @@
|
|||||||
if (!markdown.rendered) return;
|
if (!markdown.rendered) return;
|
||||||
let sheets = document.getElementsByClassName('spreadsheet');
|
let sheets = document.getElementsByClassName('spreadsheet');
|
||||||
for (let i = 0; i < sheets.length; i++) {
|
for (let i = 0; i < sheets.length; i++) {
|
||||||
let sheet = sheets[i];
|
let current_sheet = sheets[i];
|
||||||
let raw = sheet.innerHTML.trim();
|
let raw = current_sheet.innerHTML.trim();
|
||||||
if (!jspreadsheet) {
|
if (!jspreadsheet) {
|
||||||
sheet.innerHTML = t('Loading spreadsheet library…');
|
current_sheet.innerHTML = t('Loading spreadsheet library…');
|
||||||
let module = await import('jspreadsheet-ce'); // path or package name
|
let module = await import('jspreadsheet-ce'); // path or package name
|
||||||
await import('jspreadsheet-ce/dist/jspreadsheet.css');
|
await import('jspreadsheet-ce/dist/jspreadsheet.css');
|
||||||
jspreadsheet = module.default ?? module;
|
jspreadsheet = module.default ?? module;
|
||||||
}
|
}
|
||||||
if (!jspreadsheet) break; // break loop if library fails to load
|
if (!jspreadsheet) break; // break loop if library fails to load
|
||||||
sheet.innerHTML = t('Processing spreadsheet data…');
|
current_sheet.innerHTML = t('Processing spreadsheet data…');
|
||||||
|
|
||||||
|
|
||||||
// Use parseCSV from the helpers
|
// Use parseCSV from the helpers
|
||||||
@@ -60,14 +68,25 @@
|
|||||||
render: formatCell,
|
render: formatCell,
|
||||||
width:`${len}0px`
|
width:`${len}0px`
|
||||||
}});
|
}});
|
||||||
|
var w = window.innerWidth;
|
||||||
|
if (classes == 'preview') w = w/2;
|
||||||
let config = {
|
let config = {
|
||||||
worksheets : [{
|
worksheets : [{
|
||||||
data:parsed,
|
data:parsed,
|
||||||
columns
|
columns,
|
||||||
|
tableOverflow: true,
|
||||||
|
tableWidth: `${w}px`,
|
||||||
}],
|
}],
|
||||||
onchange : (instance, cell, x, y, value) => update(instance, i)
|
onchange : (instance, cell, x, y, value) => update(instance, i),
|
||||||
|
oneditionstart : (instance, cell, x, y) => oncontextmenu({sheet:current_sheet.id, x,y})
|
||||||
};
|
};
|
||||||
let wb = jspreadsheet(document.getElementById(sheet.id), config);
|
let wb = jspreadsheet(document.getElementById(current_sheet.id), config);
|
||||||
|
if (sheet && sheet.sheet == current_sheet.id) {
|
||||||
|
let cell = wb[0].getCellFromCoords(sheet.x, sheet.y);
|
||||||
|
cell.scrollIntoView({block:'center'});
|
||||||
|
wb[0].updateSelectionFromCoords(sheet.x, sheet.y);
|
||||||
|
wb[0].openEditor(cell);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
let start = 0;
|
let start = 0;
|
||||||
let stored_source = $state(store_id ? localStorage.getItem(store_id) : null);
|
let stored_source = $state(store_id ? localStorage.getItem(store_id) : null);
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
let sheet = null;
|
||||||
|
|
||||||
async function applyEdit(){
|
async function applyEdit(){
|
||||||
let success = await onSet(editValue.source);
|
let success = await onSet(editValue.source);
|
||||||
@@ -80,8 +81,11 @@
|
|||||||
|
|
||||||
|
|
||||||
function oncontextmenu(evt){
|
function oncontextmenu(evt){
|
||||||
|
if (evt.target) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
|
}
|
||||||
|
sheet = evt.sheet ? evt : null; // store position of activated cell to focus after editing starts
|
||||||
startEdit();
|
startEdit();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -96,6 +100,10 @@
|
|||||||
measured(evt, evt.timeStamp - start);
|
measured(evt, evt.timeStamp - start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onresize(evt){
|
||||||
|
console.log('onresize()',evt);
|
||||||
|
}
|
||||||
|
|
||||||
function ontouchstart(evt){
|
function ontouchstart(evt){
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
start = evt.timeStamp;
|
start = evt.timeStamp;
|
||||||
@@ -137,8 +145,8 @@
|
|||||||
{#if stored_source}
|
{#if stored_source}
|
||||||
<span id="restore_markdown" onclick={restore} class="hint">{t('unsaved_content')}</span>
|
<span id="restore_markdown" onclick={restore} class="hint">{t('unsaved_content')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<textarea bind:value={editValue.source} onkeyup={typed} autofocus={!simple}></textarea>
|
<textarea bind:value={editValue.source} onkeyup={typed} onresize={onresize} data="test" autofocus={!simple}></textarea>
|
||||||
<Display classes="preview" bind:markdown={editValue} />
|
<Display classes="preview" bind:markdown={editValue} sheet={sheet} />
|
||||||
{#if !simple}
|
{#if !simple}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button class="cancel" onclick={e => editing = false}>{t('cancel')}</button>
|
<button class="cancel" onclick={e => editing = false}>{t('cancel')}</button>
|
||||||
|
|||||||
73
frontend/src/routes/accounting/account.svelte
Normal file
73
frontend/src/routes/accounting/account.svelte
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { api, get } from '../../urls.svelte';
|
||||||
|
import { error, yikes } from '../../warn.svelte';
|
||||||
|
import { t } from '../../translations.svelte';
|
||||||
|
|
||||||
|
import EntryForm from './add_entry.svelte';
|
||||||
|
|
||||||
|
let { id } = $props();
|
||||||
|
let account = $state(null);
|
||||||
|
let transactions = [];
|
||||||
|
let users = {};
|
||||||
|
|
||||||
|
async function load(){
|
||||||
|
let url = api(`accounting/${id}`);
|
||||||
|
let res = await get(url);
|
||||||
|
if (res.ok) {
|
||||||
|
yikes();
|
||||||
|
let json = await res.json();
|
||||||
|
transactions = json.transactions;
|
||||||
|
users = json.user_list;
|
||||||
|
account = json.account;
|
||||||
|
console.log(users);
|
||||||
|
} else error(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(load);
|
||||||
|
</script>
|
||||||
|
{#if account}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{account.name}</legend>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t('date')}</th>
|
||||||
|
{#each Object.entries(users) as [id,user]}
|
||||||
|
<th>{user.name}</th>
|
||||||
|
{/each}
|
||||||
|
<th>{t('other party')}</th>
|
||||||
|
<th>{t('purpose')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each transactions as transaction, i}
|
||||||
|
<tr>
|
||||||
|
<td>{transaction.date}</td>
|
||||||
|
{#each Object.entries(users) as [id,user]}
|
||||||
|
<td>
|
||||||
|
{#if id == transaction.source.id}
|
||||||
|
{-transaction.amount} {account.currency}
|
||||||
|
{/if}
|
||||||
|
{#if id == transaction.destination.id}
|
||||||
|
{transaction.amount} {account.currency}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
<td>
|
||||||
|
{#if !transaction.source.id}
|
||||||
|
{transaction.source.value}
|
||||||
|
{/if}
|
||||||
|
{#if !transaction.destination.id}
|
||||||
|
{transaction.destination.value}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>{transaction.purpose}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<EntryForm {account} />
|
||||||
|
{/if}
|
||||||
119
frontend/src/routes/accounting/add_entry.svelte
Normal file
119
frontend/src/routes/accounting/add_entry.svelte
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<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';
|
||||||
|
|
||||||
|
let defaultAccount = {
|
||||||
|
id : 0,
|
||||||
|
name : '',
|
||||||
|
currency : ''
|
||||||
|
};
|
||||||
|
let { account = defaultAccount, new_account = false } = $props();
|
||||||
|
|
||||||
|
let entry = $state({
|
||||||
|
account,
|
||||||
|
date : new Date().toISOString().substring(0, 10),
|
||||||
|
source : {
|
||||||
|
display: user.name,
|
||||||
|
id: user.id
|
||||||
|
},
|
||||||
|
destination : {},
|
||||||
|
amount : 0.0,
|
||||||
|
purpose : {}
|
||||||
|
});
|
||||||
|
let router = useTinyRouter();
|
||||||
|
|
||||||
|
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}});
|
||||||
|
} else {
|
||||||
|
error(res);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDestinations(text){
|
||||||
|
var url = api('accounting/destinations');
|
||||||
|
return await getTerminal(text,url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSources(text){
|
||||||
|
var url = api('accounting/sources');
|
||||||
|
return await getTerminal(text,url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(){
|
||||||
|
let data = {
|
||||||
|
...entry,
|
||||||
|
purpose: entry.purpose.display
|
||||||
|
}
|
||||||
|
let url = api('accounting');
|
||||||
|
let res = await post(url, data);
|
||||||
|
if (res.ok) {
|
||||||
|
yikes();
|
||||||
|
router.navigate('/accounting');
|
||||||
|
} 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">
|
||||||
|
{#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" value={entry.date} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>{t('source')}</span>
|
||||||
|
<Autocomplete bind:candidate={entry.source} getCandidates={getSources} />
|
||||||
|
|
||||||
|
<span>{t('destination')}</span>
|
||||||
|
<Autocomplete bind:candidate={entry.destination} getCandidates={getDestinations} />
|
||||||
|
|
||||||
|
<span>{t('amount')}</span>
|
||||||
|
<span>
|
||||||
|
<input type="number" bind:value={entry.amount} /> {entry.account.currency}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>{t('purpose')}</span>
|
||||||
|
<Autocomplete bind:candidate={entry.purpose} />
|
||||||
|
|
||||||
|
<span></span>
|
||||||
|
<span>
|
||||||
|
<button onclick={save}>{t('save')}</button>
|
||||||
|
</span>
|
||||||
|
</fieldset>
|
||||||
51
frontend/src/routes/accounting/index.svelte
Normal file
51
frontend/src/routes/accounting/index.svelte
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { useTinyRouter } from 'svelte-tiny-router';
|
||||||
|
import { t } from '../../translations.svelte';
|
||||||
|
import { api, get } from '../../urls.svelte';
|
||||||
|
import { error, yikes } from '../../warn.svelte';
|
||||||
|
|
||||||
|
const router = useTinyRouter();
|
||||||
|
|
||||||
|
let accounts = [];
|
||||||
|
|
||||||
|
async function load(){
|
||||||
|
let url = api('accounting');
|
||||||
|
let res = await get(url);
|
||||||
|
if (res.ok){
|
||||||
|
yikes();
|
||||||
|
accounts = await res.json();
|
||||||
|
} else error(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function newAccount(){
|
||||||
|
router.navigate('/accounting/new');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onclick(e){
|
||||||
|
e.preventDefault();
|
||||||
|
let href = e.target.getAttribute('href');
|
||||||
|
if (href) router.navigate(href);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
|
||||||
|
<span></span>
|
||||||
|
<span>
|
||||||
|
<button onclick={newAccount}>{t('create_new_object',{'object':t('account')})}</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{#each accounts as account (account.id)}
|
||||||
|
<li>
|
||||||
|
<a {onclick} href="/account/{account.id}">{account.name} ({account.id})</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<legend>{t('accounts')}</legend>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
5
frontend/src/routes/accounting/new.svelte
Normal file
5
frontend/src/routes/accounting/new.svelte
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import EntryForm from './add_entry.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<EntryForm new_account={true} />
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import {api} from '../../urls.svelte'
|
import {api, post} from '../../urls.svelte'
|
||||||
import { error, yikes } from '../../warn.svelte';
|
import { error, yikes } from '../../warn.svelte';
|
||||||
import {t} from '../../translations.svelte';
|
import {t} from '../../translations.svelte';
|
||||||
|
|
||||||
@@ -14,11 +14,7 @@
|
|||||||
|
|
||||||
async function getCandidates(text){
|
async function getCandidates(text){
|
||||||
const url = api('user/search');
|
const url = api('user/search');
|
||||||
const resp = await fetch(url,{
|
const resp = await post(url,text);
|
||||||
credentials : 'include',
|
|
||||||
method : 'POST',
|
|
||||||
body : text
|
|
||||||
});
|
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
yikes();
|
yikes();
|
||||||
const input = await resp.json();
|
const input = await resp.json();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function post(url,data){
|
|||||||
return fetch(url,{
|
return fetch(url,{
|
||||||
credentials : 'include',
|
credentials : 'include',
|
||||||
method : 'POST',
|
method : 'POST',
|
||||||
body : JSON.stringify(data)
|
body : typeof data === 'string' ? data : JSON.stringify(data)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
rootProject.name = "Umbrella25"
|
rootProject.name = "Umbrella25"
|
||||||
|
|
||||||
|
include("accounting")
|
||||||
include("backend")
|
include("backend")
|
||||||
include("bookmark")
|
include("bookmark")
|
||||||
include("bus")
|
include("bus")
|
||||||
@@ -23,4 +24,3 @@ include("translations")
|
|||||||
include("user")
|
include("user")
|
||||||
include("web")
|
include("web")
|
||||||
include("wiki")
|
include("wiki")
|
||||||
|
|
||||||
|
|||||||
@@ -412,8 +412,7 @@ public class UserModule extends BaseHandler implements UserService {
|
|||||||
|
|
||||||
private boolean getUserList(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
|
private boolean getUserList(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
|
||||||
if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(LIST_USERS))) throw forbidden("You are not allowed to list users!");
|
if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(LIST_USERS))) throw forbidden("You are not allowed to list users!");
|
||||||
var list = users.list(0, null,null).values().stream().map(UmbrellaUser::toMap).toList();
|
return sendContent(ex,users.list(0, null,null).values().stream().map(UmbrellaUser::toMap));
|
||||||
return sendContent(ex,list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean impersonate(HttpExchange ex, Long targetId) throws IOException, UmbrellaException {
|
private boolean impersonate(HttpExchange ex, Long targetId) throws IOException, UmbrellaException {
|
||||||
@@ -556,7 +555,7 @@ public class UserModule extends BaseHandler implements UserService {
|
|||||||
var requestingUser = loadUser(ex);
|
var requestingUser = loadUser(ex);
|
||||||
if (!(requestingUser.isPresent() && requestingUser.get() instanceof DbUser dbUser)) return unauthorized(ex);
|
if (!(requestingUser.isPresent() && requestingUser.get() instanceof DbUser dbUser)) return unauthorized(ex);
|
||||||
var key = body(ex);
|
var key = body(ex);
|
||||||
return sendContent(ex,mapValues(users.search(key)));
|
return sendContent(ex,mapValues(search(key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -577,6 +576,11 @@ public class UserModule extends BaseHandler implements UserService {
|
|||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<Long, ? extends UmbrellaUser> search(String key) {
|
||||||
|
return users.search(key);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException {
|
private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException {
|
||||||
var id = user.id();
|
var id = user.id();
|
||||||
var name = json.has(NAME) && json.get(NAME) instanceof String s && !s.isBlank() ? s : user.name();
|
var name = json.has(NAME) && json.get(NAME) instanceof String s && !s.isBlank() ? s : user.name();
|
||||||
|
|||||||
@@ -334,6 +334,10 @@ tr:hover .taglist .tag button {
|
|||||||
border-left: 1px solid #333 !important;
|
border-left: 1px solid #333 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jss_worksheet tr:hover td input{
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
#app nav a{
|
#app nav a{
|
||||||
background: black;
|
background: black;
|
||||||
|
|||||||
@@ -325,6 +325,10 @@ tr:hover .taglist .tag button {
|
|||||||
border-left: 1px solid #333 !important;
|
border-left: 1px solid #333 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jss_worksheet tr:hover td input{
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
#app nav a{
|
#app nav a{
|
||||||
background: black;
|
background: black;
|
||||||
|
|||||||
Reference in New Issue
Block a user