Compare commits

..
Author SHA1 Message Date
StephanRichter 8197a0796c implemented live price update in document view
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-03 21:50:57 +01:00
StephanRichter 74a1d526ae implementd search in stock items
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-03 18:45:46 +01:00
StephanRichter 4cb9c6bd2f preparing search in stock items 2025-12-03 09:34:46 +01:00
StephanRichter fdffad6022 removed statement that alters the log level
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-02 13:24:53 +01:00
StephanRichter a4bcb02459 Merge remote-tracking branch 'origin/feature/items_stock_integration' into feature/items_stock_integration 2025-12-02 11:23:53 +01:00
StephanRichter e887a13bbb added plenty of logging, updateed tools.jdbc
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-02 11:23:04 +01:00
StephanRichter d6b56ac127 fixing document module to receive unit, price and tax value from stock item, if provided
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-01 16:03:40 +01:00
StephanRichter a3bcc66b73 finished implementation of migration from legacy item db to stock db
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-01 09:09:41 +01:00
StephanRichter 90528cfcac migration partially working:
- upon migration, items appear in stock module
- after restart, they are gone
- only one item is actually persisted in the DB???

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-30 23:44:01 +01:00
StephanRichter fd536abe11 working on migration of items to stock db
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-30 23:24:02 +01:00
StephanRichter d6b5d243c1 removed autofocus in easylist
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-29 13:12:07 +01:00
StephanRichter 8f82ca87b4 major improvement to easylist for usability on mobile devices
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-29 00:43:58 +01:00
StephanRichter 600b0f2cf4 working on event handlers for mobile devices
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 20:38:45 +01:00
StephanRichter 59f864d16f improved easylist
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 16:01:19 +01:00
StephanRichter 261a93bcc0 improved CSS
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 14:34:45 +01:00
StephanRichter 60c447a967 added easy list button to tag view
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 14:03:58 +01:00
StephanRichter b0550db5c2 implemented easy-list
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 13:56:55 +01:00
StephanRichter 40c35bc9ed added kanban button to task display
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 11:06:45 +01:00
StephanRichter 5fc79e05da Merge branch 'feature/list_task_users' 2025-11-28 09:16:10 +01:00
StephanRichter 7de79627b4 Merge remote-tracking branch 'origin' 2025-11-28 09:16:03 +01:00
StephanRichter 146ea80e4e implemented listing users on task index page
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 08:49:51 +01:00
StephanRichter 21759d1b12 Merge branch 'main' into dev 2025-11-26 21:09:31 +01:00
StephanRichter ba531a0ab1 Merge branch 'module/projects' 2025-11-26 21:08:56 +01:00
StephanRichter 7e02fe17e9 Merge branch 'module/projects' 2025-11-26 21:06:35 +01:00
StephanRichter 37e6ccd71a Merge branch 'module/projects' into dev 2025-11-26 09:12:26 +01:00
StephanRichter 11c0983dac improved button colors
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-26 09:12:17 +01:00
StephanRichter 2bad462c38 Merge branch 'module/stock' 2025-11-26 09:04:18 +01:00
StephanRichter daa25044a4 Merge branch 'module/projects' into dev 2025-11-26 08:47:34 +01:00
StephanRichter 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
StephanRichter e450585d37 fixed bug: import was missing
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-26 08:26:42 +01:00
StephanRichter 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
StephanRichter 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
44 changed files with 850 additions and 258 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ subprojects {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
implementation("de.srsoftware:configuration.api:1.0.2")
implementation("de.srsoftware:tools.jdbc:2.0.2")
implementation("de.srsoftware:tools.jdbc:2.0.7")
implementation("de.srsoftware:tools.http:6.0.5")
implementation("de.srsoftware:tools.mime:1.1.3")
implementation("de.srsoftware:tools.logging:1.3.2")
@@ -127,6 +127,7 @@ public class Constants {
public static final String TAG_COLORS = "tag_colors";
public static final String TASK_IDS = "task_ids";
public static final String TAX = "tax";
public static final String TAX_RATE = "tax_rate";
public static final String TEMPLATE = "template";
public static final String TEXT = "text";
public static final String THOUSANDS_SEPARATOR = "thousands_separator";
@@ -20,6 +20,7 @@ public class Paths {
public static final String STARTED = "started";
public static final String STOP = "stop";
public static final String SUBMIT = "submit";
public static final String TAGGED = "tagged";
public static final String TOKEN = "token";
public static final String VIEW = "view";
}
@@ -8,7 +8,7 @@ import java.util.Collection;
public interface StockService {
/**
* Das war mal die methode um zu checken, ob einer Firma noch Items zugewiesen sind.
* Diese Methode muss neu definiert werden, sobald der Stock-Service neu implementiert ist.
* TODO: Diese Methode muss neu definiert werden, sobald der Stock-Service neu implementiert ist.
* @param company_id
* @return
*/
@@ -4,11 +4,14 @@ package de.srsoftware.umbrella.core.api;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public interface TagService {
void deleteEntity(String task, long taskId);
Map<String, List<Long>> getTagUses(UmbrellaUser user, String tag);
Collection<String> getTags(String module, long entityId, UmbrellaUser user) throws UmbrellaException;
/**
@@ -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;
}
@@ -2,6 +2,7 @@
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Util.mapMarkdown;
import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.api.Owner;
@@ -13,19 +14,20 @@ import org.json.JSONObject;
public class Item implements Mappable {
private long id, ownerNumber; // id is the database key, number the owner-relative id
private Owner owner;
private String code, name;
private String code, description, name;
private Location location;
private Collection<Property> properties;
private Set<String> dirtyFields = new HashSet<>();
public Item(long id, Owner owner, long ownerNumber, Location location, String code, String name) {
this.id = id;
this.owner = owner;
public Item(long id, Owner owner, long ownerNumber, Location location, String code, String name, String description) {
this.id = id;
this.owner = owner;
this.ownerNumber = ownerNumber;
this.location = location;
this.code = code;
this.name = name;
this.properties = new HashSet<>();
this.location = location;
this.code = code;
this.name = name;
this.description = description;
this.properties = new HashSet<>();
}
public Item clear() {
@@ -37,6 +39,10 @@ public class Item implements Mappable {
return code;
}
public String description(){
return description;
}
public boolean isDirty(){
return !dirtyFields.isEmpty();
}
@@ -65,13 +71,14 @@ public class Item implements Mappable {
}
public static Item of(ResultSet rs) throws SQLException {
var id = rs.getLong(ID);
var owner = OwnerRef.of(rs);
var id = rs.getLong(ID);
var owner = OwnerRef.of(rs);
var ownerNumber = rs.getLong(OWNER_NUMBER);
var location = Location.of(rs);
var code = rs.getString(CODE);
var name = rs.getString(NAME);
return new Item(id, owner, ownerNumber, location, code, name);
var location = Location.of(rs);
var code = rs.getString(CODE);
var name = rs.getString(NAME);
var description = rs.getString(DESCRIPTION);
return new Item(id, owner, ownerNumber, location, code, name, description);
}
public Owner owner(){
@@ -82,6 +89,10 @@ public class Item implements Mappable {
return ownerNumber;
}
public void ownerNumber(long newVal) {
ownerNumber = newVal;
}
public Item patch(JSONObject json) {
for (var field : json.keySet()){
var known = true;
@@ -92,6 +103,9 @@ public class Item implements Mappable {
case NAME:
name = json.getString(field);
break;
case DESCRIPTION:
description = json.getString(field);
break;
default:
known = false;
}
@@ -112,6 +126,7 @@ public class Item implements Mappable {
map.put(LOCATION,location.toMap());
map.put(CODE,code);
map.put(NAME,name);
map.put(DESCRIPTION,mapMarkdown(description));
map.put(OWNER_NUMBER,ownerNumber);
if (properties != null) map.put(PROPERTIES,properties.stream().map(Property::toMap).toList());
return map;
@@ -2,6 +2,7 @@
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static java.text.MessageFormat.format;
import de.srsoftware.tools.Mappable;
import java.sql.ResultSet;
@@ -22,6 +23,14 @@ public class Property implements Mappable {
this.unit = unit;
}
public long id(){
return id;
}
public String name(){
return name;
}
public static Property of(ResultSet rs) throws SQLException {
var id = rs.getLong(ID);
var name = rs.getString(NAME);
@@ -45,6 +54,19 @@ public class Property implements Mappable {
return map;
}
@Override
public String toString() {
return format("{0} ({1} = {2}{3})",getClass().getSimpleName(),name,value,unit==null?"":" "+unit);
}
public String unit(){
return unit;
}
public Object value(){
return value;
}
public Property value(Object newVal){
value = newVal;
return this;
@@ -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);
}
}
@@ -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);
@@ -517,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,8 +26,6 @@ public interface DocumentDb {
CustomerSettings getCustomerSettings(long companyId, Type docType, String customerId) throws UmbrellaException;
Collection<Template> getCompanyTemplates(long l) throws UmbrellaException;
Type getType(int typeId) throws UmbrellaException;
Map<Long, Document> listDocs(long companyId) throws UmbrellaException;
@@ -42,6 +42,15 @@ public class SqliteDb extends BaseDb implements DocumentDb{
super(connection);
}
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) {
@@ -52,22 +61,15 @@ public class SqliteDb extends BaseDb implements DocumentDb{
createTablePositions();
createTableCustomerPrices();
createTableCustomerSettings();
case 1:
addTemplateColumn();
moveTemplateNames();
dropTemplateTable();
dropTemplateIdColumn();
}
return setCurrentVersion(1);
return setCurrentVersion(2);
}
/*private void createTableCompanySettings() {
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} INT NOT NULL, {3} TEXT DEFAULT \"A\", {4} TEXT DEFAULT NULL, {5} INT NOT NULL DEFAULT 1, PRIMARY KEY ({1}, {2}))";
try {
var stmt = db.prepareStatement(format(sql,TABLE_COMPANY_SETTINGS, 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);
}
}*/
private void createTableCustomerPrices() {
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} VARCHAR(255), {3} VARCHAR(50), {4} INTEGER)";
try {
@@ -172,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 {
@@ -252,17 +224,21 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
return pos;
}
@Override
public Collection<Template> getCompanyTemplates(long companyId) throws UmbrellaException {
private void dropTemplateIdColumn() {
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("ALTER TABLE {0} DROP COLUMN {1}",TABLE_DOCUMENTS,TEMPLATE_ID);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("Failed to load templates for company {0}",companyId);
throw databaseException("Failed to update column {0} → {1} of {2}",TEMPLATE_ID,TEMPLATE,TABLE_DOCUMENTS);
}
}
private void dropTemplateTable() {
try {
var sql = format("DROP TABLE IF EXISTS {0};",TABLE_TEMPLATES);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("Failed to drop table {0}",TABLE_TEMPLATES);
}
}
@@ -307,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 {
@@ -430,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();
@@ -452,6 +424,15 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
throw new UmbrellaException(500,"Failed to load document {0}.",docId);
}
private void moveTemplateNames() {
try {
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!");
}
}
@Override
public String nextDocId(String language, long companyId, Type type) {
try {
@@ -499,9 +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 templateId = doc.template() == null ? null : doc.template().id();
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(),templateId, 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;
@@ -523,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();
@@ -635,23 +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);
if (id == 0) return new Template(0,company,"",null);
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);
+2
View File
@@ -13,6 +13,7 @@
import Companies from "./routes/company/Index.svelte";
import ContactList from "./routes/contact/Index.svelte";
import DocList from "./routes/document/List.svelte";
import EasyList from "./routes/task/EasyList.svelte";
import EditService from "./routes/user/EditService.svelte";
import EditUser from "./routes/user/EditUser.svelte";
import FileIndex from "./routes/files/Index.svelte";
@@ -104,6 +105,7 @@
<Route path="/tags" component={TagList} />
<Route path="/tags/use/:tag" component={TagUses} />
<Route path="/task" component={TaskList} />
<Route path="/tags/easylist/:tag" component={EasyList} />
<Route path="/task/:parent_task_id/add_subtask" component={AddTask} />
<Route path="/task/:id/edit" component={ViewTask} />
<Route path="/task/:id/view" component={ViewTask} />
@@ -14,6 +14,23 @@
} = $props();
let editable = $derived(document.state == 1);
let sums = $derived.by(calcSums);
function calcSums(){
let data = {}
let net = 0;
let gross = 0;
for (let pos of Object.values(document.positions)){
let net_price = pos.unit_price * pos.amount;
let tax = +pos.tax;
data[tax] = net_price + (data[tax] ? data[tax] : 0);
net += net_price;
}
for (let [tax, price] of Object.entries(data)) gross += price * (+tax+100)/100;
data['net'] = net/100;
data['gross'] = (gross/100).toFixed(2);
return data;
}
async function updatePositions(resp){
let json = await resp.json();
@@ -22,7 +39,7 @@
}
async function movePos(number,step){
const url = api(`document/${document.id}/position`);
const url = api(`document/${document.id}/position`);
const resp = await fetch(url,{
method : 'PATCH',
credentials: 'include',
@@ -38,7 +55,7 @@
async function drop(number){
let confirmed = confirm(t('confirm_deletion').replace('{pos}',document.positions[number].item));
if (!confirmed) return;
const url = api(`document/${document.id}/position`);
const url = api(`document/${document.id}/position`);
const resp = await fetch(url,{
method : 'DELETE',
credentials: 'include',
@@ -73,9 +90,9 @@
<tr class="sums">
<td colspan="2"></td>
<td>{t('net_sum')}</td>
<td>{document.net_sum/100}&nbsp;{document.currency}</td>
<td>{sums['net']}&nbsp;{document.currency}</td>
<td colspan="2">{t('gross_sum')}</td>
<td>{document.gross_sum/100}&nbsp;{document.currency}</td>
<td>{sums['gross']}&nbsp;{document.currency}</td>
<td></td>
</tr>
</tbody>
@@ -30,23 +30,33 @@
function itemSelected(item){
let unit_price = null;
let unit = t('pieces');
let description = '';
let tax = null;
for (let prop of item.properties) {
if (prop.name.toLowerCase().indexOf(t('price').toLowerCase())>-1){
unit_price = 100*prop.value.replace(',','.');
let lowerName = prop.name.toLowerCase();
if (lowerName.indexOf(t('price').toLowerCase())>-1){
unit_price = 100*String(prop.value).replace(',','.');
} else if (lowerName.indexOf(t('unit').toLowerCase())>-1){
unit = prop.value;
} else if (lowerName.indexOf(t('tax_rate').toLowerCase())>-1 && prop.unit=='%'){
tax = prop.value;
} else {
description += `* ${prop.name}: ${prop.value}\n`;
}
}
select({
var data = {
item_code : item.code,
title : item.name,
description : description,
amount : 1,
unit : t('pieces'),
unit : unit,
unit_price : unit_price
});
};
if (tax) data['tax'] = tax;
select(data);
}
function timeSelected(time){
@@ -30,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 -2
View File
@@ -214,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>
+5 -1
View File
@@ -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} />
+38 -14
View File
@@ -1,7 +1,7 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api, target } from '../../urls.svelte.js';
import { api, post, target } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js';
import { display } from '../../time.svelte';
@@ -18,6 +18,7 @@
let notes = $state(null);
let pages = $state(null);
let projects = $state(null);
let stock = $state(null);
let tasks = $state(null);
let times = $state(null);
@@ -38,19 +39,16 @@
window.history.replaceState(history.state, '', url);
const data = { key : key, fulltext : fulltext };
const options = {
credentials:'include',
method: 'POST',
body: JSON.stringify(data)
};
fetch(api('bookmark/search'),options).then(handleBookmarks);
fetch(api('company/search'),options).then(handleCompanies);
fetch(api('document/search'),options).then(handleDocuments);
fetch(api('notes/search'),options).then(handleNotes);
fetch(api('project/search'),options).then(handleProjects);
fetch(api('task/search'),options).then(handleTasks);
fetch(api('time/search'),options).then(handleTimes);
fetch(api('wiki/search'),options).then(handleWikiPages);
post(api('bookmark/search'),data).then(handleBookmarks);
post(api('company/search '),data).then(handleCompanies);
post(api('document/search'),data).then(handleDocuments);
post(api('notes/search' ),data).then(handleNotes);
post(api('project/search' ),data).then(handleProjects);
post(api('task/search' ),data).then(handleTasks);
post(api('stock/search' ),data).then(handleStock);
post(api('time/search' ),data).then(handleTimes);
post(api('wiki/search' ),data).then(handleWikiPages);
}
function onclick(e){
@@ -107,6 +105,15 @@
}
}
async function handleStock(resp){
if (resp.ok){
const res = await resp.json();
stock = Object.keys(res).length ? res : null;
} else {
error(resp);
}
}
async function handleTasks(resp){
if (resp.ok){
const res = await resp.json();
@@ -212,6 +219,23 @@
</ul>
</fieldset>
{/if}
{#if stock}
<fieldset>
<legend>
{t('stock')}
</legend>
<ul>
{#each Object.values(stock) as item}
<li>
<a href="/stock/{item.owner.type}/{item.owner.id}/item/{item.owner_number}" {onclick} >
{item.name} [{t('code')}: <span class="code">{item.code}</span>]
</a>
{@html item.description.rendered}
</li>
{/each}
</ul>
</fieldset>
{/if}
{#if notes}
<fieldset>
<legend>
-1
View File
@@ -145,7 +145,6 @@
error(res);
return null;
}
}
async function loadProperties(){
@@ -69,6 +69,9 @@
{#if item}
<LineEditor type="h3" editable={true} value={item.name} onSet={v => patch('name',v)} />
Code: <LineEditor type="span" editable={true} value={item.code} onSet={v => patch('code',v)} />
<div>
{@html item.description.rendered}
</div>
<table>
<tbody>
{#each item.properties.toSorted(byName) as prop}
+3 -3
View File
@@ -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';
+9 -1
View File
@@ -1,5 +1,6 @@
<script>
import {onMount} from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte';
@@ -8,6 +9,7 @@
import Reference from './Reference.svelte';
let { tag } = $props();
let router = useTinyRouter();
let uses = $state(null);
async function loadUses(){
@@ -21,11 +23,17 @@
}
}
function goEasy(){
router.navigate(`/tags/easylist/${tag}`);
}
onMount(loadUses);
</script>
<fieldset>
<legend>{t('tag_uses',{tag:tag})}</legend>
<legend>
{t('tag_uses',{tag:tag})}
<button onclick={goEasy}>{t('easy_list')}</button>
</legend>
{#if uses}
{#each Object.entries(uses) as [module,ids]}
<h2>{t(module.endsWith('s') ? module : `${module}s`)}</h2>
+171
View File
@@ -0,0 +1,171 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api, get, patch } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte';
import Detail from './EasyListDetail.svelte';
let { tag } = $props();
let filter = $state(null);
let highlight = $state(null);
let search = $derived(filter ? filter.toLowerCase() : null);
//let input;
let tasks = $state(null);
let router = useTinyRouter();
let sorted = $derived(tasks ? Object.values(tasks).filter(noNoIndex).sort(byName) : null);
let start = 0;
let x = 0;
let y = 0;
function byName(a,b){
return a.name.localeCompare(b.name);
}
function extend(e,task){
e.preventDefault();
e.stopPropagation();
highlight = task;
return false;
}
function getTask(evt){
var link = evt.target;
var id = link.getAttribute('task_id');
return tasks[id];
}
function goTag(e,newTag){
e.preventDefault();
e.stopPropagation();
router.navigate(`/tags/easylist/${newTag}`);
tag = newTag;
load();
}
function ignore(evt){
evt.preventDefault();
evt.stopPropagation();
return false;
}
async function load(){
const url = api(`task/tagged/${tag}`);
const res = await get(url);
if (res.ok){
yikes();
tasks = await res.json();
//input.focus();
} else error(res);
}
function match(task){
if (!search) return true;
if (task.name.toLowerCase().includes(search)) return true;
if (task.tags){
for (let tag of task.tags){
if (tag.toLowerCase().includes(search)) return true;
}
}
return false;
}
function measured(evt,duration,d){
if (d > 100) return;
if (duration < 500){
onclick(evt);
} else {
oncontextmenu(evt);
}
}
function noNoIndex(task){
return !task.no_index;
}
function onclick(evt) {
ignore(evt);
let task = getTask(evt);
if (task.status <= 20) { // open
update(task,60);
} else update(task,20);
return false;
}
function oncontextmenu(evt) {
ignore(evt);
highlight = getTask(evt);
return false;
}
function ontouchstart(evt){
ignore(evt);
start = evt.timeStamp;
x = evt.touches[0].clientX;
y = evt.touches[0].clientY;
return false;
}
function ontouchend(evt){
ignore(evt);
let d = Math.abs(x - evt.changedTouches[0].clientX) + Math.abs(y - evt.changedTouches[0].clientY);
measured(evt, evt.timeStamp - start, d);
return false;
}
async function update(task,newState){
highlight = null;
const url = api(`task/${task.id}`);
const res = await patch(url,{status:newState});
if (res.ok){
task.status = newState;
yikes();
// filter = null; // not sure what is better, resetting or keeping
} else error(res);
}
onMount(load);
</script>
<h2>{t('tasks_for_tag',{tag:decodeURI(tag)})}</h2>
<div class="easylist">
<fieldset class="open">
<legend>{t('state_open')}</legend>
{#if sorted}
{#each sorted as task}
{#if task.status == 20 && match(task)}
<div href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} onmousedown={ontouchstart} onmouseup={ontouchend} >
{task.name}
</div>
{#if highlight == task}
<Detail task={task} goTag={goTag} />
{/if}
{/if}
{/each}
{/if}
</fieldset>
<fieldset class="closed">
<legend>{t('state_complete')}</legend>
{#if sorted}
{#each sorted as task}
{#if task.status > 20 && match(task)}
<div href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} onmousedown={ontouchstart} onmouseup={ontouchend} >
{task.name}
</div>
{#if highlight == task}
<Detail task={task} goTag={goTag} />
{/if}
{/if}
{/each}
{/if}
</fieldset>
<div class="filter">
{t('filter')}: <input type="text" bind:value={filter} /> <!-- bind:this={input} -->
</div>
</div>
@@ -0,0 +1,22 @@
<script>
import { useTinyRouter } from 'svelte-tiny-router';
import { t } from '../../translations.svelte';
let { goTag, task } = $props();
let router = useTinyRouter();
function onclick(){
router.navigate(`/task/${task.id}/edit`);
}
</script>
<button class="edit" {onclick}>{t('edit')}</button>
{@html task.description.rendered}
{#if task.tags}
{t('other_tags')}:<br/>
{#each task.tags as tag}
<button onclick={e => goTag(e,tag)}>{tag}</button>
{/each}
<hr />
{/if}
+19 -18
View File
@@ -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>
+7 -1
View File
@@ -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}
+1
View File
@@ -2,4 +2,5 @@ description = "Umbrella : Stock"
dependencies{
implementation(project(":core"))
implementation("de.srsoftware:configuration.json:1.0.3")
}
@@ -8,6 +8,7 @@ public class Constants {
public static final String BELOW = "below";
public static final String CONFIG_DATABASE = "umbrella.modules.stock.database";
public static final String CONFIG_ITEM_DB = "umbrella.modules.items.database";
public static final String ITEM = "item";
public static final String ITEM_ID = "item_id";
public static final String ITEMS = "items";
@@ -0,0 +1,96 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.stock;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.tools.jdbc.Query.select;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Field.COMPANY_ID;
import static de.srsoftware.umbrella.core.Field.UNIT_PRICE;
import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
import static de.srsoftware.umbrella.core.ModuleRegistry.translator;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
import static de.srsoftware.umbrella.stock.Constants.TABLE_ITEMS;
import static java.lang.System.Logger.Level.DEBUG;
import de.srsoftware.tools.ColorLogger;
import de.srsoftware.tools.Tuple;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.model.*;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Comparator;
import java.util.HashMap;
public class ItemDb {
private final System.Logger LOG = System.getLogger(getClass().getSimpleName());
private final Connection db;
public ItemDb(String dbFilePath){
db = connect(dbFilePath);
}
public void migrateTo(StockDb stockDb) {
try {
var companyLocations = new HashMap<Long,Location>();
var companyInfo = new HashMap<Long, Tuple<Company,String>>(); // map from companyId (company, language)
LOG.log(DEBUG,"Reading items:\nid: code / name / unit / price / tax rate");
var rs = select(ALL).from(TABLE_ITEMS).exec(db);
while (rs.next()){
var id = rs.getLong(ID);
var companyId = rs.getLong(COMPANY_ID);
var code = rs.getString(CODE);
var name = rs.getString(NAME);
var description = rs.getString(DESCRIPTION);
var unit = rs.getString(UNIT);
var unitPrice = rs.getLong(UNIT_PRICE);
var tax = rs.getLong(TAX);
var tuple = companyInfo.get(companyId);
LOG.log(DEBUG," - read item {0}: {1} / {2} / {3} / {4} / {5} %",id,companyId,code,name,unit,unitPrice,tax);
String lang = null;
Company company;
if (tuple == null){
LOG.log(DEBUG, " loading company {0}:",companyId);
company = companyService().get(companyId);
LOG.log(DEBUG, " → {0}",company.name());
for (var member : companyService().getMembers(companyId)){
lang = member.language();
if (lang != null){
LOG.log(DEBUG, " → language = {0}",lang);
tuple = Tuple.of(company,lang);
companyInfo.put(companyId,tuple);
break;
}
}
} else {
company = tuple.a;
lang = tuple.b;
LOG.log(DEBUG, " using company: {0} ({1})",company.name(),lang);
}
var location = companyLocations.get(companyId);
if (location == null) {
location = stockDb.save(new DbLocation(0,company,null,"virtual items",null));
companyLocations.put(companyId,location);
}
LOG.log(DEBUG, " using location: {0}",location.resolve().name());
var stockItem = new Item(0,company,0,location,code,name,description);
var props = stockItem.properties();
var keyUnitPrice = translator().translate(lang,UNIT_PRICE);
var keyUnit = translator().translate(lang,UNIT);
var keyTax = translator().translate(lang,TAX_RATE);
var keyLegacyId = translator().translate(lang,"legacy_id");
props.add(new Property(0,keyUnitPrice,unitPrice/100d,company.currency()));
props.add(new Property(0,keyUnit,unit,null));
props.add(new Property(0,keyTax,tax,"%"));
props.add(new Property(0,keyLegacyId,id,null));
LOG.log(DEBUG," saving item {0}:",stockItem);
stockDb.save(stockItem);
}
rs.close();
} catch (SQLException e) {
throw databaseException("Failed to migrate items from itemDB to stockDB!");
}
}
}
@@ -3,21 +3,21 @@ package de.srsoftware.umbrella.stock;
import static de.srsoftware.tools.Optionals.is0;
import static de.srsoftware.tools.Optionals.nullIfEmpty;
import static de.srsoftware.tools.jdbc.Condition.equal;
import static de.srsoftware.tools.jdbc.Condition.isNull;
import static de.srsoftware.tools.jdbc.Condition.*;
import static de.srsoftware.tools.jdbc.Condition.like;
import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.ModuleRegistry.noteService;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
import static de.srsoftware.umbrella.stock.Constants.*;
import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.WARNING;
import static java.lang.System.Logger.Level.*;
import static java.text.MessageFormat.format;
import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.api.Owner;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.core.model.Location;
import java.sql.Connection;
@@ -51,21 +51,22 @@ public class SqliteDb extends BaseDb implements StockDb {
Long propertyId = null;
if (rs.next()) propertyId = rs.getLong(1);
rs.close();
if (propertyId == null || propertyId == 0) throw databaseException("Failed to create new property {0} to DB",name);
insertInto(TABLE_ITEM_PROPERTIES,ITEM_ID,PROPERTY_ID,VALUE).values(itemId,propertyId,value).execute(db);
if (propertyId == null || propertyId == 0) throw databaseException("Failed to create new property {0} in DB",name);
insertInto(TABLE_ITEM_PROPERTIES,ITEM_ID,PROPERTY_ID,VALUE).values(itemId,propertyId,value).execute(db).close();
db.setAutoCommit(true);
return new Property(propertyId,name,value,unit);
} catch (SQLException e) {
throw databaseException("Failed to create new property {0} to DB",name);
throw databaseException("Failed to create new property {0} in DB",name);
}
}
@Override
public Location delete(DbLocation location) {
private void createDescriptionColumn(){
try {
Query.delete().from(TABLE_LOCATIONS).where(ID,equal(location.id())).execute(db);
return location;
} catch (SQLException e){
throw databaseException("Failed to delete \"{0}\"",location.name());
var sql = "ALTER TABLE {0} ADD COLUMN {1} TEXT";
sql = format(sql,TABLE_ITEMS,DESCRIPTION);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("failed to create {0} column in {1} table!",DESCRIPTION,TABLE_ITEMS);
}
}
@@ -144,7 +145,7 @@ public class SqliteDb extends BaseDb implements StockDb {
private void createPropertiesTable() {
try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} LONG PRIMARY KEY, {2} VARCHAR(255) NOT NULL, {3} INT NOT NULL, {4} VARCHAR(255))";
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT NULL, {3} INT NOT NULL, {4} VARCHAR(255))";
sql = format(sql, TABLE_PROPERTIES, ID, NAME, TYPE, UNIT);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
@@ -165,8 +166,20 @@ public class SqliteDb extends BaseDb implements StockDb {
dropTokenTable();
case 2:
transformTables();
case 3:
createDescriptionColumn();
}
return setCurrentVersion(4);
}
@Override
public Location delete(DbLocation location) {
try {
Query.delete().from(TABLE_LOCATIONS).where(ID,equal(location.id())).execute(db);
return location;
} catch (SQLException e){
throw databaseException("Failed to delete \"{0}\"",location.name());
}
return setCurrentVersion(3);
}
private void dropTokenTable() {
@@ -177,6 +190,30 @@ public class SqliteDb extends BaseDb implements StockDb {
}
}
@Override
public Map<Long, Item> find(Collection<Owner> owners, Collection<String> keys, boolean fulltext) {
try {
var items = new HashMap<Long,Item>();
var ownerCodes = owners.stream().map(Owner::dbCode).toArray();
var query = select(ALL).from(TABLE_ITEMS).where(OWNER, in(ownerCodes));
if (fulltext) {
query.leftJoin(ID,TABLE_ITEM_PROPERTIES,ITEM_ID);
for (var key : keys) query.where(format("CONCAT({0},\" \",{1},\" \",{2})",NAME,DESCRIPTION,VALUE),like("%"+key+"%"));
} else {
for (var key : keys) query.where(NAME,like("%"+key+"%"));
}
var rs = query.exec(db);
while (rs.next()){
var item = Item.of(rs);
items.put(item.id(),item);
}
rs.close();
return items;
} catch (SQLException e) {
throw new UmbrellaException("Failed to load items from database");
}
}
@Override
public Collection<DbLocation> listChildLocations(long parentId) {
try {
@@ -394,13 +431,13 @@ public class SqliteDb extends BaseDb implements StockDb {
@Override
public Item save(Item item) {
if (item.id() == 0){
var number = nextItemNumberFor(item.location().resolve().owner());
try {
var rs = insertInto(TABLE_ITEMS, OWNER, OWNER_NUMBER, CODE, NAME, LOCATION_ID)
.values(item.owner().dbCode(), item.ownerNumber(), item.code(), item.name(), item.location().id())
var rs = insertInto(TABLE_ITEMS, OWNER, OWNER_NUMBER, CODE, NAME, DESCRIPTION, LOCATION_ID)
.values(item.owner().dbCode(), number, item.code(), item.name(), item.description(), item.location().id())
.execute(db).getGeneratedKeys();
if (rs.next()) item.id(rs.getLong(1));
if (rs.next()) item.id(rs.getLong(1)).ownerNumber(number);
rs.close();
return item;
} catch (SQLException e) {
throw databaseException("Failed to save new item to database!");
}
@@ -409,25 +446,53 @@ public class SqliteDb extends BaseDb implements StockDb {
var location = item.location();
var query = update(TABLE_ITEMS).where(ID, equal(item.id()));
if (location == null) {
query.set(CODE,NAME);
query.set(CODE,NAME,DESCRIPTION);
} else {
query.set(CODE,NAME,LOCATION_ID);
query.set(CODE,NAME,DESCRIPTION,LOCATION_ID);
}
var pq = query.prepare(db);
if (location == null) {
pq.apply(item.code(),item.name());
pq.apply(item.code(),item.name(),item.description()).close();
} else {
pq.apply(item.code(),item.name(),item.location().id());
pq.apply(item.code(),item.name(),item.description(),item.location().id()).close();
}
return item.clear();
item.clear();
} catch (SQLException e){
throw databaseException("Failed to update item {0}",item.name());
}
}
saveProperties(item);
return item;
}
private void saveProperties(Item item){
var saved = new ArrayList<Property>();
for (var property : item.properties()) {
saved.add(saveProperty(item, property));
}
item.properties().clear();
item.properties().addAll(saved);
}
private Property saveProperty(Item item, Property property) {
Long propId = property.id();
if (is0(propId)) {
LOG.log(DEBUG,"Saving new property {0}",property);
try {
var rs = select(ID).from(TABLE_PROPERTIES).where(NAME,equal(property.name())).where(UNIT,equal(property.unit())).exec(db);
if (rs.next()) {
propId = rs.getLong(1);
}
rs.close();
} catch (SQLException e) {
throw databaseException("Failed to load property \"{}\"!",property.name());
}
}
if (is0(propId)) return addNewProperty(item.id(), property.name(), property.value(), property.unit());
return setProperty(item.id(),propId,property.value());
}
@Override
public Property setProperty(long itemId, long existingPropId, Object value) {
try {
@@ -435,11 +500,11 @@ public class SqliteDb extends BaseDb implements StockDb {
var rs = select(ALL).from(TABLE_PROPERTIES).where(ID,equal(existingPropId)).exec(db);
if (rs.next()) prop = Property.of(rs);
rs.close();
if (prop == null) throw databaseException("Failed to add new property to item {0}",itemId);
if (prop == null) throw databaseException("Failed to load property {0} for item {1}",existingPropId,itemId);
if ("".equals(value)){
Query.delete().from(TABLE_ITEM_PROPERTIES).where(ITEM_ID,equal(itemId)).where(PROPERTY_ID,equal(existingPropId)).execute(db);
} else {
replaceInto(TABLE_ITEM_PROPERTIES,ITEM_ID,PROPERTY_ID,VALUE).values(itemId,existingPropId,value).execute(db);
replaceInto(TABLE_ITEM_PROPERTIES,ITEM_ID,PROPERTY_ID,VALUE).values(itemId,existingPropId,value).execute(db).close();
}
return prop.value(value);
} catch (SQLException e) {
@@ -6,10 +6,12 @@ import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.core.model.Location;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public interface StockDb {
Property addNewProperty(long itemId, String name, Object value, String unit);
Location delete(DbLocation location);
Map<Long, Item> find(Collection<Owner> owners, Collection<String> keys, boolean fulltext);
Collection<DbLocation> listChildLocations(long parentId);
Collection<DbLocation> listCompanyLocations(Company company);
Collection<Item> listItemsAt(Location location);
@@ -24,5 +26,6 @@ public interface StockDb {
Map<String,Object> pathToLocation(Location location);
DbLocation save(DbLocation location);
Item save(Item item);
Property setProperty(long itemId, long existingPropId, Object value);
}
@@ -9,16 +9,16 @@ 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.Paths.SEARCH;
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;
import static java.text.MessageFormat.format;
import static java.util.Comparator.comparing;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Mappable;
import de.srsoftware.configuration.JsonConfig;
import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler;
@@ -41,6 +41,16 @@ public class StockModule extends BaseHandler implements StockService {
super();
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE));
stockDb = new SqliteDb(connect(dbFile));
Optional<String> itemDbConfig = config.get(CONFIG_ITEM_DB);
itemDbConfig.map(ItemDb::new).ifPresent(itemDb -> itemDb.migrateTo(stockDb));
if (itemDbConfig.isPresent()){
try {
config.drop(CONFIG_ITEM_DB);
if (config instanceof JsonConfig jsonConfig) jsonConfig.save();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
ModuleRegistry.add(this);
}
@@ -166,6 +176,7 @@ public class StockModule extends BaseHandler implements StockService {
case LIST -> postItemList(user.get(), path, ex);
case LOCATION -> postLocation(user.get(),ex);
case PROPERTY -> postProperty(user.get(),ex);
case SEARCH -> postSearch(user.get(),ex);
case null, default -> super.doPost(path,ex);
};
} catch (UmbrellaException e){
@@ -310,13 +321,13 @@ public class StockModule extends BaseHandler implements StockService {
private boolean postItem(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!json.has(NAME) || !(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
var description = json.has(DESCRIPTION) && json.get(DESCRIPTION) instanceof String d ? d : null;
if (!json.has(CODE) || !(json.get(CODE) instanceof String code)) throw missingFieldException(CODE);
if (!json.has(LOCATION) || !(json.get(LOCATION) instanceof JSONObject locationData)) throw missingFieldException(LOCATION);
var location = stockDb.loadLocation(locationData.getLong(ID));
var owner = location.owner().resolve();
if (!assigned(owner,user)) throw forbidden("You are not allowed to add items to {0}!",location);
var number = stockDb.nextItemNumberFor(owner);
var newItem = new Item(0,owner,number,location,code,name);
var newItem = new Item(0,owner,0,location,code,name,description);
return sendContent(ex,stockDb.save(newItem));
}
@@ -381,32 +392,20 @@ public class StockModule extends BaseHandler implements StockService {
return sendContent(ex,property);
}
private Mappable toOwner(JSONObject owner) {
var keys = owner.keySet();
if (keys.size() != 1) throw unprocessable("{0} expected to have only one child!",OWNER);
String key = new ArrayList<>(keys).getFirst();
return switch (key) {
case COMPANY -> companyService().get(owner.getLong(key));
case USER -> userService().loadUser(owner.getLong(key));
default -> throw invalidFieldException(format("Single child of {0}", OWNER), format("either {0} or {1}", COMPANY, USER));
};
}
private long toOwnerId(JSONObject owner) {
var keys = owner.keySet();
if (keys.size() != 1) throw unprocessable("{0} expected to have only one child!",OWNER);
String key = new ArrayList<>(keys).getFirst();
return switch (key) {
case COMPANY -> -owner.getLong(key);
case USER -> owner.getLong(key);
default -> throw invalidFieldException(format("Single child of {0}", OWNER), format("either {0} or {1}", COMPANY, USER));
};
private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingFieldException(KEY);
var keys = Arrays.asList(key.split(" "));
var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
Set<Owner> owners = new HashSet<>(companyService().listCompaniesOf(user).values());
owners.add(user);
var items = stockDb.find(owners,keys,fulltext);
return sendContent(ex,mapValues(items));
}
@Override
public Collection<Object> redefineMe(long company_id) {
// TODO
return List.of();
}
}
@@ -204,10 +204,10 @@ CREATE TABLE IF NOT EXISTS {0} (
public String delete(long userId, String module, long entityId, String tag) {
try {
Query.delete().from(TABLE_TAGS)
.where(TAG,equal(tag)).where(MODULE,equal(module)).where(ENTITY_ID,equal(entityId)).where(USER_ID,equal(userId))
.where(TAG,iEqual(tag)).where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId)).where(USER_ID,equal(userId))
.execute(db);
Query.delete().from(TABLE_TAGS)
.where(TAG,equal(tag)).where(MODULE,equal(module)).where(ENTITY_ID,equal(entityId)).where(USER_ID,isNull())
.where(TAG,iEqual(tag)).where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId)).where(USER_ID,isNull())
.execute(db);
return tag;
} catch (SQLException e){
@@ -219,7 +219,7 @@ CREATE TABLE IF NOT EXISTS {0} (
public void deleteEntity(String module, long entityId) {
try {
Query.delete().from(TABLE_TAGS)
.where(MODULE,equal(module)).where(ENTITY_ID,equal(entityId))
.where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId))
.execute(db);
} catch (SQLException e){
throw new UmbrellaException("Failed to save tags ({0} {1})",module,entityId);
@@ -229,7 +229,7 @@ CREATE TABLE IF NOT EXISTS {0} (
@Override
public Map<String, List<Long>> getUses(String tag, long userId) {
try {
var rs = select(ALL).from(TABLE_TAGS).where(TAG,equal(tag)).where(USER_ID,equal(userId)).exec(db);
var rs = select(ALL).from(TABLE_TAGS).where(TAG,iEqual(tag)).where(USER_ID,equal(userId)).exec(db);
var result = new HashMap<String,List<Long>>();
while (rs.next()){
var module = rs.getString(MODULE);
@@ -237,7 +237,7 @@ CREATE TABLE IF NOT EXISTS {0} (
result.computeIfAbsent(module, k -> new ArrayList<>()).add(entityId);
}
rs.close();
rs = select(ALL).from(TABLE_TAGS).where(TAG,equal(tag)).where(USER_ID,isNull()).exec(db);
rs = select(ALL).from(TABLE_TAGS).where(TAG,iEqual(tag)).where(USER_ID,isNull()).exec(db);
while (rs.next()){
var module = rs.getString(MODULE);
var entityId = rs.getLong(ENTITY_ID);
@@ -256,12 +256,12 @@ CREATE TABLE IF NOT EXISTS {0} (
var tags = new HashSet<String>();
// load tags assigned to user
var rs = select(TAG).from(TABLE_TAGS).where(MODULE,equal(module)).where(ENTITY_ID,equal(entityId)).where(USER_ID,equal(userId)).exec(db);
var rs = select(TAG).from(TABLE_TAGS).where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId)).where(USER_ID,equal(userId)).exec(db);
while (rs.next()) tags.add(rs.getString(1));
rs.close();
// load tags assigned to no user
rs = select(TAG).from(TABLE_TAGS).where(MODULE,equal(module)).where(ENTITY_ID,equal(entityId)).where(USER_ID,isNull()).exec(db);
rs = select(TAG).from(TABLE_TAGS).where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId)).where(USER_ID,isNull()).exec(db);
while (rs.next()) tags.add(rs.getString(1));
rs.close();
return tags;
@@ -294,12 +294,12 @@ CREATE TABLE IF NOT EXISTS {0} (
var tags = new HashMap<Long,HashSet<String>>();
// load tags assigned to user
var rs = select(ENTITY_ID,TAG).from(TABLE_TAGS).where(MODULE,equal(module)).where(ENTITY_ID,in(entityIds.toArray())).where(USER_ID,equal(userId)).exec(db);
var rs = select(ENTITY_ID,TAG).from(TABLE_TAGS).where(MODULE,iEqual(module)).where(ENTITY_ID,in(entityIds.toArray())).where(USER_ID,equal(userId)).exec(db);
while (rs.next()) tags.computeIfAbsent(rs.getLong(ENTITY_ID), k -> new HashSet<>()).add(rs.getString(TAG));
rs.close();
// load tags assigned to no user
rs = select(ENTITY_ID,TAG).from(TABLE_TAGS).where(MODULE,equal(module)).where(ENTITY_ID,in(entityIds.toArray())).where(USER_ID,isNull()).exec(db);
rs = select(ENTITY_ID,TAG).from(TABLE_TAGS).where(MODULE,iEqual(module)).where(ENTITY_ID,in(entityIds.toArray())).where(USER_ID,isNull()).exec(db);
while (rs.next()) tags.computeIfAbsent(rs.getLong(ENTITY_ID), k -> new HashSet<>()).add(rs.getString(TAG));
rs.close();
return tags;
@@ -328,7 +328,7 @@ CREATE TABLE IF NOT EXISTS {0} (
@Override
public void updateId(String module, Object oldId, Object newId) {
try {
update(TABLE_TAGS).set(ENTITY_ID).where(MODULE,equal(module)).where(ENTITY_ID,equal(oldId)).prepare(db).apply(newId).close();
update(TABLE_TAGS).set(ENTITY_ID).where(MODULE,iEqual(module)).where(ENTITY_ID,equal(oldId)).prepare(db).apply(newId).close();
LOG.log(DEBUG,"Updated tag @ {0}.{1} → {0}.{2}",module,oldId,newId);
} catch (SQLException e) {
throw databaseException("Failed to update {0}.{1} → {0}.{2}",module,oldId,newId);
@@ -112,8 +112,12 @@ public class TagModule extends BaseHandler implements TagService {
}
}
public Map<String, List<Long>> getTagUses(UmbrellaUser user, String tag){
return tagDb.getUses(tag,user.id());
}
private boolean getTagUses(HttpExchange ex, String tag, UmbrellaUser user) throws IOException {
return sendContent(ex,tagDb.getUses(tag,user.id()));
return sendContent(ex,getTagUses(user,tag));
}
public Collection<String> getTags(String module, long entityId, UmbrellaUser user) throws UmbrellaException{
@@ -1,8 +1,7 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.task;
import static de.srsoftware.tools.Optionals.is0;
import static de.srsoftware.tools.Optionals.isSet;
import static de.srsoftware.tools.Optionals.*;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.ModuleRegistry.*;
@@ -15,6 +14,8 @@ import static de.srsoftware.umbrella.core.model.Permission.OWNER;
import static de.srsoftware.umbrella.project.Constants.PERMISSIONS;
import static de.srsoftware.umbrella.task.Constants.*;
import static java.lang.System.Logger.Level.WARNING;
import static java.net.URLDecoder.decode;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
@@ -92,6 +93,7 @@ public class TaskModule extends BaseHandler implements TaskService {
var head = path.pop();
return switch (head) {
case PERMISSIONS -> getPermissionList(ex);
case TAGGED -> getTaggedTasks(path, user.get(), ex);
case null -> getUserTasks(user.get(), ex);
default -> {
var taskId = Long.parseLong(head);
@@ -179,6 +181,19 @@ public class TaskModule extends BaseHandler implements TaskService {
return sendContent(ex, map);
}
private boolean getTaggedTasks(Path path, UmbrellaUser user, HttpExchange ex) throws IOException {
var tag = decode(path.toString(), UTF_8);
var tags = tagService().getTagUses(user,tag);
var taskIds = nullable(tags.get(TASK)).orElseGet(ArrayList::new);
var tasks = mapValues(taskDb.load(taskIds));
var taskTags = tagService().getTags(TASK,taskIds,user);
for (var entry : tasks.entrySet()){
var list = taskTags.get(entry.getKey());
entry.getValue().put(TAGS,list==null?List.of():list);
}
return sendContent(ex, tasks);
}
private boolean getTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException {
var task = loadMembers(taskDb.load(taskId));
if (!task.hasMember(user)) throw forbidden("You are not a member of {0}",task.name());
@@ -199,12 +214,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
+3
View File
@@ -79,6 +79,7 @@
"due_date": "Fälligkeitsdatum",
"duration": "Dauer",
"easy_list": "Easy List",
"edit": "Bearbeiten",
"edit_object" : "{object} bearbeiten",
"editing": "Nutzer {0} bearbeiten",
@@ -190,6 +191,7 @@
"oidc_Login" : "Anmeldung mit OIDC",
"old_password": "altes Passwort",
"organization": "Organisation",
"other_tags": "andere Tags",
"page": "Seite",
"parent_task": "übergeordnete Aufgabe",
@@ -277,6 +279,7 @@
"task": "Aufgabe",
"task_list": "Aufgabenliste",
"tasks": "Aufgaben",
"tasks_for_tag": "Aufgaben mit Tag „{tag}“",
"tax_id": "Steuernummer",
"TAX-NUMBER": "Steuernummer",
"tax_rate": "Steuersatz",
+3
View File
@@ -79,6 +79,7 @@
"drag_n_drop": "drag & drop",
"duration": "duration",
"easy_list": "Easy List",
"edit": "edit",
"edit_object" : "edit {object}",
"editing": "edit user {0}",
@@ -190,6 +191,7 @@
"oidc_Login" : "Login via OIDC",
"old_password": "old password",
"organization": "organization",
"other_tags": "other tags",
"page": "page",
"parent_task": "parent task",
@@ -277,6 +279,7 @@
"task": "task",
"task_list": "task list",
"tasks": "tasks",
"tasks_for_tag": "tasks with tag „{tag}“",
"tax_id": "tax ID",
"TAX-NUMBER": "tax ID",
"tax_rate": "tax rate",
@@ -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,17 @@ tr:hover .taglist .tag button {
.vcard span.inactive{
color: #222200;
}
.easylist > fieldset > div {
border-color: orange;
color: orange;
}
.easylist fieldset {
border-color: red;
color: red;
}
.easylist .filter{
background: black;
}
+28 -1
View File
@@ -407,6 +407,25 @@ a.wikilink{
grid-column-end: span 2;
}
.easylist > fieldset > div {
display: block;
border: 1px solid;
margin: 7px;
padding: 5px;
border-radius: 5px;
text-align: center;
user-select: none;
}
.easylist .filter{
position: sticky;
bottom: 22px;
z-index: 10;
}
.easylist .edit{
float: right;
}
@media screen and (max-width: 600px) {
.grid2{
display: grid;
@@ -428,6 +447,14 @@ a.wikilink{
width: calc(100% - 10px);
min-height: 50px;
}
.easylist > fieldset > div {
font-size: 25px;
padding: 10px;
}
.easylist input{
font-size: 20px;
}
}
fieldset.vcard{
@@ -473,4 +500,4 @@ fieldset.vcard{
margin: 0 6px;
white-space: nowrap;
display: inline flow-root;
}
}
@@ -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;
@@ -276,3 +285,16 @@ tr:hover .taglist .tag button {
.vcard span.inactive{
color: #222200;
}
.easylist > fieldset > div {
border-color: orange;
color: orange;
}
.easylist fieldset {
border-color: #ff7726;
color: #ff7726;
}
.easylist .filter{
background: black;
}
+28 -1
View File
@@ -485,6 +485,25 @@ a.wikilink{
grid-column-end: span 2;
}
.easylist > fieldset > div {
display: block;
border: 1px solid;
margin: 7px;
padding: 5px;
border-radius: 5px;
text-align: center;
user-select: none;
}
.easylist .filter{
position: sticky;
bottom: 22px;
z-index: 10;
}
.easylist .edit{
float: right;
}
@media screen and (max-width: 600px) {
.grid2{
display: grid;
@@ -506,6 +525,14 @@ a.wikilink{
width: calc(100% - 10px);
min-height: 50px;
}
.easylist > fieldset > div {
font-size: 25px;
padding: 10px;
}
.easylist input{
font-size: 20px;
}
}
fieldset.vcard{
@@ -551,4 +578,4 @@ fieldset.vcard{
margin: 0 6px;
white-space: nowrap;
display: inline flow-root;
}
}
@@ -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,14 @@ tr:hover .taglist .tag button {
.vcard span.inactive{
color: #bbb;
}
.easylist > fieldset > div {
border-color: blue;
color: blue;
background: #dfe4ff;
}
.easylist fieldset {
border-color: blue;
color: blue;
}
+28 -1
View File
@@ -407,6 +407,25 @@ a.wikilink{
grid-column-end: span 2;
}
.easylist > fieldset > div {
display: block;
border: 1px solid;
margin: 7px;
padding: 5px;
border-radius: 5px;
text-align: center;
user-select: none;
}
.easylist .filter{
position: sticky;
bottom: 22px;
z-index: 10;
}
.easylist .edit{
float: right;
}
@media screen and (max-width: 600px) {
.grid2{
display: grid;
@@ -428,6 +447,14 @@ a.wikilink{
width: calc(100% - 10px);
min-height: 50px;
}
.easylist > fieldset > div {
font-size: 25px;
padding: 10px;
}
.easylist input{
font-size: 20px;
}
}
fieldset.vcard{
@@ -473,4 +500,4 @@ fieldset.vcard{
margin: 0 6px;
white-space: nowrap;
display: inline flow-root;
}
}