Compare commits

...

15 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
16 changed files with 138 additions and 152 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

@@ -363,7 +363,7 @@ public class DocumentApi extends BaseHandler implements DocumentService {
}
private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException {
var template = document.template().name();
var template = document.template();
var templateName = template+".html.pdf";
var type = document.type().name();
var zugferd = "invoice".equals(type);
@@ -517,8 +517,10 @@ public class DocumentApi extends BaseHandler implements DocumentService {
if (!(json.has(COMPANY) && json.get(COMPANY) instanceof Number companyId)) throw missingFieldException(COMPANY);
var company = companyService().get(companyId.longValue());
if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are not a member of {0}",company.name());
var templates = db.getCompanyTemplates(companyId.longValue());
return sendContent(ex,templates.stream().map(Template::toMap));
var templates = registry.documents()
.filter(d -> d.name().endsWith(".template"))
.map(d -> d.name().replaceAll("(\\.[^.]+)?\\.template$",""));
return sendContent(ex,templates);
}
private boolean postSearch(HttpExchange ex, UmbrellaUser user) throws IOException {

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,8 +26,6 @@ public interface DocumentDb {
CustomerSettings getCustomerSettings(long companyId, Type docType, String customerId) throws UmbrellaException;
Collection<Template> getCompanyTemplates(long l) throws UmbrellaException;
Type getType(int typeId) throws UmbrellaException;
Map<Long, Document> listDocs(long companyId) throws UmbrellaException;

View File

@@ -42,6 +42,15 @@ public class SqliteDb extends BaseDb implements DocumentDb{
super(connection);
}
private void addTemplateColumn() {
try {
var sql = format("ALTER TABLE {0} ADD COLUMN {1} VARCHAR(255)",TABLE_DOCUMENTS,TEMPLATE);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("Failed to update column {0} → {1} of {2}",TEMPLATE_ID,TEMPLATE,TABLE_DOCUMENTS);
}
}
protected int createTables() {
int currentVersion = createSettingsTable();
switch (currentVersion) {
@@ -52,22 +61,15 @@ public class SqliteDb extends BaseDb implements DocumentDb{
createTablePositions();
createTableCustomerPrices();
createTableCustomerSettings();
case 1:
addTemplateColumn();
moveTemplateNames();
dropTemplateTable();
dropTemplateIdColumn();
}
return setCurrentVersion(1);
return setCurrentVersion(2);
}
/*private void createTableCompanySettings() {
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} INT NOT NULL, {3} TEXT DEFAULT \"A\", {4} TEXT DEFAULT NULL, {5} INT NOT NULL DEFAULT 1, PRIMARY KEY ({1}, {2}))";
try {
var stmt = db.prepareStatement(format(sql,TABLE_COMPANY_SETTINGS, COMPANY_ID,DOC_TYPE_ID,TYPE_PREFIX,TYPE_SUFFIX,TYPE_NUMBER));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_COMPANY_SETTINGS,e);
throw new RuntimeException(e);
}
}*/
private void createTableCustomerPrices() {
var sql = "CREATE TABLE IF NOT EXISTS {0} ({1} INT NOT NULL, {2} VARCHAR(255), {3} VARCHAR(50), {4} INTEGER)";
try {
@@ -172,36 +174,6 @@ CREATE TABLE IF NOT EXISTS {0} (
}
}
private int createTableSettings() {
var createTable = """
CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL);
""";
try {
var stmt = db.prepareStatement(format(createTable,TABLE_SETTINGS, KEY, VALUE));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SETTINGS,e);
throw new RuntimeException(e);
}
Integer version = null;
try {
var rs = select(VALUE).from(TABLE_SETTINGS).where(KEY, equal(DB_VERSION)).exec(db);
if (rs.next()) version = rs.getInt(VALUE);
rs.close();
if (version == null) {
version = INITIAL_DB_VERSION;
insertInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
}
return version;
} catch (SQLException e) {
LOG.log(ERROR,ERROR_READ_TABLE,DB_VERSION,TABLE_SETTINGS,e);
throw new RuntimeException(e);
}
}
private void createTableTemplates() {
var createTable = "CREATE TABLE IF NOT EXISTS {0} ({1} INTEGER PRIMARY KEY, {2} INT NOT NULL, {3} VARCHAR(255) NOT NULL, {4} BLOB)";
try {
@@ -252,17 +224,21 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
return pos;
}
@Override
public Collection<Template> getCompanyTemplates(long companyId) throws UmbrellaException {
private void dropTemplateIdColumn() {
try {
var rs = select(ALL).from(TABLE_TEMPLATES).where(COMPANY_ID,equal(companyId)).exec(db);
var templates = new HashSet<Template>();
while (rs.next()) templates.add(Template.of(rs));
rs.close();
return templates;
var sql = format("ALTER TABLE {0} DROP COLUMN {1}",TABLE_DOCUMENTS,TEMPLATE_ID);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("Failed to load templates for company {0}",companyId);
throw databaseException("Failed to update column {0} → {1} of {2}",TEMPLATE_ID,TEMPLATE,TABLE_DOCUMENTS);
}
}
private void dropTemplateTable() {
try {
var sql = format("DROP TABLE IF EXISTS {0};",TABLE_TEMPLATES);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("Failed to drop table {0}",TABLE_TEMPLATES);
}
}
@@ -307,11 +283,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
}
throw new UmbrellaException(500,"No type with id = {0}",typeId);
}
private void init() {
var version = createTables();
}
@Override
public Map<Long, Map<Long, String>> docReferencedByTimes(Set<Long> timeIds) throws UmbrellaException {
try {
@@ -430,7 +402,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
while (rs.next()) types.put(rs.getInt(ID),toType(rs));
rs.close();
rs = Query.select(ALL).from(TABLE_DOCUMENTS).leftJoin(TEMPLATE_ID,TABLE_TEMPLATES, ID).where(TABLE_DOCUMENTS+"."+ ID,equal(docId)).exec(db);
rs = Query.select(ALL).from(TABLE_DOCUMENTS).where(TABLE_DOCUMENTS+"."+ ID,equal(docId)).exec(db);
Document doc = null;
while (rs.next()) doc = toDoc(rs,types);
rs.close();
@@ -452,6 +424,15 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
throw new UmbrellaException(500,"Failed to load document {0}.",docId);
}
private void moveTemplateNames() {
try {
var sql = format("UPDATE {0} SET template = (SELECT name FROM templates WHERE templates.id = documents.template_id);",TABLE_DOCUMENTS);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("Failed to move template.names to document.templates!");
}
}
@Override
public String nextDocId(String language, long companyId, Type type) {
try {
@@ -499,9 +480,8 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
var timestamp = doc.date().atStartOfDay(UTC).toInstant().getEpochSecond();
var sender = doc.sender();
var custom = doc.customer();
var templateId = doc.template() == null ? null : doc.template().id();
var stmt = insertInto(TABLE_DOCUMENTS,TYPE_ID,COMPANY_ID, DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE_ID,CURRENCY)
.values(doc.type().id(),doc.companyId(),timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber(),templateId, doc.currency())
var stmt = insertInto(TABLE_DOCUMENTS,TYPE_ID,COMPANY_ID, DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE,CURRENCY)
.values(doc.type().id(),doc.companyId(),timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber(),doc.template(), doc.currency())
.execute(db);
var rs = stmt.getGeneratedKeys();
Long newId = null;
@@ -523,10 +503,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
var sender = doc.sender();
var custom = doc.customer();
update(TABLE_DOCUMENTS)
.set(DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE_ID)
.set(DATE, DELIVERY_DATE,FOOTER,HEAD, NUMBER, STATE, SENDER,TAX_NUMBER,BANK_ACCOUNT,COURT,CUSTOMER,CUSTOMER_EMAIL,CUSTOMER_NUMBER,CUSTOMER_TAX_NUMBER,TEMPLATE)
.where(ID,equal(doc.id()))
.prepare(db)
.apply(timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber(),doc.template().id())
.apply(timestamp,doc.delivery(),doc.footer(),doc.head(),doc.number(),doc.state().code(),sender.name(),sender.taxNumber(),sender.bankAccount(),sender.court(),custom.name(),custom.email(),custom.id(),custom.taxNumber(),doc.template())
.close();
sender.clean();
custom.clean();
@@ -635,23 +615,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
var customerEmail = rs.getString(CUSTOMER_EMAIL);
var customer = new Customer(customerId, customerName, customerEmail, customerTaxNumber,FALLBACK_LANG);
var sender = new Sender(senderName,bankAccount,taxNumber,court);
var template = toTemplate(rs);
var template = rs.getString(TEMPLATE);
return new Document(id,company,number,type,date, Document.State.of(state).orElse(State.ERROR),template,delivery,head,footer,currency, DEFAULT_THOUSANDS_SEPARATOR,sender,customer,new PositionList());
}
private Template toTemplate(ResultSet rs) throws SQLException {
try {
var id = rs.getLong(TEMPLATE_ID);
var company = rs.getLong(COMPANY_ID);
var name = rs.getString(NAME);
var data = rs.getBytes(TEMPLATE);
if (id == 0) return new Template(0,company,"",null);
return new Template(id,company,name,data);
} catch (SQLException ignored){
return null;
}
}
private Position toPosition(ResultSet rs) throws SQLException {
var num = rs.getInt(POS);
var itemCode = rs.getString(ITEM_CODE);

View File

@@ -30,8 +30,8 @@
{#if templates}
<select bind:value onchange={onchange}>
<option value={0}>{caption}</option>
{#each Object.entries(templates) as [id,template]}
<option value={template.id}>{template.name}</option>
{#each templates as template}
<option value={template}>{template}</option>
{/each}
</select>
{:else}

View File

@@ -214,9 +214,9 @@
<th>{t('template')}:</th>
<td>
{#if editable}
<TemplateSelector company={doc.company.id} bind:value={doc.template.id} onchange={() => update('template_id',doc.template.id)} />
<TemplateSelector company={doc.company.id} bind:value={doc.template} onchange={() => update('template',doc.template)} />
{:else}
{doc.template.name}
{doc.template}
{/if}
</td>
</tr>

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

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

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