Compare commits
21 Commits
module/wik
...
feature/ka
| Author | SHA1 | Date | |
|---|---|---|---|
| 40c35bc9ed | |||
| 5fc79e05da | |||
| 7de79627b4 | |||
| 146ea80e4e | |||
| 21759d1b12 | |||
| ba531a0ab1 | |||
| 7e02fe17e9 | |||
| 37e6ccd71a | |||
| 11c0983dac | |||
| 2bad462c38 | |||
| daa25044a4 | |||
| c37df2ddec | |||
| e450585d37 | |||
| 93907a839d | |||
| ccb84995cb | |||
| fad9c78f87 | |||
| 2cd53b19cb | |||
| 288acd90f4 | |||
| 0d202677ad | |||
| 6bb03f4e04 | |||
| 438d8d4aad |
@@ -1,6 +1,7 @@
|
||||
/* © SRSoftware 2025 */
|
||||
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.Field.*;
|
||||
import static de.srsoftware.umbrella.core.Field.COMPANY;
|
||||
@@ -57,14 +58,14 @@ public final class Document implements Mappable {
|
||||
private final Type type;
|
||||
private LocalDate date;
|
||||
private State state;
|
||||
private Template template;
|
||||
private String 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 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.companyId = companyId;
|
||||
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 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 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;
|
||||
}
|
||||
if (key != null) dirtyFields.add(key);
|
||||
@@ -225,7 +226,7 @@ public final class Document implements Mappable {
|
||||
map.put(TYPE, type.name());
|
||||
map.put(DATE, date);
|
||||
map.put(STATE, state.code);
|
||||
map.put(DELIVERY, delivery == null ? "" : delivery);
|
||||
map.put(DELIVERY, emptyIfNull(delivery));
|
||||
map.put(HEAD, mapMarkdown(head));
|
||||
map.put(FOOTER, mapMarkdown(footer));
|
||||
map.put(CURRENCY, currency);
|
||||
@@ -235,7 +236,7 @@ public final class Document implements Mappable {
|
||||
map.put("taxes",positions.taxNetSums(true));
|
||||
map.put(NET_SUM, netSum());
|
||||
map.put(GROSS_SUM, grossSum());
|
||||
if (template != null) map.put("template", template.toMap());
|
||||
map.put("template", emptyIfNull(template));
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -286,7 +287,7 @@ public final class Document implements Mappable {
|
||||
);
|
||||
}
|
||||
|
||||
public Template template() {
|
||||
public String template() {
|
||||
return template;
|
||||
}
|
||||
|
||||
@@ -309,7 +310,7 @@ public final class Document implements Mappable {
|
||||
map.put("taxes",positions.taxNetSums(true));
|
||||
map.put(NET_SUM, netSum());
|
||||
map.put(GROSS_SUM, grossSum());
|
||||
if (template != null) map.put("template", template.toMap());
|
||||
if (template != null) map.put("template", template);
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ 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 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 MOVE = "move";
|
||||
|
||||
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 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_DOCUMENTS = "documents";
|
||||
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_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";
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
}
|
||||
|
||||
private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException {
|
||||
var template = document.template().name();
|
||||
var template = document.template();
|
||||
var templateName = template+".html.pdf";
|
||||
var type = document.type().name();
|
||||
var zugferd = "invoice".equals(type);
|
||||
@@ -432,8 +432,7 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
|
||||
Document doc = getDocument(docId, user).a;
|
||||
|
||||
var companySettings = db.getCompanySettings(doc.companyId(),docType);
|
||||
var nextNumber = companySettings.nextDocId();
|
||||
var nextNumber = db.nextDocId(user.language(),doc.companyId(),docType);
|
||||
|
||||
Document clone = new Document(
|
||||
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);
|
||||
var type = db.getType(docTypeId.intValue());
|
||||
var customer = Customer.of(customerData);
|
||||
Template template = new Template(6,companyId,"unknwon",null);
|
||||
String currency = company.currency();
|
||||
String sep = company.decimalSeparator();
|
||||
var settings = db.getCustomerSettings(companyId,type,customer.id());
|
||||
var newCustomer = settings == null;
|
||||
if (newCustomer) settings = CustomerSettings.empty();
|
||||
var companySettings = db.getCompanySettings(companyId,type);
|
||||
var nextNumber = companySettings.nextDocId();
|
||||
var nextNumber = db.nextDocId(user.language(), companyId,type);
|
||||
String lastHead = settings.header();
|
||||
String lastFooter = settings.footer();
|
||||
var sender = Sender.of(senderData);
|
||||
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);
|
||||
if (newCustomer) {
|
||||
if (customerData.get(CONTACT_ID) instanceof Number contactId) {
|
||||
@@ -491,7 +488,6 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
}
|
||||
companyService().saveNewCustomer(companyId,customer.id());
|
||||
}
|
||||
db.step(companySettings);
|
||||
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);
|
||||
var company = companyService().get(companyId.longValue());
|
||||
if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are not a member of {0}",company.name());
|
||||
var templates = db.getCompanyTemplates(companyId.longValue());
|
||||
return sendContent(ex,templates.stream().map(Template::toMap));
|
||||
var templates = registry.documents()
|
||||
.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 {
|
||||
|
||||
@@ -4,7 +4,6 @@ package de.srsoftware.umbrella.documents;
|
||||
import de.srsoftware.tools.Pair;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
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.documents.model.*;
|
||||
import java.util.*;
|
||||
@@ -27,10 +26,6 @@ public interface DocumentDb {
|
||||
|
||||
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;
|
||||
|
||||
Map<Long, Document> listDocs(long companyId) throws UmbrellaException;
|
||||
@@ -39,12 +34,7 @@ public interface DocumentDb {
|
||||
|
||||
Document loadDoc(long docId) throws UmbrellaException;
|
||||
|
||||
/**
|
||||
* decrement the document number
|
||||
* @param companyId
|
||||
* @param type
|
||||
*/
|
||||
void rollback(long companyId, Type type) throws UmbrellaException;
|
||||
String nextDocId(String language, long companyId, Type type);
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@@ -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.TAX;
|
||||
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.model.Document.DEFAULT_THOUSANDS_SEPARATOR;
|
||||
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.jdbc.Query;
|
||||
import de.srsoftware.umbrella.core.BaseDb;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.*;
|
||||
import de.srsoftware.umbrella.documents.model.*;
|
||||
@@ -28,41 +30,46 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
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 final Connection db;
|
||||
private static final String DB_VERSION = "message_db_version";
|
||||
private static final int INITIAL_DB_VERSION = 1;
|
||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\D*)(\\d+)(.*)");
|
||||
|
||||
public SqliteDb(Connection conn){
|
||||
db = conn;
|
||||
init();
|
||||
public SqliteDb(Connection connection) {
|
||||
super(connection);
|
||||
}
|
||||
|
||||
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}))";
|
||||
private void addTemplateColumn() {
|
||||
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();
|
||||
var sql = format("ALTER TABLE {0} ADD COLUMN {1} VARCHAR(255)",TABLE_DOCUMENTS,TEMPLATE);
|
||||
db.prepareStatement(sql).execute();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_COMPANY_SETTINGS,e);
|
||||
throw new RuntimeException(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();
|
||||
createTableTemplates();
|
||||
createTableDocuments();
|
||||
createTablePositions();
|
||||
createTableCustomerPrices();
|
||||
createTableCustomerSettings();
|
||||
case 1:
|
||||
addTemplateColumn();
|
||||
moveTemplateNames();
|
||||
dropTemplateTable();
|
||||
dropTemplateIdColumn();
|
||||
}
|
||||
return setCurrentVersion(2);
|
||||
}
|
||||
|
||||
private void createTableCustomerPrices() {
|
||||
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} VARCHAR(255), {3} VARCHAR(50), {4} INTEGER)";
|
||||
try {
|
||||
@@ -82,7 +89,7 @@ public class SqliteDb implements DocumentDb{
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@@ -96,8 +103,8 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
{3} INT NOT NULL,
|
||||
{4} TEXT NOT NULL,
|
||||
{5} TIMESTAMP NOT NULL,
|
||||
{6} INT NOT NULL NULL,
|
||||
{7} INT NOT NULL,
|
||||
{6} INT NOT NULL,
|
||||
{7} INT,
|
||||
{8} VARCHAR(100),
|
||||
{9} 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));
|
||||
stmt.execute();
|
||||
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) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_DOCUMENT_TYPES,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() {
|
||||
var createTable = "CREATE TABLE IF NOT EXISTS {0} ({1} INTEGER PRIMARY KEY, {2} INT NOT NULL, {3} VARCHAR(255) NOT NULL, {4} BLOB)";
|
||||
try {
|
||||
@@ -241,29 +224,21 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
||||
return pos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompanySettings getCompanySettings(long companyId, Type docType) throws UmbrellaException {
|
||||
private void dropTemplateIdColumn() {
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_COMPANY_SETTINGS).where(COMPANY_ID,equal(companyId)).where(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 ignored) {
|
||||
var sql = format("ALTER TABLE {0} DROP COLUMN {1}",TABLE_DOCUMENTS,TEMPLATE_ID);
|
||||
db.prepareStatement(sql).execute();
|
||||
} catch (SQLException e) {
|
||||
throw databaseException("Failed to update column {0} → {1} of {2}",TEMPLATE_ID,TEMPLATE,TABLE_DOCUMENTS);
|
||||
}
|
||||
throw databaseException("Failed to load customer settings (company: {0}, document type: {1})",companyId, docType.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Template> getCompanyTemplates(long companyId) throws UmbrellaException {
|
||||
private void dropTemplateTable() {
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_TEMPLATES).where(COMPANY_ID,equal(companyId)).exec(db);
|
||||
var templates = new HashSet<Template>();
|
||||
while (rs.next()) templates.add(Template.of(rs));
|
||||
rs.close();
|
||||
return templates;
|
||||
var sql = format("DROP TABLE IF EXISTS {0};",TABLE_TEMPLATES);
|
||||
db.prepareStatement(sql).execute();
|
||||
} catch (SQLException e) {
|
||||
throw databaseException("Failed to load templates for company {0}",companyId);
|
||||
throw databaseException("Failed to drop table {0}",TABLE_TEMPLATES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,11 +283,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
||||
}
|
||||
throw new UmbrellaException(500,"No type with id = {0}",typeId);
|
||||
}
|
||||
|
||||
private void init() {
|
||||
var version = createTables();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Long, Map<Long, String>> docReferencedByTimes(Set<Long> timeIds) throws UmbrellaException {
|
||||
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));
|
||||
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;
|
||||
while (rs.next()) doc = toDoc(rs,types);
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback(long companyId, Type type) throws UmbrellaException {
|
||||
private void moveTemplateNames() {
|
||||
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>();
|
||||
var rs = select(NUMBER).from(TABLE_DOCUMENTS).where(COMPANY_ID,equal(companyId)).exec(db);
|
||||
while (rs.next()) numbers.add(rs.getString(NUMBER));
|
||||
@Override
|
||||
public String nextDocId(String language, long companyId, Type type) {
|
||||
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();
|
||||
|
||||
var previous = settings.previous();
|
||||
while (previous.isPresent() && !numbers.contains(previous.get().nextDocId())) previous = previous.get().previous();
|
||||
|
||||
previous.ifPresent(this::step);
|
||||
} catch (SQLException e){
|
||||
// TODO
|
||||
if (lastId == null) return translator().translate(language,type.name())+"-0001";
|
||||
var numeric = NUMBER_PATTERN.matcher(lastId);
|
||||
if (numeric.find()){
|
||||
var prefix = numeric.group(1);
|
||||
var digits = numeric.group(2);
|
||||
var suffix = numeric.group(3);
|
||||
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 sender = doc.sender();
|
||||
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)
|
||||
.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())
|
||||
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(), doc.currency())
|
||||
.execute(db);
|
||||
var rs = stmt.getGeneratedKeys();
|
||||
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 custom = doc.customer();
|
||||
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()))
|
||||
.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();
|
||||
sender.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
|
||||
public Pair<Integer> switchPositions(long docId, Pair<Integer> pair) throws UmbrellaException {
|
||||
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 customer = new Customer(customerId, customerName, customerEmail, customerTaxNumber,FALLBACK_LANG);
|
||||
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());
|
||||
}
|
||||
|
||||
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 {
|
||||
var num = rs.getInt(POS);
|
||||
var itemCode = rs.getString(ITEM_CODE);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,26 @@
|
||||
<script>
|
||||
import { t } from '../translations.svelte';
|
||||
let { item, onclick } = $props();
|
||||
</script>
|
||||
|
||||
<fieldset {onclick}>
|
||||
<legend>{item.code} | {item.name}</legend>
|
||||
<div>{@html item.description.rendered}</div>
|
||||
<span>{item.unit_price/100} {item.currency} / {item.unit}</span>
|
||||
{#if item.properties}
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { api } from '../../urls.svelte.js';
|
||||
import { api, post } from '../../urls.svelte.js';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte.js';
|
||||
|
||||
@@ -15,13 +15,9 @@
|
||||
let items = $state(null);
|
||||
|
||||
async function loadItems(){
|
||||
const url = api('items/list');
|
||||
let data = { company_id: company_id };
|
||||
const resp = await fetch(url,{
|
||||
credentials: 'include',
|
||||
method : 'POST',
|
||||
body : JSON.stringify(data)
|
||||
});
|
||||
const url = api('stock/list');
|
||||
let data = { company_id };
|
||||
const resp = await post(url,data);
|
||||
if (resp.ok){
|
||||
items = await resp.json();
|
||||
yikes();
|
||||
@@ -37,7 +33,7 @@
|
||||
<div>
|
||||
<h1>{t('items')}</h1>
|
||||
{#if items}
|
||||
{#each items as item,id}
|
||||
{#each items as item,idx}
|
||||
<Item item={item} onclick={() => onSelect(item)} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -29,14 +29,23 @@
|
||||
}
|
||||
|
||||
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({
|
||||
item_code : item.code,
|
||||
title : item.name,
|
||||
description : item.description.source,
|
||||
description : description,
|
||||
amount : 1,
|
||||
unit : item.unit,
|
||||
unit_price : item.unit_price,
|
||||
tax : item.tax
|
||||
unit : t('pieces'),
|
||||
unit_price : unit_price
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import {onMount} from 'svelte';
|
||||
import {onMount} from 'svelte';
|
||||
|
||||
import {api} from '../../urls.svelte.js';
|
||||
import {t} from '../../translations.svelte.js';
|
||||
import {api, post} from '../../urls.svelte.js';
|
||||
import {t} from '../../translations.svelte.js';
|
||||
|
||||
let {
|
||||
caption,
|
||||
@@ -16,11 +16,7 @@
|
||||
|
||||
async function loadTemplates(){
|
||||
const url = api('document/templates');
|
||||
var resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'POST',
|
||||
body : JSON.stringify({company:company})
|
||||
});
|
||||
var resp = await post(url,{company:company});
|
||||
if (resp.ok){
|
||||
templates = await resp.json();
|
||||
} else {
|
||||
@@ -34,8 +30,8 @@
|
||||
{#if templates}
|
||||
<select bind:value onchange={onchange}>
|
||||
<option value={0}>{caption}</option>
|
||||
{#each Object.entries(templates) as [id,template]}
|
||||
<option value={template.id}>{template.name}</option>
|
||||
{#each templates as template}
|
||||
<option value={template}>{template}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
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 { t } from '../../translations.svelte.js';
|
||||
import { user } from '../../user.svelte.js';
|
||||
@@ -29,11 +29,7 @@
|
||||
|
||||
async function addPosition(selected){
|
||||
const url = api(`document/${doc.id}/position`);
|
||||
const resp = await fetch(url,{
|
||||
method : 'POST',
|
||||
credentials : 'include',
|
||||
body : JSON.stringify(selected)
|
||||
});
|
||||
const resp = await post(url,selected);
|
||||
if (resp.ok){
|
||||
doc.positions = await resp.json();
|
||||
yikes();
|
||||
@@ -218,9 +214,9 @@
|
||||
<th>{t('template')}:</th>
|
||||
<td>
|
||||
{#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}
|
||||
{doc.template.name}
|
||||
{doc.template}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -185,6 +185,10 @@
|
||||
highlight.archive = true;
|
||||
}
|
||||
|
||||
function is_custom(state){
|
||||
return [10,20,40,60,100].includes(state);
|
||||
}
|
||||
|
||||
function openTask(task_id){
|
||||
window.open(`/task/${task_id}/view`, '_blank').focus();
|
||||
}
|
||||
@@ -244,7 +248,7 @@
|
||||
{#each users as u}
|
||||
<div class="user">{u.name}</div>
|
||||
{#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}
|
||||
{#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} />
|
||||
|
||||
@@ -145,7 +145,6 @@
|
||||
error(res);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function loadProperties(){
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { api, get } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
import { api, get, post } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
|
||||
import LineEditor from '../../Components/LineEditor.svelte';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api } from '../../urls.svelte.js';
|
||||
import { api, get, patch } from '../../urls.svelte.js';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte.js';
|
||||
|
||||
@@ -23,16 +23,10 @@
|
||||
const prj = projects[task.project_id];
|
||||
const stat = Object.keys(prj.allowed_states).find(k => prj.allowed_states[k] === state);
|
||||
const url = api(`task/${tid}`);
|
||||
const resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'PATCH',
|
||||
body : JSON.stringify({status:stat})
|
||||
});
|
||||
const resp = await patch(url,{status:stat});
|
||||
if (resp.ok){
|
||||
tasks[idx] = await resp.json();
|
||||
} else {
|
||||
error(resp);
|
||||
}
|
||||
} else error(resp);
|
||||
}
|
||||
|
||||
function abort(idx){
|
||||
@@ -66,17 +60,15 @@
|
||||
|
||||
async function loadProject(pid){
|
||||
const url = api(`project/${pid}`);
|
||||
const resp = await fetch(url,{credentials:'include'});
|
||||
const resp = await get(url);
|
||||
if (resp.ok){
|
||||
projects[pid] = await resp.json();
|
||||
} else {
|
||||
error(resp);
|
||||
}
|
||||
} else error(resp);
|
||||
}
|
||||
|
||||
async function load(){
|
||||
const url = api(`task?offset=${bounds.offset}&limit=${bounds.limit}`);
|
||||
const resp = await fetch(url,{credentials:'include'});
|
||||
const resp = await get(url);
|
||||
if (resp.ok){
|
||||
let newTasks = await resp.json();
|
||||
if (bounds.offset == 0) {
|
||||
@@ -93,9 +85,7 @@
|
||||
bounds.offset += bounds.limit;
|
||||
load();
|
||||
}
|
||||
} else {
|
||||
error(resp);
|
||||
}
|
||||
} else error(resp);
|
||||
}
|
||||
|
||||
function open(idx){
|
||||
@@ -139,13 +129,14 @@
|
||||
<th>{t('state')}</th>
|
||||
<th>{t('start_date')}</th>
|
||||
<th>{t('due_date')}</th>
|
||||
<th>{t('users')}</th>
|
||||
<th>{t('actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each tasks as task,idx}
|
||||
{#if task.status > 10 && task.status < 60 && !task.no_index && projects[task.project_id]?.status < 60 && !hidden[task.id] && filterApplies(task)}
|
||||
<tr>
|
||||
<tr title={task.description.source}>
|
||||
<td onclick={() => go('task',task.id)}>{task.name}</td>
|
||||
<td>
|
||||
<a href="#" onclick={() => go('project',task.project_id)}> {projects[task.project_id]?.name}</a>
|
||||
@@ -162,6 +153,16 @@
|
||||
<td>
|
||||
{task.due_date}
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
{#each Object.values(task.members) as member}
|
||||
<li>
|
||||
{member.user.name}:
|
||||
{t('permission_'+member.permission.name.toLowerCase())}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<button class="symbol" onclick={() => edit(task.id)} title={t('edit')} ></button>
|
||||
<button class="symbol" onclick={() => postpone(idx)} title={t('postpone')} ></button>
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
return Object.fromEntries(candidates);
|
||||
}
|
||||
|
||||
function gotoKanban(){
|
||||
if (!project) return;
|
||||
router.navigate(`/project/${project.id}/kanban`)
|
||||
}
|
||||
|
||||
|
||||
function gotoParent(){
|
||||
if (!task.parent_task_id) return;
|
||||
router.navigate(`/task/${task.parent_task_id}/view`)
|
||||
@@ -139,7 +145,6 @@
|
||||
loadChildren();
|
||||
}
|
||||
|
||||
|
||||
function showPrjFiles(){
|
||||
var url = `/files/project/${project.id}`;
|
||||
window.open(url, '_blank').focus();
|
||||
@@ -206,6 +211,7 @@
|
||||
<div>{t('project')}</div>
|
||||
<div class="project">
|
||||
<a href="#" onclick={gotoProject}>{project.name}</a>
|
||||
<button class="symbol" title={t('kanban')} onclick={gotoKanban}></button>
|
||||
<button class="symbol" title={t('files')} onclick={showPrjFiles}></button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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
|
||||
public Item loadProperties(Item item){
|
||||
try {
|
||||
|
||||
@@ -13,6 +13,7 @@ public interface StockDb {
|
||||
Collection<DbLocation> listChildLocations(long parentId);
|
||||
Collection<DbLocation> listCompanyLocations(Company company);
|
||||
Collection<Item> listItemsAt(Location location);
|
||||
Collection<Item> listItemsOf(Company company);
|
||||
Collection<Property> listProperties();
|
||||
Collection<DbLocation> listUserLocations(UmbrellaUser userId);
|
||||
Item loadItem(long id);
|
||||
|
||||
@@ -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.ModuleRegistry.companyService;
|
||||
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.stock.Constants.*;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
@@ -33,6 +35,7 @@ import org.json.JSONObject;
|
||||
public class StockModule extends BaseHandler implements StockService {
|
||||
|
||||
private final StockDb stockDb;
|
||||
private Comparator<Item> byName = (a,b) -> a.name().compareToIgnoreCase(b.name());
|
||||
|
||||
public StockModule(Configuration config) throws UmbrellaException {
|
||||
super();
|
||||
@@ -160,6 +163,7 @@ public class StockModule extends BaseHandler implements StockService {
|
||||
var head = path.pop();
|
||||
return switch (head) {
|
||||
case ITEM -> postItem(user.get(), ex);
|
||||
case LIST -> postItemList(user.get(), path, ex);
|
||||
case LOCATION -> postLocation(user.get(),ex);
|
||||
case PROPERTY -> postProperty(user.get(),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));
|
||||
}
|
||||
|
||||
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 {
|
||||
var json = json(ex);
|
||||
if (!(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
|
||||
|
||||
@@ -199,12 +199,9 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
} catch (NumberFormatException e) {
|
||||
throw invalidFieldException(LIMIT, "number");
|
||||
}
|
||||
Set<Long> projectIds = projectService().listUserProjects(user.id(), true).keySet();
|
||||
var list = taskDb.listUserTasks(user.id(), limit, offset, false).stream()
|
||||
.filter(task -> projectIds.contains(task.projectId())) // drop tasks assigned to project we are not member of
|
||||
.map(Task::toMap)
|
||||
.toList();
|
||||
return sendContent(ex, list);
|
||||
var tasks = taskDb.listUserTasks(user.id(), limit, offset, false);
|
||||
var mapped = loadMembers(tasks).stream().map(Task::toMap);
|
||||
return sendContent(ex, mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"permission_owner": "Besitzer",
|
||||
"permission_read_only": "lesen",
|
||||
"phone": "Telefon",
|
||||
"pieces": "Stück",
|
||||
"pos": "Pos",
|
||||
"position": "Position",
|
||||
"positions": "Positionen",
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"permission_owner": "owner",
|
||||
"permission_read_only": "read-only",
|
||||
"phone": "phone",
|
||||
"pieces": "pieces",
|
||||
"pos": "pos",
|
||||
"position": "position",
|
||||
"positions": "positions",
|
||||
|
||||
@@ -139,6 +139,10 @@ tr:hover .taglist .tag button {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.states .active{
|
||||
background: red;
|
||||
}
|
||||
|
||||
.taglist .tag{
|
||||
border-color: red;
|
||||
}
|
||||
@@ -186,8 +190,6 @@ tr:hover .taglist .tag button {
|
||||
border-color: 1px solid;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.warn {
|
||||
background-color: yellow;
|
||||
color: black;
|
||||
@@ -201,6 +203,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p10 .name{
|
||||
color: #ffa736;
|
||||
}
|
||||
.kanban .state_custom .box.p10,
|
||||
.kanban .state_20 .box.p10,
|
||||
.kanban .state_40 .box.p10{
|
||||
border: 5px solid #ffa736;
|
||||
@@ -209,6 +212,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p20 .name{
|
||||
color: #ff8f00;
|
||||
}
|
||||
.kanban .state_custom .box.p20,
|
||||
.kanban .state_20 .box.p20,
|
||||
.kanban .state_40 .box.p20{
|
||||
border: 5px solid #ff8f00;
|
||||
@@ -217,6 +221,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p30 .name{
|
||||
color: #ff7b06;
|
||||
}
|
||||
.kanban .state_custom .box.p30,
|
||||
.kanban .state_20 .box.p30,
|
||||
.kanban .state_40 .box.p30{
|
||||
border: 5px solid #ff7b06;
|
||||
@@ -225,6 +230,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p40 .name{
|
||||
color: #ff6306;
|
||||
}
|
||||
.kanban .state_custom .box.p40,
|
||||
.kanban .state_20 .box.p40,
|
||||
.kanban .state_40 .box.p40{
|
||||
border: 5px solid #ff6306;
|
||||
@@ -233,6 +239,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p50 .name{
|
||||
color: #ff4c06;
|
||||
}
|
||||
.kanban .state_custom .box.p50,
|
||||
.kanban .state_20 .box.p50,
|
||||
.kanban .state_40 .box.p50{
|
||||
border: 5px solid #ff4c06;
|
||||
@@ -241,6 +248,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p60 .name{
|
||||
color: #ff3506;
|
||||
}
|
||||
.kanban .state_custom .box.p60,
|
||||
.kanban .state_20 .box.p60,
|
||||
.kanban .state_40 .box.p60{
|
||||
border: 5px solid #ff3506;
|
||||
@@ -249,6 +257,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p70 .name{
|
||||
color: #ff0000;
|
||||
}
|
||||
.kanban .state_custom .box.p70,
|
||||
.kanban .state_20 .box.p70,
|
||||
.kanban .state_40 .box.p70{
|
||||
border: 5px solid #ff0000;
|
||||
@@ -257,6 +266,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p80 .name{
|
||||
color: #df153b;
|
||||
}
|
||||
.kanban .state_custom .box.p80,
|
||||
.kanban .state_20 .box.p80,
|
||||
.kanban .state_40 .box.p80{
|
||||
border: 5px solid #df153b;
|
||||
@@ -266,6 +276,7 @@ tr:hover .taglist .tag button {
|
||||
background-color: #991c34;
|
||||
color: #ffff00;
|
||||
}
|
||||
.kanban .state_custom .box.p90,
|
||||
.kanban .state_20 .box.p90,
|
||||
.kanban .state_40 .box.p90{
|
||||
border: 5px solid #991c34;
|
||||
@@ -275,6 +286,7 @@ tr:hover .taglist .tag button {
|
||||
background-color: #733440;
|
||||
color: #ffff00;
|
||||
}
|
||||
.kanban .state_custom .box.p100,
|
||||
.kanban .state_20 .box.p100,
|
||||
.kanban .state_40 .box.p100{
|
||||
border: 5px solid #733440;
|
||||
@@ -282,4 +294,4 @@ tr:hover .taglist .tag button {
|
||||
|
||||
.vcard span.inactive{
|
||||
color: #222200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,6 @@ tr:hover .taglist .tag button {
|
||||
background: black;
|
||||
}
|
||||
|
||||
|
||||
.version a.selected{
|
||||
border-color: orange;
|
||||
}
|
||||
@@ -194,6 +193,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p10 .name{
|
||||
color: #fff066;
|
||||
}
|
||||
.kanban .state_custom .box.p10,
|
||||
.kanban .state_20 .box.p10,
|
||||
.kanban .state_40 .box.p10{
|
||||
border: 5px solid #fff066;
|
||||
@@ -202,6 +202,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p20 .name{
|
||||
color: #ffe706;
|
||||
}
|
||||
.kanban .state_custom .box.p20,
|
||||
.kanban .state_20 .box.p20,
|
||||
.kanban .state_40 .box.p20{
|
||||
border: 5px solid #ffe706;
|
||||
@@ -210,6 +211,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p30 .name{
|
||||
color: #ffa906;
|
||||
}
|
||||
.kanban .state_custom .box.p30,
|
||||
.kanban .state_20 .box.p30,
|
||||
.kanban .state_40 .box.p30{
|
||||
border: 5px solid #ffa906;
|
||||
@@ -218,6 +220,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p40 .name{
|
||||
color: #ff8606;
|
||||
}
|
||||
.kanban .state_custom .box.p40,
|
||||
.kanban .state_20 .box.p40,
|
||||
.kanban .state_40 .box.p40{
|
||||
border: 5px solid #ff8606;
|
||||
@@ -226,6 +229,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p50 .name{
|
||||
color: #ff4c06;
|
||||
}
|
||||
.kanban .state_custom .box.p50,
|
||||
.kanban .state_20 .box.p50,
|
||||
.kanban .state_40 .box.p50{
|
||||
border: 5px solid #ff4c06;
|
||||
@@ -234,6 +238,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p60 .name{
|
||||
color: #ff3506;
|
||||
}
|
||||
.kanban .state_custom .box.p60,
|
||||
.kanban .state_20 .box.p60,
|
||||
.kanban .state_40 .box.p60{
|
||||
border: 5px solid #ff3506;
|
||||
@@ -242,6 +247,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p70 .name{
|
||||
color: #ff0000;
|
||||
}
|
||||
.kanban .state_custom .box.p70,
|
||||
.kanban .state_20 .box.p70,
|
||||
.kanban .state_40 .box.p70{
|
||||
border: 5px solid #ff0000;
|
||||
@@ -250,6 +256,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p80 .name{
|
||||
color: #df153b;
|
||||
}
|
||||
.kanban .state_custom .box.p80,
|
||||
.kanban .state_20 .box.p80,
|
||||
.kanban .state_40 .box.p80{
|
||||
border: 5px solid #df153b;
|
||||
@@ -259,6 +266,7 @@ tr:hover .taglist .tag button {
|
||||
background-color: #991c34;
|
||||
color: #ffff00;
|
||||
}
|
||||
.kanban .state_custom .box.p90,
|
||||
.kanban .state_20 .box.p90,
|
||||
.kanban .state_40 .box.p90{
|
||||
border: 5px solid #991c34;
|
||||
@@ -268,6 +276,7 @@ tr:hover .taglist .tag button {
|
||||
background-color: #733440;
|
||||
color: #ffff00;
|
||||
}
|
||||
.kanban .state_custom .box.p100,
|
||||
.kanban .state_20 .box.p100,
|
||||
.kanban .state_40 .box.p100{
|
||||
border: 5px solid #733440;
|
||||
|
||||
@@ -124,6 +124,10 @@ tr:hover .taglist .tag button {
|
||||
border-color: blue;
|
||||
}
|
||||
|
||||
.states .active{
|
||||
background: #dfe4ff;
|
||||
}
|
||||
|
||||
.taglist .tag{
|
||||
border-color: blue;
|
||||
}
|
||||
@@ -179,6 +183,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p10 .name{
|
||||
color: #c9fbb2;
|
||||
}
|
||||
.kanban .state_custom .box.p10,
|
||||
.kanban .state_20 .box.p10,
|
||||
.kanban .state_40 .box.p10{
|
||||
border: 5px solid #c9fbb2;
|
||||
@@ -187,6 +192,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p20 .name{
|
||||
color: #cbff57;
|
||||
}
|
||||
.kanban .state_custom .box.p20,
|
||||
.kanban .state_20 .box.p20,
|
||||
.kanban .state_40 .box.p20{
|
||||
border: 5px solid #cbff57;
|
||||
@@ -195,6 +201,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p30 .name{
|
||||
color: #dfff44;
|
||||
}
|
||||
.kanban .state_custom .box.p30,
|
||||
.kanban .state_20 .box.p30,
|
||||
.kanban .state_40 .box.p30{
|
||||
border: 5px solid #dfff44;
|
||||
@@ -203,6 +210,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p40 .name{
|
||||
color: #f8ff29;
|
||||
}
|
||||
.kanban .state_custom .box.p40,
|
||||
.kanban .state_20 .box.p40,
|
||||
.kanban .state_40 .box.p40{
|
||||
border: 5px solid #f8ff29;
|
||||
@@ -211,6 +219,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p50 .name{
|
||||
color: #ffdb1b;
|
||||
}
|
||||
.kanban .state_custom .box.p50,
|
||||
.kanban .state_20 .box.p50,
|
||||
.kanban .state_40 .box.p50{
|
||||
border: 5px solid #ffdb1b;
|
||||
@@ -219,6 +228,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p60 .name{
|
||||
color: #ff9309;
|
||||
}
|
||||
.kanban .state_custom .box.p60,
|
||||
.kanban .state_20 .box.p60,
|
||||
.kanban .state_40 .box.p60{
|
||||
border: 5px solid #ff9309;
|
||||
@@ -227,6 +237,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p70 .name{
|
||||
color: #ff6c00;
|
||||
}
|
||||
.kanban .state_custom .box.p70,
|
||||
.kanban .state_20 .box.p70,
|
||||
.kanban .state_40 .box.p70{
|
||||
border: 5px solid #ff6c00;
|
||||
@@ -235,6 +246,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p80 .name{
|
||||
color: #ff3c00;
|
||||
}
|
||||
.kanban .state_custom .box.p80,
|
||||
.kanban .state_20 .box.p80,
|
||||
.kanban .state_40 .box.p80{
|
||||
border: 5px solid #ff3c00;
|
||||
@@ -243,6 +255,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p90 .name{
|
||||
color: #ff0000;
|
||||
}
|
||||
.kanban .state_custom .box.p90,
|
||||
.kanban .state_20 .box.p90,
|
||||
.kanban .state_40 .box.p90{
|
||||
border: 5px solid #ff0000;
|
||||
@@ -251,6 +264,7 @@ tr:hover .taglist .tag button {
|
||||
.task.p100 .name{
|
||||
color: #ff0048;
|
||||
}
|
||||
.kanban .state_custom .box.p100,
|
||||
.kanban .state_20 .box.p100,
|
||||
.kanban .state_40 .box.p100{
|
||||
border: 5px solid #ff0048;
|
||||
@@ -258,4 +272,4 @@ tr:hover .taglist .tag button {
|
||||
|
||||
.vcard span.inactive{
|
||||
color: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user