working on document list

This commit is contained in:
2025-07-09 00:27:06 +02:00
parent 58f69689e2
commit 5b5e6a9387
22 changed files with 1914 additions and 26 deletions

View File

@@ -1,9 +1,10 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.backend;
import static de.srsoftware.umbrella.core.Constants.UMBRELLA;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Util.mapLogLevel;
import static java.lang.System.Logger.Level.INFO;
import static java.text.MessageFormat.format;
import com.sun.net.httpserver.HttpServer;
import de.srsoftware.configuration.JsonConfig;
@@ -11,6 +12,7 @@ import de.srsoftware.tools.ColorLogger;
import de.srsoftware.umbrella.core.ConnectionProvider;
import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.documents.DocumentApi;
import de.srsoftware.umbrella.documents.SqliteDb;
import de.srsoftware.umbrella.legacy.LegacyApi;
import de.srsoftware.umbrella.message.MessageApi;
import de.srsoftware.umbrella.message.MessageSystem;
@@ -46,25 +48,27 @@ public class Application {
configureLogging(config);
var port = config.get("umbrella.http.port", 8080);
var threads = config.get("umbrella.threads", 16);
var docDbFile = config.get("umbrella.database.documents",config.file().getParent()+"/documents.db");
var userDbFile = config.get("umbrella.database.user", config.file().getParent()+"/umbrella.db");
var loginDbFile = config.get("umbrella.database.login_services",config.file().getParent()+"/umbrella.db");
var messageDbFile = config.get("umbrella.database.messages", config.file().getParent()+"/umbrella.db");
var connectionProvider = new ConnectionProvider();
var documentDb = new SqliteDb(connectionProvider.get(docDbFile));
var messageDb = new SqliteMessageDb(connectionProvider.get(messageDbFile));
var userDb = new SqliteDB(connectionProvider.get(userDbFile));
var loginServiceDb = new SqliteDB(connectionProvider.get(loginDbFile));
var moduleConfig = config.subset("umbrella.modules").orElseThrow(() -> new RuntimeException(format(ERROR_MISSING_CONFIG,"umbrella.modules")));
var translationModule = new Translations();
var messageSystem = new MessageSystem(messageDb,translationModule,config.subset("umbrella.modules.message").orElseThrow());
var messageSystem = new MessageSystem(messageDb,translationModule,moduleConfig.subset("message").orElseThrow(() -> new RuntimeException(format(ERROR_MISSING_CONFIG,"umbrella.modules.message"))));
var server = HttpServer.create(new InetSocketAddress(port), 0);
server.setExecutor(Executors.newFixedThreadPool(threads));
var userModule = new UserModule(userDb,loginServiceDb,messageSystem);
new LegacyApi(userDb,config) .bindPath("/legacy") .on(server);
new DocumentApi().bindPath("/api/document").on(server);
new DocumentApi(documentDb, userModule, moduleConfig) .bindPath("/api/document") .on(server);
new MessageApi(messageSystem).bindPath("/api/messages") .on(server);
translationModule .bindPath("/api/translations").on(server);
new UserModule(userDb,loginServiceDb,messageSystem).bindPath("/api/user").on(server);
userModule .bindPath("/api/user") .on(server);
new WebHandler() .bindPath("/") .on(server);
server.start();
LOG.log(INFO,"Started web server at {0}",port);

View File

@@ -21,6 +21,7 @@ public class Constants {
public static final String ERROR_FAILED_CREATE_TABLE = "Failed to create \"{0}\" table!";
public static final String ERROR_INVALID_FIELD = "Expected {0} to be {1}!";
public static final String ERROR_MISSING_CONFIG = "Config is missing value for {0}!";
public static final String ERROR_MISSING_FIELD = "Json is missing {0} field!";
public static final String ERROR_READ_TABLE = "Failed to read {0} from {1} table";

View File

@@ -31,6 +31,11 @@ public class Util {
};
}
public static String markdown(String code){
LOG.log(ERROR,"{0}.markdown(…) not implemented",Util.class.getCanonicalName());
return code;
}
public static HttpURLConnection open(URL url) throws IOException {
var conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Accept","*/*");

View File

@@ -1,11 +1,13 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.api;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.umbrella.core.Token;
import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Optional;
public interface UserHelper {
Optional<UmbrellaUser> loadUser(Optional<Token> sessionToken) throws UmbrellaException;
Optional<UmbrellaUser> loadUser(HttpExchange ex) throws UmbrellaException;
}

View File

@@ -2,4 +2,10 @@ description = "Umbrella : Documents"
dependencies{
implementation(project(":core"))
implementation("de.srsoftware:configuration.api:1.0.2")
implementation("de.srsoftware:tools.jdbc:1.3.2")
implementation("de.srsoftware:tools.mime:1.1.2")
implementation("de.srsoftware:tools.optionals:1.0.0")
implementation("de.srsoftware:tools.util:2.0.3")
implementation("org.json:json:20240303")
}

View File

@@ -1,8 +1,97 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents;
import static java.util.regex.Pattern.DOTALL;
import static java.util.regex.Pattern.compile;
import java.util.regex.Pattern;
public class Constants {
private Constants(){}
public static final Pattern POST_CODE = compile("(.*\\w+.*)\n(.*\\d+.*)\n(\\d{5}) (\\w+)",DOTALL);
public static final String CUSTOMER_NUMBER_PREFIX = "customer_number_prefix";
public static final String DECIMAL_SEPARATOR = "decimal_separator";
public static final String DECIMALS = "decimals";
public static final String ERROR_ADDRESS_MISSING = "{0} address does not contain street address / post code / city";
public static final String LAST_CUSTOMER_NUMBER = "last_customer_number";
public static final String THOUSANDS_SEPARATOR = "thousands_separator";
public static final String PROJECT_ID = "project_id";
public static final String FIELD_AMOUNT = "amount";
public static final String FIELD_BANK_ACCOUNT = "bank_account";
public static final String FIELD_CODE = "code";
public static final String FIELD_COMPANY = "company";
public static final String FIELD_COMPANY_ID = "company_id";
public static final String FIELD_COURT = "court";
public static final String FIELD_CUSTOMER = "customer";
public static final String FIELD_CUSTOMER_EMAIL = "customer_email";
public static final String FIELD_CUSTOMER_NUMBER = "customer_number";
public static final String FIELD_CUSTOMER_TAX_NUMBER = "customer_tax_number";
public static final String FIELD_CURRENCY = "currency";
public static final String FIELD_DEFAULT_HEADER = "default_header";
public static final String FIELD_DEFAULT_FOOTER = "default_footer";
public static final String FIELD_DEFAULT_MAIL = "type_mail_text";
public static final String FIELD_DELIVERY = "delivery";
public static final String FIELD_DELIVERY_DATE = "delivery_date";
public static final String FIELD_DOC = "doc";
public static final String FIELD_DOCID = "doc_id";
public static final String FIELD_DOCUMENT = "document";
public static final String FIELD_DOCUMENT_ID = "document_id";
public static final String FIELD_DOC_TYPE_ID = "document_type_id";
public static final String FIELD_END_TIME = "end_time";
public static final String FIELD_FOOTER = "footer";
public static final String FIELD_GROSS_SUM = "gross_sum";
public static final String FIELD_HEAD = "head";
public static final String FIELD_ITEM = "item";
public static final String FIELD_ITEM_CODE = "item_code";
public static final String FIELD_NET_PRICE = "net_price";
public static final String FIELD_NET_SUM = "net_sum";
public static final String FIELD_NEXT_TYPE = "next_type_id";
public static final String FIELD_PHONE = "phone";
public static final String FIELD_POS = "pos";
public static final String FIELD_POSITIONS = "positions";
public static final String FIELD_PRICE = "single_price";
public static final String FIELD_PRICE_FORMAT = "price_format";
public static final String FIELD_PROJECTS = "projects";
public static final String FIELD_RECEIVER = "receiver";
public static final String FIELD_START_TIME = "start_time";
public static final String FIELD_TAX_NUMBER = "tax_number";
public static final String FIELD_TEMPLATE_ID = "template_id";
public static final String FIELD_TASKS = "tasks";
public static final String FIELD_TAX = "tax";
public static final String FIELD_TAX_ID = "tax_id";
public static final String FIELD_TIME_ID = "time_id";
public static final String FIELD_TYPE = "type";
public static final String FIELD_TYPE_ID = "type_id";
public static final String FIELD_TYPE_NUMBER = "type_number";
public static final String FIELD_TYPE_PREFIX = "type_prefix";
public static final String FIELD_TYPE_SUFFIX = "type_suffix";
public static final String FIELD_UNIT = "unit";
public static final String FIELD_UNIT_PRICE = "unit_price";
public static final String PATH_ADD_ITEM = "add_item";
public static final String PATH_ADD_TASK = "add_task";
public static final String PATH_ADD_TIME = "add_time";
public static final String PATH_COMPANIES = "companies";
public static final String PATH_COMPANY = "company";
public static final String PATH_CONTACTS = "contacts";
public static final String PATH_DOCUMENT = "document";
public static final String PATH_PDF = "pdf";
public static final String PATH_POSITIONS = "positions";
public static final String PATH_SEND = "send";
public static final String PATH_TYPES = "types";
public static final String TABLE_COMPANY_SETTINGS = "company_settings";
public static final String TABLE_CUSTOMER_SETTINGS = "company_customer_settings";
public static final String TABLE_DOCUMENTS = "documents";
public static final String TABLE_DOCUMENT_TYPES = "document_types";
public static final String TABLE_POSITIONS = "document_positions";
public static final String TABLE_PRICES = "customer_prices";
public static final String TABLE_TEMPLATES = "templates";
public static final String COMPANIES = "companies";
public static final String COMPANY = "company";
}

View File

@@ -1,26 +1,104 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_NOT_IMPLEMENTED;
import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
import static de.srsoftware.umbrella.core.Constants.ERROR_MISSING_FIELD;
import static de.srsoftware.umbrella.core.Paths.LIST;
import static de.srsoftware.umbrella.core.Util.request;
import static de.srsoftware.umbrella.documents.Constants.COMPANIES;
import static de.srsoftware.umbrella.documents.Constants.COMPANY;
import static java.lang.System.Logger.Level.WARNING;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
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.Token;
import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.core.api.UserHelper;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.json.JSONObject;
public class DocumentApi extends BaseHandler {
private final UserHelper users;
private final Configuration config;
private final DocumentDb db;
public DocumentApi(DocumentDb documentDb, UserHelper userHelper, Configuration moduleConfig){
config = moduleConfig;
db = documentDb;
users = userHelper;
}
@Override
public boolean doGet(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = users.loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head){
case COMPANIES -> getCompanies(ex);
case COMPANIES -> getCompanies(ex,user.get(),token.orElse(null));
case null, default -> super.doGet(path,ex);
};
} catch (UmbrellaException e) {
return send(ex,e);
}
}
private boolean getCompanies(HttpExchange ex) throws IOException {
return sendEmptyResponse(HTTP_NOT_IMPLEMENTED,ex);
@Override
public boolean doPost(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = users.loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head){
case LIST -> listDocuments(ex,user.get(),token.orElse(null));
case null, default -> super.doPost(path,ex);
};
} catch (UmbrellaException e) {
return send(ex,e);
}
}
private boolean getCompanies(HttpExchange ex, UmbrellaUser user, Token token) throws IOException, UmbrellaException {
return sendContent(ex,getLegacyCompanies(ex,user,token));
}
private HashMap<Long, Map<String, Object>> getLegacyCompanies(HttpExchange ex, UmbrellaUser umbrellaUser, Token token) throws IOException, UmbrellaException {
var location = config.get("company.baseUrl").map(s -> s+"/json").orElseThrow(() -> new UmbrellaException(500,"umbrella.modules.company.baseUrl not configured!"));
var resp = request(location, token.asMap(),MIME_FORM_URL,null);
if (!(resp instanceof JSONObject json)) throw new UmbrellaException(500,"{0} did not return JSON!",location);
var result = new HashMap<Long, Map<String,Object>>();
for (var key : json.keySet()) result.put(Long.parseLong(key),json.getJSONObject(key).toMap());
return result;
}
private boolean listDocuments(HttpExchange ex, UmbrellaUser user, Token token) throws UmbrellaException {
try {
var json = json(ex);
if (!json.has(COMPANY)) throw new UmbrellaException(HTTP_BAD_REQUEST,ERROR_MISSING_FIELD, COMPANY);
long companyId = json.getLong(COMPANY);
var companies = getLegacyCompanies(ex,user, token);
var company = companies.get(companyId);
if (company == null) return forbidden(ex);
var docs = db.listDocs(companyId);
var map = new HashMap<Long,Object>();
for (var entry : docs.entrySet()) map.put(entry.getKey(),entry.getValue().summary());
return sendContent(ex,new JSONObject(map).toString(2));
} catch (IOException e) {
LOG.log(WARNING,"Failed to parse JSON data from request",e);
throw new UmbrellaException( 500,"Failed to parse JSON data from request").causedBy(e);
}
}
}

View File

@@ -0,0 +1,55 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents;
import de.srsoftware.tools.Pair;
import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.documents.model.CompanySettings;
import de.srsoftware.umbrella.documents.model.CustomerSettings;
import de.srsoftware.umbrella.documents.model.Document;
import de.srsoftware.umbrella.documents.model.Type;
import java.util.HashMap;
import java.util.Map;
public interface DocumentDb {
Long dropPosition(long documentId, long pos) throws UmbrellaException;
/**
* Tries to delete the document belonging to the given id
* @param docId the internal id of the document
* @return the human readable number of the document, if the query succeeded
*/
String deleteDoc(Long docId) throws UmbrellaException;
Long getCustomerPrice(long company, String id, String itemCode) throws UmbrellaException;
CustomerSettings getCustomerSettings(long companyId, Type docType, String customerId) throws UmbrellaException;
CompanySettings getCompanySettings(long companyId, Type docType) throws UmbrellaException;
Type getType(int typeId) throws UmbrellaException;
Map<Long, Document> listDocs(long companyId) throws UmbrellaException;
HashMap<Integer, Type> listTypes() throws UmbrellaException;
Document loadDoc(long docId) throws UmbrellaException;
/**
* decrement the document number
* @param companyId
* @param type
*/
void rollback(long companyId, Type type) throws UmbrellaException;
Document save(Document document) throws UmbrellaException;
CustomerSettings save(long companyId, Type docType, String customerId, CustomerSettings settings) throws UmbrellaException;
/**
* increment the document number
* @param settings containing company id and document type
*/
void step(CompanySettings settings);
Pair<Integer> switchPositions(long docId, Pair<Integer> longPair) throws UmbrellaException;
}

View File

@@ -0,0 +1,585 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents;
import static de.srsoftware.tools.jdbc.Condition.equal;
import static de.srsoftware.tools.jdbc.Condition.in;
import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.documents.Constants.*;
import static de.srsoftware.umbrella.documents.model.Document.DEFAULT_THOUSANDS_SEPARATOR;
import static de.srsoftware.umbrella.documents.model.Document.State;
import static java.lang.System.Logger.Level.*;
import static java.text.MessageFormat.format;
import static java.time.ZoneOffset.UTC;
import de.srsoftware.tools.Pair;
import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.documents.model.*;
import de.srsoftware.umbrella.documents.model.Type;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class SqliteDb implements DocumentDb{
private static final System.Logger LOG = System.getLogger(SqliteDb.class.getSimpleName());
private final Connection db;
private static final String DB_VERSION = "message_db_version";
private static final int INITIAL_DB_VERSION = 1;
public SqliteDb(Connection conn){
db = conn;
init();
}
private int createTables() {
createTableDocumentTypes();
createTableTemplates();
createTableDocuments();
createTablePositions();
createTableCustomerPrices();
createTableCompanySettings();
createTableCustomerSettings();
return createTableSettings();
}
private void createTableCompanySettings() {
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} INT NOT NULL, {3} TEXT DEFAULT \"A\", {4} TEXT DEFAULT NULL, {5} INT NOT NULL DEFAULT 1, PRIMARY KEY ({1}, {2}))";
try {
var stmt = db.prepareStatement(format(sql,TABLE_COMPANY_SETTINGS, FIELD_COMPANY_ID,FIELD_DOC_TYPE_ID,FIELD_TYPE_PREFIX,FIELD_TYPE_SUFFIX,FIELD_TYPE_NUMBER));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_COMPANY_SETTINGS,e);
throw new RuntimeException(e);
}
}
private void createTableCustomerPrices() {
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} VARCHAR(255), {3} VARCHAR(50), {4} INTEGER)";
try {
var stmt = db.prepareStatement(format(sql,TABLE_PRICES, FIELD_COMPANY_ID,FIELD_CUSTOMER_NUMBER, FIELD_ITEM_CODE,FIELD_PRICE));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_PRICES,e);
throw new RuntimeException(e);
}
}
private void createTableCustomerSettings() {
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} INT NOT NULL, {3} VARCHAR(255), {4} TEXT, {5} TEXT, {6} TEXT, PRIMARY KEY ({1}, {2}, {3}))";
try {
var stmt = db.prepareStatement(format(sql,TABLE_CUSTOMER_SETTINGS, FIELD_COMPANY_ID,FIELD_DOC_TYPE_ID,FIELD_CUSTOMER_NUMBER,FIELD_DEFAULT_HEADER,FIELD_DEFAULT_FOOTER,FIELD_DEFAULT_MAIL));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_COMPANY_SETTINGS,e);
throw new RuntimeException(e);
}
}
private void createTableDocuments() {
var createTable = """
CREATE TABLE IF NOT EXISTS {0} (
{1} INTEGER PRIMARY KEY,
{2} INT NOT NULL,
{3} INT NOT NULL,
{4} TEXT NOT NULL,
{5} TIMESTAMP NOT NULL,
{6} INT NOT NULL NULL,
{7} INT NOT NULL,
{8} VARCHAR(100),
{9} TEXT,
{10} TEXT,
{11} VARCHAR(10) NOT NULL,
{12} TEXT NOT NULL,
{13} VARCHAR(255),
{14} TEXT,
{15} TEXT,
{16} TEXT,
{17} VARCHAR(255),
{18} VARCHAR(255),
{19} VARCHAR(255)
)""";
createTable = format(createTable,TABLE_DOCUMENTS, ID, FIELD_TYPE_ID, FIELD_COMPANY_ID, NUMBER, DATE, STATE, FIELD_TEMPLATE_ID, FIELD_DELIVERY_DATE,FIELD_HEAD,FIELD_FOOTER,FIELD_CURRENCY, SENDER,FIELD_TAX_NUMBER,FIELD_BANK_ACCOUNT,FIELD_COURT,FIELD_CUSTOMER,FIELD_CUSTOMER_NUMBER,FIELD_CUSTOMER_TAX_NUMBER,FIELD_CUSTOMER_EMAIL);
try {
var stmt = db.prepareStatement(createTable);
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_DOCUMENTS,e);
throw new RuntimeException(e);
}
}
private void createTableDocumentTypes() {
var createTable = "CREATE TABLE IF NOT EXISTS {0} ({1} INTEGER PRIMARY KEY, {2} INT, {3} VARCHAR(255) NOT NULL)";
try {
var stmt = db.prepareStatement(format(createTable,TABLE_DOCUMENT_TYPES, ID,FIELD_NEXT_TYPE, NAME));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_DOCUMENT_TYPES,e);
throw new RuntimeException(e);
}
}
private void createTablePositions() {
var sql = """
CREATE TABLE IF NOT EXISTS {0} (
{1} INTEGER NOT NULL,
{2} INTEGER NOT NULL,
{3} VARCHAR(50),
{4} INTEGER NOT NULL NULL,
{5} VARCHAR(12),
{6} VARCHAR(255),
{7} TEXT,
{8} INTEGER,
{9} INTEGER,
{10} INTEGER,
{11} BOOLEAN DEFAULT 0
)
""";
sql = format(sql,TABLE_POSITIONS,FIELD_DOCUMENT_ID,FIELD_POS, FIELD_ITEM_CODE,FIELD_AMOUNT,FIELD_UNIT, TITLE, DESCRIPTION,FIELD_PRICE,FIELD_TAX,FIELD_TIME_ID, OPTIONAL);
try {
var stmt = db.prepareStatement(sql);
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_POSITIONS,e);
throw new RuntimeException(e);
}
}
private int createTableSettings() {
var createTable = """
CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL);
""";
try {
var stmt = db.prepareStatement(format(createTable,TABLE_SETTINGS, KEY, VALUE));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SETTINGS,e);
throw new RuntimeException(e);
}
Integer version = null;
try {
var rs = select(VALUE).from(TABLE_SETTINGS).where(KEY, equal(DB_VERSION)).exec(db);
if (rs.next()) version = rs.getInt(VALUE);
rs.close();
if (version == null) {
version = INITIAL_DB_VERSION;
insertInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
}
return version;
} catch (SQLException e) {
LOG.log(ERROR,ERROR_READ_TABLE,DB_VERSION,TABLE_SETTINGS,e);
throw new RuntimeException(e);
}
}
private void createTableTemplates() {
var createTable = "CREATE TABLE IF NOT EXISTS {0} ({1} INTEGER PRIMARY KEY, {2} INT NOT NULL, {3} VARCHAR(255) NOT NULL, {4} BLOB)";
try {
var stmt = db.prepareStatement(format(createTable,TABLE_TEMPLATES, ID, FIELD_COMPANY_ID, NAME, TEMPLATE));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_TEMPLATES,e);
throw new RuntimeException(e);
}
}
@Override
public String deleteDoc(Long docId) throws UmbrellaException {
try {
db.setAutoCommit(false);
var rs = select(NUMBER).from(TABLE_DOCUMENTS).where(ID,equal(docId)).exec(db);
String number = null;
if (rs.next()) number = rs.getString(NUMBER);
rs.close();
delete().from(TABLE_POSITIONS).where(FIELD_DOCUMENT_ID,equal(docId)).execute(db);
delete().from(TABLE_DOCUMENTS).where(ID,equal(docId)).execute(db);
db.setAutoCommit(true);
if (number != null) return number;
} catch (SQLException e){
LOG.log(WARNING,"Failed to delete document {0}",docId);
}
throw new UmbrellaException(500,"Failed to delete document with id {0}",docId);
}
@Override
public Long dropPosition(long docId, long pos) throws UmbrellaException {
try {
db.setAutoCommit(false);
delete().from(TABLE_POSITIONS).where(FIELD_DOCUMENT_ID,equal(docId)).where(FIELD_POS,equal(pos)).execute(db);
var sql = format("UPDATE {0} SET {1} = {1}-1 WHERE {2} = ? AND {1} > ?",TABLE_POSITIONS,FIELD_POS,FIELD_DOCUMENT_ID);
LOG.log(DEBUG,sql.replaceFirst("\\?",docId+"").replace("?",pos+""));
var stmt = db.prepareStatement(sql);
stmt.setLong(1,docId);
stmt.setLong(2,pos);
stmt.execute();
stmt.close();
db.setAutoCommit(true);
} catch (SQLException e) {
LOG.log(WARNING,"Failed to delete position {0} of document {1}",pos,docId);
throw new UmbrellaException(500,"Failed to delete position {0} of document {1}",pos,docId);
}
return pos;
}
@Override
public CompanySettings getCompanySettings(long companyId, Type docType) throws UmbrellaException {
try {
var rs = select(ALL).from(TABLE_COMPANY_SETTINGS).where(FIELD_COMPANY_ID,equal(companyId)).where(FIELD_DOC_TYPE_ID,equal(docType.id())).exec(db);
CompanySettings settings = null;
if (rs.next()) settings = CompanySettings.of(rs);
rs.close();
if (settings != null) return settings;
} catch (SQLException e) {
LOG.log(WARNING,"Failed to load customer settings (company: {0}, document type: {1}",companyId, docType.name(),e);
}
throw new UmbrellaException(500,"Failed to load customer settings (company: {0}, document type: {1}",companyId, docType.name());
}
@Override
public Long getCustomerPrice(long company, String customer, String itemCode) throws UmbrellaException {
try {
var rs = select(FIELD_PRICE).from(TABLE_PRICES).where(FIELD_COMPANY_ID,equal(company)).where(FIELD_CUSTOMER_NUMBER,equal(customer)).where(FIELD_ITEM_CODE,equal(itemCode)).exec(db);
Long price = null;
if (rs.next()) price = rs.getLong(FIELD_PRICE);
rs.close();
if (price != null) return price;
} catch (SQLException e) {
LOG.log(WARNING,"Failed to load customer price (company: {0}, customer: {1}, item: {2}",company,customer,itemCode,e);
}
throw new UmbrellaException(500,"Failed to load customer price (company: {0}, customer: {1}, item: {2}",company,customer,itemCode);
}
@Override
public CustomerSettings getCustomerSettings(long companyId, Type docType, String customerId) throws UmbrellaException {
try {
var rs = select(ALL).from(TABLE_CUSTOMER_SETTINGS).where(FIELD_COMPANY_ID,equal(companyId)).where(FIELD_DOC_TYPE_ID,equal(docType.id())).where(FIELD_CUSTOMER_NUMBER,equal(customerId)).exec(db);
CustomerSettings settings = null;
if (rs.next()) settings = CustomerSettings.of(rs);
rs.close();
if (settings != null) return settings;
} catch (SQLException e) {
LOG.log(WARNING,"Failed to load customer settings (company: {0}, document type: {1}",companyId, docType.name(),e);
}
throw new UmbrellaException(500,"Failed to load customer settings (company: {0}, document type: {1}",companyId, docType.name());
}
@Override
public Type getType(int typeId) throws UmbrellaException {
try {
var rs = select(ALL).from(TABLE_DOCUMENT_TYPES).where(ID,equal(typeId)).exec(db);
Type type = null;
if (rs.next()) type = toType(rs);
rs.close();
if (type != null) return type;
} catch (SQLException e) {
LOG.log(WARNING,"Failed to read document type ({0})!",typeId);
}
throw new UmbrellaException(500,"No type with id = {0}",typeId);
}
private void init() {
var version = createTables();
}
@Override
public Map<Long, Document> listDocs(long companyId) throws UmbrellaException {
try {
var rs = Query.select(ALL).from(TABLE_DOCUMENT_TYPES).exec(db);
var types = new HashMap<Integer,Type>();
while (rs.next()) types.put(rs.getInt(ID),toType(rs));
rs.close();
rs = Query.select(ALL).from(TABLE_DOCUMENTS).where(FIELD_COMPANY_ID,equal(companyId)).exec(db);
var map = new HashMap<Long,Document>();
while (rs.next()) map.put(rs.getLong(ID),toDoc(rs,types));
rs.close();
rs = Query.select(ALL).from(TABLE_POSITIONS).where(FIELD_DOCUMENT_ID,in(map.keySet().toArray())).exec(db);
while (rs.next()){
var docId = rs.getLong(FIELD_DOCUMENT_ID);
var position = toPosition(rs);
map.get(docId).positions().add(position);
position.clean();
}
rs.close();
return map;
} catch (SQLException e) {
LOG.log(WARNING,"Failed to read list of documents for company {0}.",companyId,e);
throw new UmbrellaException(500,"Failed to read list of documents for company {0}.",companyId).causedBy(e);
}
}
@Override
public HashMap<Integer, Type> listTypes() throws UmbrellaException {
try {
var rs = Query.select(ALL).from(TABLE_DOCUMENT_TYPES).exec(db);
var types = new HashMap<Integer,Type>();
while (rs.next()) types.put(rs.getInt(ID),toType(rs));
rs.close();
return types;
} catch (SQLException e) {
LOG.log(WARNING,"Failed to read list of document types.",e);
throw new UmbrellaException(500,"Failed to read list of document types.").causedBy(e);
}
}
@Override
public Document loadDoc(long docId) throws UmbrellaException {
try {
var rs = Query.select(ALL).from(TABLE_DOCUMENT_TYPES).exec(db);
var types = new HashMap<Integer,Type>();
while (rs.next()) types.put(rs.getInt(ID),toType(rs));
rs.close();
rs = Query.select(ALL).from(TABLE_DOCUMENTS).leftJoin(FIELD_TEMPLATE_ID,TABLE_TEMPLATES, ID).where(TABLE_DOCUMENTS+"."+ ID,equal(docId)).exec(db);
Document doc = null;
while (rs.next()) doc = toDoc(rs,types);
rs.close();
if (doc != null) {
var positions = doc.positions();
rs = Query.select(ALL).from(TABLE_POSITIONS).where(FIELD_DOCUMENT_ID, equal(docId)).exec(db);
while (rs.next()) {
var position = toPosition(rs);
positions.add(position);
position.clean();
}
rs.close();
return doc;
}
} catch (SQLException e) {
LOG.log(WARNING,"Failed to load document {0}.",docId,e);
}
throw new UmbrellaException(500,"Failed to load document {0}.",docId);
}
@Override
public void rollback(long companyId, Type type) throws UmbrellaException {
try {
var settings = getCompanySettings(companyId,type);
var numbers = new HashSet<String>();
var rs = select(NUMBER).from(TABLE_DOCUMENTS).where(FIELD_COMPANY_ID,equal(companyId)).exec(db);
while (rs.next()) numbers.add(rs.getString(NUMBER));
rs.close();
var previous = settings.previous();
while (previous.isPresent() && !numbers.contains(previous.get().nextDocId())) previous = previous.get().previous();
previous.ifPresent(this::step);
} catch (SQLException e){
// TODO
}
}
@Override
public Document save(Document doc) throws UmbrellaException {
if (doc.isNew()) try {
var timestamp = doc.date().atStartOfDay(UTC).toInstant().getEpochSecond();
var sender = doc.sender();
var custom = doc.customer();
var stmt = insertInto(TABLE_DOCUMENTS,FIELD_TYPE_ID,FIELD_COMPANY_ID, DATE, FIELD_DELIVERY_DATE,FIELD_FOOTER,FIELD_HEAD, NUMBER, STATE, SENDER,FIELD_TAX_NUMBER,FIELD_BANK_ACCOUNT,FIELD_COURT,FIELD_CUSTOMER,FIELD_CUSTOMER_EMAIL,FIELD_CUSTOMER_NUMBER,FIELD_CUSTOMER_TAX_NUMBER,FIELD_TEMPLATE_ID,FIELD_CURRENCY)
.values(doc.type().id(),doc.companyId(),timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber(),doc.template().id(),doc.currency())
.execute(db);
var rs = stmt.getGeneratedKeys();
Long newId = null;
if (rs.next()) newId = rs.getLong(1);
rs.close();
stmt.close();
sender.clean();
custom.clean();
doc.clean();
if (newId == null) throw new UmbrellaException(500,"Failed to save new document");
return loadDoc(newId);
} catch (Exception e) {
throw new UmbrellaException(500,"Failed to update document ({0}) in database",doc.number()).causedBy(e);
}
if (doc.isDirty()) try {
var timestamp = doc.date().atStartOfDay(UTC).toInstant().getEpochSecond();
var sender = doc.sender();
var custom = doc.customer();
update(TABLE_DOCUMENTS)
.set(DATE, FIELD_DELIVERY_DATE,FIELD_FOOTER,FIELD_HEAD, NUMBER, STATE, SENDER,FIELD_TAX_NUMBER,FIELD_BANK_ACCOUNT,FIELD_COURT,FIELD_CUSTOMER,FIELD_CUSTOMER_EMAIL,FIELD_CUSTOMER_NUMBER,FIELD_CUSTOMER_TAX_NUMBER)
.where(ID,equal(doc.id()))
.prepare(db)
.apply(timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber())
.close();
sender.clean();
custom.clean();
doc.clean();
} catch (Exception e) {
throw new UmbrellaException(500,"Failed to update document ({0}) in database",doc.number()).causedBy(e);
}
if (doc.positions().isDirty()) try {
for (var entry : doc.positions().entrySet()){
var pos = entry.getValue();
if (pos.isNew()){
insertInto(TABLE_POSITIONS,FIELD_DOCUMENT_ID,FIELD_POS, FIELD_ITEM_CODE,FIELD_AMOUNT, DESCRIPTION, TITLE,FIELD_UNIT,FIELD_PRICE,FIELD_TAX,FIELD_TIME_ID, OPTIONAL)
.values(pos.docId(),pos.num(),pos.itemCode(),pos.amount(),pos.description(),pos.title(),pos.unit(),pos.unitPrice(),pos.tax(),pos.timeId(),pos.optional())
.execute(db)
.close();
pos.clean();
}
if (pos.isDirty()){
update(TABLE_POSITIONS)
.set(FIELD_ITEM_CODE,FIELD_AMOUNT, DESCRIPTION, TITLE,FIELD_UNIT,FIELD_PRICE,FIELD_TAX,FIELD_TIME_ID, OPTIONAL)
.where(FIELD_DOCUMENT_ID,equal(doc.id()))
.where(FIELD_POS,equal(pos.num())).prepare(db)
.apply(pos.itemCode(),pos.amount(),pos.description(),pos.title(),pos.unit(),pos.unitPrice(),pos.tax(),pos.timeId(),pos.optional())
.close();
if (pos.isDirty(FIELD_UNIT_PRICE)) saveCustomerPrice(doc.companyId(),doc.customer().id(),pos.itemCode(),pos.unitPrice());
pos.clean();
}
}
} catch (Exception e) {
throw new UmbrellaException(500,"Failed to save positions of document ({0}) in database",doc.number()).causedBy(e);
}
return doc;
}
@Override
public CustomerSettings save(long companyId, Type docType, String customerId, CustomerSettings settings) throws UmbrellaException {
try {
replaceInto(TABLE_CUSTOMER_SETTINGS,FIELD_COMPANY_ID,FIELD_DOC_TYPE_ID,FIELD_CUSTOMER_NUMBER,FIELD_DEFAULT_HEADER,FIELD_DEFAULT_FOOTER,FIELD_DEFAULT_MAIL)
.values(companyId,docType.id(),customerId,settings.header(),settings.footer(),settings.mailText())
.execute(db)
.close();
return settings;
} catch (SQLException e){
throw new UmbrellaException(500,"Failed to update customer settings!").causedBy(e);
}
}
private void saveCustomerPrice(long company, String customer, String item, long price) {
try {
var rs = select(FIELD_PRICE).from(TABLE_PRICES).where(FIELD_COMPANY_ID,equal(company)).where(FIELD_CUSTOMER_NUMBER,equal(customer)).where(FIELD_ITEM_CODE,equal(item)).exec(db);
Long oldPrice = null;
if (rs.next()) oldPrice = rs.getLong(FIELD_PRICE);
rs.close();
if (oldPrice == null) {
insertInto(TABLE_PRICES, FIELD_COMPANY_ID, FIELD_CUSTOMER_NUMBER, FIELD_ITEM_CODE, FIELD_PRICE).values(company, customer, item, price).execute(db).close();
} else {
update(TABLE_PRICES).where(FIELD_COMPANY_ID,equal(company)).where(FIELD_CUSTOMER_NUMBER,equal(customer)).where(FIELD_ITEM_CODE,equal(item))
.set(FIELD_PRICE).prepare(db).apply(price).close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void step(CompanySettings settings) {
try {
update(TABLE_COMPANY_SETTINGS)
.set(FIELD_TYPE_NUMBER)
.where(FIELD_COMPANY_ID,equal(settings.companyId())).where(FIELD_DOC_TYPE_ID,equal(settings.typeId()))
.prepare(db)
.apply(settings.typeNumber()+1)
.close();
} catch (SQLException e) {
LOG.log(WARNING,"Failed to increment doc number");
}
}
@Override
public Pair<Integer> switchPositions(long docId, Pair<Integer> pair) throws UmbrellaException {
try {
db.setAutoCommit(false);
update(TABLE_POSITIONS).set(FIELD_POS).where(FIELD_DOCUMENT_ID,equal(docId)).where(FIELD_POS,equal(pair.left())).prepare(db).apply(-pair.right()).close();
update(TABLE_POSITIONS).set(FIELD_POS).where(FIELD_DOCUMENT_ID,equal(docId)).where(FIELD_POS,equal(pair.right())).prepare(db).apply(pair.left()).close();
update(TABLE_POSITIONS).set(FIELD_POS).where(FIELD_DOCUMENT_ID,equal(docId)).where(FIELD_POS,equal(-pair.right())).prepare(db).apply(pair.right()).close();
db.setAutoCommit(true);
} catch (SQLException e) {
LOG.log(ERROR,"Failed to switch positions {0} and {1} of document {2}",pair.left(),pair.right(),docId);
throw new UmbrellaException(500,"Failed to switch positions {0} and {1} of document {2}",pair.left(),pair.right(),docId).causedBy(e);
}
return pair;
}
private Document toDoc(ResultSet rs, HashMap<Integer, Type> types) throws SQLException {
var id = rs.getLong(ID);
var typeId = rs.getInt(FIELD_TYPE_ID);
var type = types.get(typeId);
var company = rs.getLong(FIELD_COMPANY_ID);
var number = rs.getString(NUMBER);
var timestamp = rs.getLong(DATE);
var date = timestamp > 0 ? Instant.ofEpochSecond(timestamp).atOffset(UTC).toLocalDate() : null;
var state = rs.getInt(STATE);
var delivery = rs.getString(FIELD_DELIVERY_DATE);
var head = rs.getString(FIELD_HEAD);
var footer = rs.getString(FIELD_FOOTER);
var currency = rs.getString(FIELD_CURRENCY);
var senderName = rs.getString(SENDER);
var taxNumber = rs.getString(FIELD_TAX_NUMBER);
var bankAccount = rs.getString(FIELD_BANK_ACCOUNT);
var court = rs.getString(FIELD_COURT);
var customerName = rs.getString(FIELD_CUSTOMER);
var customerId = rs.getString(FIELD_CUSTOMER_NUMBER);
var customerTaxNumber = rs.getString(FIELD_CUSTOMER_TAX_NUMBER);
var customerEmail = rs.getString(FIELD_CUSTOMER_EMAIL);
var customer = new Customer(customerId, customerName, customerEmail, customerTaxNumber);
var sender = new Sender(senderName,bankAccount,taxNumber,court);
var template = toTemplate(rs);
return new Document(id,company,number,type,date, Document.State.of(state).orElse(State.ERROR),template,delivery,head,footer,currency, DEFAULT_THOUSANDS_SEPARATOR,sender,customer,new PositionList());
}
private Template toTemplate(ResultSet rs) throws SQLException {
try {
var id = rs.getLong(FIELD_TEMPLATE_ID);
var company = rs.getLong(FIELD_COMPANY_ID);
var name = rs.getString(NAME);
var data = rs.getBytes(TEMPLATE);
return new Template(id,company,name,data);
} catch (SQLException ignored){
return null;
}
}
private Position toPosition(ResultSet rs) throws SQLException {
var num = rs.getInt(FIELD_POS);
var itemCode = rs.getString(FIELD_ITEM_CODE);
var amount = rs.getDouble(FIELD_AMOUNT);
var unit = rs.getString(FIELD_UNIT);
var title = rs.getString(TITLE);
var description = rs.getString(DESCRIPTION);
var unitPrice = rs.getLong(FIELD_PRICE);
var tax = rs.getInt(FIELD_TAX);
var timeId = rs.getLong(FIELD_TIME_ID);
var optional = rs.getBoolean(OPTIONAL);
return new Position(num,itemCode,amount,unit,title,description,unitPrice,tax,timeId,optional);
}
private Type toType(ResultSet rs) throws SQLException {
return new Type(rs.getInt(ID),rs.getInt(FIELD_NEXT_TYPE),rs.getString(NAME));
}
}

View File

@@ -0,0 +1,34 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.documents.Constants.*;
import de.srsoftware.umbrella.core.UmbrellaException;
import org.json.JSONException;
import org.json.JSONObject;
public record Company(long id, String name, String address, String court, String taxId, String phone, String decimalSeparator, String thousandsSeparator, long lastCustomerNumber, int decimals, String customerNumberPrefix, String currency, String email, String bankAccount) {
public static Company of(JSONObject json) throws UmbrellaException {
try {
var id = json.getLong(ID);
var name = json.getString(NAME);
var address = json.getString(ADDRESS);
var court = json.getString(FIELD_COURT);
var taxId = json.getString(FIELD_TAX_NUMBER);
var phone = json.getString(FIELD_PHONE);
var decimalSep = json.getString(DECIMAL_SEPARATOR);
var thousandsSep = json.getString(THOUSANDS_SEPARATOR);
var lastCustomerNumber = json.getLong(LAST_CUSTOMER_NUMBER);
var decimals = json.getInt(DECIMALS);
var customerNumberPrefix = json.getString(CUSTOMER_NUMBER_PREFIX);
var currency = json.getString(FIELD_CURRENCY);
var email = json.getString(EMAIL);
var bankAccount = json.getString(FIELD_BANK_ACCOUNT);
return new Company(id,name,address,court,taxId,phone,decimalSep,thousandsSep,lastCustomerNumber,decimals,customerNumberPrefix,currency,email,bankAccount);
} catch (JSONException e){
throw new UmbrellaException(500,"Failed to convert JSON to Company!").causedBy(e);
}
}
}

View File

@@ -0,0 +1,30 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.tools.Optionals.emptyIfNull;
import static de.srsoftware.umbrella.documents.Constants.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
public record CompanySettings(long companyId, long typeId, String typePrefix, String typeSuffix, long typeNumber) {
public String nextDocId(){
return typePrefix+typeNumber+typeSuffix;
}
public static CompanySettings of(ResultSet rs) throws SQLException {
var companyId = rs.getLong(FIELD_COMPANY_ID);
var typeId = rs.getLong(FIELD_DOC_TYPE_ID);
var typePrefix = emptyIfNull(rs.getString(FIELD_TYPE_PREFIX));
var typeSuffix = emptyIfNull(rs.getString(FIELD_TYPE_SUFFIX));
var typeNumber = rs.getLong(FIELD_TYPE_NUMBER);
return new CompanySettings(companyId,typeId,typePrefix,typeSuffix,typeNumber);
}
public Optional<CompanySettings> previous(){
if (typeNumber<1) return Optional.empty();
return Optional.of(new CompanySettings(companyId,typeId,typePrefix,typeSuffix,typeNumber-1));
}
}

View File

@@ -0,0 +1,99 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.documents.Constants.FIELD_TAX_ID;
import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.UmbrellaException;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.json.JSONObject;
public final class Customer implements Mappable {
private String id;
private String name;
private String email;
private String taxNumber;
private final Set<String> dirtyFields = new HashSet<>();
public Customer(String id, String name, String email, String taxNumber) {
this.id = id;
this.name = name;
this.email = email;
this.taxNumber = taxNumber;
}
public void clean() {
dirtyFields.clear();
}
public String email() {
return email;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Customer) obj;
return Objects.equals(this.id, that.id) &&
Objects.equals(this.name, that.name) &&
Objects.equals(this.email, that.email) &&
Objects.equals(this.taxNumber, that.taxNumber);
}
@Override
public int hashCode() {
return Objects.hash(id, name, email, taxNumber);
}
public String id() {
return id;
}
public boolean isDirty() {
return !dirtyFields.isEmpty();
}
public String name() {
return name;
}
public static Customer of(JSONObject json) throws UmbrellaException {
if (!json.has(ID) || !(json.get(ID) instanceof String id)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,ID);
if (!json.has(NAME) || !(json.get(NAME) instanceof String name)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,NAME);
if (!json.has(EMAIL) || !(json.get(EMAIL) instanceof String email)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,EMAIL);
if (!json.has(FIELD_TAX_ID) || !(json.get(FIELD_TAX_ID) instanceof String taxId)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,FIELD_TAX_ID);
return new Customer(id,name,email,taxId);
}
public void patch(JSONObject json) {
for (var key : json.keySet()){
switch (key){
case ID: id = json.getString(key); break;
case NAME: name = json.getString(key); break;
case EMAIL: email = json.getString(key); break;
case FIELD_TAX_ID: taxNumber = json.getString(key); break;
default: key = null;
}
if (key != null) dirtyFields.add(key);
}
}
public String taxNumber() {
return taxNumber;
}
@Override
public Map<String, Object> toMap() {
return Map.of(
"id", id,
"name", name,
"email", email,
"tax_id", taxNumber
);
}
}

View File

@@ -0,0 +1,17 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.umbrella.documents.Constants.*;
import java.sql.ResultSet;
import java.sql.SQLException;
public record CustomerSettings(String header, String footer, String mailText) {
public static CustomerSettings of(ResultSet rs) throws SQLException {
var header = rs.getString(FIELD_DEFAULT_HEADER);
var footer = rs.getString(FIELD_DEFAULT_FOOTER);
var mailText = rs.getString(FIELD_DEFAULT_MAIL);
return new CustomerSettings(header,footer,mailText);
}
}

View File

@@ -0,0 +1,327 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Util.markdown;
import static de.srsoftware.umbrella.documents.Constants.*;
import static de.srsoftware.umbrella.documents.Constants.FIELD_CUSTOMER;
import static java.text.MessageFormat.format;
import static java.util.Optional.empty;
import de.srsoftware.tools.Mappable;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import org.json.JSONObject;
public final class Document implements Mappable {
public static final String DEFAULT_THOUSANDS_SEPARATOR = ".";
public enum State {
NEW(1), SENT(2), DELAYED(3), PAYED(4), DECLINED(5), ERROR(99);
private final int code;
State(int code) {
this.code = code;
}
public int code() {
return code;
}
public static Map<Integer, Object> names() {
return Arrays.stream(values()).collect(Collectors.toMap(state -> state.code, state -> Map.of("name", state.toString(), "display", "<? " + state + " ?>")));
}
public static Optional<State> of(int code) {
for (var s : State.values()) {
if (s.code == code) return Optional.of(s);
}
return empty();
}
@Override
public String toString() {
return name().toLowerCase();
}
}
private final long companyId, id;
private String companyName, currency, decimalSeparator, delivery, footer, head, number;
private final Type type;
private LocalDate date;
private State state;
private final Template template;
private final Sender sender;
private final Customer customer;
private final PositionList positions;
private final Set<String> dirtyFields = new HashSet<>();
public Document(long id, long companyId, String number, Type type, LocalDate date, State state, Template template, String delivery, String head, String footer, String currency, String thousandsSeparator, Sender sender, Customer customer, PositionList positions) {
this.id = id;
this.companyId = companyId;
this.number = number;
this.type = type;
this.date = date;
this.state = state;
this.template = template;
this.delivery = delivery;
this.head = head;
this.footer = footer;
this.currency = currency;
this.sender = sender;
this.customer = customer;
this.positions = positions;
this.decimalSeparator = thousandsSeparator;
positions.setDocId(id);
if (id == 0) dirtyFields.add(ID);
}
public void clean() {
dirtyFields.clear();
}
public long companyId() {
return companyId;
}
public String currency() {
return currency;
}
public Customer customer() {
return customer;
}
public LocalDate date() {
return date;
}
public String decimalSeparator() {
return decimalSeparator;
}
public String delivery() {
return delivery;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Document) obj;
return this.id == that.id &&
this.companyId == that.companyId &&
Objects.equals(this.number, that.number) &&
Objects.equals(this.type, that.type) &&
Objects.equals(this.date, that.date) &&
Objects.equals(this.state, that.state) &&
Objects.equals(this.template, that.template) &&
Objects.equals(this.delivery, that.delivery) &&
Objects.equals(this.head, that.head) &&
Objects.equals(this.footer, that.footer) &&
Objects.equals(this.currency, that.currency) &&
Objects.equals(this.sender, that.sender) &&
Objects.equals(this.customer, that.customer) &&
Objects.equals(this.positions, that.positions);
}
public String footer() {
return footer;
}
public long grossSum() {
var taxMap = new HashMap<Integer, Double>();
for (var pos : positions.values()) {
var taxRate = pos.tax();
var sum = taxMap.get(taxRate);
if (sum == null) sum = 0d;
sum += pos.unitPrice() * pos.amount();
taxMap.put(taxRate, sum);
}
var sum = 0d;
for (var entry : taxMap.entrySet()) {
var taxRate = entry.getKey();
var taxSum = entry.getValue();
sum += taxSum * (100 + taxRate) / 100d;
}
return Math.round(sum);
}
@Override
public int hashCode() {
return Objects.hash(id, companyId, number, type, date, state, template, delivery, head, footer, currency, sender, customer, positions);
}
public String head() {
return head;
}
public long id() {
return id;
}
public boolean isDirty() {
return !dirtyFields.isEmpty() || sender.isDirty() || customer.isDirty();
}
public boolean isNew(){
return dirtyFields.contains(ID);
}
public long netSum() {
var sum = 0d;
for (var pos : positions.values()) sum += pos.unitPrice() * pos.amount();
return Math.round(sum);
}
public String number() {
return number;
}
public void patch(JSONObject json) {
for (var key : json.keySet()){
switch (key){
case FIELD_CUSTOMER: if (json.get(key) instanceof JSONObject nested) customer.patch(nested); break;
case FIELD_CURRENCY: currency = json.getString(key); break;
case DATE: date = LocalDate.parse(json.getString(key)); break;
case FIELD_DELIVERY: delivery = json.getString(key); break;
case FIELD_FOOTER: footer = json.getString(key); break;
case FIELD_HEAD: head = json.getString(key); break;
case NUMBER: number = json.getString(key); break;
case SENDER: if (json.get(key) instanceof JSONObject nested) sender.patch(nested); break;
default: key = null;
}
if (key != null) dirtyFields.add(key);
}
}
public PositionList positions() {
return positions;
}
public Map<String, Object> renderToMap() {
var map = new HashMap<String, Object>();
map.put(ID, id);
map.put(FIELD_COMPANY, Map.of(ID,companyId, NAME,companyName));
map.put(NUMBER, number);
map.put(FIELD_TYPE, format("<? {0} ?>", type.name()));
map.put(DATE, date);
map.put(STATE, state.code);
map.put(FIELD_DELIVERY, delivery);
map.put(FIELD_HEAD, markdown(head));
map.put(FIELD_FOOTER, markdown(footer));
map.put(FIELD_CURRENCY, currency);
map.put(SENDER, sender.toMap());
map.put(FIELD_CUSTOMER, customer.toMap());
map.put(FIELD_POSITIONS, positions.asMap(true));
map.put("taxes",positions.taxNetSums(true));
map.put(FIELD_NET_SUM, netSum());
map.put(FIELD_GROSS_SUM, grossSum());
if (template != null) map.put("template", template.toMap());
return map;
}
public Sender sender() {
return sender;
}
public Document set(State newState) {
state = newState;
dirtyFields.add(STATE);
return this;
}
/**
* alter the separator for decimals
* @param newValue new value for the separator
* @return this document instance
*/
public Document setDecimalSeparator(String newValue) {
decimalSeparator = newValue;
return this;
}
/**
* set the company name
* @param newValue new value for the company name
* @return this document instance
*/
public Document setCompanyName(String newValue){
companyName = newValue;
return this;
}
public State state() {
return state;
}
public Map<String, Object> summary() {
return Map.of(
ID, id,
NUMBER, number,
"type", format("<? {0} ?>", type.name()),
STATE, Map.of(NAME,format("<? {0} ?>", state),ID,state.code),
DATE, date,
FIELD_CURRENCY, currency,
FIELD_CUSTOMER, customer.toMap(),
"sum", positions.grossSum(true)
);
}
public Template template() {
return template;
}
@Override
public Map<String, Object> toMap() {
var map = new HashMap<String, Object>();
map.put(ID, id);
map.put(FIELD_COMPANY, companyId);
map.put(NUMBER, number);
map.put(FIELD_TYPE, type);
map.put(DATE, date);
map.put(STATE, state.code);
map.put(FIELD_DELIVERY, delivery);
map.put(FIELD_HEAD, head);
map.put(FIELD_FOOTER, footer);
map.put(FIELD_CURRENCY, currency);
map.put(SENDER, sender.toMap());
map.put(FIELD_CUSTOMER, customer.toMap());
map.put(FIELD_POSITIONS, positions.asMap(false));
map.put("taxes",positions.taxNetSums(true));
map.put(FIELD_NET_SUM, netSum());
map.put(FIELD_GROSS_SUM, grossSum());
if (template != null) map.put("template", template.toMap());
return map;
}
@Override
public String toString() {
return "Document[" +
"id=" + id + ", " +
"company=" + companyId + ", " +
"number=" + number + ", " +
"type=" + type + ", " +
"date=" + date + ", " +
"state=" + state + ", " +
"template=" + template + ", " +
"delivery=" + delivery + ", " +
"head=" + head + ", " +
"footer=" + footer + ", " +
"currency=" + currency + ", " +
"sender=" + sender + ", " +
"customer=" + customer + ", " +
"positions=" + positions + ']';
}
public Type type() {
return type;
}
}

