working on adding new properties to existing items
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -21,7 +21,7 @@ import de.srsoftware.umbrella.markdown.MarkdownApi;
|
||||
import de.srsoftware.umbrella.message.MessageSystem;
|
||||
import de.srsoftware.umbrella.notes.NoteModule;
|
||||
import de.srsoftware.umbrella.project.ProjectModule;
|
||||
import de.srsoftware.umbrella.stock.StockApi;
|
||||
import de.srsoftware.umbrella.stock.StockModule;
|
||||
import de.srsoftware.umbrella.tags.TagModule;
|
||||
import de.srsoftware.umbrella.task.TaskModule;
|
||||
import de.srsoftware.umbrella.time.TimeModule;
|
||||
@@ -71,7 +71,7 @@ public class Application {
|
||||
new CompanyLegacy(config).bindPath("/legacy/company").on(server);
|
||||
new ContactModule(config).bindPath("/api/contact").on(server);
|
||||
new DocumentApi(config).bindPath("/api/document").on(server);
|
||||
new StockApi(config).bindPath("/api/stock").on(server);
|
||||
new StockModule(config).bindPath("/api/stock").on(server);
|
||||
new UserLegacy(config).bindPath("/legacy/user").on(server);
|
||||
new NotesLegacy(config).bindPath("/legacy/notes").on(server);
|
||||
new MarkdownApi().bindPath("/api/markdown").on(server);
|
||||
|
||||
@@ -149,6 +149,7 @@ public class Constants {
|
||||
public static final String PROJECT = "project";
|
||||
public static final String PROJECT_ID = "project_id";
|
||||
public static final String PROPERTIES = "properties";
|
||||
public static final String PROPERTY = "property";
|
||||
|
||||
public static final String RECEIVERS = "receivers";
|
||||
public static final String REDIRECT = "redirect";
|
||||
|
||||
@@ -47,8 +47,9 @@ public class Item implements Mappable {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
var ownerMap = owner instanceof Company comp ? Map.of(COMPANY,comp.id()) : (owner instanceof UmbrellaUser u ? Map.of(USER,u.id()) : Map.of());
|
||||
return Map.of(
|
||||
OWNER, owner.toMap(),
|
||||
OWNER, ownerMap,
|
||||
ID, id,
|
||||
LOCATION, location.toMap(),
|
||||
CODE, code,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core.model;
|
||||
|
||||
import static de.srsoftware.tools.Optionals.nullable;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Property implements Mappable {
|
||||
@@ -24,18 +26,23 @@ public class Property implements Mappable {
|
||||
public static Property of(ResultSet rs) throws SQLException {
|
||||
var id = rs.getLong(ID);
|
||||
var name = rs.getString(NAME);
|
||||
var value = rs.getObject(VALUE);
|
||||
Object value = null;
|
||||
try {
|
||||
value = rs.getObject(VALUE);
|
||||
} catch (SQLException e){
|
||||
if (!e.getMessage().contains("no such column")) throw e;
|
||||
}
|
||||
var unit = rs.getString(UNIT);
|
||||
return new Property(id, name, value, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
return Map.of(
|
||||
ID, id,
|
||||
NAME, name,
|
||||
VALUE, value,
|
||||
UNIT, unit
|
||||
);
|
||||
var map = new HashMap<String,Object>();
|
||||
map.put(ID,id);
|
||||
if (name != null) map.put(NAME,name);
|
||||
if (value != null) map.put(VALUE,value);
|
||||
if (unit != null) map.put(UNIT,unit);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
async function loadItems(){
|
||||
if (!location) return null;
|
||||
const url = api(`stock/items_at/${location.id}`)
|
||||
const url = api(`stock/location/${location.id}`)
|
||||
const res = await fetch(url,{credentials:'include'});
|
||||
if (res.ok){
|
||||
yikes();
|
||||
@@ -28,7 +28,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function load(){
|
||||
async function loadProperties(){
|
||||
const url = api('stock/properties')
|
||||
const res = await fetch(url,{credentials:'include'});
|
||||
if (res.ok){
|
||||
var json = await res.json();
|
||||
var dict = {}
|
||||
for (var entry of json.sort((a,b) => b.id - a.id)) dict[entry.name+'.'+entry.unit] = entry;
|
||||
properties = Object.values(dict).sort((a,b) => a.name.localeCompare(b.name));
|
||||
yikes();
|
||||
} else error(res);
|
||||
}
|
||||
|
||||
async function loadUserLocations(){
|
||||
const url = api('stock/locations/of_user')
|
||||
const res = await fetch(url,{credentials:'include'});
|
||||
if (res.ok){
|
||||
@@ -37,6 +49,11 @@
|
||||
} else error(res);
|
||||
}
|
||||
|
||||
function load(){
|
||||
loadUserLocations();
|
||||
loadProperties();
|
||||
}
|
||||
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
@@ -63,20 +80,6 @@
|
||||
{/await}
|
||||
</div>
|
||||
<div class="properties">
|
||||
<ItemProps {item} />
|
||||
<ItemProps {item} {properties} />
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="locations">
|
||||
</td>
|
||||
<td class="items">
|
||||
|
||||
</td>
|
||||
<td class="properties">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1,10 +1,42 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
let { item } = $props();
|
||||
|
||||
let { item, properties } = $props();
|
||||
|
||||
let add_prop = $state({
|
||||
existing_prop_id : 0,
|
||||
new_prop : {
|
||||
name: null,
|
||||
unit: null
|
||||
}
|
||||
});
|
||||
|
||||
async function onclick(){
|
||||
const url = api('stock/property');
|
||||
const data = {
|
||||
item : {
|
||||
id : item.id,
|
||||
owner : item.owner
|
||||
},
|
||||
add_prop : add_prop
|
||||
}
|
||||
const res = await fetch(url,{
|
||||
credentials:'include',
|
||||
method:'POST',
|
||||
body:JSON.stringify(data)
|
||||
});
|
||||
if (res.ok){
|
||||
yikes();
|
||||
} else error(res);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if item}
|
||||
<h3>{item.name}</h3>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
{#each item.properties as prop}
|
||||
@@ -20,16 +52,27 @@
|
||||
{/each}
|
||||
<tr>
|
||||
<td>
|
||||
<select>
|
||||
<option>this is an existing property</option>
|
||||
<option>B</option>
|
||||
<option>C</option>
|
||||
<select bind:value={add_prop.existing_prop_id}>
|
||||
<option value={0}>{t('select_property')}</option>
|
||||
{#each properties as p}
|
||||
<option value={p.id}>
|
||||
{p.name}
|
||||
{#if p.unit}({p.unit}){/if}
|
||||
</option>
|
||||
{/each}
|
||||
</select><br/>
|
||||
<input type="text" placeholder="new prop"/>
|
||||
{#if !add_prop.existing_prop_id}
|
||||
<input type="text" placeholder="new prop" bind:value={add_prop.new_prop.name} />
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" />
|
||||
<button>{t('save')}</button>
|
||||
<input type="text" placeholder="value" bind:value={add_prop.value} />
|
||||
{#if !add_prop.existing_prop_id}
|
||||
<input type="text" placeholder="unit" bind:value={add_prop.new_prop.unit} />
|
||||
{:else}
|
||||
{properties.filter(p => p.id == add_prop.existing_prop_id)[0].unit}
|
||||
{/if}
|
||||
<button {onclick}>{t('save')}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -50,6 +50,20 @@ public class SqliteDb extends BaseDb implements StockDb {
|
||||
super(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addProperty(long ownerId, long itemId, long existingPropId, Object value) {
|
||||
try {
|
||||
insertInto(TABLE_ITEM_PROPERTIES,OWNER,ITEM_ID,PROPERTY_ID,VALUE).values(ownerId,itemId,existingPropId,value).execute(db);
|
||||
} catch (SQLException e) {
|
||||
throw databaseException("Failed to add new property to item {0}",itemId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addProperty(long ownerId, long itemId, String name, Object value, String unit) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private void createIntermediateItemsTable() throws SQLException { // create intermediate table
|
||||
var sql = "CREATE TABLE IF NOT EXISTS items_temp ({0} LONG NOT NULL, {1} LONG NOT NULL, {2} VARCHAR(255), {3} VARCHAR(255) NOT NULL, {4} LONG NOT NULL, PRIMARY KEY({0}, {1}))";
|
||||
sql = format(sql, OWNER, ID, CODE, NAME, LOCATION_ID);
|
||||
@@ -204,6 +218,19 @@ public class SqliteDb extends BaseDb implements StockDb {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Property> listProperties() {
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_PROPERTIES).exec(db);
|
||||
var list = new ArrayList<Property>();
|
||||
while (rs.next()) list.add(Property.of(rs));
|
||||
rs.close();
|
||||
return list;
|
||||
} catch (SQLException e){
|
||||
throw databaseException("Failed to load properties!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Location> listUserLocations(UmbrellaUser user) {
|
||||
try {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.stock;
|
||||
|
||||
import de.srsoftware.umbrella.core.model.Company;
|
||||
import de.srsoftware.umbrella.core.model.Item;
|
||||
import de.srsoftware.umbrella.core.model.Location;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import de.srsoftware.umbrella.core.model.*;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface StockDb {
|
||||
void addProperty(long ownerId, long itemId, long existingPropId, Object value);
|
||||
void addProperty(long ownerId, long itemId, String name, Object value, String unit);
|
||||
Collection<Location> listChildLocations(long parentId);
|
||||
Collection<Location> listCompanyLocations(Company company);
|
||||
Collection<Item> listItemsAt(long locationId);
|
||||
Collection<Property> listProperties();
|
||||
Collection<Location> listUserLocations(UmbrellaUser userId);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.stock;
|
||||
|
||||
import static de.srsoftware.tools.Optionals.nullIfEmpty;
|
||||
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
|
||||
import static de.srsoftware.umbrella.core.Constants.ID;
|
||||
import static de.srsoftware.umbrella.core.Constants.NAME;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
|
||||
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
|
||||
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;
|
||||
@@ -19,18 +20,17 @@ import de.srsoftware.umbrella.core.BaseHandler;
|
||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||
import de.srsoftware.umbrella.core.api.StockService;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.Item;
|
||||
import de.srsoftware.umbrella.core.model.Location;
|
||||
import de.srsoftware.umbrella.core.model.Token;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import de.srsoftware.umbrella.core.model.*;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class StockApi extends BaseHandler implements StockService {
|
||||
public class StockModule extends BaseHandler implements StockService {
|
||||
|
||||
private final StockDb stockDb;
|
||||
|
||||
public StockApi(Configuration config) throws UmbrellaException {
|
||||
public StockModule(Configuration config) throws UmbrellaException {
|
||||
super();
|
||||
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE));
|
||||
stockDb = new SqliteDb(connect(dbFile));
|
||||
@@ -46,15 +46,16 @@ public class StockApi extends BaseHandler implements StockService {
|
||||
if (user.isEmpty()) return unauthorized(ex);
|
||||
var head = path.pop();
|
||||
return switch (head) {
|
||||
case ITEMS_AT -> {
|
||||
case LOCATION -> {
|
||||
try {
|
||||
var id = Long.parseLong(path.pop());
|
||||
yield getItemsAt(user.get(),id,ex);
|
||||
yield getLocation(user.get(),id,ex);
|
||||
} catch (Exception e){
|
||||
yield super.doGet(path,ex);
|
||||
}
|
||||
}
|
||||
case LOCATIONS -> getLocations(path,user.get(),ex);
|
||||
case PROPERTIES -> getProperties(ex);
|
||||
case null, default -> super.doGet(path,ex);
|
||||
};
|
||||
} catch (UmbrellaException e){
|
||||
@@ -62,7 +63,30 @@ public class StockApi extends BaseHandler implements StockService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean getItemsAt(UmbrellaUser user, long locationId, HttpExchange ex) throws IOException {
|
||||
@Override
|
||||
public boolean doPost(Path path, HttpExchange ex) throws IOException {
|
||||
addCors(ex);
|
||||
try {
|
||||
Optional<Token> token = SessionToken.from(ex).map(Token::of);
|
||||
var user = userService().loadUser(token);
|
||||
if (user.isEmpty()) return unauthorized(ex);
|
||||
var head = path.pop();
|
||||
return switch (head) {
|
||||
case PROPERTY -> postProperty(user.get(),ex);
|
||||
case null, default -> super.doPost(path,ex);
|
||||
};
|
||||
} catch (UmbrellaException e){
|
||||
return send(ex,e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean getChildLocations(UmbrellaUser user, long parentId, HttpExchange ex) throws IOException {
|
||||
LOG.log(WARNING,"No security check implemented for {0}.getChildLocations(user, parentId, ex)!",getClass().getSimpleName()); // TODO check, that user is allowed to request that location
|
||||
return sendContent(ex, stockDb.listChildLocations(parentId).stream().sorted(comparing(l -> l.name().toLowerCase())).map(Location::toMap));
|
||||
|
||||
}
|
||||
|
||||
private boolean getLocation(UmbrellaUser user, long locationId, HttpExchange ex) throws IOException {
|
||||
return sendContent(ex, stockDb.listItemsAt(locationId).stream().map(Item::toMap).toList());
|
||||
}
|
||||
|
||||
@@ -83,10 +107,8 @@ public class StockApi extends BaseHandler implements StockService {
|
||||
};
|
||||
}
|
||||
|
||||
private boolean getChildLocations(UmbrellaUser user, long parentId, HttpExchange ex) throws IOException {
|
||||
LOG.log(WARNING,"No security check implemented for {0}.getChildLocations(user, parentId, ex)!",getClass().getSimpleName()); // TODO check, that user is allowed to request that location
|
||||
return sendContent(ex, stockDb.listChildLocations(parentId).stream().sorted(comparing(l -> l.name().toLowerCase())).map(Location::toMap));
|
||||
|
||||
private boolean getProperties(HttpExchange ex) throws IOException {
|
||||
return sendContent(ex,stockDb.listProperties().stream().map(Property::toMap).toList());
|
||||
}
|
||||
|
||||
private boolean getUserLocations(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
@@ -109,6 +131,35 @@ public class StockApi extends BaseHandler implements StockService {
|
||||
return sendContent(ex, result);
|
||||
}
|
||||
|
||||
private boolean postProperty(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
var json = json(ex);
|
||||
if (!(json.get(ID) instanceof Number id)) throw missingFieldException(ID);
|
||||
if (!(json.get(FIELD_ITEM) instanceof JSONObject itemData)) throw missingFieldException(FIELD_ITEM);
|
||||
if (!(itemData.get(OWNER) instanceof JSONObject owner)) throw missingFieldException(OWNER);
|
||||
if (!(json.get("add_prop") instanceof JSONObject propData)) throw missingFieldException("add_prop");
|
||||
if (!propData.has(VALUE)) throw missingFieldException(VALUE);
|
||||
var value = propData.get(VALUE);
|
||||
if (value == null) throw missingFieldException(VALUE);
|
||||
var keys = owner.keySet();
|
||||
if (keys.size() != 1) throw unprocessable("{0} expected to have only one child!",OWNER);
|
||||
var key = new ArrayList<>(keys).getFirst();
|
||||
var ownerId = switch (key) {
|
||||
case COMPANY -> -json.getLong(key);
|
||||
case USER -> json.getLong(key);
|
||||
case null, default -> throw invalidFieldException(format("Single child of {0}", OWNER), format("either {0} or {1}", COMPANY, USER));
|
||||
};
|
||||
|
||||
if (propData.get("existing_prop_id") instanceof Number existingPropId && existingPropId.longValue() != 0L){
|
||||
stockDb.addProperty(ownerId,id.longValue(),existingPropId.longValue(),value);
|
||||
} else {
|
||||
if (!(propData.get("new_prop") instanceof JSONObject newProp)) throw unprocessable("data must contain either add_prop.existing_prop_id or add_prop.new_prop!");
|
||||
if (!(newProp.get(NAME) instanceof String name) || name.isBlank()) throw unprocessable("data.add_prop.new_prop does not contain name!");
|
||||
var unit = newProp.get(UNIT) instanceof String u ? nullIfEmpty(u) : null;
|
||||
stockDb.addProperty(ownerId,id.longValue(),name,value,unit);
|
||||
}
|
||||
return sendEmptyResponse(500,ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Object> redefineMe(long company_id) {
|
||||
return List.of();
|
||||
Reference in New Issue
Block a user