Compare commits

...

10 Commits

Author SHA1 Message Date
11c0983dac improved button colors
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-26 09:12:17 +01:00
c37df2ddec added priority colors for tasks in custom state
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-26 08:47:23 +01:00
93907a839d altered table update:
now retaining the template name values from the template column as new template values in the document table

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-25 15:22:55 +01:00
ccb84995cb document db no longer storing complete template information in separate table:
- dropped table templates
- altered table documents: template_id (ref into templates) → template (name of template)
- templates are now picked up by the document registry

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-25 10:21:36 +01:00
fad9c78f87 Merge remote-tracking branch 'origin/dev' into dev 2025-11-24 23:26:35 +01:00
2cd53b19cb Re-implemented guessing of next document id.
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-24 23:24:31 +01:00
288acd90f4 Merge branch 'main' into dev 2025-11-24 20:12:08 +01:00
0d202677ad Merge branch 'main' into dev 2025-11-24 16:54:43 +01:00
6bb03f4e04 implemented adding stock items to documents
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-24 12:49:24 +01:00
438d8d4aad preparing to make items available as document positions
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-24 09:16:28 +01:00
21 changed files with 234 additions and 257 deletions

View File

@@ -1,6 +1,7 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model; package de.srsoftware.umbrella.core.model;
import static de.srsoftware.tools.Optionals.emptyIfNull;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Field.*; import static de.srsoftware.umbrella.core.Field.*;
import static de.srsoftware.umbrella.core.Field.COMPANY; import static de.srsoftware.umbrella.core.Field.COMPANY;
@@ -57,14 +58,14 @@ public final class Document implements Mappable {
private final Type type; private final Type type;
private LocalDate date; private LocalDate date;
private State state; private State state;
private Template template; private String template;
private final Sender sender; private final Sender sender;
private final Customer customer; private final Customer customer;
private final PositionList positions; private final PositionList positions;
private final Set<String> dirtyFields = new HashSet<>(); 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 decimalSeparator, Sender sender, Customer customer, PositionList positions) { public Document(long id, long companyId, String number, Type type, LocalDate date, State state, String template, String delivery, String head, String footer, String currency, String decimalSeparator, Sender sender, Customer customer, PositionList positions) {
this.id = id; this.id = id;
this.companyId = companyId; this.companyId = companyId;
this.number = number; this.number = number;
@@ -206,7 +207,7 @@ public final class Document implements Mappable {
case SENDER: if (json.get(key) instanceof JSONObject nested) sender.patch(nested); break; case SENDER: if (json.get(key) instanceof JSONObject nested) sender.patch(nested); break;
case STATE: state = State.of(json.getInt(key)).orElseThrow(() -> new UmbrellaException(HTTP_UNPROCESSABLE,"Invalid state")); break; case STATE: state = State.of(json.getInt(key)).orElseThrow(() -> new UmbrellaException(HTTP_UNPROCESSABLE,"Invalid state")); break;
case POS: if (json.get(key) instanceof JSONObject nested) positions.patch(nested); break; case POS: if (json.get(key) instanceof JSONObject nested) positions.patch(nested); break;
case TEMPLATE_ID: if (json.get(key) instanceof Number num) template = new Template(num.longValue(),companyId,null,null); break; case TEMPLATE_ID: if (json.get(key) instanceof String templateId) template = templateId; break;
default: key = null; default: key = null;
} }
if (key != null) dirtyFields.add(key); if (key != null) dirtyFields.add(key);
@@ -225,7 +226,7 @@ public final class Document implements Mappable {
map.put(TYPE, type.name()); map.put(TYPE, type.name());
map.put(DATE, date); map.put(DATE, date);
map.put(STATE, state.code); map.put(STATE, state.code);
map.put(DELIVERY, delivery == null ? "" : delivery); map.put(DELIVERY, emptyIfNull(delivery));
map.put(HEAD, mapMarkdown(head)); map.put(HEAD, mapMarkdown(head));
map.put(FOOTER, mapMarkdown(footer)); map.put(FOOTER, mapMarkdown(footer));
map.put(CURRENCY, currency); map.put(CURRENCY, currency);
@@ -235,7 +236,7 @@ public final class Document implements Mappable {
map.put("taxes",positions.taxNetSums(true)); map.put("taxes",positions.taxNetSums(true));
map.put(NET_SUM, netSum()); map.put(NET_SUM, netSum());
map.put(GROSS_SUM, grossSum()); map.put(GROSS_SUM, grossSum());
if (template != null) map.put("template", template.toMap()); map.put("template", emptyIfNull(template));
return map; return map;
} }
@@ -286,7 +287,7 @@ public final class Document implements Mappable {
); );
} }
public Template template() { public String template() {
return template; return template;
} }
@@ -309,7 +310,7 @@ public final class Document implements Mappable {
map.put("taxes",positions.taxNetSums(true)); map.put("taxes",positions.taxNetSums(true));
map.put(NET_SUM, netSum()); map.put(NET_SUM, netSum());
map.put(GROSS_SUM, grossSum()); map.put(GROSS_SUM, grossSum());
if (template != null) map.put("template", template.toMap()); if (template != null) map.put("template", template);
return map; return map;
} }

View File

@@ -1,23 +0,0 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Field.COMPANY;
import static de.srsoftware.umbrella.core.Field.COMPANY_ID;
import de.srsoftware.tools.Mappable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
public record Template(long id, long company, String name, byte[] data) implements Mappable {
public static Template of(ResultSet rs) throws SQLException {
return new Template(rs.getLong(ID),rs.getLong(COMPANY_ID),rs.getString(NAME),rs.getBytes(TEMPLATE));
}
@Override
public Map<String, Object> toMap() {
return Map.of(ID,id, COMPANY,company, NAME,name);
}
}

View File

