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