Compare commits

...

16 Commits

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

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-30 23:44:01 +01:00
fd536abe11 working on migration of items to stock db
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-30 23:24:02 +01:00
d6b5d243c1 removed autofocus in easylist
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-29 13:12:07 +01:00
8f82ca87b4 major improvement to easylist for usability on mobile devices
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-29 00:43:58 +01:00
600b0f2cf4 working on event handlers for mobile devices
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 20:38:45 +01:00
59f864d16f improved easylist
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 16:01:19 +01:00
261a93bcc0 improved CSS
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 14:34:45 +01:00
60c447a967 added easy list button to tag view
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 14:03:58 +01:00
b0550db5c2 implemented easy-list
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-11-28 13:56:55 +01:00
31 changed files with 693 additions and 104 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,171 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api, get, patch } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte';
import Detail from './EasyListDetail.svelte';
let { tag } = $props();
let filter = $state(null);
let highlight = $state(null);
let search = $derived(filter ? filter.toLowerCase() : null);
//let input;
let tasks = $state(null);
let router = useTinyRouter();
let sorted = $derived(tasks ? Object.values(tasks).filter(noNoIndex).sort(byName) : null);
let start = 0;
let x = 0;
let y = 0;
function byName(a,b){
return a.name.localeCompare(b.name);
}
function extend(e,task){
e.preventDefault();
e.stopPropagation();
highlight = task;
return false;
}
function getTask(evt){
var link = evt.target;
var id = link.getAttribute('task_id');
return tasks[id];
}
function goTag(e,newTag){
e.preventDefault();
e.stopPropagation();
router.navigate(`/tags/easylist/${newTag}`);
tag = newTag;
load();
}
function ignore(evt){
evt.preventDefault();
evt.stopPropagation();
return false;
}
async function load(){
const url = api(`task/tagged/${tag}`);
const res = await get(url);
if (res.ok){
yikes();
tasks = await res.json();
//input.focus();
} else error(res);
}
function match(task){
if (!search) return true;
if (task.name.toLowerCase().includes(search)) return true;
if (task.tags){
for (let tag of task.tags){
if (tag.toLowerCase().includes(search)) return true;
}
}
return false;
}
function measured(evt,duration,d){
if (d > 100) return;
if (duration < 500){
onclick(evt);
} else {
oncontextmenu(evt);
}
}
function noNoIndex(task){
return !task.no_index;
}
function onclick(evt) {
ignore(evt);
let task = getTask(evt);
if (task.status <= 20) { // open
update(task,60);
} else update(task,20);
return false;
}
function oncontextmenu(evt) {
ignore(evt);
highlight = getTask(evt);
return false;
}
function ontouchstart(evt){
ignore(evt);
start = evt.timeStamp;
x = evt.touches[0].clientX;
y = evt.touches[0].clientY;
return false;
}
function ontouchend(evt){
ignore(evt);
let d = Math.abs(x - evt.changedTouches[0].clientX) + Math.abs(y - evt.changedTouches[0].clientY);
measured(evt, evt.timeStamp - start, d);
return false;
}
async function update(task,newState){
highlight = null;
const url = api(`task/${task.id}`);
const res = await patch(url,{status:newState});
if (res.ok){
task.status = newState;
yikes();
// filter = null; // not sure what is better, resetting or keeping
} else error(res);
}
onMount(load);
</script>
<h2>{t('tasks_for_tag',{tag:decodeURI(tag)})}</h2>
<div class="easylist">
<fieldset class="open">
<legend>{t('state_open')}</legend>
{#if sorted}
{#each sorted as task}
{#if task.status == 20 && match(task)}
<div href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} onmousedown={ontouchstart} onmouseup={ontouchend} >
{task.name}
</div>
{#if highlight == task}
<Detail task={task} goTag={goTag} />
{/if}
{/if}
{/each}
{/if}
</fieldset>
<fieldset class="closed">
<legend>{t('state_complete')}</legend>
{#if sorted}
{#each sorted as task}
{#if task.status > 20 && match(task)}
<div href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} onmousedown={ontouchstart} onmouseup={ontouchend} >
{task.name}
</div>
{#if highlight == task}
<Detail task={task} goTag={goTag} />
{/if}
{/if}
{/each}
{/if}
</fieldset>
<div class="filter">
{t('filter')}: <input type="text" bind:value={filter} /> <!-- bind:this={input} -->
</div>
</div>

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

View File

@@ -2,4 +2,5 @@ description = "Umbrella : Stock"
dependencies{ dependencies{
implementation(project(":core")) implementation(project(":core"))
implementation("de.srsoftware:configuration.json:1.0.3")
} }

View File

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

View 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!");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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