Compare commits

...

21 Commits

Author SHA1 Message Date
40c35bc9ed added kanban button to task display
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 11:06:45 +01:00
5fc79e05da Merge branch 'feature/list_task_users' 2025-11-28 09:16:10 +01:00
7de79627b4 Merge remote-tracking branch 'origin' 2025-11-28 09:16:03 +01:00
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
21759d1b12 Merge branch 'main' into dev 2025-11-26 21:09:31 +01:00
ba531a0ab1 Merge branch 'module/projects' 2025-11-26 21:08:56 +01:00
7e02fe17e9 Merge branch 'module/projects' 2025-11-26 21:06:35 +01:00
37e6ccd71a Merge branch 'module/projects' into dev 2025-11-26 09:12:26 +01:00
11c0983dac improved button colors
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-26 09:12:17 +01:00
2bad462c38 Merge branch 'module/stock' 2025-11-26 09:04:18 +01:00
daa25044a4 Merge branch 'module/projects' into dev 2025-11-26 08:47:34 +01:00
c37df2ddec added priority colors for tasks in custom state
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-26 08:47:23 +01:00
e450585d37 fixed bug: import was missing
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-26 08:26:42 +01:00
93907a839d altered table update:
now retaining the template name values from the template column as new template values in the document table

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

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

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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} />

View File

@@ -145,7 +145,6 @@
error(res);
return null;
}
}
async function loadProperties(){

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';

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>

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}

View File

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

View File

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

View File

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

View File

@@ -199,12 +199,9 @@ public class TaskModule extends BaseHandler implements TaskService {
} catch (NumberFormatException e) {
throw invalidFieldException(LIMIT, "number");
}
Set<Long> projectIds = projectService().listUserProjects(user.id(), true).keySet();
var list = taskDb.listUserTasks(user.id(), limit, offset, false).stream()
.filter(task -> projectIds.contains(task.projectId())) // drop tasks assigned to project we are not member of
.map(Task::toMap)
.toList();
return sendContent(ex, list);
var tasks = taskDb.listUserTasks(user.id(), limit, offset, false);
var mapped = loadMembers(tasks).stream().map(Task::toMap);
return sendContent(ex, mapped);
}
@Override

View File

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

View File

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

View File

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

View File

@@ -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;

View File

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