@@ -9,7 +9,6 @@ import java.util.regex.Pattern;
public class Constants { public class Constants {
private Constants(){} private Constants(){}
public static final Pattern POST_CODE = compile("(.*\\w+.*)\n(.*\\d+.*)\n(\\d{5}) (\\w+)",DOTALL); public static final Pattern POST_CODE = compile("(.*\\w+.*)\n(.*\\d+.*)\n(\\d{5}) (\\w+)",DOTALL);
public static final String CLONE = "clone"; public static final String CLONE = "clone";
@@ -21,8 +20,6 @@ public class Constants {
public static final String ERROR_ADDRESS_MISSING = "{0} address does not contain street address / post code / city"; public static final String ERROR_ADDRESS_MISSING = "{0} address does not contain street address / post code / city";
public static final String MOVE = "move"; public static final String MOVE = "move";
public static final String PATH_ADD_ITEM = "add_item"; public static final String PATH_ADD_ITEM = "add_item";
@@ -38,8 +35,6 @@ public class Constants {
public static final String POSITION = "position"; public static final String POSITION = "position";
public static final String PROJECT_ID = "project_id"; public static final String PROJECT_ID = "project_id";
public static final String TABLE_COMPANY_SETTINGS = "company_settings";
public static final String TABLE_CUSTOMER_SETTINGS = "company_customer_settings"; public static final String TABLE_CUSTOMER_SETTINGS = "company_customer_settings";
public static final String TABLE_DOCUMENTS = "documents"; public static final String TABLE_DOCUMENTS = "documents";
public static final String TABLE_DOCUMENT_TYPES = "document_types"; public static final String TABLE_DOCUMENT_TYPES = "document_types";
@@ -47,4 +42,9 @@ public class Constants {
public static final String TABLE_PRICES = "customer_prices"; public static final String TABLE_PRICES = "customer_prices";
public static final String TABLE_TEMPLATES = "templates"; public static final String TABLE_TEMPLATES = "templates";
public static final String TEMPLATES = "templates"; public static final String TEMPLATES = "templates";
public static final String TYPE_CONFIRMATION = "confirmation";
public static final String TYPE_INVOICE = "invoice";
public static final String TYPE_OFFER = "offer";
public static final String TYPE_REMINDER = "reminder";
} }

View File

@@ -363,7 +363,7 @@ public class DocumentApi extends BaseHandler implements DocumentService {
} }
private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException { private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException {
var template = document.template().name(); var template = document.template();
var templateName = template+".html.pdf"; var templateName = template+".html.pdf";
var type = document.type().name(); var type = document.type().name();
var zugferd = "invoice".equals(type); var zugferd = "invoice".equals(type);
@@ -432,8 +432,7 @@ public class DocumentApi extends BaseHandler implements DocumentService {
Document doc = getDocument(docId, user).a; Document doc = getDocument(docId, user).a;
var companySettings = db.getCompanySettings(doc.companyId(),docType); var nextNumber = db.nextDocId(user.language(),doc.companyId(),docType);
var nextNumber = companySettings.nextDocId();
Document clone = new Document( Document clone = new Document(
0, 0,
@@ -469,19 +468,17 @@ public class DocumentApi extends BaseHandler implements DocumentService {
if (!json.has(TYPE) || !(json.get(TYPE) instanceof Number docTypeId)) throw missingFieldException(TYPE); if (!json.has(TYPE) || !(json.get(TYPE) instanceof Number docTypeId)) throw missingFieldException(TYPE);
var type = db.getType(docTypeId.intValue()); var type = db.getType(docTypeId.intValue());
var customer = Customer.of(customerData); var customer = Customer.of(customerData);
Template template = new Template(6,companyId,"unknwon",null);
String currency = company.currency(); String currency = company.currency();
String sep = company.decimalSeparator(); String sep = company.decimalSeparator();
var settings = db.getCustomerSettings(companyId,type,customer.id()); var settings = db.getCustomerSettings(companyId,type,customer.id());
var newCustomer = settings == null; var newCustomer = settings == null;
if (newCustomer) settings = CustomerSettings.empty(); if (newCustomer) settings = CustomerSettings.empty();
var companySettings = db.getCompanySettings(companyId,type); var nextNumber = db.nextDocId(user.language(), companyId,type);
var nextNumber = companySettings.nextDocId();
String lastHead = settings.header(); String lastHead = settings.header();
String lastFooter = settings.footer(); String lastFooter = settings.footer();
var sender = Sender.of(senderData); var sender = Sender.of(senderData);
LOG.log(DEBUG,json.toString(2)); LOG.log(DEBUG,json.toString(2));
var doc = new Document(0,companyId,nextNumber,type, LocalDate.now(), NEW,template,null,lastHead,lastFooter,currency,sep,sender,customer,new PositionList()); var doc = new Document(0,companyId,nextNumber,type, LocalDate.now(), NEW,null,null,lastHead,lastFooter,currency,sep,sender,customer,new PositionList());
var saved = db.save(doc); var saved = db.save(doc);
if (newCustomer) { if (newCustomer) {
if (customerData.get(CONTACT_ID) instanceof Number contactId) { if (customerData.get(CONTACT_ID) instanceof Number contactId) {
@@ -491,7 +488,6 @@ public class DocumentApi extends BaseHandler implements DocumentService {
} }
companyService().saveNewCustomer(companyId,customer.id()); companyService().saveNewCustomer(companyId,customer.id());
} }
db.step(companySettings);
return sendContent(ex,saved); return sendContent(ex,saved);
} }
@@ -521,8 +517,10 @@ public class DocumentApi extends BaseHandler implements DocumentService {
if (!(json.has(COMPANY) && json.get(COMPANY) instanceof Number companyId)) throw missingFieldException(COMPANY); if (!(json.has(COMPANY) && json.get(COMPANY) instanceof Number companyId)) throw missingFieldException(COMPANY);
var company = companyService().get(companyId.longValue()); var company = companyService().get(companyId.longValue());
if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are not a member of {0}",company.name()); if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are not a member of {0}",company.name());
var templates = db.getCompanyTemplates(companyId.longValue()); var templates = registry.documents()
return sendContent(ex,templates.stream().map(Template::toMap)); .filter(d -> d.name().endsWith(".template"))
.map(d -> d.name().replaceAll("(\\.[^.]+)?\\.template$",""));
return sendContent(ex,templates);
} }
private boolean postSearch(HttpExchange ex, UmbrellaUser user) throws IOException { private boolean postSearch(HttpExchange ex, UmbrellaUser user) throws IOException {

View File

@@ -4,7 +4,6 @@ package de.srsoftware.umbrella.documents;
import de.srsoftware.tools.Pair; import de.srsoftware.tools.Pair;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Document; import de.srsoftware.umbrella.core.model.Document;
import de.srsoftware.umbrella.core.model.Template;
import de.srsoftware.umbrella.core.model.Type; import de.srsoftware.umbrella.core.model.Type;
import de.srsoftware.umbrella.documents.model.*; import de.srsoftware.umbrella.documents.model.*;
import java.util.*; import java.util.*;
@@ -27,10 +26,6 @@ public interface DocumentDb {
CustomerSettings getCustomerSettings(long companyId, Type docType, String customerId) throws UmbrellaException; CustomerSettings getCustomerSettings(long companyId, Type docType, String customerId) throws UmbrellaException;
CompanySettings getCompanySettings(long companyId, Type docType) throws UmbrellaException;
Collection<Template> getCompanyTemplates(long l) throws UmbrellaException;
Type getType(int typeId) throws UmbrellaException; Type getType(int typeId) throws UmbrellaException;
Map<Long, Document> listDocs(long companyId) throws UmbrellaException; Map<Long, Document> listDocs(long companyId) throws UmbrellaException;
@@ -39,12 +34,7 @@ public interface DocumentDb {
Document loadDoc(long docId) throws UmbrellaException; Document loadDoc(long docId) throws UmbrellaException;
/** String nextDocId(String language, long companyId, Type type);
* decrement the document number
* @param companyId
* @param type
*/
void rollback(long companyId, Type type) throws UmbrellaException;
Document save(Document document) throws UmbrellaException; Document save(Document document) throws UmbrellaException;
@@ -52,11 +42,5 @@ public interface DocumentDb {
CustomerSettings save(long companyId, Type docType, String customerId, CustomerSettings settings) 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; Pair<Integer> switchPositions(long docId, Pair<Integer> longPair) throws UmbrellaException;
} }

View File

@@ -10,6 +10,7 @@ import static de.srsoftware.umbrella.core.Field.*;
import static de.srsoftware.umbrella.core.Field.COMPANY_ID; import static de.srsoftware.umbrella.core.Field.COMPANY_ID;
import static de.srsoftware.umbrella.core.Field.TAX; import static de.srsoftware.umbrella.core.Field.TAX;
import static de.srsoftware.umbrella.core.Field.UNIT; import static de.srsoftware.umbrella.core.Field.UNIT;
import static de.srsoftware.umbrella.core.ModuleRegistry.translator;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
import static de.srsoftware.umbrella.core.model.Document.DEFAULT_THOUSANDS_SEPARATOR; import static de.srsoftware.umbrella.core.model.Document.DEFAULT_THOUSANDS_SEPARATOR;
import static de.srsoftware.umbrella.core.model.Document.State; import static de.srsoftware.umbrella.core.model.Document.State;
@@ -20,6 +21,7 @@ import static java.time.ZoneOffset.UTC;
import de.srsoftware.tools.Pair; import de.srsoftware.tools.Pair;
import de.srsoftware.tools.jdbc.Query; import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*; import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.documents.model.*; import de.srsoftware.umbrella.documents.model.*;
@@ -28,39 +30,44 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.regex.Pattern;
public class SqliteDb implements DocumentDb{ public class SqliteDb extends BaseDb implements DocumentDb{
private static final System.Logger LOG = System.getLogger(SqliteDb.class.getSimpleName()); 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 String DB_VERSION = "message_db_version";
private static final int INITIAL_DB_VERSION = 1; private static final int INITIAL_DB_VERSION = 1;
private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\D*)(\\d+)(.*)");
public SqliteDb(Connection conn){ public SqliteDb(Connection connection) {
db = conn; super(connection);
init();
} }
private int createTables() { private void addTemplateColumn() {
try {
var sql = format("ALTER TABLE {0} ADD COLUMN {1} VARCHAR(255)",TABLE_DOCUMENTS,TEMPLATE);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("Failed to update column {0} → {1} of {2}",TEMPLATE_ID,TEMPLATE,TABLE_DOCUMENTS);
}
}
protected int createTables() {
int currentVersion = createSettingsTable();
switch (currentVersion) {
case 0:
createTableDocumentTypes(); createTableDocumentTypes();
createTableTemplates(); createTableTemplates();
createTableDocuments(); createTableDocuments();
createTablePositions(); createTablePositions();
createTableCustomerPrices(); createTableCustomerPrices();
createTableCompanySettings();
createTableCustomerSettings(); createTableCustomerSettings();
return createTableSettings(); case 1:
} addTemplateColumn();
moveTemplateNames();
private void createTableCompanySettings() { dropTemplateTable();
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}))"; dropTemplateIdColumn();
try {
var stmt = db.prepareStatement(format(sql,TABLE_COMPANY_SETTINGS, COMPANY_ID,DOC_TYPE_ID,TYPE_PREFIX,TYPE_SUFFIX,TYPE_NUMBER));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_COMPANY_SETTINGS,e);
throw new RuntimeException(e);
} }
return setCurrentVersion(2);
} }
private void createTableCustomerPrices() { private void createTableCustomerPrices() {
@@ -82,7 +89,7 @@ public class SqliteDb implements DocumentDb{
stmt.execute(); stmt.execute();
stmt.close(); stmt.close();
} catch (SQLException e) { } catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_COMPANY_SETTINGS,e); LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_CUSTOMER_SETTINGS,e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@@ -96,8 +103,8 @@ CREATE TABLE IF NOT EXISTS {0} (
{3} INT NOT NULL, {3} INT NOT NULL,
{4} TEXT NOT NULL, {4} TEXT NOT NULL,
{5} TIMESTAMP NOT NULL, {5} TIMESTAMP NOT NULL,
{6} INT NOT NULL NULL, {6} INT NOT NULL,
{7} INT NOT NULL, {7} INT,
{8} VARCHAR(100), {8} VARCHAR(100),
{9} TEXT, {9} TEXT,
{10} TEXT, {10} TEXT,
@@ -128,6 +135,12 @@ CREATE TABLE IF NOT EXISTS {0} (
var stmt = db.prepareStatement(format(createTable,TABLE_DOCUMENT_TYPES, ID,NEXT_TYPE, NAME)); var stmt = db.prepareStatement(format(createTable,TABLE_DOCUMENT_TYPES, ID,NEXT_TYPE, NAME));
stmt.execute(); stmt.execute();
stmt.close(); stmt.close();
insertInto(TABLE_DOCUMENT_TYPES,ID,NEXT_TYPE,NAME)
.values(1,2,TYPE_OFFER)
.values(2,3,TYPE_CONFIRMATION)
.values(3,4,TYPE_INVOICE)
.values(4,4,TYPE_REMINDER)
.execute(db);
} catch (SQLException e) { } catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_DOCUMENT_TYPES,e); LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_DOCUMENT_TYPES,e);
throw new RuntimeException(e); throw new RuntimeException(e);
@@ -161,36 +174,6 @@ CREATE TABLE IF NOT EXISTS {0} (
} }
} }
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() { 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)"; var createTable = "CREATE TABLE IF NOT EXISTS {0} ({1} INTEGER PRIMARY KEY, {2} INT NOT NULL, {3} VARCHAR(255) NOT NULL, {4} BLOB)";
try { try {
@@ -241,29 +224,21 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
return pos; return pos;
} }
@Override private void dropTemplateIdColumn() {
public CompanySettings getCompanySettings(long companyId, Type docType) throws UmbrellaException {
try { try {
var rs = select(ALL).from(TABLE_COMPANY_SETTINGS).where(COMPANY_ID,equal(companyId)).where(DOC_TYPE_ID,equal(docType.id())).exec(db); var sql = format("ALTER TABLE {0} DROP COLUMN {1}",TABLE_DOCUMENTS,TEMPLATE_ID);
CompanySettings settings = null; db.prepareStatement(sql).execute();
if (rs.next()) settings = CompanySettings.of(rs); } catch (SQLException e) {
rs.close(); throw databaseException("Failed to update column {0} → {1} of {2}",TEMPLATE_ID,TEMPLATE,TABLE_DOCUMENTS);
if (settings != null) return settings;
} catch (SQLException ignored) {
} }
throw databaseException("Failed to load customer settings (company: {0}, document type: {1})",companyId, docType.name());
} }
@Override private void dropTemplateTable() {
public Collection<Template> getCompanyTemplates(long companyId) throws UmbrellaException {
try { try {
var rs = select(ALL).from(TABLE_TEMPLATES).where(COMPANY_ID,equal(companyId)).exec(db); var sql = format("DROP TABLE IF EXISTS {0};",TABLE_TEMPLATES);
var templates = new HashSet<Template>(); db.prepareStatement(sql).execute();
while (rs.next()) templates.add(Template.of(rs));
rs.close();
return templates;
} catch (SQLException e) { } catch (SQLException e) {
throw databaseException("Failed to load templates for company {0}",companyId); throw databaseException("Failed to drop table {0}",TABLE_TEMPLATES);
} }
} }
@@ -309,10 +284,6 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
throw new UmbrellaException(500,"No type with id = {0}",typeId); throw new UmbrellaException(500,"No type with id = {0}",typeId);
} }
private void init() {
var version = createTables();
}
@Override @Override
public Map<Long, Map<Long, String>> docReferencedByTimes(Set<Long> timeIds) throws UmbrellaException { public Map<Long, Map<Long, String>> docReferencedByTimes(Set<Long> timeIds) throws UmbrellaException {
try { try {
@@ -431,7 +402,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
while (rs.next()) types.put(rs.getInt(ID),toType(rs)); while (rs.next()) types.put(rs.getInt(ID),toType(rs));
rs.close(); rs.close();
rs = Query.select(ALL).from(TABLE_DOCUMENTS).leftJoin(TEMPLATE_ID,TABLE_TEMPLATES, ID).where(TABLE_DOCUMENTS+"."+ ID,equal(docId)).exec(db); rs = Query.select(ALL).from(TABLE_DOCUMENTS).where(TABLE_DOCUMENTS+"."+ ID,equal(docId)).exec(db);
Document doc = null; Document doc = null;
while (rs.next()) doc = toDoc(rs,types); while (rs.next()) doc = toDoc(rs,types);
rs.close(); rs.close();
@@ -453,22 +424,38 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
throw new UmbrellaException(500,"Failed to load document {0}.",docId); throw new UmbrellaException(500,"Failed to load document {0}.",docId);
} }
@Override private void moveTemplateNames() {
public void rollback(long companyId, Type type) throws UmbrellaException {
try { try {
var settings = getCompanySettings(companyId,type); var sql = format("UPDATE {0} SET template = (SELECT name FROM templates WHERE templates.id = documents.template_id);",TABLE_DOCUMENTS);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("Failed to move template.names to document.templates!");
}
}
var numbers = new HashSet<String>(); @Override
var rs = select(NUMBER).from(TABLE_DOCUMENTS).where(COMPANY_ID,equal(companyId)).exec(db); public String nextDocId(String language, long companyId, Type type) {
while (rs.next()) numbers.add(rs.getString(NUMBER)); try {
var rs = select(NUMBER).from(TABLE_DOCUMENTS).where(COMPANY_ID,equal(companyId)).where(TYPE_ID,equal(type.id())).sort(ID+" DESC").limit(1).exec(db);
String lastId = null;
if (rs.next()) lastId = rs.getString(1);
rs.close(); rs.close();
if (lastId == null) return translator().translate(language,type.name())+"-0001";
var previous = settings.previous(); var numeric = NUMBER_PATTERN.matcher(lastId);
while (previous.isPresent() && !numbers.contains(previous.get().nextDocId())) previous = previous.get().previous(); if (numeric.find()){
var prefix = numeric.group(1);
previous.ifPresent(this::step); var digits = numeric.group(2);
} catch (SQLException e){ var suffix = numeric.group(3);
// TODO var len = digits.length();
while (digits.startsWith("0")) digits = digits.substring(1);
var lid = Long.parseLong(digits)+1;
digits = ""+lid;
while (digits.length()<len) digits = "0"+digits;
return prefix+digits+suffix;
}
return lastId;
} catch (SQLException e) {
throw databaseException("Failed to read last document id");
} }
} }
@@ -493,8 +480,8 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
var timestamp = doc.date().atStartOfDay(UTC).toInstant().getEpochSecond(); var timestamp = doc.date().atStartOfDay(UTC).toInstant().getEpochSecond();
var sender = doc.sender(); var sender = doc.sender();
var custom = doc.customer(); var custom = doc.customer();
var stmt = insertInto(TABLE_DOCUMENTS,TYPE_ID,COMPANY_ID, DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE_ID,CURRENCY) var stmt = insertInto(TABLE_DOCUMENTS,TYPE_ID,COMPANY_ID, DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE,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()) .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(), doc.currency())
.execute(db); .execute(db);
var rs = stmt.getGeneratedKeys(); var rs = stmt.getGeneratedKeys();
Long newId = null; Long newId = null;
@@ -516,10 +503,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
var sender = doc.sender(); var sender = doc.sender();
var custom = doc.customer(); var custom = doc.customer();
update(TABLE_DOCUMENTS) update(TABLE_DOCUMENTS)
.set(DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE_ID) .set(DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE)
.where(ID,equal(doc.id())) .where(ID,equal(doc.id()))
.prepare(db) .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(),doc.template().id()) .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(),doc.template())
.close(); .close();
sender.clean(); sender.clean();
custom.clean(); custom.clean();
@@ -589,20 +576,6 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
} }
} }
@Override
public void step(CompanySettings settings) {
try {
update(TABLE_COMPANY_SETTINGS)
.set(TYPE_NUMBER)
.where(COMPANY_ID,equal(settings.companyId())).where(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 @Override
public Pair<Integer> switchPositions(long docId, Pair<Integer> pair) throws UmbrellaException { public Pair<Integer> switchPositions(long docId, Pair<Integer> pair) throws UmbrellaException {
try { try {
@@ -642,22 +615,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
var customerEmail = rs.getString(CUSTOMER_EMAIL); var customerEmail = rs.getString(CUSTOMER_EMAIL);
var customer = new Customer(customerId, customerName, customerEmail, customerTaxNumber,FALLBACK_LANG); var customer = new Customer(customerId, customerName, customerEmail, customerTaxNumber,FALLBACK_LANG);
var sender = new Sender(senderName,bankAccount,taxNumber,court); var sender = new Sender(senderName,bankAccount,taxNumber,court);
var template = toTemplate(rs); var template = rs.getString(TEMPLATE);
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()); 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(TEMPLATE_ID);
var company = rs.getLong(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 { private Position toPosition(ResultSet rs) throws SQLException {
var num = rs.getInt(POS); var num = rs.getInt(POS);
var itemCode = rs.getString(ITEM_CODE); var itemCode = rs.getString(ITEM_CODE);

View File

@@ -1,33 +0,0 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents.model;
import static de.srsoftware.tools.Optionals.emptyIfNull;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Field.*;
import static de.srsoftware.umbrella.core.Field.COMPANY_ID;
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(COMPANY_ID);
var typeId = rs.getLong(DOC_TYPE_ID);
var typePrefix = emptyIfNull(rs.getString(TYPE_PREFIX));
var typeSuffix = emptyIfNull(rs.getString(TYPE_SUFFIX));
var typeNumber = rs.getLong(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

@@ -1,9 +1,26 @@
<script> <script>
import { t } from '../translations.svelte';
let { item, onclick } = $props(); let { item, onclick } = $props();
</script> </script>
<fieldset {onclick}> <fieldset {onclick}>
<legend>{item.code} | {item.name}</legend> <legend>{item.code} | {item.name}</legend>
<div>{@html item.description.rendered}</div> {#if item.properties}
<span>{item.unit_price/100} {item.currency} / {item.unit}</span> <table>
<tbody>
{#if item.location.name}
<tr>
<th>{t('location')}</th>
<td>{item.location.name}</td>
</tr>
{/if}
{#each Object.entries(item.properties) as [idx,prop]}
<tr>
<th>{prop.name}</th>
<td>{prop.value} {prop.unit}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</fieldset> </fieldset>

View File

@@ -1,7 +1,7 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { api } from '../../urls.svelte.js'; import { api, post } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte'; import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js'; import { t } from '../../translations.svelte.js';
@@ -15,13 +15,9 @@
let items = $state(null); let items = $state(null);
async function loadItems(){ async function loadItems(){
const url = api('items/list'); const url = api('stock/list');
let data = { company_id: company_id }; let data = { company_id };
const resp = await fetch(url,{ const resp = await post(url,data);
credentials: 'include',
method : 'POST',
body : JSON.stringify(data)
});
if (resp.ok){ if (resp.ok){
items = await resp.json(); items = await resp.json();
yikes(); yikes();
@@ -37,7 +33,7 @@
<div> <div>
<h1>{t('items')}</h1> <h1>{t('items')}</h1>
{#if items} {#if items}
{#each items as item,id} {#each items as item,idx}
<Item item={item} onclick={() => onSelect(item)} /> <Item item={item} onclick={() => onSelect(item)} />
{/each} {/each}
{/if} {/if}

View File

@@ -29,14 +29,23 @@
} }
function itemSelected(item){ function itemSelected(item){
let unit_price = null;
let description = '';
for (let prop of item.properties) {
if (prop.name.toLowerCase().indexOf(t('price').toLowerCase())>-1){
unit_price = 100*prop.value.replace(',','.');
} else {
description += `* ${prop.name}: ${prop.value}\n`;
}
}
select({ select({
item_code : item.code, item_code : item.code,
title : item.name, title : item.name,
description : item.description.source, description : description,
amount : 1, amount : 1,
unit : item.unit, unit : t('pieces'),
unit_price : item.unit_price, unit_price : unit_price
tax : item.tax
}); });
} }

View File

@@ -1,7 +1,7 @@
<script> <script>
import {onMount} from 'svelte'; import {onMount} from 'svelte';
import {api} from '../../urls.svelte.js'; import {api, post} from '../../urls.svelte.js';
import {t} from '../../translations.svelte.js'; import {t} from '../../translations.svelte.js';
let { let {
@@ -16,11 +16,7 @@
async function loadTemplates(){ async function loadTemplates(){
const url = api('document/templates'); const url = api('document/templates');
var resp = await fetch(url,{ var resp = await post(url,{company:company});
credentials : 'include',
method : 'POST',
body : JSON.stringify({company:company})
});
if (resp.ok){ if (resp.ok){
templates = await resp.json(); templates = await resp.json();
} else { } else {
@@ -34,8 +30,8 @@
{#if templates} {#if templates}
<select bind:value onchange={onchange}> <select bind:value onchange={onchange}>
<option value={0}>{caption}</option> <option value={0}>{caption}</option>
{#each Object.entries(templates) as [id,template]} {#each templates as template}
<option value={template.id}>{template.name}</option> <option value={template}>{template}</option>
{/each} {/each}
</select> </select>
{:else} {:else}

View File

@@ -2,7 +2,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router'; import { useTinyRouter } from 'svelte-tiny-router';
import {api} from '../../urls.svelte.js'; import { api, post } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte'; import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js'; import { t } from '../../translations.svelte.js';
import { user } from '../../user.svelte.js'; import { user } from '../../user.svelte.js';
@@ -29,11 +29,7 @@
async function addPosition(selected){ async function addPosition(selected){
const url = api(`document/${doc.id}/position`); const url = api(`document/${doc.id}/position`);
const resp = await fetch(url,{ const resp = await post(url,selected);
method : 'POST',
credentials : 'include',
body : JSON.stringify(selected)
});
if (resp.ok){ if (resp.ok){
doc.positions = await resp.json(); doc.positions = await resp.json();
yikes(); yikes();
@@ -218,9 +214,9 @@
<th>{t('template')}:</th> <th>{t('template')}:</th>
<td> <td>
{#if editable} {#if editable}
<TemplateSelector company={doc.company.id} bind:value={doc.template.id} onchange={() => update('template_id',doc.template.id)} /> <TemplateSelector company={doc.company.id} bind:value={doc.template} onchange={() => update('template',doc.template)} />
{:else} {:else}
{doc.template.name} {doc.template}
{/if} {/if}
</td> </td>
</tr> </tr>

View File

@@ -185,6 +185,10 @@
highlight.archive = true; highlight.archive = true;
} }
function is_custom(state){
return [10,20,40,60,100].includes(state);
}
function openTask(task_id){ function openTask(task_id){
window.open(`/task/${task_id}/view`, '_blank').focus(); window.open(`/task/${task_id}/view`, '_blank').focus();
} }
@@ -244,7 +248,7 @@
{#each users as u} {#each users as u}
<div class="user">{u.name}</div> <div class="user">{u.name}</div>
{#each Object.entries(project.allowed_states) as [state,name]} {#each Object.entries(project.allowed_states) as [state,name]}
<div class={['state_'+state, highlight.user == u.id && highlight.state == state ? 'highlight':'']} ondragover={ev => hover(ev,u.id,state)} ondragleave={e => delete highlight.user} ondrop={ev => drop(u.id,state)} > <div class={['state_'+state, is_custom(state) ? '':'state_custom' ,highlight.user == u.id && highlight.state == state ? 'highlight':'']} ondragover={ev => hover(ev,u.id,state)} ondragleave={e => delete highlight.user} ondrop={ev => drop(u.id,state)} >
{#each Object.values(tasks[u.id][state]).sort(byName) as task} {#each Object.values(tasks[u.id][state]).sort(byName) as task}
{#if !filter || task.name.toLowerCase().includes(filter) || (task.tags && task.tags.filter(tag => tag.toLowerCase().includes(filter)).length)} {#if !filter || task.name.toLowerCase().includes(filter) || (task.tags && task.tags.filter(tag => tag.toLowerCase().includes(filter)).length)}
<Card onclick={e => openTask(task.id)} ondragstart={ev => dragged=task} {task} tag_colors={project.tag_colors} /> <Card onclick={e => openTask(task.id)} ondragstart={ev => dragged=task} {task} tag_colors={project.tag_colors} />

View File

@@ -218,6 +218,22 @@ public class SqliteDb extends BaseDb implements StockDb {
} }
} }
@Override
public Collection<Item> listItemsOf(Company company) {
try {
var owner = company.dbCode();
var rs = select(ALL).from(TABLE_ITEMS).where(OWNER,equal(owner)).exec(db);
var list = new ArrayList<Item>();
while (rs.next()) list.add(Item.of(rs));
rs.close();
for (var item : list) loadProperties(item);
return list;
} catch (SQLException e){
throw databaseException("Failed to load items of {0}",company);
}
}
@Override @Override
public Item loadProperties(Item item){ public Item loadProperties(Item item){
try { try {

View File

@@ -13,6 +13,7 @@ public interface StockDb {
Collection<DbLocation> listChildLocations(long parentId); Collection<DbLocation> listChildLocations(long parentId);
Collection<DbLocation> listCompanyLocations(Company company); Collection<DbLocation> listCompanyLocations(Company company);
Collection<Item> listItemsAt(Location location); Collection<Item> listItemsAt(Location location);
Collection<Item> listItemsOf(Company company);
Collection<Property> listProperties(); Collection<Property> listProperties();
Collection<DbLocation> listUserLocations(UmbrellaUser userId); Collection<DbLocation> listUserLocations(UmbrellaUser userId);
Item loadItem(long id); Item loadItem(long id);

View File

@@ -8,6 +8,8 @@ import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Field.ITEM; import static de.srsoftware.umbrella.core.Field.ITEM;
import static de.srsoftware.umbrella.core.ModuleRegistry.companyService; import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService; import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.Paths.LIST;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.stock.Constants.*; import static de.srsoftware.umbrella.stock.Constants.*;
import static java.lang.System.Logger.Level.WARNING; import static java.lang.System.Logger.Level.WARNING;
@@ -33,6 +35,7 @@ import org.json.JSONObject;
public class StockModule extends BaseHandler implements StockService { public class StockModule extends BaseHandler implements StockService {
private final StockDb stockDb; private final StockDb stockDb;
private Comparator<Item> byName = (a,b) -> a.name().compareToIgnoreCase(b.name());
public StockModule(Configuration config) throws UmbrellaException { public StockModule(Configuration config) throws UmbrellaException {
super(); super();
@@ -160,6 +163,7 @@ public class StockModule extends BaseHandler implements StockService {
var head = path.pop(); var head = path.pop();
return switch (head) { return switch (head) {
case ITEM -> postItem(user.get(), ex); case ITEM -> postItem(user.get(), ex);
case LIST -> postItemList(user.get(), path, ex);
case LOCATION -> postLocation(user.get(),ex); case LOCATION -> postLocation(user.get(),ex);
case PROPERTY -> postProperty(user.get(),ex); case PROPERTY -> postProperty(user.get(),ex);
case null, default -> super.doPost(path,ex); case null, default -> super.doPost(path,ex);
@@ -316,6 +320,19 @@ public class StockModule extends BaseHandler implements StockService {
return sendContent(ex,stockDb.save(newItem)); return sendContent(ex,stockDb.save(newItem));
} }
private boolean postItemList(UmbrellaUser user, Path path, HttpExchange ex) throws IOException {
var json = json(ex);
if (!json.has(COMPANY_ID) || !(json.get(COMPANY_ID) instanceof Number company_id)) throw missingFieldException(COMPANY_ID);
var company = companyService().get(company_id.longValue());
if (!companyService().membership(company_id.longValue(),user.id())) throw forbidden("You are not a member of {0}!", company.name());
var map = new HashMap<Long,Location>();
var items = stockDb.listItemsOf(company)
.stream()
.peek(item -> item.location(map.computeIfAbsent(item.location().id(), k -> item.location().resolve()))).sorted(byName)
.map(Item::toMap);
return sendContent(ex,items);
}
private boolean postLocation(UmbrellaUser user, HttpExchange ex) throws IOException { private boolean postLocation(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex); var json = json(ex);
if (!(json.get(NAME) instanceof String name)) throw missingFieldException(NAME); if (!(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);

View File

@@ -204,6 +204,7 @@
"permission_owner": "Besitzer", "permission_owner": "Besitzer",
"permission_read_only": "lesen", "permission_read_only": "lesen",
"phone": "Telefon", "phone": "Telefon",
"pieces": "Stück",
"pos": "Pos", "pos": "Pos",
"position": "Position", "position": "Position",
"positions": "Positionen", "positions": "Positionen",

View File

@@ -204,6 +204,7 @@
"permission_owner": "owner", "permission_owner": "owner",
"permission_read_only": "read-only", "permission_read_only": "read-only",
"phone": "phone", "phone": "phone",
"pieces": "pieces",
"pos": "pos", "pos": "pos",
"position": "position", "position": "position",
"positions": "positions", "positions": "positions",

View File

@@ -139,6 +139,10 @@ tr:hover .taglist .tag button {
color: red; color: red;
} }
.states .active{
background: red;
}
.taglist .tag{ .taglist .tag{
border-color: red; border-color: red;
} }
@@ -186,8 +190,6 @@ tr:hover .taglist .tag button {
border-color: 1px solid; border-color: 1px solid;
} }
.warn { .warn {
background-color: yellow; background-color: yellow;
color: black; color: black;
@@ -201,6 +203,7 @@ tr:hover .taglist .tag button {
.task.p10 .name{ .task.p10 .name{
color: #ffa736; color: #ffa736;
} }
.kanban .state_custom .box.p10,
.kanban .state_20 .box.p10, .kanban .state_20 .box.p10,
.kanban .state_40 .box.p10{ .kanban .state_40 .box.p10{
border: 5px solid #ffa736; border: 5px solid #ffa736;
@@ -209,6 +212,7 @@ tr:hover .taglist .tag button {
.task.p20 .name{ .task.p20 .name{
color: #ff8f00; color: #ff8f00;
} }
.kanban .state_custom .box.p20,
.kanban .state_20 .box.p20, .kanban .state_20 .box.p20,
.kanban .state_40 .box.p20{ .kanban .state_40 .box.p20{
border: 5px solid #ff8f00; border: 5px solid #ff8f00;
@@ -217,6 +221,7 @@ tr:hover .taglist .tag button {
.task.p30 .name{ .task.p30 .name{
color: #ff7b06; color: #ff7b06;
} }
.kanban .state_custom .box.p30,
.kanban .state_20 .box.p30, .kanban .state_20 .box.p30,
.kanban .state_40 .box.p30{ .kanban .state_40 .box.p30{
border: 5px solid #ff7b06; border: 5px solid #ff7b06;
@@ -225,6 +230,7 @@ tr:hover .taglist .tag button {
.task.p40 .name{ .task.p40 .name{
color: #ff6306; color: #ff6306;
} }
.kanban .state_custom .box.p40,
.kanban .state_20 .box.p40, .kanban .state_20 .box.p40,
.kanban .state_40 .box.p40{ .kanban .state_40 .box.p40{
border: 5px solid #ff6306; border: 5px solid #ff6306;
@@ -233,6 +239,7 @@ tr:hover .taglist .tag button {
.task.p50 .name{ .task.p50 .name{
color: #ff4c06; color: #ff4c06;
} }
.kanban .state_custom .box.p50,
.kanban .state_20 .box.p50, .kanban .state_20 .box.p50,
.kanban .state_40 .box.p50{ .kanban .state_40 .box.p50{
border: 5px solid #ff4c06; border: 5px solid #ff4c06;
@@ -241,6 +248,7 @@ tr:hover .taglist .tag button {
.task.p60 .name{ .task.p60 .name{
color: #ff3506; color: #ff3506;
} }
.kanban .state_custom .box.p60,
.kanban .state_20 .box.p60, .kanban .state_20 .box.p60,
.kanban .state_40 .box.p60{ .kanban .state_40 .box.p60{
border: 5px solid #ff3506; border: 5px solid #ff3506;
@@ -249,6 +257,7 @@ tr:hover .taglist .tag button {
.task.p70 .name{ .task.p70 .name{
color: #ff0000; color: #ff0000;
} }
.kanban .state_custom .box.p70,
.kanban .state_20 .box.p70, .kanban .state_20 .box.p70,
.kanban .state_40 .box.p70{ .kanban .state_40 .box.p70{
border: 5px solid #ff0000; border: 5px solid #ff0000;
@@ -257,6 +266,7 @@ tr:hover .taglist .tag button {
.task.p80 .name{ .task.p80 .name{
color: #df153b; color: #df153b;
} }
.kanban .state_custom .box.p80,
.kanban .state_20 .box.p80, .kanban .state_20 .box.p80,
.kanban .state_40 .box.p80{ .kanban .state_40 .box.p80{
border: 5px solid #df153b; border: 5px solid #df153b;
@@ -266,6 +276,7 @@ tr:hover .taglist .tag button {
background-color: #991c34; background-color: #991c34;
color: #ffff00; color: #ffff00;
} }
.kanban .state_custom .box.p90,
.kanban .state_20 .box.p90, .kanban .state_20 .box.p90,
.kanban .state_40 .box.p90{ .kanban .state_40 .box.p90{
border: 5px solid #991c34; border: 5px solid #991c34;
@@ -275,6 +286,7 @@ tr:hover .taglist .tag button {
background-color: #733440; background-color: #733440;
color: #ffff00; color: #ffff00;
} }
.kanban .state_custom .box.p100,
.kanban .state_20 .box.p100, .kanban .state_20 .box.p100,
.kanban .state_40 .box.p100{ .kanban .state_40 .box.p100{
border: 5px solid #733440; border: 5px solid #733440;

View File

@@ -176,7 +176,6 @@ tr:hover .taglist .tag button {
background: black; background: black;
} }
.version a.selected{ .version a.selected{
border-color: orange; border-color: orange;
} }
@@ -194,6 +193,7 @@ tr:hover .taglist .tag button {
.task.p10 .name{ .task.p10 .name{
color: #fff066; color: #fff066;
} }
.kanban .state_custom .box.p10,
.kanban .state_20 .box.p10, .kanban .state_20 .box.p10,
.kanban .state_40 .box.p10{ .kanban .state_40 .box.p10{
border: 5px solid #fff066; border: 5px solid #fff066;
@@ -202,6 +202,7 @@ tr:hover .taglist .tag button {
.task.p20 .name{ .task.p20 .name{
color: #ffe706; color: #ffe706;
} }
.kanban .state_custom .box.p20,
.kanban .state_20 .box.p20, .kanban .state_20 .box.p20,
.kanban .state_40 .box.p20{ .kanban .state_40 .box.p20{
border: 5px solid #ffe706; border: 5px solid #ffe706;
@@ -210,6 +211,7 @@ tr:hover .taglist .tag button {
.task.p30 .name{ .task.p30 .name{
color: #ffa906; color: #ffa906;
} }
.kanban .state_custom .box.p30,
.kanban .state_20 .box.p30, .kanban .state_20 .box.p30,
.kanban .state_40 .box.p30{ .kanban .state_40 .box.p30{
border: 5px solid #ffa906; border: 5px solid #ffa906;
@@ -218,6 +220,7 @@ tr:hover .taglist .tag button {
.task.p40 .name{ .task.p40 .name{
color: #ff8606; color: #ff8606;
} }
.kanban .state_custom .box.p40,
.kanban .state_20 .box.p40, .kanban .state_20 .box.p40,
.kanban .state_40 .box.p40{ .kanban .state_40 .box.p40{
border: 5px solid #ff8606; border: 5px solid #ff8606;
@@ -226,6 +229,7 @@ tr:hover .taglist .tag button {
.task.p50 .name{ .task.p50 .name{
color: #ff4c06; color: #ff4c06;
} }
.kanban .state_custom .box.p50,
.kanban .state_20 .box.p50, .kanban .state_20 .box.p50,
.kanban .state_40 .box.p50{ .kanban .state_40 .box.p50{
border: 5px solid #ff4c06; border: 5px solid #ff4c06;
@@ -234,6 +238,7 @@ tr:hover .taglist .tag button {
.task.p60 .name{ .task.p60 .name{
color: #ff3506; color: #ff3506;
} }
.kanban .state_custom .box.p60,
.kanban .state_20 .box.p60, .kanban .state_20 .box.p60,
.kanban .state_40 .box.p60{ .kanban .state_40 .box.p60{
border: 5px solid #ff3506; border: 5px solid #ff3506;
@@ -242,6 +247,7 @@ tr:hover .taglist .tag button {
.task.p70 .name{ .task.p70 .name{
color: #ff0000; color: #ff0000;
} }
.kanban .state_custom .box.p70,
.kanban .state_20 .box.p70, .kanban .state_20 .box.p70,
.kanban .state_40 .box.p70{ .kanban .state_40 .box.p70{
border: 5px solid #ff0000; border: 5px solid #ff0000;
@@ -250,6 +256,7 @@ tr:hover .taglist .tag button {
.task.p80 .name{ .task.p80 .name{
color: #df153b; color: #df153b;
} }
.kanban .state_custom .box.p80,
.kanban .state_20 .box.p80, .kanban .state_20 .box.p80,
.kanban .state_40 .box.p80{ .kanban .state_40 .box.p80{
border: 5px solid #df153b; border: 5px solid #df153b;
@@ -259,6 +266,7 @@ tr:hover .taglist .tag button {
background-color: #991c34; background-color: #991c34;
color: #ffff00; color: #ffff00;
} }
.kanban .state_custom .box.p90,
.kanban .state_20 .box.p90, .kanban .state_20 .box.p90,
.kanban .state_40 .box.p90{ .kanban .state_40 .box.p90{
border: 5px solid #991c34; border: 5px solid #991c34;
@@ -268,6 +276,7 @@ tr:hover .taglist .tag button {
background-color: #733440; background-color: #733440;
color: #ffff00; color: #ffff00;
} }
.kanban .state_custom .box.p100,
.kanban .state_20 .box.p100, .kanban .state_20 .box.p100,
.kanban .state_40 .box.p100{ .kanban .state_40 .box.p100{
border: 5px solid #733440; border: 5px solid #733440;

View File

@@ -124,6 +124,10 @@ tr:hover .taglist .tag button {
border-color: blue; border-color: blue;
} }
.states .active{
background: #dfe4ff;
}
.taglist .tag{ .taglist .tag{
border-color: blue; border-color: blue;
} }
@@ -179,6 +183,7 @@ tr:hover .taglist .tag button {
.task.p10 .name{ .task.p10 .name{
color: #c9fbb2; color: #c9fbb2;
} }
.kanban .state_custom .box.p10,
.kanban .state_20 .box.p10, .kanban .state_20 .box.p10,
.kanban .state_40 .box.p10{ .kanban .state_40 .box.p10{
border: 5px solid #c9fbb2; border: 5px solid #c9fbb2;
@@ -187,6 +192,7 @@ tr:hover .taglist .tag button {
.task.p20 .name{ .task.p20 .name{
color: #cbff57; color: #cbff57;
} }
.kanban .state_custom .box.p20,
.kanban .state_20 .box.p20, .kanban .state_20 .box.p20,
.kanban .state_40 .box.p20{ .kanban .state_40 .box.p20{
border: 5px solid #cbff57; border: 5px solid #cbff57;
@@ -195,6 +201,7 @@ tr:hover .taglist .tag button {
.task.p30 .name{ .task.p30 .name{
color: #dfff44; color: #dfff44;
} }
.kanban .state_custom .box.p30,
.kanban .state_20 .box.p30, .kanban .state_20 .box.p30,
.kanban .state_40 .box.p30{ .kanban .state_40 .box.p30{
border: 5px solid #dfff44; border: 5px solid #dfff44;
@@ -203,6 +210,7 @@ tr:hover .taglist .tag button {
.task.p40 .name{ .task.p40 .name{
color: #f8ff29; color: #f8ff29;
} }
.kanban .state_custom .box.p40,
.kanban .state_20 .box.p40, .kanban .state_20 .box.p40,
.kanban .state_40 .box.p40{ .kanban .state_40 .box.p40{
border: 5px solid #f8ff29; border: 5px solid #f8ff29;
@@ -211,6 +219,7 @@ tr:hover .taglist .tag button {
.task.p50 .name{ .task.p50 .name{
color: #ffdb1b; color: #ffdb1b;
} }
.kanban .state_custom .box.p50,
.kanban .state_20 .box.p50, .kanban .state_20 .box.p50,
.kanban .state_40 .box.p50{ .kanban .state_40 .box.p50{
border: 5px solid #ffdb1b; border: 5px solid #ffdb1b;
@@ -219,6 +228,7 @@ tr:hover .taglist .tag button {
.task.p60 .name{ .task.p60 .name{
color: #ff9309; color: #ff9309;
} }
.kanban .state_custom .box.p60,
.kanban .state_20 .box.p60, .kanban .state_20 .box.p60,
.kanban .state_40 .box.p60{ .kanban .state_40 .box.p60{
border: 5px solid #ff9309; border: 5px solid #ff9309;
@@ -227,6 +237,7 @@ tr:hover .taglist .tag button {
.task.p70 .name{ .task.p70 .name{
color: #ff6c00; color: #ff6c00;
} }
.kanban .state_custom .box.p70,
.kanban .state_20 .box.p70, .kanban .state_20 .box.p70,
.kanban .state_40 .box.p70{ .kanban .state_40 .box.p70{
border: 5px solid #ff6c00; border: 5px solid #ff6c00;
@@ -235,6 +246,7 @@ tr:hover .taglist .tag button {
.task.p80 .name{ .task.p80 .name{
color: #ff3c00; color: #ff3c00;
} }
.kanban .state_custom .box.p80,
.kanban .state_20 .box.p80, .kanban .state_20 .box.p80,
.kanban .state_40 .box.p80{ .kanban .state_40 .box.p80{
border: 5px solid #ff3c00; border: 5px solid #ff3c00;
@@ -243,6 +255,7 @@ tr:hover .taglist .tag button {
.task.p90 .name{ .task.p90 .name{
color: #ff0000; color: #ff0000;
} }
.kanban .state_custom .box.p90,
.kanban .state_20 .box.p90, .kanban .state_20 .box.p90,
.kanban .state_40 .box.p90{ .kanban .state_40 .box.p90{
border: 5px solid #ff0000; border: 5px solid #ff0000;
@@ -251,6 +264,7 @@ tr:hover .taglist .tag button {
.task.p100 .name{ .task.p100 .name{
color: #ff0048; color: #ff0048;
} }
.kanban .state_custom .box.p100,
.kanban .state_20 .box.p100, .kanban .state_20 .box.p100,
.kanban .state_40 .box.p100{ .kanban .state_40 .box.p100{
border: 5px solid #ff0048; border: 5px solid #ff0048;