Compare commits
10 Commits
module/tim
...
module/pro
| Author | SHA1 | Date | |
|---|---|---|---|
| 11c0983dac | |||
| c37df2ddec | |||
| 93907a839d | |||
| ccb84995cb | |||
| fad9c78f87 | |||
| 2cd53b19cb | |||
| 288acd90f4 | |||
| 0d202677ad | |||
| 6bb03f4e04 | |||
| 438d8d4aad |
@@ -1,6 +1,7 @@
|
|||||||
/* © SRSoftware 2025 */
|
/* © SRSoftware 2025 */
|
||||||
package de.srsoftware.umbrella.core.model;
|
package de.srsoftware.umbrella.core.model;
|
||||||
|
|
||||||
|
import static de.srsoftware.tools.Optionals.emptyIfNull;
|
||||||
import static de.srsoftware.umbrella.core.Constants.*;
|
import static de.srsoftware.umbrella.core.Constants.*;
|
||||||
import static de.srsoftware.umbrella.core.Field.*;
|
import static de.srsoftware.umbrella.core.Field.*;
|
||||||
import static de.srsoftware.umbrella.core.Field.COMPANY;
|
import static de.srsoftware.umbrella.core.Field.COMPANY;
|
||||||
@@ -57,14 +58,14 @@ public final class Document implements Mappable {
|
|||||||
private final Type type;
|
private final Type type;
|
||||||
private LocalDate date;
|
private LocalDate date;
|
||||||
private State state;
|
private State state;
|
||||||
private Template template;
|
private String template;
|
||||||
private final Sender sender;
|
private final Sender sender;
|
||||||
private final Customer customer;
|
private final Customer customer;
|
||||||
private final PositionList positions;
|
private final PositionList positions;
|
||||||
private final Set<String> dirtyFields = new HashSet<>();
|
private final Set<String> dirtyFields = new HashSet<>();
|
||||||
|
|
||||||
|
|
||||||
public Document(long id, long companyId, String number, Type type, LocalDate date, State state, Template template, String delivery, String head, String footer, String currency, String decimalSeparator, Sender sender, Customer customer, PositionList positions) {
|
public Document(long id, long companyId, String number, Type type, LocalDate date, State state, String template, String delivery, String head, String footer, String currency, String decimalSeparator, Sender sender, Customer customer, PositionList positions) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.companyId = companyId;
|
this.companyId = companyId;
|
||||||
this.number = number;
|
this.number = number;
|
||||||
@@ -206,7 +207,7 @@ public final class Document implements Mappable {
|
|||||||
case SENDER: if (json.get(key) instanceof JSONObject nested) sender.patch(nested); break;
|
case SENDER: if (json.get(key) instanceof JSONObject nested) sender.patch(nested); break;
|
||||||
case STATE: state = State.of(json.getInt(key)).orElseThrow(() -> new UmbrellaException(HTTP_UNPROCESSABLE,"Invalid state")); break;
|
case STATE: state = State.of(json.getInt(key)).orElseThrow(() -> new UmbrellaException(HTTP_UNPROCESSABLE,"Invalid state")); break;
|
||||||
case POS: if (json.get(key) instanceof JSONObject nested) positions.patch(nested); break;
|
case POS: if (json.get(key) instanceof JSONObject nested) positions.patch(nested); break;
|
||||||
case TEMPLATE_ID: if (json.get(key) instanceof Number num) template = new Template(num.longValue(),companyId,null,null); break;
|
case TEMPLATE_ID: if (json.get(key) instanceof String templateId) template = templateId; break;
|
||||||
default: key = null;
|
default: key = null;
|
||||||
}
|
}
|
||||||
if (key != null) dirtyFields.add(key);
|
if (key != null) dirtyFields.add(key);
|
||||||
@@ -225,7 +226,7 @@ public final class Document implements Mappable {
|
|||||||
map.put(TYPE, type.name());
|
map.put(TYPE, type.name());
|
||||||
map.put(DATE, date);
|
map.put(DATE, date);
|
||||||
map.put(STATE, state.code);
|
map.put(STATE, state.code);
|
||||||
map.put(DELIVERY, delivery == null ? "" : delivery);
|
map.put(DELIVERY, emptyIfNull(delivery));
|
||||||
map.put(HEAD, mapMarkdown(head));
|
map.put(HEAD, mapMarkdown(head));
|
||||||
map.put(FOOTER, mapMarkdown(footer));
|
map.put(FOOTER, mapMarkdown(footer));
|
||||||
map.put(CURRENCY, currency);
|
map.put(CURRENCY, currency);
|
||||||
@@ -235,7 +236,7 @@ public final class Document implements Mappable {
|
|||||||
map.put("taxes",positions.taxNetSums(true));
|
map.put("taxes",positions.taxNetSums(true));
|
||||||
map.put(NET_SUM, netSum());
|
map.put(NET_SUM, netSum());
|
||||||
map.put(GROSS_SUM, grossSum());
|
map.put(GROSS_SUM, grossSum());
|
||||||
if (template != null) map.put("template", template.toMap());
|
map.put("template", emptyIfNull(template));
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +287,7 @@ public final class Document implements Mappable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Template template() {
|
public String template() {
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +310,7 @@ public final class Document implements Mappable {
|
|||||||
map.put("taxes",positions.taxNetSums(true));
|
map.put("taxes",positions.taxNetSums(true));
|
||||||
map.put(NET_SUM, netSum());
|
map.put(NET_SUM, netSum());
|
||||||
map.put(GROSS_SUM, grossSum());
|
map.put(GROSS_SUM, grossSum());
|
||||||
if (template != null) map.put("template", template.toMap());
|
if (template != null) map.put("template", template);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
public class Constants {
|
||||||
private Constants(){}
|
private Constants(){}
|
||||||
|
|
||||||
|
|
||||||
public static final Pattern POST_CODE = compile("(.*\\w+.*)\n(.*\\d+.*)\n(\\d{5}) (\\w+)",DOTALL);
|
public static final Pattern POST_CODE = compile("(.*\\w+.*)\n(.*\\d+.*)\n(\\d{5}) (\\w+)",DOTALL);
|
||||||
|
|
||||||
public static final String CLONE = "clone";
|
public static final String CLONE = "clone";
|
||||||
@@ -21,8 +20,6 @@ public class Constants {
|
|||||||
|
|
||||||
public static final String ERROR_ADDRESS_MISSING = "{0} address does not contain street address / post code / city";
|
public static final String ERROR_ADDRESS_MISSING = "{0} address does not contain street address / post code / city";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static final String MOVE = "move";
|
public static final String MOVE = "move";
|
||||||
|
|
||||||
public static final String PATH_ADD_ITEM = "add_item";
|
public static final String PATH_ADD_ITEM = "add_item";
|
||||||
@@ -38,8 +35,6 @@ public class Constants {
|
|||||||
public static final String POSITION = "position";
|
public static final String POSITION = "position";
|
||||||
public static final String PROJECT_ID = "project_id";
|
public static final String PROJECT_ID = "project_id";
|
||||||
|
|
||||||
|
|
||||||
public static final String TABLE_COMPANY_SETTINGS = "company_settings";
|
|
||||||
public static final String TABLE_CUSTOMER_SETTINGS = "company_customer_settings";
|
public static final String TABLE_CUSTOMER_SETTINGS = "company_customer_settings";
|
||||||
public static final String TABLE_DOCUMENTS = "documents";
|
public static final String TABLE_DOCUMENTS = "documents";
|
||||||
public static final String TABLE_DOCUMENT_TYPES = "document_types";
|
public static final String TABLE_DOCUMENT_TYPES = "document_types";
|
||||||
@@ -47,4 +42,9 @@ public class Constants {
|
|||||||
public static final String TABLE_PRICES = "customer_prices";
|
public static final String TABLE_PRICES = "customer_prices";
|
||||||
public static final String TABLE_TEMPLATES = "templates";
|
public static final String TABLE_TEMPLATES = "templates";
|
||||||
public static final String TEMPLATES = "templates";
|
public static final String TEMPLATES = "templates";
|
||||||
|
|
||||||
|
public static final String TYPE_CONFIRMATION = "confirmation";
|
||||||
|
public static final String TYPE_INVOICE = "invoice";
|
||||||
|
public static final String TYPE_OFFER = "offer";
|
||||||
|
public static final String TYPE_REMINDER = "reminder";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException {
|
private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException {
|
||||||
var template = document.template().name();
|
var template = document.template();
|
||||||
var templateName = template+".html.pdf";
|
var templateName = template+".html.pdf";
|
||||||
var type = document.type().name();
|
var type = document.type().name();
|
||||||
var zugferd = "invoice".equals(type);
|
var zugferd = "invoice".equals(type);
|
||||||
@@ -432,8 +432,7 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
|||||||
|
|
||||||
Document doc = getDocument(docId, user).a;
|
Document doc = getDocument(docId, user).a;
|
||||||
|
|
||||||
var companySettings = db.getCompanySettings(doc.companyId(),docType);
|
var nextNumber = db.nextDocId(user.language(),doc.companyId(),docType);
|
||||||
var nextNumber = companySettings.nextDocId();
|
|
||||||
|
|
||||||
Document clone = new Document(
|
Document clone = new Document(
|
||||||
0,
|
0,
|
||||||
@@ -469,19 +468,17 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
|||||||
if (!json.has(TYPE) || !(json.get(TYPE) instanceof Number docTypeId)) throw missingFieldException(TYPE);
|
if (!json.has(TYPE) || !(json.get(TYPE) instanceof Number docTypeId)) throw missingFieldException(TYPE);
|
||||||
var type = db.getType(docTypeId.intValue());
|
var type = db.getType(docTypeId.intValue());
|
||||||
var customer = Customer.of(customerData);
|
var customer = Customer.of(customerData);
|
||||||
Template template = new Template(6,companyId,"unknwon",null);
|
|
||||||
String currency = company.currency();
|
String currency = company.currency();
|
||||||
String sep = company.decimalSeparator();
|
String sep = company.decimalSeparator();
|
||||||
var settings = db.getCustomerSettings(companyId,type,customer.id());
|
var settings = db.getCustomerSettings(companyId,type,customer.id());
|
||||||
var newCustomer = settings == null;
|
var newCustomer = settings == null;
|
||||||
if (newCustomer) settings = CustomerSettings.empty();
|
if (newCustomer) settings = CustomerSettings.empty();
|
||||||
var companySettings = db.getCompanySettings(companyId,type);
|
var nextNumber = db.nextDocId(user.language(), companyId,type);
|
||||||
var nextNumber = companySettings.nextDocId();
|
|
||||||
String lastHead = settings.header();
|
String lastHead = settings.header();
|
||||||
String lastFooter = settings.footer();
|
String lastFooter = settings.footer();
|
||||||
var sender = Sender.of(senderData);
|
var sender = Sender.of(senderData);
|
||||||
LOG.log(DEBUG,json.toString(2));
|
LOG.log(DEBUG,json.toString(2));
|
||||||
var doc = new Document(0,companyId,nextNumber,type, LocalDate.now(), NEW,template,null,lastHead,lastFooter,currency,sep,sender,customer,new PositionList());
|
var doc = new Document(0,companyId,nextNumber,type, LocalDate.now(), NEW,null,null,lastHead,lastFooter,currency,sep,sender,customer,new PositionList());
|
||||||
var saved = db.save(doc);
|
var saved = db.save(doc);
|
||||||
if (newCustomer) {
|
if (newCustomer) {
|
||||||
if (customerData.get(CONTACT_ID) instanceof Number contactId) {
|
if (customerData.get(CONTACT_ID) instanceof Number contactId) {
|
||||||
@@ -491,7 +488,6 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
|||||||
}
|
}
|
||||||
companyService().saveNewCustomer(companyId,customer.id());
|
companyService().saveNewCustomer(companyId,customer.id());
|
||||||
}
|
}
|
||||||
db.step(companySettings);
|
|
||||||
return sendContent(ex,saved);
|
return sendContent(ex,saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,8 +517,10 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
|||||||
if (!(json.has(COMPANY) && json.get(COMPANY) instanceof Number companyId)) throw missingFieldException(COMPANY);
|
if (!(json.has(COMPANY) && json.get(COMPANY) instanceof Number companyId)) throw missingFieldException(COMPANY);
|
||||||
var company = companyService().get(companyId.longValue());
|
var company = companyService().get(companyId.longValue());
|
||||||
if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are not a member of {0}",company.name());
|
if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are not a member of {0}",company.name());
|
||||||
var templates = db.getCompanyTemplates(companyId.longValue());
|
var templates = registry.documents()
|
||||||
return sendContent(ex,templates.stream().map(Template::toMap));
|
.filter(d -> d.name().endsWith(".template"))
|
||||||
|
.map(d -> d.name().replaceAll("(\\.[^.]+)?\\.template$",""));
|
||||||
|
return sendContent(ex,templates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean postSearch(HttpExchange ex, UmbrellaUser user) throws IOException {
|
private boolean postSearch(HttpExchange ex, UmbrellaUser user) throws IOException {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package de.srsoftware.umbrella.documents;
|
|||||||
import de.srsoftware.tools.Pair;
|
import de.srsoftware.tools.Pair;
|
||||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||||
import de.srsoftware.umbrella.core.model.Document;
|
import de.srsoftware.umbrella.core.model.Document;
|
||||||
import de.srsoftware.umbrella.core.model.Template;
|
|
||||||
import de.srsoftware.umbrella.core.model.Type;
|
import de.srsoftware.umbrella.core.model.Type;
|
||||||
import de.srsoftware.umbrella.documents.model.*;
|
import de.srsoftware.umbrella.documents.model.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -27,10 +26,6 @@ public interface DocumentDb {
|
|||||||
|
|
||||||
CustomerSettings getCustomerSettings(long companyId, Type docType, String customerId) throws UmbrellaException;
|
CustomerSettings getCustomerSettings(long companyId, Type docType, String customerId) throws UmbrellaException;
|
||||||
|
|
||||||
CompanySettings getCompanySettings(long companyId, Type docType) throws UmbrellaException;
|
|
||||||
|
|
||||||
Collection<Template> getCompanyTemplates(long l) throws UmbrellaException;
|
|
||||||
|
|
||||||
Type getType(int typeId) throws UmbrellaException;
|
Type getType(int typeId) throws UmbrellaException;
|
||||||
|
|
||||||
Map<Long, Document> listDocs(long companyId) throws UmbrellaException;
|
Map<Long, Document> listDocs(long companyId) throws UmbrellaException;
|
||||||
@@ -39,12 +34,7 @@ public interface DocumentDb {
|
|||||||
|
|
||||||
Document loadDoc(long docId) throws UmbrellaException;
|
Document loadDoc(long docId) throws UmbrellaException;
|
||||||
|
|
||||||
/**
|
String nextDocId(String language, long companyId, Type type);
|
||||||
* decrement the document number
|
|
||||||
* @param companyId
|
|
||||||
* @param type
|
|
||||||
*/
|
|
||||||
void rollback(long companyId, Type type) throws UmbrellaException;
|
|
||||||
|
|
||||||
Document save(Document document) throws UmbrellaException;
|
Document save(Document document) throws UmbrellaException;
|
||||||
|
|
||||||
@@ -52,11 +42,5 @@ public interface DocumentDb {
|
|||||||
|
|
||||||
CustomerSettings save(long companyId, Type docType, String customerId, CustomerSettings settings) throws UmbrellaException;
|
CustomerSettings save(long companyId, Type docType, String customerId, CustomerSettings settings) throws UmbrellaException;
|
||||||
|
|
||||||
/**
|
|
||||||
* increment the document number
|
|
||||||
* @param settings containing company id and document type
|
|
||||||
*/
|
|
||||||
void step(CompanySettings settings);
|
|
||||||
|
|
||||||
Pair<Integer> switchPositions(long docId, Pair<Integer> longPair) throws UmbrellaException;
|
Pair<Integer> switchPositions(long docId, Pair<Integer> longPair) throws UmbrellaException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import static de.srsoftware.umbrella.core.Field.*;
|
|||||||
import static de.srsoftware.umbrella.core.Field.COMPANY_ID;
|
import static de.srsoftware.umbrella.core.Field.COMPANY_ID;
|
||||||
import static de.srsoftware.umbrella.core.Field.TAX;
|
import static de.srsoftware.umbrella.core.Field.TAX;
|
||||||
import static de.srsoftware.umbrella.core.Field.UNIT;
|
import static de.srsoftware.umbrella.core.Field.UNIT;
|
||||||
|
import static de.srsoftware.umbrella.core.ModuleRegistry.translator;
|
||||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
|
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
|
||||||
import static de.srsoftware.umbrella.core.model.Document.DEFAULT_THOUSANDS_SEPARATOR;
|
import static de.srsoftware.umbrella.core.model.Document.DEFAULT_THOUSANDS_SEPARATOR;
|
||||||
import static de.srsoftware.umbrella.core.model.Document.State;
|
import static de.srsoftware.umbrella.core.model.Document.State;
|
||||||
@@ -20,6 +21,7 @@ import static java.time.ZoneOffset.UTC;
|
|||||||
|
|
||||||
import de.srsoftware.tools.Pair;
|
import de.srsoftware.tools.Pair;
|
||||||
import de.srsoftware.tools.jdbc.Query;
|
import de.srsoftware.tools.jdbc.Query;
|
||||||
|
import de.srsoftware.umbrella.core.BaseDb;
|
||||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||||
import de.srsoftware.umbrella.core.model.*;
|
import de.srsoftware.umbrella.core.model.*;
|
||||||
import de.srsoftware.umbrella.documents.model.*;
|
import de.srsoftware.umbrella.documents.model.*;
|
||||||
@@ -28,41 +30,46 @@ import java.sql.ResultSet;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class SqliteDb implements DocumentDb{
|
public class SqliteDb extends BaseDb implements DocumentDb{
|
||||||
private static final System.Logger LOG = System.getLogger(SqliteDb.class.getSimpleName());
|
private static final System.Logger LOG = System.getLogger(SqliteDb.class.getSimpleName());
|
||||||
private final Connection db;
|
|
||||||
private static final String DB_VERSION = "message_db_version";
|
private static final String DB_VERSION = "message_db_version";
|
||||||
private static final int INITIAL_DB_VERSION = 1;
|
private static final int INITIAL_DB_VERSION = 1;
|
||||||
|
private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\D*)(\\d+)(.*)");
|
||||||
|
|
||||||
public SqliteDb(Connection conn){
|
public SqliteDb(Connection connection) {
|
||||||
db = conn;
|
super(connection);
|
||||||
init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int createTables() {
|
private void addTemplateColumn() {
|
||||||
createTableDocumentTypes();
|
|
||||||
createTableTemplates();
|
|
||||||
createTableDocuments();
|
|
||||||
createTablePositions();
|
|
||||||
createTableCustomerPrices();
|
|
||||||
createTableCompanySettings();
|
|
||||||
createTableCustomerSettings();
|
|
||||||
return createTableSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createTableCompanySettings() {
|
|
||||||
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} INT NOT NULL, {3} TEXT DEFAULT \"A\", {4} TEXT DEFAULT NULL, {5} INT NOT NULL DEFAULT 1, PRIMARY KEY ({1}, {2}))";
|
|
||||||
try {
|
try {
|
||||||
var stmt = db.prepareStatement(format(sql,TABLE_COMPANY_SETTINGS, COMPANY_ID,DOC_TYPE_ID,TYPE_PREFIX,TYPE_SUFFIX,TYPE_NUMBER));
|
var sql = format("ALTER TABLE {0} ADD COLUMN {1} VARCHAR(255)",TABLE_DOCUMENTS,TEMPLATE);
|
||||||
stmt.execute();
|
db.prepareStatement(sql).execute();
|
||||||
stmt.close();
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_COMPANY_SETTINGS,e);
|
throw databaseException("Failed to update column {0} → {1} of {2}",TEMPLATE_ID,TEMPLATE,TABLE_DOCUMENTS);
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private void createTableCustomerPrices() {
|
||||||
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} VARCHAR(255), {3} VARCHAR(50), {4} INTEGER)";
|
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} VARCHAR(255), {3} VARCHAR(50), {4} INTEGER)";
|
||||||
try {
|
try {
|
||||||
@@ -82,7 +89,7 @@ public class SqliteDb implements DocumentDb{
|
|||||||
stmt.execute();
|
stmt.execute();
|
||||||
stmt.close();
|
stmt.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_COMPANY_SETTINGS,e);
|
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_CUSTOMER_SETTINGS,e);
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,8 +103,8 @@ CREATE TABLE IF NOT EXISTS {0} (
|
|||||||
{3} INT NOT NULL,
|
{3} INT NOT NULL,
|
||||||
{4} TEXT NOT NULL,
|
{4} TEXT NOT NULL,
|
||||||
{5} TIMESTAMP NOT NULL,
|
{5} TIMESTAMP NOT NULL,
|
||||||
{6} INT NOT NULL NULL,
|
{6} INT NOT NULL,
|
||||||
{7} INT NOT NULL,
|
{7} INT,
|
||||||
{8} VARCHAR(100),
|
{8} VARCHAR(100),
|
||||||
{9} TEXT,
|
{9} TEXT,
|
||||||
{10} TEXT,
|
{10} TEXT,
|
||||||
@@ -128,6 +135,12 @@ CREATE TABLE IF NOT EXISTS {0} (
|
|||||||
var stmt = db.prepareStatement(format(createTable,TABLE_DOCUMENT_TYPES, ID,NEXT_TYPE, NAME));
|
var stmt = db.prepareStatement(format(createTable,TABLE_DOCUMENT_TYPES, ID,NEXT_TYPE, NAME));
|
||||||
stmt.execute();
|
stmt.execute();
|
||||||
stmt.close();
|
stmt.close();
|
||||||
|
insertInto(TABLE_DOCUMENT_TYPES,ID,NEXT_TYPE,NAME)
|
||||||
|
.values(1,2,TYPE_OFFER)
|
||||||
|
.values(2,3,TYPE_CONFIRMATION)
|
||||||
|
.values(3,4,TYPE_INVOICE)
|
||||||
|
.values(4,4,TYPE_REMINDER)
|
||||||
|
.execute(db);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_DOCUMENT_TYPES,e);
|
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_DOCUMENT_TYPES,e);
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
@@ -161,36 +174,6 @@ CREATE TABLE IF NOT EXISTS {0} (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int createTableSettings() {
|
|
||||||
var createTable = """
|
|
||||||
CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL);
|
|
||||||
""";
|
|
||||||
try {
|
|
||||||
var stmt = db.prepareStatement(format(createTable,TABLE_SETTINGS, KEY, VALUE));
|
|
||||||
stmt.execute();
|
|
||||||
stmt.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SETTINGS,e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Integer version = null;
|
|
||||||
try {
|
|
||||||
var rs = select(VALUE).from(TABLE_SETTINGS).where(KEY, equal(DB_VERSION)).exec(db);
|
|
||||||
if (rs.next()) version = rs.getInt(VALUE);
|
|
||||||
rs.close();
|
|
||||||
if (version == null) {
|
|
||||||
version = INITIAL_DB_VERSION;
|
|
||||||
insertInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOG.log(ERROR,ERROR_READ_TABLE,DB_VERSION,TABLE_SETTINGS,e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createTableTemplates() {
|
private void createTableTemplates() {
|
||||||
var createTable = "CREATE TABLE IF NOT EXISTS {0} ({1} INTEGER PRIMARY KEY, {2} INT NOT NULL, {3} VARCHAR(255) NOT NULL, {4} BLOB)";
|
var createTable = "CREATE TABLE IF NOT EXISTS {0} ({1} INTEGER PRIMARY KEY, {2} INT NOT NULL, {3} VARCHAR(255) NOT NULL, {4} BLOB)";
|
||||||
try {
|
try {
|
||||||
@@ -241,29 +224,21 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
|||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void dropTemplateIdColumn() {
|
||||||
public CompanySettings getCompanySettings(long companyId, Type docType) throws UmbrellaException {
|
|
||||||
try {
|
try {
|
||||||
var rs = select(ALL).from(TABLE_COMPANY_SETTINGS).where(COMPANY_ID,equal(companyId)).where(DOC_TYPE_ID,equal(docType.id())).exec(db);
|
var sql = format("ALTER TABLE {0} DROP COLUMN {1}",TABLE_DOCUMENTS,TEMPLATE_ID);
|
||||||
CompanySettings settings = null;
|
db.prepareStatement(sql).execute();
|
||||||
if (rs.next()) settings = CompanySettings.of(rs);
|
} catch (SQLException e) {
|
||||||
rs.close();
|
throw databaseException("Failed to update column {0} → {1} of {2}",TEMPLATE_ID,TEMPLATE,TABLE_DOCUMENTS);
|
||||||
if (settings != null) return settings;
|
|
||||||
} catch (SQLException ignored) {
|
|
||||||
}
|
}
|
||||||
throw databaseException("Failed to load customer settings (company: {0}, document type: {1})",companyId, docType.name());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void dropTemplateTable() {
|
||||||
public Collection<Template> getCompanyTemplates(long companyId) throws UmbrellaException {
|
|
||||||
try {
|
try {
|
||||||
var rs = select(ALL).from(TABLE_TEMPLATES).where(COMPANY_ID,equal(companyId)).exec(db);
|
var sql = format("DROP TABLE IF EXISTS {0};",TABLE_TEMPLATES);
|
||||||
var templates = new HashSet<Template>();
|
db.prepareStatement(sql).execute();
|
||||||
while (rs.next()) templates.add(Template.of(rs));
|
|
||||||
rs.close();
|
|
||||||
return templates;
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw databaseException("Failed to load templates for company {0}",companyId);
|
throw databaseException("Failed to drop table {0}",TABLE_TEMPLATES);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,10 +284,6 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
|||||||
throw new UmbrellaException(500,"No type with id = {0}",typeId);
|
throw new UmbrellaException(500,"No type with id = {0}",typeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init() {
|
|
||||||
var version = createTables();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Long, Map<Long, String>> docReferencedByTimes(Set<Long> timeIds) throws UmbrellaException {
|
public Map<Long, Map<Long, String>> docReferencedByTimes(Set<Long> timeIds) throws UmbrellaException {
|
||||||
try {
|
try {
|
||||||
@@ -431,7 +402,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
|||||||
while (rs.next()) types.put(rs.getInt(ID),toType(rs));
|
while (rs.next()) types.put(rs.getInt(ID),toType(rs));
|
||||||
rs.close();
|
rs.close();
|
||||||
|
|
||||||
rs = Query.select(ALL).from(TABLE_DOCUMENTS).leftJoin(TEMPLATE_ID,TABLE_TEMPLATES, ID).where(TABLE_DOCUMENTS+"."+ ID,equal(docId)).exec(db);
|
rs = Query.select(ALL).from(TABLE_DOCUMENTS).where(TABLE_DOCUMENTS+"."+ ID,equal(docId)).exec(db);
|
||||||
Document doc = null;
|
Document doc = null;
|
||||||
while (rs.next()) doc = toDoc(rs,types);
|
while (rs.next()) doc = toDoc(rs,types);
|
||||||
rs.close();
|
rs.close();
|
||||||
@@ -453,22 +424,38 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
|||||||
throw new UmbrellaException(500,"Failed to load document {0}.",docId);
|
throw new UmbrellaException(500,"Failed to load document {0}.",docId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void moveTemplateNames() {
|
||||||
public void rollback(long companyId, Type type) throws UmbrellaException {
|
|
||||||
try {
|
try {
|
||||||
var settings = getCompanySettings(companyId,type);
|
var sql = format("UPDATE {0} SET template = (SELECT name FROM templates WHERE templates.id = documents.template_id);",TABLE_DOCUMENTS);
|
||||||
|
db.prepareStatement(sql).execute();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw databaseException("Failed to move template.names to document.templates!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var numbers = new HashSet<String>();
|
@Override
|
||||||
var rs = select(NUMBER).from(TABLE_DOCUMENTS).where(COMPANY_ID,equal(companyId)).exec(db);
|
public String nextDocId(String language, long companyId, Type type) {
|
||||||
while (rs.next()) numbers.add(rs.getString(NUMBER));
|
try {
|
||||||
|
var rs = select(NUMBER).from(TABLE_DOCUMENTS).where(COMPANY_ID,equal(companyId)).where(TYPE_ID,equal(type.id())).sort(ID+" DESC").limit(1).exec(db);
|
||||||
|
String lastId = null;
|
||||||
|
if (rs.next()) lastId = rs.getString(1);
|
||||||
rs.close();
|
rs.close();
|
||||||
|
if (lastId == null) return translator().translate(language,type.name())+"-0001";
|
||||||
var previous = settings.previous();
|
var numeric = NUMBER_PATTERN.matcher(lastId);
|
||||||
while (previous.isPresent() && !numbers.contains(previous.get().nextDocId())) previous = previous.get().previous();
|
if (numeric.find()){
|
||||||
|
var prefix = numeric.group(1);
|
||||||
previous.ifPresent(this::step);
|
var digits = numeric.group(2);
|
||||||
} catch (SQLException e){
|
var suffix = numeric.group(3);
|
||||||
// TODO
|
var len = digits.length();
|
||||||
|
while (digits.startsWith("0")) digits = digits.substring(1);
|
||||||
|
var lid = Long.parseLong(digits)+1;
|
||||||
|
digits = ""+lid;
|
||||||
|
while (digits.length()<len) digits = "0"+digits;
|
||||||
|
return prefix+digits+suffix;
|
||||||
|
}
|
||||||
|
return lastId;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw databaseException("Failed to read last document id");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,8 +480,8 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
|||||||
var timestamp = doc.date().atStartOfDay(UTC).toInstant().getEpochSecond();
|
var timestamp = doc.date().atStartOfDay(UTC).toInstant().getEpochSecond();
|
||||||
var sender = doc.sender();
|
var sender = doc.sender();
|
||||||
var custom = doc.customer();
|
var custom = doc.customer();
|
||||||
var stmt = insertInto(TABLE_DOCUMENTS,TYPE_ID,COMPANY_ID, DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE_ID,CURRENCY)
|
var stmt = insertInto(TABLE_DOCUMENTS,TYPE_ID,COMPANY_ID, DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE,CURRENCY)
|
||||||
.values(doc.type().id(),doc.companyId(),timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber(),doc.template().id(),doc.currency())
|
.values(doc.type().id(),doc.companyId(),timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber(),doc.template(), doc.currency())
|
||||||
.execute(db);
|
.execute(db);
|
||||||
var rs = stmt.getGeneratedKeys();
|
var rs = stmt.getGeneratedKeys();
|
||||||
Long newId = null;
|
Long newId = null;
|
||||||
@@ -516,10 +503,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
|||||||
var sender = doc.sender();
|
var sender = doc.sender();
|
||||||
var custom = doc.customer();
|
var custom = doc.customer();
|
||||||
update(TABLE_DOCUMENTS)
|
update(TABLE_DOCUMENTS)
|
||||||
.set(DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE_ID)
|
.set(DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE)
|
||||||
.where(ID,equal(doc.id()))
|
.where(ID,equal(doc.id()))
|
||||||
.prepare(db)
|
.prepare(db)
|
||||||
.apply(timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber(),doc.template().id())
|
.apply(timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber(),doc.template())
|
||||||
.close();
|
.close();
|
||||||
sender.clean();
|
sender.clean();
|
||||||
custom.clean();
|
custom.clean();
|
||||||
@@ -589,20 +576,6 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void step(CompanySettings settings) {
|
|
||||||
try {
|
|
||||||
update(TABLE_COMPANY_SETTINGS)
|
|
||||||
.set(TYPE_NUMBER)
|
|
||||||
.where(COMPANY_ID,equal(settings.companyId())).where(DOC_TYPE_ID,equal(settings.typeId()))
|
|
||||||
.prepare(db)
|
|
||||||
.apply(settings.typeNumber()+1)
|
|
||||||
.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOG.log(WARNING,"Failed to increment doc number");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pair<Integer> switchPositions(long docId, Pair<Integer> pair) throws UmbrellaException {
|
public Pair<Integer> switchPositions(long docId, Pair<Integer> pair) throws UmbrellaException {
|
||||||
try {
|
try {
|
||||||
@@ -642,22 +615,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
|||||||
var customerEmail = rs.getString(CUSTOMER_EMAIL);
|
var customerEmail = rs.getString(CUSTOMER_EMAIL);
|
||||||
var customer = new Customer(customerId, customerName, customerEmail, customerTaxNumber,FALLBACK_LANG);
|
var customer = new Customer(customerId, customerName, customerEmail, customerTaxNumber,FALLBACK_LANG);
|
||||||
var sender = new Sender(senderName,bankAccount,taxNumber,court);
|
var sender = new Sender(senderName,bankAccount,taxNumber,court);
|
||||||
var template = toTemplate(rs);
|
var template = rs.getString(TEMPLATE);
|
||||||
return new Document(id,company,number,type,date, Document.State.of(state).orElse(State.ERROR),template,delivery,head,footer,currency, DEFAULT_THOUSANDS_SEPARATOR,sender,customer,new PositionList());
|
return new Document(id,company,number,type,date, Document.State.of(state).orElse(State.ERROR),template,delivery,head,footer,currency, DEFAULT_THOUSANDS_SEPARATOR,sender,customer,new PositionList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Template toTemplate(ResultSet rs) throws SQLException {
|
|
||||||
try {
|
|
||||||
var id = rs.getLong(TEMPLATE_ID);
|
|
||||||
var company = rs.getLong(COMPANY_ID);
|
|
||||||
var name = rs.getString(NAME);
|
|
||||||
var data = rs.getBytes(TEMPLATE);
|
|
||||||
return new Template(id,company,name,data);
|
|
||||||
} catch (SQLException ignored){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Position toPosition(ResultSet rs) throws SQLException {
|
private Position toPosition(ResultSet rs) throws SQLException {
|
||||||
var num = rs.getInt(POS);
|
var num = rs.getInt(POS);
|
||||||
var itemCode = rs.getString(ITEM_CODE);
|
var itemCode = rs.getString(ITEM_CODE);
|
||||||
|
|||||||
@@ -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>
|
<script>
|
||||||
|
import { t } from '../translations.svelte';
|
||||||
let { item, onclick } = $props();
|
let { item, onclick } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset {onclick}>
|
<fieldset {onclick}>
|
||||||
<legend>{item.code} | {item.name}</legend>
|
<legend>{item.code} | {item.name}</legend>
|
||||||
<div>{@html item.description.rendered}</div>
|
{#if item.properties}
|
||||||
<span>{item.unit_price/100} {item.currency} / {item.unit}</span>
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{#if item.location.name}
|
||||||
|
<tr>
|
||||||
|
<th>{t('location')}</th>
|
||||||
|
<td>{item.location.name}</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{#each Object.entries(item.properties) as [idx,prop]}
|
||||||
|
<tr>
|
||||||
|
<th>{prop.name}</th>
|
||||||
|
<td>{prop.value} {prop.unit}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { api } from '../../urls.svelte.js';
|
import { api, post } from '../../urls.svelte.js';
|
||||||
import { error, yikes } from '../../warn.svelte';
|
import { error, yikes } from '../../warn.svelte';
|
||||||
import { t } from '../../translations.svelte.js';
|
import { t } from '../../translations.svelte.js';
|
||||||
|
|
||||||
@@ -15,13 +15,9 @@
|
|||||||
let items = $state(null);
|
let items = $state(null);
|
||||||
|
|
||||||
async function loadItems(){
|
async function loadItems(){
|
||||||
const url = api('items/list');
|
const url = api('stock/list');
|
||||||
let data = { company_id: company_id };
|
let data = { company_id };
|
||||||
const resp = await fetch(url,{
|
const resp = await post(url,data);
|
||||||
credentials: 'include',
|
|
||||||
method : 'POST',
|
|
||||||
body : JSON.stringify(data)
|
|
||||||
});
|
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
items = await resp.json();
|
items = await resp.json();
|
||||||
yikes();
|
yikes();
|
||||||
@@ -37,7 +33,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1>{t('items')}</h1>
|
<h1>{t('items')}</h1>
|
||||||
{#if items}
|
{#if items}
|
||||||
{#each items as item,id}
|
{#each items as item,idx}
|
||||||
<Item item={item} onclick={() => onSelect(item)} />
|
<Item item={item} onclick={() => onSelect(item)} />
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -29,14 +29,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function itemSelected(item){
|
function itemSelected(item){
|
||||||
|
let unit_price = null;
|
||||||
|
let description = '';
|
||||||
|
for (let prop of item.properties) {
|
||||||
|
if (prop.name.toLowerCase().indexOf(t('price').toLowerCase())>-1){
|
||||||
|
unit_price = 100*prop.value.replace(',','.');
|
||||||
|
} else {
|
||||||
|
description += `* ${prop.name}: ${prop.value}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
select({
|
select({
|
||||||
item_code : item.code,
|
item_code : item.code,
|
||||||
title : item.name,
|
title : item.name,
|
||||||
description : item.description.source,
|
description : description,
|
||||||
amount : 1,
|
amount : 1,
|
||||||
unit : item.unit,
|
unit : t('pieces'),
|
||||||
unit_price : item.unit_price,
|
unit_price : unit_price
|
||||||
tax : item.tax
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import {onMount} from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
|
|
||||||
import {api} from '../../urls.svelte.js';
|
import {api, post} from '../../urls.svelte.js';
|
||||||
import {t} from '../../translations.svelte.js';
|
import {t} from '../../translations.svelte.js';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
caption,
|
caption,
|
||||||
@@ -16,11 +16,7 @@
|
|||||||
|
|
||||||
async function loadTemplates(){
|
async function loadTemplates(){
|
||||||
const url = api('document/templates');
|
const url = api('document/templates');
|
||||||
var resp = await fetch(url,{
|
var resp = await post(url,{company:company});
|
||||||
credentials : 'include',
|
|
||||||
method : 'POST',
|
|
||||||
body : JSON.stringify({company:company})
|
|
||||||
});
|
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
templates = await resp.json();
|
templates = await resp.json();
|
||||||
} else {
|
} else {
|
||||||
@@ -34,8 +30,8 @@
|
|||||||
{#if templates}
|
{#if templates}
|
||||||
<select bind:value onchange={onchange}>
|
<select bind:value onchange={onchange}>
|
||||||
<option value={0}>{caption}</option>
|
<option value={0}>{caption}</option>
|
||||||
{#each Object.entries(templates) as [id,template]}
|
{#each templates as template}
|
||||||
<option value={template.id}>{template.name}</option>
|
<option value={template}>{template}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { useTinyRouter } from 'svelte-tiny-router';
|
import { useTinyRouter } from 'svelte-tiny-router';
|
||||||
|
|
||||||
import {api} from '../../urls.svelte.js';
|
import { api, post } from '../../urls.svelte.js';
|
||||||
import { error, yikes } from '../../warn.svelte';
|
import { error, yikes } from '../../warn.svelte';
|
||||||
import { t } from '../../translations.svelte.js';
|
import { t } from '../../translations.svelte.js';
|
||||||
import { user } from '../../user.svelte.js';
|
import { user } from '../../user.svelte.js';
|
||||||
@@ -29,11 +29,7 @@
|
|||||||
|
|
||||||
async function addPosition(selected){
|
async function addPosition(selected){
|
||||||
const url = api(`document/${doc.id}/position`);
|
const url = api(`document/${doc.id}/position`);
|
||||||
const resp = await fetch(url,{
|
const resp = await post(url,selected);
|
||||||
method : 'POST',
|
|
||||||
credentials : 'include',
|
|
||||||
body : JSON.stringify(selected)
|
|
||||||
});
|
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
doc.positions = await resp.json();
|
doc.positions = await resp.json();
|
||||||
yikes();
|
yikes();
|
||||||
@@ -218,9 +214,9 @@
|
|||||||
<th>{t('template')}:</th>
|
<th>{t('template')}:</th>
|
||||||
<td>
|
<td>
|
||||||
{#if editable}
|
{#if editable}
|
||||||
<TemplateSelector company={doc.company.id} bind:value={doc.template.id} onchange={() => update('template_id',doc.template.id)} />
|
<TemplateSelector company={doc.company.id} bind:value={doc.template} onchange={() => update('template',doc.template)} />
|
||||||
{:else}
|
{:else}
|
||||||
{doc.template.name}
|
{doc.template}
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -185,6 +185,10 @@
|
|||||||
highlight.archive = true;
|
highlight.archive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function is_custom(state){
|
||||||
|
return [10,20,40,60,100].includes(state);
|
||||||
|
}
|
||||||
|
|
||||||
function openTask(task_id){
|
function openTask(task_id){
|
||||||
window.open(`/task/${task_id}/view`, '_blank').focus();
|
window.open(`/task/${task_id}/view`, '_blank').focus();
|
||||||
}
|
}
|
||||||
@@ -244,7 +248,7 @@
|
|||||||
{#each users as u}
|
{#each users as u}
|
||||||
<div class="user">{u.name}</div>
|
<div class="user">{u.name}</div>
|
||||||
{#each Object.entries(project.allowed_states) as [state,name]}
|
{#each Object.entries(project.allowed_states) as [state,name]}
|
||||||
<div class={['state_'+state, highlight.user == u.id && highlight.state == state ? 'highlight':'']} ondragover={ev => hover(ev,u.id,state)} ondragleave={e => delete highlight.user} ondrop={ev => drop(u.id,state)} >
|
<div class={['state_'+state, is_custom(state) ? '':'state_custom' ,highlight.user == u.id && highlight.state == state ? 'highlight':'']} ondragover={ev => hover(ev,u.id,state)} ondragleave={e => delete highlight.user} ondrop={ev => drop(u.id,state)} >
|
||||||
{#each Object.values(tasks[u.id][state]).sort(byName) as task}
|
{#each Object.values(tasks[u.id][state]).sort(byName) as task}
|
||||||
{#if !filter || task.name.toLowerCase().includes(filter) || (task.tags && task.tags.filter(tag => tag.toLowerCase().includes(filter)).length)}
|
{#if !filter || task.name.toLowerCase().includes(filter) || (task.tags && task.tags.filter(tag => tag.toLowerCase().includes(filter)).length)}
|
||||||
<Card onclick={e => openTask(task.id)} ondragstart={ev => dragged=task} {task} tag_colors={project.tag_colors} />
|
<Card onclick={e => openTask(task.id)} ondragstart={ev => dragged=task} {task} tag_colors={project.tag_colors} />
|
||||||
|
|||||||
@@ -218,6 +218,22 @@ public class SqliteDb extends BaseDb implements StockDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Item> listItemsOf(Company company) {
|
||||||
|
try {
|
||||||
|
var owner = company.dbCode();
|
||||||
|
var rs = select(ALL).from(TABLE_ITEMS).where(OWNER,equal(owner)).exec(db);
|
||||||
|
var list = new ArrayList<Item>();
|
||||||
|
while (rs.next()) list.add(Item.of(rs));
|
||||||
|
rs.close();
|
||||||
|
|
||||||
|
for (var item : list) loadProperties(item);
|
||||||
|
return list;
|
||||||
|
} catch (SQLException e){
|
||||||
|
throw databaseException("Failed to load items of {0}",company);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Item loadProperties(Item item){
|
public Item loadProperties(Item item){
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public interface StockDb {
|
|||||||
Collection<DbLocation> listChildLocations(long parentId);
|
Collection<DbLocation> listChildLocations(long parentId);
|
||||||
Collection<DbLocation> listCompanyLocations(Company company);
|
Collection<DbLocation> listCompanyLocations(Company company);
|
||||||
Collection<Item> listItemsAt(Location location);
|
Collection<Item> listItemsAt(Location location);
|
||||||
|
Collection<Item> listItemsOf(Company company);
|
||||||
Collection<Property> listProperties();
|
Collection<Property> listProperties();
|
||||||
Collection<DbLocation> listUserLocations(UmbrellaUser userId);
|
Collection<DbLocation> listUserLocations(UmbrellaUser userId);
|
||||||
Item loadItem(long id);
|
Item loadItem(long id);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import static de.srsoftware.umbrella.core.Constants.*;
|
|||||||
import static de.srsoftware.umbrella.core.Field.ITEM;
|
import static de.srsoftware.umbrella.core.Field.ITEM;
|
||||||
import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
|
import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
|
||||||
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
|
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
|
||||||
|
import static de.srsoftware.umbrella.core.Paths.LIST;
|
||||||
|
import static de.srsoftware.umbrella.core.Util.mapValues;
|
||||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||||
import static de.srsoftware.umbrella.stock.Constants.*;
|
import static de.srsoftware.umbrella.stock.Constants.*;
|
||||||
import static java.lang.System.Logger.Level.WARNING;
|
import static java.lang.System.Logger.Level.WARNING;
|
||||||
@@ -33,6 +35,7 @@ import org.json.JSONObject;
|
|||||||
public class StockModule extends BaseHandler implements StockService {
|
public class StockModule extends BaseHandler implements StockService {
|
||||||
|
|
||||||
private final StockDb stockDb;
|
private final StockDb stockDb;
|
||||||
|
private Comparator<Item> byName = (a,b) -> a.name().compareToIgnoreCase(b.name());
|
||||||
|
|
||||||
public StockModule(Configuration config) throws UmbrellaException {
|
public StockModule(Configuration config) throws UmbrellaException {
|
||||||
super();
|
super();
|
||||||
@@ -160,6 +163,7 @@ public class StockModule extends BaseHandler implements StockService {
|
|||||||
var head = path.pop();
|
var head = path.pop();
|
||||||
return switch (head) {
|
return switch (head) {
|
||||||
case ITEM -> postItem(user.get(), ex);
|
case ITEM -> postItem(user.get(), ex);
|
||||||
|
case LIST -> postItemList(user.get(), path, ex);
|
||||||
case LOCATION -> postLocation(user.get(),ex);
|
case LOCATION -> postLocation(user.get(),ex);
|
||||||
case PROPERTY -> postProperty(user.get(),ex);
|
case PROPERTY -> postProperty(user.get(),ex);
|
||||||
case null, default -> super.doPost(path,ex);
|
case null, default -> super.doPost(path,ex);
|
||||||
@@ -316,6 +320,19 @@ public class StockModule extends BaseHandler implements StockService {
|
|||||||
return sendContent(ex,stockDb.save(newItem));
|
return sendContent(ex,stockDb.save(newItem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean postItemList(UmbrellaUser user, Path path, HttpExchange ex) throws IOException {
|
||||||
|
var json = json(ex);
|
||||||
|
if (!json.has(COMPANY_ID) || !(json.get(COMPANY_ID) instanceof Number company_id)) throw missingFieldException(COMPANY_ID);
|
||||||
|
var company = companyService().get(company_id.longValue());
|
||||||
|
if (!companyService().membership(company_id.longValue(),user.id())) throw forbidden("You are not a member of {0}!", company.name());
|
||||||
|
var map = new HashMap<Long,Location>();
|
||||||
|
var items = stockDb.listItemsOf(company)
|
||||||
|
.stream()
|
||||||
|
.peek(item -> item.location(map.computeIfAbsent(item.location().id(), k -> item.location().resolve()))).sorted(byName)
|
||||||
|
.map(Item::toMap);
|
||||||
|
return sendContent(ex,items);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean postLocation(UmbrellaUser user, HttpExchange ex) throws IOException {
|
private boolean postLocation(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||||
var json = json(ex);
|
var json = json(ex);
|
||||||
if (!(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
|
if (!(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
|
||||||
|
|||||||
@@ -204,6 +204,7 @@
|
|||||||
"permission_owner": "Besitzer",
|
"permission_owner": "Besitzer",
|
||||||
"permission_read_only": "lesen",
|
"permission_read_only": "lesen",
|
||||||
"phone": "Telefon",
|
"phone": "Telefon",
|
||||||
|
"pieces": "Stück",
|
||||||
"pos": "Pos",
|
"pos": "Pos",
|
||||||
"position": "Position",
|
"position": "Position",
|
||||||
"positions": "Positionen",
|
"positions": "Positionen",
|
||||||
|
|||||||
@@ -204,6 +204,7 @@
|
|||||||
"permission_owner": "owner",
|
"permission_owner": "owner",
|
||||||
"permission_read_only": "read-only",
|
"permission_read_only": "read-only",
|
||||||
"phone": "phone",
|
"phone": "phone",
|
||||||
|
"pieces": "pieces",
|
||||||
"pos": "pos",
|
"pos": "pos",
|
||||||
"position": "position",
|
"position": "position",
|
||||||
"positions": "positions",
|
"positions": "positions",
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ tr:hover .taglist .tag button {
|
|||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.states .active{
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
|
||||||
.taglist .tag{
|
.taglist .tag{
|
||||||
border-color: red;
|
border-color: red;
|
||||||
}
|
}
|
||||||
@@ -186,8 +190,6 @@ tr:hover .taglist .tag button {
|
|||||||
border-color: 1px solid;
|
border-color: 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.warn {
|
.warn {
|
||||||
background-color: yellow;
|
background-color: yellow;
|
||||||
color: black;
|
color: black;
|
||||||
@@ -201,6 +203,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p10 .name{
|
.task.p10 .name{
|
||||||
color: #ffa736;
|
color: #ffa736;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p10,
|
||||||
.kanban .state_20 .box.p10,
|
.kanban .state_20 .box.p10,
|
||||||
.kanban .state_40 .box.p10{
|
.kanban .state_40 .box.p10{
|
||||||
border: 5px solid #ffa736;
|
border: 5px solid #ffa736;
|
||||||
@@ -209,6 +212,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p20 .name{
|
.task.p20 .name{
|
||||||
color: #ff8f00;
|
color: #ff8f00;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p20,
|
||||||
.kanban .state_20 .box.p20,
|
.kanban .state_20 .box.p20,
|
||||||
.kanban .state_40 .box.p20{
|
.kanban .state_40 .box.p20{
|
||||||
border: 5px solid #ff8f00;
|
border: 5px solid #ff8f00;
|
||||||
@@ -217,6 +221,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p30 .name{
|
.task.p30 .name{
|
||||||
color: #ff7b06;
|
color: #ff7b06;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p30,
|
||||||
.kanban .state_20 .box.p30,
|
.kanban .state_20 .box.p30,
|
||||||
.kanban .state_40 .box.p30{
|
.kanban .state_40 .box.p30{
|
||||||
border: 5px solid #ff7b06;
|
border: 5px solid #ff7b06;
|
||||||
@@ -225,6 +230,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p40 .name{
|
.task.p40 .name{
|
||||||
color: #ff6306;
|
color: #ff6306;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p40,
|
||||||
.kanban .state_20 .box.p40,
|
.kanban .state_20 .box.p40,
|
||||||
.kanban .state_40 .box.p40{
|
.kanban .state_40 .box.p40{
|
||||||
border: 5px solid #ff6306;
|
border: 5px solid #ff6306;
|
||||||
@@ -233,6 +239,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p50 .name{
|
.task.p50 .name{
|
||||||
color: #ff4c06;
|
color: #ff4c06;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p50,
|
||||||
.kanban .state_20 .box.p50,
|
.kanban .state_20 .box.p50,
|
||||||
.kanban .state_40 .box.p50{
|
.kanban .state_40 .box.p50{
|
||||||
border: 5px solid #ff4c06;
|
border: 5px solid #ff4c06;
|
||||||
@@ -241,6 +248,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p60 .name{
|
.task.p60 .name{
|
||||||
color: #ff3506;
|
color: #ff3506;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p60,
|
||||||
.kanban .state_20 .box.p60,
|
.kanban .state_20 .box.p60,
|
||||||
.kanban .state_40 .box.p60{
|
.kanban .state_40 .box.p60{
|
||||||
border: 5px solid #ff3506;
|
border: 5px solid #ff3506;
|
||||||
@@ -249,6 +257,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p70 .name{
|
.task.p70 .name{
|
||||||
color: #ff0000;
|
color: #ff0000;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p70,
|
||||||
.kanban .state_20 .box.p70,
|
.kanban .state_20 .box.p70,
|
||||||
.kanban .state_40 .box.p70{
|
.kanban .state_40 .box.p70{
|
||||||
border: 5px solid #ff0000;
|
border: 5px solid #ff0000;
|
||||||
@@ -257,6 +266,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p80 .name{
|
.task.p80 .name{
|
||||||
color: #df153b;
|
color: #df153b;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p80,
|
||||||
.kanban .state_20 .box.p80,
|
.kanban .state_20 .box.p80,
|
||||||
.kanban .state_40 .box.p80{
|
.kanban .state_40 .box.p80{
|
||||||
border: 5px solid #df153b;
|
border: 5px solid #df153b;
|
||||||
@@ -266,6 +276,7 @@ tr:hover .taglist .tag button {
|
|||||||
background-color: #991c34;
|
background-color: #991c34;
|
||||||
color: #ffff00;
|
color: #ffff00;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p90,
|
||||||
.kanban .state_20 .box.p90,
|
.kanban .state_20 .box.p90,
|
||||||
.kanban .state_40 .box.p90{
|
.kanban .state_40 .box.p90{
|
||||||
border: 5px solid #991c34;
|
border: 5px solid #991c34;
|
||||||
@@ -275,6 +286,7 @@ tr:hover .taglist .tag button {
|
|||||||
background-color: #733440;
|
background-color: #733440;
|
||||||
color: #ffff00;
|
color: #ffff00;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p100,
|
||||||
.kanban .state_20 .box.p100,
|
.kanban .state_20 .box.p100,
|
||||||
.kanban .state_40 .box.p100{
|
.kanban .state_40 .box.p100{
|
||||||
border: 5px solid #733440;
|
border: 5px solid #733440;
|
||||||
|
|||||||
@@ -176,7 +176,6 @@ tr:hover .taglist .tag button {
|
|||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.version a.selected{
|
.version a.selected{
|
||||||
border-color: orange;
|
border-color: orange;
|
||||||
}
|
}
|
||||||
@@ -194,6 +193,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p10 .name{
|
.task.p10 .name{
|
||||||
color: #fff066;
|
color: #fff066;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p10,
|
||||||
.kanban .state_20 .box.p10,
|
.kanban .state_20 .box.p10,
|
||||||
.kanban .state_40 .box.p10{
|
.kanban .state_40 .box.p10{
|
||||||
border: 5px solid #fff066;
|
border: 5px solid #fff066;
|
||||||
@@ -202,6 +202,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p20 .name{
|
.task.p20 .name{
|
||||||
color: #ffe706;
|
color: #ffe706;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p20,
|
||||||
.kanban .state_20 .box.p20,
|
.kanban .state_20 .box.p20,
|
||||||
.kanban .state_40 .box.p20{
|
.kanban .state_40 .box.p20{
|
||||||
border: 5px solid #ffe706;
|
border: 5px solid #ffe706;
|
||||||
@@ -210,6 +211,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p30 .name{
|
.task.p30 .name{
|
||||||
color: #ffa906;
|
color: #ffa906;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p30,
|
||||||
.kanban .state_20 .box.p30,
|
.kanban .state_20 .box.p30,
|
||||||
.kanban .state_40 .box.p30{
|
.kanban .state_40 .box.p30{
|
||||||
border: 5px solid #ffa906;
|
border: 5px solid #ffa906;
|
||||||
@@ -218,6 +220,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p40 .name{
|
.task.p40 .name{
|
||||||
color: #ff8606;
|
color: #ff8606;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p40,
|
||||||
.kanban .state_20 .box.p40,
|
.kanban .state_20 .box.p40,
|
||||||
.kanban .state_40 .box.p40{
|
.kanban .state_40 .box.p40{
|
||||||
border: 5px solid #ff8606;
|
border: 5px solid #ff8606;
|
||||||
@@ -226,6 +229,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p50 .name{
|
.task.p50 .name{
|
||||||
color: #ff4c06;
|
color: #ff4c06;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p50,
|
||||||
.kanban .state_20 .box.p50,
|
.kanban .state_20 .box.p50,
|
||||||
.kanban .state_40 .box.p50{
|
.kanban .state_40 .box.p50{
|
||||||
border: 5px solid #ff4c06;
|
border: 5px solid #ff4c06;
|
||||||
@@ -234,6 +238,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p60 .name{
|
.task.p60 .name{
|
||||||
color: #ff3506;
|
color: #ff3506;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p60,
|
||||||
.kanban .state_20 .box.p60,
|
.kanban .state_20 .box.p60,
|
||||||
.kanban .state_40 .box.p60{
|
.kanban .state_40 .box.p60{
|
||||||
border: 5px solid #ff3506;
|
border: 5px solid #ff3506;
|
||||||
@@ -242,6 +247,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p70 .name{
|
.task.p70 .name{
|
||||||
color: #ff0000;
|
color: #ff0000;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p70,
|
||||||
.kanban .state_20 .box.p70,
|
.kanban .state_20 .box.p70,
|
||||||
.kanban .state_40 .box.p70{
|
.kanban .state_40 .box.p70{
|
||||||
border: 5px solid #ff0000;
|
border: 5px solid #ff0000;
|
||||||
@@ -250,6 +256,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p80 .name{
|
.task.p80 .name{
|
||||||
color: #df153b;
|
color: #df153b;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p80,
|
||||||
.kanban .state_20 .box.p80,
|
.kanban .state_20 .box.p80,
|
||||||
.kanban .state_40 .box.p80{
|
.kanban .state_40 .box.p80{
|
||||||
border: 5px solid #df153b;
|
border: 5px solid #df153b;
|
||||||
@@ -259,6 +266,7 @@ tr:hover .taglist .tag button {
|
|||||||
background-color: #991c34;
|
background-color: #991c34;
|
||||||
color: #ffff00;
|
color: #ffff00;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p90,
|
||||||
.kanban .state_20 .box.p90,
|
.kanban .state_20 .box.p90,
|
||||||
.kanban .state_40 .box.p90{
|
.kanban .state_40 .box.p90{
|
||||||
border: 5px solid #991c34;
|
border: 5px solid #991c34;
|
||||||
@@ -268,6 +276,7 @@ tr:hover .taglist .tag button {
|
|||||||
background-color: #733440;
|
background-color: #733440;
|
||||||
color: #ffff00;
|
color: #ffff00;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p100,
|
||||||
.kanban .state_20 .box.p100,
|
.kanban .state_20 .box.p100,
|
||||||
.kanban .state_40 .box.p100{
|
.kanban .state_40 .box.p100{
|
||||||
border: 5px solid #733440;
|
border: 5px solid #733440;
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ tr:hover .taglist .tag button {
|
|||||||
border-color: blue;
|
border-color: blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.states .active{
|
||||||
|
background: #dfe4ff;
|
||||||
|
}
|
||||||
|
|
||||||
.taglist .tag{
|
.taglist .tag{
|
||||||
border-color: blue;
|
border-color: blue;
|
||||||
}
|
}
|
||||||
@@ -179,6 +183,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p10 .name{
|
.task.p10 .name{
|
||||||
color: #c9fbb2;
|
color: #c9fbb2;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p10,
|
||||||
.kanban .state_20 .box.p10,
|
.kanban .state_20 .box.p10,
|
||||||
.kanban .state_40 .box.p10{
|
.kanban .state_40 .box.p10{
|
||||||
border: 5px solid #c9fbb2;
|
border: 5px solid #c9fbb2;
|
||||||
@@ -187,6 +192,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p20 .name{
|
.task.p20 .name{
|
||||||
color: #cbff57;
|
color: #cbff57;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p20,
|
||||||
.kanban .state_20 .box.p20,
|
.kanban .state_20 .box.p20,
|
||||||
.kanban .state_40 .box.p20{
|
.kanban .state_40 .box.p20{
|
||||||
border: 5px solid #cbff57;
|
border: 5px solid #cbff57;
|
||||||
@@ -195,6 +201,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p30 .name{
|
.task.p30 .name{
|
||||||
color: #dfff44;
|
color: #dfff44;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p30,
|
||||||
.kanban .state_20 .box.p30,
|
.kanban .state_20 .box.p30,
|
||||||
.kanban .state_40 .box.p30{
|
.kanban .state_40 .box.p30{
|
||||||
border: 5px solid #dfff44;
|
border: 5px solid #dfff44;
|
||||||
@@ -203,6 +210,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p40 .name{
|
.task.p40 .name{
|
||||||
color: #f8ff29;
|
color: #f8ff29;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p40,
|
||||||
.kanban .state_20 .box.p40,
|
.kanban .state_20 .box.p40,
|
||||||
.kanban .state_40 .box.p40{
|
.kanban .state_40 .box.p40{
|
||||||
border: 5px solid #f8ff29;
|
border: 5px solid #f8ff29;
|
||||||
@@ -211,6 +219,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p50 .name{
|
.task.p50 .name{
|
||||||
color: #ffdb1b;
|
color: #ffdb1b;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p50,
|
||||||
.kanban .state_20 .box.p50,
|
.kanban .state_20 .box.p50,
|
||||||
.kanban .state_40 .box.p50{
|
.kanban .state_40 .box.p50{
|
||||||
border: 5px solid #ffdb1b;
|
border: 5px solid #ffdb1b;
|
||||||
@@ -219,6 +228,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p60 .name{
|
.task.p60 .name{
|
||||||
color: #ff9309;
|
color: #ff9309;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p60,
|
||||||
.kanban .state_20 .box.p60,
|
.kanban .state_20 .box.p60,
|
||||||
.kanban .state_40 .box.p60{
|
.kanban .state_40 .box.p60{
|
||||||
border: 5px solid #ff9309;
|
border: 5px solid #ff9309;
|
||||||
@@ -227,6 +237,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p70 .name{
|
.task.p70 .name{
|
||||||
color: #ff6c00;
|
color: #ff6c00;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p70,
|
||||||
.kanban .state_20 .box.p70,
|
.kanban .state_20 .box.p70,
|
||||||
.kanban .state_40 .box.p70{
|
.kanban .state_40 .box.p70{
|
||||||
border: 5px solid #ff6c00;
|
border: 5px solid #ff6c00;
|
||||||
@@ -235,6 +246,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p80 .name{
|
.task.p80 .name{
|
||||||
color: #ff3c00;
|
color: #ff3c00;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p80,
|
||||||
.kanban .state_20 .box.p80,
|
.kanban .state_20 .box.p80,
|
||||||
.kanban .state_40 .box.p80{
|
.kanban .state_40 .box.p80{
|
||||||
border: 5px solid #ff3c00;
|
border: 5px solid #ff3c00;
|
||||||
@@ -243,6 +255,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p90 .name{
|
.task.p90 .name{
|
||||||
color: #ff0000;
|
color: #ff0000;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p90,
|
||||||
.kanban .state_20 .box.p90,
|
.kanban .state_20 .box.p90,
|
||||||
.kanban .state_40 .box.p90{
|
.kanban .state_40 .box.p90{
|
||||||
border: 5px solid #ff0000;
|
border: 5px solid #ff0000;
|
||||||
@@ -251,6 +264,7 @@ tr:hover .taglist .tag button {
|
|||||||
.task.p100 .name{
|
.task.p100 .name{
|
||||||
color: #ff0048;
|
color: #ff0048;
|
||||||
}
|
}
|
||||||
|
.kanban .state_custom .box.p100,
|
||||||
.kanban .state_20 .box.p100,
|
.kanban .state_20 .box.p100,
|
||||||
.kanban .state_40 .box.p100{
|
.kanban .state_40 .box.p100{
|
||||||
border: 5px solid #ff0048;
|
border: 5px solid #ff0048;
|
||||||
|
|||||||
Reference in New Issue
Block a user