Compare commits
10 Commits
improvemen
...
accounting
| Author | SHA1 | Date | |
|---|---|---|---|
| 677d6c9797 | |||
| 153584a031 | |||
| 15f8116430 | |||
| bde0901302 | |||
| 9f5e1e0853 | |||
| ed26f6e46f | |||
| 55dfea65b0 | |||
| c04dfe225c | |||
| eb4a983d11 | |||
| d7c32ef69a |
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,7 @@
|
||||
package de.srsoftware.umbrella.accounting;
|
||||
|
||||
import de.srsoftware.umbrella.core.model.Account;
|
||||
|
||||
public interface AccountDb {
|
||||
Account save(Account account);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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.Account;
|
||||
import de.srsoftware.umbrella.core.model.Token;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
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.constants.Path.JSON;
|
||||
import static de.srsoftware.umbrella.core.constants.Path.SEARCH;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField;
|
||||
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
|
||||
|
||||
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 doPost(Path path, HttpExchange ex) throws IOException {
|
||||
addCors(ex);
|
||||
try {
|
||||
Optional<Token> token = SessionToken.from(ex).map(Token::of);
|
||||
var user = ModuleRegistry.userService().loadUser(token);
|
||||
if (user.isEmpty()) return unauthorized(ex);
|
||||
var head = path.pop();
|
||||
return switch (head) {
|
||||
case null -> postEntry(user.get(),ex);
|
||||
default -> super.doPost(path,ex);
|
||||
};
|
||||
} catch (NumberFormatException e){
|
||||
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
|
||||
} catch (UmbrellaException e){
|
||||
return send(ex,e);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// TODO more tests
|
||||
|
||||
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();
|
||||
}
|
||||
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;
|
||||
|
||||
var account = accountDb.save(new Account(0, accountName, currency, user.id()));
|
||||
accountId = account.id();
|
||||
}
|
||||
|
||||
// TODO: save entry
|
||||
|
||||
return notFound(ex);
|
||||
}
|
||||
}
|
||||
@@ -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,88 @@
|
||||
package de.srsoftware.umbrella.accounting;
|
||||
|
||||
import de.srsoftware.tools.jdbc.Query;
|
||||
import de.srsoftware.umbrella.core.BaseDb;
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
import de.srsoftware.umbrella.core.model.Account;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import static de.srsoftware.tools.NotImplemented.notImplemented;
|
||||
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.failedToCreateTable;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.failedToStoreObject;
|
||||
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 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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ application{
|
||||
}
|
||||
|
||||
dependencies{
|
||||
implementation(project(":accounting"));
|
||||
implementation(project(":bookmark"));
|
||||
implementation(project(":bus"));
|
||||
implementation(project(":company"))
|
||||
@@ -47,6 +48,7 @@ tasks.jar {
|
||||
.map(::zipTree) // OR .map { zipTree(it) }
|
||||
from(dependencies)
|
||||
dependsOn(
|
||||
":accounting:jar",
|
||||
":bookmark:jar",
|
||||
":bus:jar",
|
||||
":company:jar",
|
||||
|
||||
@@ -9,11 +9,13 @@ import static java.lang.System.Logger.Level.INFO;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import de.srsoftware.configuration.JsonConfig;
|
||||
import de.srsoftware.tools.ColorLogger;
|
||||
import de.srsoftware.umbrella.accounting.AccountingModule;
|
||||
import de.srsoftware.umbrella.bookmarks.BookmarkApi;
|
||||
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;
|
||||
@@ -92,6 +94,7 @@ public class Application {
|
||||
new WikiModule(config).bindPath("/api/wiki").on(server);
|
||||
new FileModule(config).bindPath("/api/files").on(server);
|
||||
new SettingsService(config).bindPath("/api/settings").on(server);
|
||||
new AccountingModule(config).bindPath("/api/accounting").on(server);
|
||||
} catch (Exception e) {
|
||||
LOG.log(ERROR,"Startup failed",e);
|
||||
System.exit(-1);
|
||||
|
||||
@@ -5,6 +5,7 @@ package de.srsoftware.umbrella.core;
|
||||
import de.srsoftware.umbrella.core.api.*;
|
||||
|
||||
public class ModuleRegistry {
|
||||
private AccountingService accountingService;
|
||||
private BookmarkService bookmarkService;
|
||||
private CompanyService companyService;
|
||||
private ContactService contactService;
|
||||
@@ -29,23 +30,24 @@ public class ModuleRegistry {
|
||||
|
||||
public static void add(Object service) {
|
||||
switch (service) {
|
||||
case BookmarkService bs: singleton.bookmarkService = bs; break;
|
||||
case CompanyService cs: singleton.companyService = cs; break;
|
||||
case ContactService cs: singleton.contactService = cs; break;
|
||||
case DocumentService ds: singleton.documentService = ds; break;
|
||||
case FileService fs: singleton.fileService = fs; break;
|
||||
case StockService is: singleton.stockService = is; break;
|
||||
case MarkdownService ms: singleton.markdownService = ms; break;
|
||||
case NoteService ns: singleton.noteService = ns; break;
|
||||
case PollService ps: singleton.pollService = ps; break;
|
||||
case PostBox pb: singleton.postBox = pb; break;
|
||||
case ProjectService ps: singleton.projectService = ps; break;
|
||||
case TagService ts: singleton.tagService = ts; break;
|
||||
case TaskService ts: singleton.taskService = ts; break;
|
||||
case TimeService ts: singleton.timeService = ts; break;
|
||||
case Translator tr: singleton.translator = tr; break;
|
||||
case UserService us: singleton.userService = us; break;
|
||||
case WikiService ws: singleton.wikiService = ws; break;
|
||||
case AccountingService as: singleton.accountingService = as; break;
|
||||
case BookmarkService bs: singleton.bookmarkService = bs; break;
|
||||
case CompanyService cs: singleton.companyService = cs; break;
|
||||
case ContactService cs: singleton.contactService = cs; break;
|
||||
case DocumentService ds: singleton.documentService = ds; break;
|
||||
case FileService fs: singleton.fileService = fs; break;
|
||||
case StockService is: singleton.stockService = is; break;
|
||||
case MarkdownService ms: singleton.markdownService = ms; break;
|
||||
case NoteService ns: singleton.noteService = ns; break;
|
||||
case PollService ps: singleton.pollService = ps; break;
|
||||
case PostBox pb: singleton.postBox = pb; break;
|
||||
case ProjectService ps: singleton.projectService = ps; break;
|
||||
case TagService ts: singleton.tagService = ts; break;
|
||||
case TaskService ts: singleton.taskService = ts; break;
|
||||
case TimeService ts: singleton.timeService = ts; break;
|
||||
case Translator tr: singleton.translator = tr; break;
|
||||
case UserService us: singleton.userService = us; break;
|
||||
case WikiService ws: singleton.wikiService = ws; break;
|
||||
case null: break;
|
||||
default: System.getLogger(ModuleRegistry.class.getSimpleName()).log(System.Logger.Level.WARNING,"Trying to add untracked class {0}",service.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ public class SettingsService extends BaseHandler {
|
||||
entries.add(MenuEntry.of(13,Module.STOCK));
|
||||
entries.add(MenuEntry.of(14,Module.MESSAGE, MESSAGES));
|
||||
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++){
|
||||
var entry = entries.get(i);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package de.srsoftware.umbrella.core.api;
|
||||
|
||||
public interface AccountingService {
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.core.constants;
|
||||
|
||||
public class Field {
|
||||
public static final String ACTION = "action";
|
||||
public static final String ACCOUNT = "account";
|
||||
public static final String ADDRESS = "address";
|
||||
public static final String ALLOWED_STATES = "allowed_states";
|
||||
public static final String AMOUNT = "amount";
|
||||
@@ -43,6 +44,7 @@ public class Field {
|
||||
public static final String DELIVERY = "delivery";
|
||||
public static final String DELIVERY_DATE = "delivery_date";
|
||||
public static final String DESCRIPTION = "description";
|
||||
public static final String DESTINATION = "destination";
|
||||
public static final String DOCUMENT = "document";
|
||||
public static final String DOCUMENT_ID = "document_id";
|
||||
public static final String DOC_TYPE_ID = "document_type_id";
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
package de.srsoftware.umbrella.core.constants;
|
||||
|
||||
public class Module {
|
||||
public static final String BOOKMARK = "bookmark";
|
||||
public static final String COMPANY = "company";
|
||||
public static final String CONTACT = "contact";
|
||||
public static final String DOCUMENT = "document";
|
||||
public static final String FILES = "files";
|
||||
public static final String MESSAGE = "message";
|
||||
public static final String NOTES = "notes";
|
||||
public static final String POLL = "poll";
|
||||
public static final String PROJECT = "project";
|
||||
public static final String STOCK = "stock";
|
||||
public static final String TAGS = "tags";
|
||||
public static final String TASK = "task";
|
||||
public static final String TIME = "time";
|
||||
public static final String USER = "user";
|
||||
public static final String WIKI = "wiki";
|
||||
public static final String ACCOUNTING = "accounting";
|
||||
public static final String BOOKMARK = "bookmark";
|
||||
public static final String COMPANY = "company";
|
||||
public static final String CONTACT = "contact";
|
||||
public static final String DOCUMENT = "document";
|
||||
public static final String FILES = "files";
|
||||
public static final String MESSAGE = "message";
|
||||
public static final String NOTES = "notes";
|
||||
public static final String POLL = "poll";
|
||||
public static final String PROJECT = "project";
|
||||
public static final String STOCK = "stock";
|
||||
public static final String TAGS = "tags";
|
||||
public static final String TASK = "task";
|
||||
public static final String TIME = "time";
|
||||
public static final String USER = "user";
|
||||
public static final String WIKI = "wiki";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ package de.srsoftware.umbrella.core.constants;
|
||||
* This is a collection of messages that appear throughout the project
|
||||
*/
|
||||
public class Text {
|
||||
public static final String ACCOUNTING = "accounting";
|
||||
|
||||
public static final String BOOKMARK = "bookmark";
|
||||
public static final String BOOKMARKS = "bookmarks";
|
||||
public static final String BOOLEAN = "Boolean";
|
||||
@@ -47,8 +49,8 @@ public class Text {
|
||||
public static final String NOTE_WITH_ID = "note ({id})";
|
||||
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 PERMISSION = "permission";
|
||||
public static final String POLL = "poll";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.srsoftware.umbrella.core.model;
|
||||
|
||||
public record Account(long id, String name, String currency, long ownerId) {
|
||||
public Account withId(long newId) {
|
||||
return new Account(newId,name,currency,ownerId);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public class Project implements Mappable {
|
||||
private final Map<Long,Member> members;
|
||||
private final Collection<Status> allowedStates;
|
||||
private boolean showClosed;
|
||||
private final Long companyId;
|
||||
private Long companyId;
|
||||
private int status;
|
||||
private String name;
|
||||
private final long id;
|
||||
@@ -96,6 +96,7 @@ public class Project implements Mappable {
|
||||
public Project patch(JSONObject json) {
|
||||
for (var key : json.keySet()){
|
||||
switch (key){
|
||||
case COMPANY_ID: companyId = json.getLong(COMPANY_ID); break;
|
||||
case DESCRIPTION: description = json.getString(key); break;
|
||||
case NAME: name = json.getString(key); break;
|
||||
case SHOW_CLOSED: showClosed = json.getBoolean(SHOW_CLOSED); break;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { loadTranslation } from './translations.svelte';
|
||||
import { checkUser, user } from './user.svelte';
|
||||
|
||||
import Accounts from "./routes/accounting/index.svelte";
|
||||
import AddDoc from "./routes/document/Add.svelte";
|
||||
import AddTask from "./routes/task/Add.svelte";
|
||||
import Bookmark from "./routes/bookmark/View.svelte";
|
||||
@@ -25,6 +26,7 @@
|
||||
import Messages from "./routes/message/Messages.svelte";
|
||||
import MsgSettings from "./routes/message/Settings.svelte";
|
||||
import Menu from "./Components/Menu.svelte";
|
||||
import NewAccount from "./routes/accounting/new.svelte";
|
||||
import NewPage from "./routes/wiki/AddPage.svelte";
|
||||
import Notes from "./routes/notes/Index.svelte";
|
||||
import PollList from "./routes/poll/Index.svelte";
|
||||
@@ -88,6 +90,8 @@
|
||||
<span class="warn">{@html messages.warning}</span>
|
||||
{/if}
|
||||
<Route path="/" component={User} />
|
||||
<Route path="/accounting" component={Accounts} />
|
||||
<Route path="/accounting/new" component={NewAccount} />
|
||||
<Route path="/bookmark" component={Bookmarks} />
|
||||
<Route path="/bookmark/:id/view" component={Bookmark} />
|
||||
<Route path="/calc" component={Spreadsheet} />
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
candidate = candidates[idx];
|
||||
candidates = [];
|
||||
selected = [];
|
||||
console.log(candidate);
|
||||
onSelect(candidate);
|
||||
}
|
||||
|
||||
@@ -93,6 +92,7 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
candidate = { display : candidate.display };
|
||||
candidates = await getCandidates(candidate.display);
|
||||
if (selected>candidates.length) selected = candidates.length;
|
||||
return false;
|
||||
@@ -103,7 +103,7 @@
|
||||
span { position : relative }
|
||||
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; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
import { onMount, onDestroy } from '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;
|
||||
const regex = /@startsheet[\s\S]*?@endsheet/g;
|
||||
const number = /^[0-9.-]+$/
|
||||
@@ -31,16 +39,16 @@
|
||||
if (!markdown.rendered) return;
|
||||
let sheets = document.getElementsByClassName('spreadsheet');
|
||||
for (let i = 0; i < sheets.length; i++) {
|
||||
let sheet = sheets[i];
|
||||
let raw = sheet.innerHTML.trim();
|
||||
let current_sheet = sheets[i];
|
||||
let raw = current_sheet.innerHTML.trim();
|
||||
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
|
||||
await import('jspreadsheet-ce/dist/jspreadsheet.css');
|
||||
jspreadsheet = module.default ?? module;
|
||||
}
|
||||
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
|
||||
@@ -60,14 +68,25 @@
|
||||
render: formatCell,
|
||||
width:`${len}0px`
|
||||
}});
|
||||
var w = window.innerWidth;
|
||||
if (classes == 'preview') w = w/2;
|
||||
let config = {
|
||||
worksheets : [{
|
||||
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 stored_source = $state(store_id ? localStorage.getItem(store_id) : null);
|
||||
let timer = null;
|
||||
let sheet = null;
|
||||
|
||||
async function applyEdit(){
|
||||
let success = await onSet(editValue.source);
|
||||
@@ -80,10 +81,13 @@
|
||||
|
||||
|
||||
function oncontextmenu(evt){
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
startEdit();
|
||||
return false;
|
||||
if (evt.target) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
sheet = evt.sheet ? evt : null; // store position of activated cell to focus after editing starts
|
||||
startEdit();
|
||||
return false;
|
||||
}
|
||||
|
||||
function onmousedown(evt){
|
||||
@@ -96,6 +100,10 @@
|
||||
measured(evt, evt.timeStamp - start);
|
||||
}
|
||||
|
||||
function onresize(evt){
|
||||
console.log('onresize()',evt);
|
||||
}
|
||||
|
||||
function ontouchstart(evt){
|
||||
evt.preventDefault();
|
||||
start = evt.timeStamp;
|
||||
@@ -137,8 +145,8 @@
|
||||
{#if stored_source}
|
||||
<span id="restore_markdown" onclick={restore} class="hint">{t('unsaved_content')}</span>
|
||||
{/if}
|
||||
<textarea bind:value={editValue.source} onkeyup={typed} autofocus={!simple}></textarea>
|
||||
<Display classes="preview" bind:markdown={editValue} />
|
||||
<textarea bind:value={editValue.source} onkeyup={typed} onresize={onresize} data="test" autofocus={!simple}></textarea>
|
||||
<Display classes="preview" bind:markdown={editValue} sheet={sheet} />
|
||||
{#if !simple}
|
||||
<div class="buttons">
|
||||
<button class="cancel" onclick={e => editing = false}>{t('cancel')}</button>
|
||||
@@ -146,6 +154,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Display classes={{editable}} markdown={value} {onclick} {oncontextmenu} title={t('right_click_to_edit')} wrapper={type} />
|
||||
<Display classes={{editable}} markdown={value} {onclick} {oncontextmenu} title={t('right_click_to_edit')} wrapper={type} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
99
frontend/src/routes/accounting/add_entry.svelte
Normal file
99
frontend/src/routes/accounting/add_entry.svelte
Normal file
@@ -0,0 +1,99 @@
|
||||
<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 { new_account = false } = $props();
|
||||
|
||||
let entry = $state({
|
||||
account : {
|
||||
id : 0,
|
||||
name : '',
|
||||
currency : ''
|
||||
},
|
||||
date : new Date().toISOString().substring(0, 10),
|
||||
source : {
|
||||
display: user.name,
|
||||
user_id: user.id
|
||||
},
|
||||
destination : {},
|
||||
amount : 0.0,
|
||||
purpose : {}
|
||||
});
|
||||
let router = useTinyRouter();
|
||||
|
||||
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>{t('account name')}</span>
|
||||
<input type="text" bind:value={entry.account.name} />
|
||||
|
||||
<span>{t('currency')}</span>
|
||||
<input type="text" bind:value={entry.account.currency} />
|
||||
<hr/>
|
||||
{:else}
|
||||
<legend>{t('add_object',{object:t('entry')})}</legend>
|
||||
{/if}
|
||||
|
||||
<span>{t('date')}</span>
|
||||
<input type="date" value={entry.date} />
|
||||
|
||||
<span>{t('source')}</span>
|
||||
<Autocomplete bind:candidate={entry.source} />
|
||||
|
||||
<span>{t('destination')}</span>
|
||||
<Autocomplete bind:candidate={entry.destination} />
|
||||
|
||||
<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>
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(entry,null,2)}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(user,null,2)}
|
||||
</code>
|
||||
</pre>
|
||||
16
frontend/src/routes/accounting/index.svelte
Normal file
16
frontend/src/routes/accounting/index.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
import { t } from '../../translations.svelte';
|
||||
|
||||
const router = useTinyRouter();
|
||||
|
||||
function newAccount(){
|
||||
router.navigate('/accounting/new');
|
||||
}
|
||||
</script>
|
||||
|
||||
<fieldset>
|
||||
<legend>{t('accounts')}</legend>
|
||||
|
||||
<button onclick={newAccount}>{t('create_new_object',{'object':t('account')})}</button>
|
||||
</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} />
|
||||
@@ -6,6 +6,7 @@
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
|
||||
import CompanySelector from '../../Components/CompanySelector.svelte';
|
||||
import LineEditor from '../../Components/LineEditor.svelte';
|
||||
import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
|
||||
import PermissionEditor from '../../Components/PermissionEditor.svelte';
|
||||
@@ -211,6 +212,11 @@
|
||||
{#if project.company}
|
||||
<div>{t('company')}</div>
|
||||
<div class="company">{project.company.name}</div>
|
||||
{:else}
|
||||
{#if showSettings}
|
||||
<div>{t('company')}</div>
|
||||
<span><CompanySelector caption={t('select_company')} onselect={c => update({company_id:c.id})} /></span>
|
||||
{/if}
|
||||
{/if}
|
||||
<div>{t('context')}</div>
|
||||
<div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
import { api, eventStream } from '../../urls.svelte';
|
||||
import { api, eventStream, get } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
import { user } from '../../user.svelte';
|
||||
@@ -48,8 +48,13 @@
|
||||
});
|
||||
if (res.ok){
|
||||
let json = await res.json();
|
||||
router.navigate(`/wiki/${page.id}/view`);
|
||||
yikes();
|
||||
let target = `/wiki/${page.id}/view`;
|
||||
if (window.location.pathname == target) {
|
||||
loadPage();
|
||||
} else {
|
||||
router.navigate(target);
|
||||
yikes();
|
||||
}
|
||||
} else {
|
||||
error(res);
|
||||
}
|
||||
@@ -100,7 +105,7 @@
|
||||
let path = `wiki/page/${key}`;
|
||||
if (version) path += `/version/${version}`;
|
||||
const url = api(path);
|
||||
const res = await fetch(url,{credentials:'include'});
|
||||
const res = await get(url);
|
||||
loadJson(res);
|
||||
}
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
}
|
||||
if (prj.isDirty()){
|
||||
update(TABLE_PROJECTS).set(NAME, DESCRIPTION, STATUS, COMPANY_ID, SHOW_CLOSED).where(ID,equal(prj.id())).prepare(db)
|
||||
.apply(prj.name(),prj.description(),prj.status(),prj.companyId(),prj.showClosed())
|
||||
.apply(prj.name(),prj.description(),prj.status(),prj.companyId().orElse(null),prj.showClosed())
|
||||
.execute();
|
||||
prj.clean();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
rootProject.name = "Umbrella25"
|
||||
|
||||
include("accounting")
|
||||
include("backend")
|
||||
include("bookmark")
|
||||
include("bus")
|
||||
@@ -22,5 +23,4 @@ include("time")
|
||||
include("translations")
|
||||
include("user")
|
||||
include("web")
|
||||
include("wiki")
|
||||
|
||||
include("wiki")
|
||||
@@ -334,6 +334,10 @@ tr:hover .taglist .tag button {
|
||||
border-left: 1px solid #333 !important;
|
||||
}
|
||||
|
||||
.jss_worksheet tr:hover td input{
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
#app nav a{
|
||||
background: black;
|
||||
|
||||
@@ -325,6 +325,10 @@ tr:hover .taglist .tag button {
|
||||
border-left: 1px solid #333 !important;
|
||||
}
|
||||
|
||||
.jss_worksheet tr:hover td input{
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
#app nav a{
|
||||
background: black;
|
||||
|
||||
Reference in New Issue
Block a user