View File

@@ -0,0 +1,231 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Util.markdown;
import static de.srsoftware.umbrella.documents.Constants.*;
import de.srsoftware.tools.Mappable;
import java.util.*;
import org.json.JSONObject;
public final class Position implements Mappable {
private long docId;
private final int num;
private String itemCode;
private double amount;
private String unit;
private String title;
private String description;
private long unitPrice;
private int tax;
private final Long timeId;
private boolean optional;
private Set<String> dirtyFields = new HashSet<>();
public Position(int num, String itemCode, double amount, String unit, String title, String description, long unitPrice, int tax, Long timeId, boolean optional) {
this.num = num;
this.itemCode = itemCode;
this.amount = amount;
this.unit = unit;
this.title = title;
this.description = description;
this.unitPrice = unitPrice;
this.tax = tax;
this.timeId = timeId;
this.optional = optional;
}
public double amount() {
return amount;
}
public void clean() {
dirtyFields.clear();
}
public String description() {
return description;
}
public long docId() {
return docId;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Position) obj;
return this.docId == that.docId &&
this.num == that.num &&
Objects.equals(this.itemCode, that.itemCode) &&
Double.doubleToLongBits(this.amount) == Double.doubleToLongBits(that.amount) &&
Objects.equals(this.unit, that.unit) &&
Objects.equals(this.title, that.title) &&
Objects.equals(this.description, that.description) &&
this.unitPrice == that.unitPrice &&
this.tax == that.tax &&
this.timeId == that.timeId &&
this.optional == that.optional;
}
public long grossPrice() {
var taxRate = (100 + tax) / 100d;
var sum = unitPrice * amount * taxRate;
return Math.round(sum);
}
@Override
public int hashCode() {
return Objects.hash(docId, num, itemCode, amount, unit, title, description, unitPrice, tax, timeId, optional);
}
public boolean isDirty() {
return !dirtyFields.isEmpty();
}
public boolean isDirty(String field){
return dirtyFields.contains(field);
}
public boolean isNew(){
return dirtyFields.contains(FIELD_DOCUMENT_ID);
}
public String itemCode() {
return itemCode;
}
public long netPrice() {
return Math.round(amount * unitPrice);
}
public int num() {
return num;
}
public boolean optional() {
return optional;
}
public void patch(JSONObject json) {
for (var key : json.keySet()) {
switch (key) {
case FIELD_AMOUNT: amount = json.getDouble(key); break;
case DESCRIPTION: description = json.getString(key); break;
case FIELD_ITEM: itemCode = json.getString(key); break;
case OPTIONAL: optional = json.getBoolean(key); break;
case FIELD_TAX: tax = json.getInt(key); break;
case TITLE: title = json.getString(key); break;
case FIELD_UNIT: unit = json.getString(key); break;
case FIELD_UNIT_PRICE: unitPrice = json.getLong(key); break;
default: key = null;
}
if (key != null) dirtyFields.add(key);
}
}
public Position setDocId(long docId) {
this.docId = docId;
dirtyFields.add(FIELD_DOCUMENT_ID);
return this;
}
public Position setItemCode(String newValue) {
this.itemCode = newValue;
dirtyFields.add(FIELD_ITEM);
return this;
}
public Position setUnit(String newValue) {
unit = newValue;
dirtyFields.add(FIELD_UNIT);
return this;
}
public void setUnitPrice(long price) {
unitPrice = price;
dirtyFields.add(FIELD_UNIT_PRICE);
}
public int tax() {
return tax;
}
public long taxAmount() {
var taxRate = tax / 100d;
var tax = unitPrice * amount * taxRate;
return Math.round(tax);
}
public Long timeId() {
return timeId;
}
public String title() {
return title;
}
@Override
public Map<String, Object> toMap() {
var map = new HashMap<String, Object>();
map.put(FIELD_DOCUMENT_ID, docId);
map.put(NUMBER, num);
map.put(FIELD_ITEM, itemCode);
map.put(FIELD_AMOUNT, amount);
map.put(FIELD_UNIT, unit);
map.put(TITLE, title);
map.put(DESCRIPTION, description);
map.put(FIELD_UNIT_PRICE, unitPrice);
map.put(FIELD_TAX, tax);
map.put(FIELD_TIME_ID, timeId);
map.put(OPTIONAL, optional);
map.put(FIELD_NET_PRICE, netPrice());
return map;
}
public Map<String, Object> renderToMap() {
var map = new HashMap<String, Object>();
map.put(FIELD_DOCUMENT_ID, docId);
map.put(NUMBER, num);
map.put(FIELD_ITEM, itemCode);
map.put(FIELD_AMOUNT, amount);
map.put(FIELD_UNIT, unit);
map.put(TITLE, title);
map.put(DESCRIPTION, markdown(description));
map.put(FIELD_UNIT_PRICE, unitPrice);
map.put(FIELD_TAX, tax);
map.put(FIELD_TIME_ID, timeId);
map.put(OPTIONAL, optional);
map.put(FIELD_NET_PRICE, netPrice());
return map;
}
@Override
public String toString() {
return "Position[" +
"docId=" + docId + ", " +
"num=" + num + ", " +
"itemCode=" + itemCode + ", " +
"amount=" + amount + ", " +
"unit=" + unit + ", " +
"title=" + title + ", " +
"description=" + description + ", " +
"unitPrice=" + unitPrice + ", " +
"tax=" + tax + ", " +
"timeId=" + timeId + ", " +
"optional=" + optional + ']';
}
public String unit() {
return unit;
}
public long unitPrice() {
return unitPrice;
}
}

