Compare commits

...

11 Commits

Author SHA1 Message Date
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
29 changed files with 604 additions and 91 deletions

View File

@@ -41,7 +41,7 @@ subprojects {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
implementation("de.srsoftware:configuration.api:1.0.2")
implementation("de.srsoftware:tools.jdbc:2.0.2")
implementation("de.srsoftware:tools.jdbc:2.0.4")
implementation("de.srsoftware:tools.http:6.0.5")
implementation("de.srsoftware:tools.mime:1.1.3")
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 TASK_IDS = "task_ids";
public static final String TAX = "tax";
public static final String TAX_RATE = "tax_rate";
public static final String TEMPLATE = "template";
public static final String TEXT = "text";
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 STOP = "stop";
public static final String SUBMIT = "submit";
public static final String TAGGED = "tagged";
public static final String TOKEN = "token";
public static final String VIEW = "view";
}

View File

@@ -8,7 +8,7 @@ import java.util.Collection;
public interface StockService {
/**
* 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
* @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.model.UmbrellaUser;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public interface TagService {
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;
/**

View File

@@ -2,6 +2,7 @@
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Util.mapMarkdown;
import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.api.Owner;
@@ -13,19 +14,20 @@ import org.json.JSONObject;
public class Item implements Mappable {
private long id, ownerNumber; // id is the database key, number the owner-relative id
private Owner owner;
private String code, name;
private String code, description, name;
private Location location;
private Collection<Property> properties;
private Set<String> dirtyFields = new HashSet<>();
public Item(long id, Owner owner, long ownerNumber, Location location, String code, String name) {
this.id = id;
this.owner = owner;
public Item(long id, Owner owner, long ownerNumber, Location location, String code, String name, String description) {
this.id = id;
this.owner = owner;
this.ownerNumber = ownerNumber;
this.location = location;
this.code = code;
this.name = name;
this.properties = new HashSet<>();
this.location = location;
this.code = code;
this.name = name;
this.description = description;
this.properties = new HashSet<>();
}
public Item clear() {
@@ -37,6 +39,10 @@ public class Item implements Mappable {
return code;
}
public String description(){
return description;
}
public boolean isDirty(){
return !dirtyFields.isEmpty();
}
@@ -65,13 +71,14 @@ public class Item implements Mappable {
}
public static Item of(ResultSet rs) throws SQLException {
var id = rs.getLong(ID);
var owner = OwnerRef.of(rs);
var id = rs.getLong(ID);
var owner = OwnerRef.of(rs);
var ownerNumber = rs.getLong(OWNER_NUMBER);
var location = Location.of(rs);
var code = rs.getString(CODE);
var name = rs.getString(NAME);
return new Item(id, owner, ownerNumber, location, code, name);
var location = Location.of(rs);
var code = rs.getString(CODE);
var name = rs.getString(NAME);
var description = rs.getString(DESCRIPTION);
return new Item(id, owner, ownerNumber, location, code, name, description);
}
public Owner owner(){
@@ -82,6 +89,10 @@ public class Item implements Mappable {
return ownerNumber;
}
public void ownerNumber(long newVal) {
ownerNumber = newVal;
}
public Item patch(JSONObject json) {
for (var field : json.keySet()){
var known = true;
@@ -92,6 +103,9 @@ public class Item implements Mappable {
case NAME:
name = json.getString(field);
break;
case DESCRIPTION:
description = json.getString(field);
break;
default:
known = false;
}
@@ -112,6 +126,7 @@ public class Item implements Mappable {
map.put(LOCATION,location.toMap());
map.put(CODE,code);
map.put(NAME,name);
map.put(DESCRIPTION,mapMarkdown(description));
map.put(OWNER_NUMBER,ownerNumber);
if (properties != null) map.put(PROPERTIES,properties.stream().map(Property::toMap).toList());
return map;

View File

@@ -2,6 +2,7 @@
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static java.text.MessageFormat.format;
import de.srsoftware.tools.Mappable;
import java.sql.ResultSet;
@@ -22,6 +23,14 @@ public class Property implements Mappable {
this.unit = unit;
}
public long id(){
return id;
}
public String name(){
return name;
}
public static Property of(ResultSet rs) throws SQLException {
var id = rs.getLong(ID);
var name = rs.getString(NAME);
@@ -45,6 +54,19 @@ public class Property implements Mappable {
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){
value = newVal;
return this;

View File

@@ -13,6 +13,7 @@
import Companies from "./routes/company/Index.svelte";
import ContactList from "./routes/contact/Index.svelte";
import DocList from "./routes/document/List.svelte";
import EasyList from "./routes/task/EasyList.svelte";
import EditService from "./routes/user/EditService.svelte";
import EditUser from "./routes/user/EditUser.svelte";
import FileIndex from "./routes/files/Index.svelte";
@@ -104,6 +105,7 @@
<Route path="/tags" component={TagList} />
<Route path="/tags/use/:tag" component={TagUses} />
<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/:id/edit" component={ViewTask} />
<Route path="/task/:id/view" component={ViewTask} />

View File

@@ -30,23 +30,33 @@
function itemSelected(item){
let unit_price = null;
let unit = t('pieces');
let description = '';
let tax = null;
for (let prop of item.properties) {
if (prop.name.toLowerCase().indexOf(t('price').toLowerCase())>-1){
unit_price = 100*prop.value.replace(',','.');
let lowerName = prop.name.toLowerCase();
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 {
description += `* ${prop.name}: ${prop.value}\n`;
}
}
select({
var data = {
item_code : item.code,
title : item.name,
description : description,
amount : 1,
unit : t('pieces'),
unit : unit,
unit_price : unit_price
});
};
if (tax) data['tax'] = tax;
select(data);
}
function timeSelected(time){

View File

@@ -69,6 +69,9 @@
{#if item}
<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)} />
<div>
{@html item.description.rendered}
</div>
<table>
<tbody>
{#each item.properties.toSorted(byName) as prop}

View File

@@ -1,5 +1,6 @@
<script>
import {onMount} from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte';
@@ -8,6 +9,7 @@
import Reference from './Reference.svelte';
let { tag } = $props();
let router = useTinyRouter();
let uses = $state(null);
async function loadUses(){
@@ -21,11 +23,17 @@
}
}
function goEasy(){
router.navigate(`/tags/easylist/${tag}`);
}
onMount(loadUses);
</script>
<fieldset>
<legend>{t('tag_uses',{tag:tag})}</legend>
<legend>
{t('tag_uses',{tag:tag})}
<button onclick={goEasy}>{t('easy_list')}</button>
</legend>
{#if uses}
{#each Object.entries(uses) as [module,ids]}
<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{
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 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_ID = "item_id";
public static final String ITEMS = "items";

View File

@@ -0,0 +1,86 @@
/* © 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 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)
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);
String lang = null;
Company company;
if (tuple == null){
company = companyService().get(companyId);
for (var member : companyService().getMembers(companyId)){
lang = member.language();
if (lang != null){
tuple = Tuple.of(company,lang);
companyInfo.put(companyId,tuple);
break;
}
}
} else {
company = tuple.a;
lang = tuple.b;
}
var location = companyLocations.get(companyId);
if (location == null) {
location = stockDb.save(new DbLocation(0,company,null,"virtual items",null));
companyLocations.put(companyId,location);
}
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));
stockDb.save(stockItem);
}
rs.close();
} catch (SQLException e) {
throw databaseException("Failed to migrate items from itemDB to stockDB!");
}
LOG.log(System.Logger.Level.WARNING,"migrateTo({0}) not implemented", stockDb);
}
}

View File

@@ -11,8 +11,7 @@ import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.ModuleRegistry.noteService;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
import static de.srsoftware.umbrella.stock.Constants.*;
import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.WARNING;
import static java.lang.System.Logger.Level.*;
import static java.text.MessageFormat.format;
import de.srsoftware.tools.jdbc.Query;
@@ -51,21 +50,22 @@ public class SqliteDb extends BaseDb implements StockDb {
Long propertyId = null;
if (rs.next()) propertyId = rs.getLong(1);
rs.close();
if (propertyId == null || propertyId == 0) throw databaseException("Failed to create new property {0} to DB",name);
insertInto(TABLE_ITEM_PROPERTIES,ITEM_ID,PROPERTY_ID,VALUE).values(itemId,propertyId,value).execute(db);
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).close();
db.setAutoCommit(true);
return new Property(propertyId,name,value,unit);
} 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
public Location delete(DbLocation location) {
private void createDescriptionColumn(){
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());
var sql = "ALTER TABLE {0} ADD COLUMN {1} TEXT";
sql = format(sql,TABLE_ITEMS,DESCRIPTION);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException("failed to create {0} column in {1} table!",DESCRIPTION,TABLE_ITEMS);
}
}
@@ -144,7 +144,7 @@ public class SqliteDb extends BaseDb implements StockDb {
private void createPropertiesTable() {
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);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
@@ -165,8 +165,20 @@ public class SqliteDb extends BaseDb implements StockDb {
dropTokenTable();
case 2:
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() {
@@ -394,13 +406,13 @@ public class SqliteDb extends BaseDb implements StockDb {
@Override
public Item save(Item item) {
if (item.id() == 0){
var number = nextItemNumberFor(item.location().resolve().owner());
try {
var rs = insertInto(TABLE_ITEMS, OWNER, OWNER_NUMBER, CODE, NAME, LOCATION_ID)
.values(item.owner().dbCode(), item.ownerNumber(), item.code(), item.name(), item.location().id())
var rs = insertInto(TABLE_ITEMS, OWNER, OWNER_NUMBER, CODE, NAME, DESCRIPTION, LOCATION_ID)
.values(item.owner().dbCode(), number, item.code(), item.name(), item.description(), item.location().id())
.execute(db).getGeneratedKeys();
if (rs.next()) item.id(rs.getLong(1));
if (rs.next()) item.id(rs.getLong(1)).ownerNumber(number);
rs.close();
return item;
} catch (SQLException e) {
throw databaseException("Failed to save new item to database!");
}
@@ -409,25 +421,53 @@ public class SqliteDb extends BaseDb implements StockDb {
var location = item.location();
var query = update(TABLE_ITEMS).where(ID, equal(item.id()));
if (location == null) {
query.set(CODE,NAME);
query.set(CODE,NAME,DESCRIPTION);
} else {
query.set(CODE,NAME,LOCATION_ID);
query.set(CODE,NAME,DESCRIPTION,LOCATION_ID);
}
var pq = query.prepare(db);
if (location == null) {
pq.apply(item.code(),item.name());
pq.apply(item.code(),item.name(),item.description()).close();
} 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){
throw databaseException("Failed to update item {0}",item.name());
}
}
saveProperties(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
public Property setProperty(long itemId, long existingPropId, Object value) {
try {
@@ -435,11 +475,11 @@ public class SqliteDb extends BaseDb implements StockDb {
var rs = select(ALL).from(TABLE_PROPERTIES).where(ID,equal(existingPropId)).exec(db);
if (rs.next()) prop = Property.of(rs);
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)){
Query.delete().from(TABLE_ITEM_PROPERTIES).where(ITEM_ID,equal(itemId)).where(PROPERTY_ID,equal(existingPropId)).execute(db);
} 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);
} catch (SQLException e) {

View File

@@ -9,16 +9,14 @@ import static de.srsoftware.umbrella.core.Field.ITEM;
import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.Paths.LIST;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.stock.Constants.*;
import static java.lang.System.Logger.Level.WARNING;
import static java.text.MessageFormat.format;
import static java.util.Comparator.comparing;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Mappable;
import de.srsoftware.configuration.JsonConfig;
import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler;
@@ -41,6 +39,16 @@ public class StockModule extends BaseHandler implements StockService {
super();
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE));
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);
}
@@ -310,13 +318,13 @@ public class StockModule extends BaseHandler implements StockService {
private boolean postItem(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
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(LOCATION) || !(json.get(LOCATION) instanceof JSONObject locationData)) throw missingFieldException(LOCATION);
var location = stockDb.loadLocation(locationData.getLong(ID));
var owner = location.owner().resolve();
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,number,location,code,name);
var newItem = new Item(0,owner,0,location,code,name,description);
return sendContent(ex,stockDb.save(newItem));
}
@@ -381,32 +389,9 @@ public class StockModule extends BaseHandler implements StockService {
return sendContent(ex,property);
}
private Mappable toOwner(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 -> companyService().get(owner.getLong(key));
case USER -> userService().loadUser(owner.getLong(key));
default -> throw invalidFieldException(format("Single child of {0}", OWNER), format("either {0} or {1}", COMPANY, USER));
};
}
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
public Collection<Object> redefineMe(long company_id) {
// TODO
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) {
try {
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);
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);
return tag;
} catch (SQLException e){
@@ -219,7 +219,7 @@ CREATE TABLE IF NOT EXISTS {0} (
public void deleteEntity(String module, long entityId) {
try {
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);
} catch (SQLException e){
throw new UmbrellaException("Failed to save tags ({0} {1})",module,entityId);
@@ -229,7 +229,7 @@ CREATE TABLE IF NOT EXISTS {0} (
@Override
public Map<String, List<Long>> getUses(String tag, long userId) {
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>>();
while (rs.next()){
var module = rs.getString(MODULE);
@@ -237,7 +237,7 @@ CREATE TABLE IF NOT EXISTS {0} (
result.computeIfAbsent(module, k -> new ArrayList<>()).add(entityId);
}
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()){
var module = rs.getString(MODULE);
var entityId = rs.getLong(ENTITY_ID);
@@ -256,12 +256,12 @@ CREATE TABLE IF NOT EXISTS {0} (
var tags = new HashSet<String>();
// 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));
rs.close();
// 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));
rs.close();
return tags;
@@ -294,12 +294,12 @@ CREATE TABLE IF NOT EXISTS {0} (
var tags = new HashMap<Long,HashSet<String>>();
// 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));
rs.close();
// 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));
rs.close();
return tags;
@@ -328,7 +328,7 @@ CREATE TABLE IF NOT EXISTS {0} (
@Override
public void updateId(String module, Object oldId, Object newId) {
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);
} catch (SQLException e) {
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 {
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{

View File

@@ -1,8 +1,7 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.task;
import static de.srsoftware.tools.Optionals.is0;
import static de.srsoftware.tools.Optionals.isSet;
import static de.srsoftware.tools.Optionals.*;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.Constants.*;
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.task.Constants.*;
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 de.srsoftware.configuration.Configuration;
@@ -92,6 +93,7 @@ public class TaskModule extends BaseHandler implements TaskService {
var head = path.pop();
return switch (head) {
case PERMISSIONS -> getPermissionList(ex);
case TAGGED -> getTaggedTasks(path, user.get(), ex);
case null -> getUserTasks(user.get(), ex);
default -> {
var taskId = Long.parseLong(head);
@@ -179,6 +181,19 @@ public class TaskModule extends BaseHandler implements TaskService {
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 {
var task = loadMembers(taskDb.load(taskId));
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",
"duration": "Dauer",
"easy_list": "Easy List",
"edit": "Bearbeiten",
"edit_object" : "{object} bearbeiten",
"editing": "Nutzer {0} bearbeiten",
@@ -190,6 +191,7 @@
"oidc_Login" : "Anmeldung mit OIDC",
"old_password": "altes Passwort",
"organization": "Organisation",
"other_tags": "andere Tags",
"page": "Seite",
"parent_task": "übergeordnete Aufgabe",
@@ -277,6 +279,7 @@
"task": "Aufgabe",
"task_list": "Aufgabenliste",
"tasks": "Aufgaben",
"tasks_for_tag": "Aufgaben mit Tag „{tag}“",
"tax_id": "Steuernummer",
"TAX-NUMBER": "Steuernummer",
"tax_rate": "Steuersatz",

View File

@@ -79,6 +79,7 @@
"drag_n_drop": "drag & drop",
"duration": "duration",
"easy_list": "Easy List",
"edit": "edit",
"edit_object" : "edit {object}",
"editing": "edit user {0}",
@@ -190,6 +191,7 @@
"oidc_Login" : "Login via OIDC",
"old_password": "old password",
"organization": "organization",
"other_tags": "other tags",
"page": "page",
"parent_task": "parent task",
@@ -277,6 +279,7 @@
"task": "task",
"task_list": "task list",
"tasks": "tasks",
"tasks_for_tag": "tasks with tag „{tag}“",
"tax_id": "tax ID",
"TAX-NUMBER": "tax ID",
"tax_rate": "tax rate",

View File

@@ -295,3 +295,16 @@ tr:hover .taglist .tag button {
.vcard span.inactive{
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;
}
.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) {
.grid2{
display: grid;
@@ -428,6 +447,14 @@ a.wikilink{
width: calc(100% - 10px);
min-height: 50px;
}
.easylist > fieldset > div {
font-size: 25px;
padding: 10px;
}
.easylist input{
font-size: 20px;
}
}
fieldset.vcard{
@@ -473,4 +500,4 @@ fieldset.vcard{
margin: 0 6px;
white-space: nowrap;
display: inline flow-root;
}
}

View File

@@ -285,3 +285,16 @@ tr:hover .taglist .tag button {
.vcard span.inactive{
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;
}
.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) {
.grid2{
display: grid;
@@ -506,6 +525,14 @@ a.wikilink{
width: calc(100% - 10px);
min-height: 50px;
}
.easylist > fieldset > div {
font-size: 25px;
padding: 10px;
}
.easylist input{
font-size: 20px;
}
}
fieldset.vcard{
@@ -551,4 +578,4 @@ fieldset.vcard{
margin: 0 6px;
white-space: nowrap;
display: inline flow-root;
}
}

View File

@@ -273,3 +273,13 @@ tr:hover .taglist .tag button {
.vcard span.inactive{
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;
}
.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) {
.grid2{
display: grid;
@@ -428,6 +447,14 @@ a.wikilink{
width: calc(100% - 10px);
min-height: 50px;
}
.easylist > fieldset > div {
font-size: 25px;
padding: 10px;
}
.easylist input{
font-size: 20px;
}
}
fieldset.vcard{
@@ -473,4 +500,4 @@ fieldset.vcard{
margin: 0 6px;
white-space: nowrap;
display: inline flow-root;
}
}