View File

@@ -0,0 +1,148 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.documents.Constants.*;
import de.srsoftware.tools.Pair;
import de.srsoftware.umbrella.core.UmbrellaException;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONObject;
public class PositionList extends HashMap<Integer,Position> {
private long docId;
public PositionList add(Position pos) {
put(pos.num(),pos);
pos.setDocId(docId);
return this;
}
public Position addItem(JSONObject item) {
var itemCode = item.getString(FIELD_CODE);
var unit = item.getString(FIELD_UNIT);
var name = item.getString(NAME);
var description = item.getString(DESCRIPTION);
var unitPrice = item.getLong(FIELD_UNIT_PRICE);
var tax = item.getInt(FIELD_TAX);
var position = new Position(size()+1,itemCode,1.0,unit,name,description,unitPrice,tax,null,false);
add(position);
return position;
}
public Position addTask(JSONObject task) {
var amount = task.getLong("est_time");
var name = task.getString(NAME);
var description = task.getString(DESCRIPTION);
var unitPrice = 0;
var tax = 19; // TODO: make adjustable
var position = new Position(size()+1,null,amount,null,name,description,unitPrice,tax,null,false);
add(position);
return position;
}
public Position addTime(JSONObject time) {
var id = time.getLong(ID);
var start = time.getLong(FIELD_START_TIME);
var end = time.getLong(FIELD_END_TIME);
var amount = (end - start)/3600d;
var name = time.getString(SUBJECT);
var desc = time.getString(DESCRIPTION).trim();
var description = new StringBuilder();
if (!desc.isEmpty()) description.append(desc);
if (time.has(FIELD_TASKS)){
var tasks = time.getJSONObject(FIELD_TASKS);
var taskKeys = tasks.keySet();
if (taskKeys.size()>1) { // description normally came from first task
if (!desc.isEmpty()) description.append("\n\n");
for (var taskId : tasks.keySet()) {
var task = tasks.getJSONObject(taskId);
description.append("* ").append(task.getString(NAME)).append("\n");
}
}
}
var unitPrice = 0;
var tax = 19; // TODO: make adjustable
var position = new Position(size()+1,null,amount,null,name,description.toString(),unitPrice,tax,id,false);
add(position);
return position;
}
public Map<Integer,Object> asMap(boolean renderMarkdown) {
var map = new HashMap<Integer,Object>();
for (var entry : entrySet()) {
var pos = entry.getValue();
map.put(entry.getKey(),renderMarkdown ? pos.renderToMap() : pos.toMap());
}
return map;
}
public Pair<Integer> getMoveablePair(JSONObject data) throws UmbrellaException {
if (!data.has("pos")) throw new UmbrellaException(400,"Missing nested value move.pos!");
if (!data.has("step")) throw new UmbrellaException(400,"Missing nested value move.step!");
if (!(data.get("pos") instanceof Number pos)) throw new UmbrellaException(400,"Invalid value for move.pos");
if (!(data.get("step") instanceof Number step)) throw new UmbrellaException(400,"Invalid value for move.step");
int one = pos.intValue();
int other = one + (step.longValue() < 0 ? -1 : 1);
if (!containsKey(one) || !containsKey(other)) throw new UmbrellaException(400,"Invalid value for move.pos");
return new Pair<>(one,other);
}
public long grossSum(boolean includeOptionals) {
var gross = netSum(includeOptionals);
for (var taxSum : taxNetSums(includeOptionals).entrySet()) {
var percent = taxSum.getKey();
var netSum = taxSum.getValue();
gross += Math.round(netSum * percent / 100d);
}
return gross;
}
public boolean isDirty() {
return values().stream().anyMatch(Position::isDirty);
}
public long netSum(boolean includeOptionals) {
long sum = 0;
for (var pos : values()){
if (includeOptionals || !pos.optional()) sum += pos.netPrice();
}
return sum;
}
public void patch(JSONObject json) {
for (var key : json.keySet()){
try {
var pos = Integer.parseInt(key);
if (get(pos) instanceof Position position && json.get(key) instanceof JSONObject data) position.patch(data);
} catch (NumberFormatException ignored){}
}
}
public void setDocId(long id) {
docId = id;
}
/**
* Map from Percent to net sums of relevant positions
* @param includeOptionals
* @return
*/
public Map<Integer,Long> taxNetSums(boolean includeOptionals){
var netSums = new HashMap<Integer,Long>();
for (var pos : values()){
if (includeOptionals || !pos.optional()) {
var tax = pos.tax();
var netSumForTax = netSums.get(tax);
if (netSumForTax == null) netSumForTax = 0L;
netSumForTax += pos.netPrice();
netSums.put(tax, netSumForTax);
}
}
return netSums;
}
}

View File

@@ -0,0 +1,111 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.umbrella.core.Constants.ERROR_MISSING_FIELD;
import static de.srsoftware.umbrella.core.Constants.NAME;
import static de.srsoftware.umbrella.documents.Constants.*;
import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.UmbrellaException;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.json.JSONObject;
public final class Sender implements Mappable {
private String name;
private String bankAccount;
private String taxNumber;
private String court;
private final Set<String> dirtyFields = new HashSet<>();
public Sender(String name, String bankAccount, String taxNumber, String court) {
this.name = name;
this.bankAccount = bankAccount;
this.taxNumber = taxNumber;
this.court = court;
}
public String bankAccount() {
return bankAccount;
}
public void clean() {
dirtyFields.clear();
}
public String court() {
return court;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Sender) obj;
return Objects.equals(this.name, that.name) &&
Objects.equals(this.bankAccount, that.bankAccount) &&
Objects.equals(this.taxNumber, that.taxNumber) &&
Objects.equals(this.court, that.court);
}
@Override
public int hashCode() {
return Objects.hash(name, bankAccount, taxNumber, court);
}
public boolean isDirty() {
return !dirtyFields.isEmpty();
}
public String name() {
return name;
}
public static Sender of(JSONObject json) throws UmbrellaException {
if (!json.has(NAME) || !(json.get(NAME) instanceof String name)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,NAME);
if (!json.has(FIELD_BANK_ACCOUNT) || !(json.get(FIELD_BANK_ACCOUNT) instanceof String bankAccount)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,FIELD_BANK_ACCOUNT);
if (!json.has(FIELD_TAX_ID) || !(json.get(FIELD_TAX_ID) instanceof String taxId)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,FIELD_TAX_ID);
if (!json.has(FIELD_COURT) || !(json.get(FIELD_COURT) instanceof String court)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,FIELD_COURT);
return new Sender(name,bankAccount,taxId,court);
}
public void patch(JSONObject json) {
for (var key : json.keySet()){
switch (key){
case FIELD_BANK_ACCOUNT: bankAccount = json.getString(key); break;
case NAME: name = json.getString(key); break;
case FIELD_COURT: court = json.getString(key); break;
case FIELD_TAX_ID: taxNumber = json.getString(key); break;
default: key = null;
}
if (key != null) dirtyFields.add(key);
}
}
public String taxNumber() {
return taxNumber;
}
@Override
public Map<String, Object> toMap() {
return Map.of(
FIELD_BANK_ACCOUNT, bankAccount,
FIELD_COURT, court,
NAME, name,
FIELD_TAX_ID, taxNumber
);
}
@Override
public String toString() {
return "Sender[" +
"name=" + name + ", " +
"bankAccount=" + bankAccount + ", " +
"taxNumber=" + taxNumber + ", " +
"court=" + court + ']';
}
}

View File

@@ -0,0 +1,16 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.umbrella.core.Constants.ID;
import static de.srsoftware.umbrella.core.Constants.NAME;
import static de.srsoftware.umbrella.documents.Constants.FIELD_COMPANY;
import de.srsoftware.tools.Mappable;
import java.util.Map;
public record Template(long id, long company, String name, byte[] data) implements Mappable {
@Override
public Map<String, Object> toMap() {
return Map.of(ID,id,FIELD_COMPANY,company, NAME,name);
}
}

View File

@@ -0,0 +1,6 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
public record Type(int id, int successor, String name) {}

View File

@@ -2,28 +2,68 @@
import { onMount } from 'svelte';
import { t } from '../../translations.svelte.js';
let company = null;
let error = null;
let companies = {};
let documents = null;
let name = null;
async function loadCompanies(){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/companies`;
var resp = await fetch(url,{ credentials: 'include'});
if (resp.ok){
companies = await resp.json();
console.log(companies);
} else {
error = await resp.text();
}
}
async function load(company){
name = company.name;
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/list`;
const resp = await fetch(url,{
credentials: 'include',
method: 'POST',
body: JSON.stringify({company:company.id})
});
if (resp.ok){
documents = await resp.json();
console.log(documents);
} else {
error = await resp.text();
}
}
onMount(loadCompanies);
</script>
<fieldset>
<legend>{t('documents.documents')}</legend>
<legend>{name ? t( 'document.list_of',name) : t('document.list')}</legend>
{#if error}
<div class="error">{error}</div>
{/if}
<div>
{t('documents.select_company')}
{t('document.select_company')}
{#each Object.entries(companies) as [id,company]}
<button onclick={() => load(company)}>{company.name}</button>
{/each}
</div>
{#if documents}
<table>
<thead>
</thead>
<tbody>
{#each Object.entries(documents) as [id,document]}
<tr>
<td>{id}</td>
<td>{document.number}</td>
<td>{document.date}</td>
<td>{document.customer.name.split('\n')[0]}</td>
<td>{document.sum/100 + document.currency}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</fieldset>

View File

@@ -1,4 +1,9 @@
{
"document": {
"list": "Dokumente",
"list_of": "Dokumente von {0}",
"select_company" : "Wählen Sie eine ihrer Firmen:"
},
"footer": {
"message" : "Umbrella ist ein Produkt von {0}."
},

View File

@@ -126,11 +126,10 @@ public class UserModule extends BaseHandler implements UserHelper {
}
}
private Optional<UmbrellaUser> loadUser(Optional<Token> sessionToken) throws UmbrellaException {
public Optional<UmbrellaUser> loadUser(Optional<Token> sessionToken) throws UmbrellaException {
if (sessionToken.isEmpty()) return empty();
var session = users.load(sessionToken.get());
return Optional.of(users.load(session));
}
public Optional<UmbrellaUser> loadUser(HttpExchange ex) throws UmbrellaException {