working on location tree

This commit is contained in:
2025-10-13 16:14:31 +02:00
parent b361731cab
commit 2cd022451a
10 changed files with 205 additions and 72 deletions

View File

@@ -133,7 +133,9 @@ public class Constants {
public static final String OFFSET = "offset"; public static final String OFFSET = "offset";
public static final String OPTIONAL = "optional"; public static final String OPTIONAL = "optional";
public static final String OWNER = "owner";
public static final String PARENT_LOCATION_ID = "parent_location_id";
public static final String PARENT_TASK_ID = "parent_task_id"; public static final String PARENT_TASK_ID = "parent_task_id";
public static final String PASS = "pass"; public static final String PASS = "pass";
public static final String PASSWORD = "password"; public static final String PASSWORD = "password";

View File

@@ -1,13 +1,16 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model; package de.srsoftware.umbrella.core.model;
import java.sql.ResultSet; import de.srsoftware.tools.Mappable;
import java.sql.SQLException;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
public class Location { import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
public class Location implements Mappable {
private long owner; private long owner;
private boolean ownerIsCompany = false; private boolean ownerIsCompany = false;
private long id; private long id;
@@ -37,8 +40,12 @@ public class Location {
return name; return name;
} }
public static Location of(ResultSet rs){ public static Location of(ResultSet rs) throws SQLException {
return null; var owner = rs.getLong(OWNER);
var id = rs.getLong(ID);
var isCompany = owner < 0;
if (isCompany) owner = -owner;
return new Location(owner,isCompany,id, rs.getLong(PARENT_LOCATION_ID), rs.getString(NAME),rs.getString(DESCRIPTION));
} }
@@ -50,4 +57,13 @@ public class Location {
public Long parent(){ public Long parent(){
return parentLocationId; return parentLocationId;
} }
@Override
public Map<String, Object> toMap() {
return Map.of(
ownerIsCompany ? COMPANY : USER,owner,
ID,id,
NAME,name,
DESCRIPTION,description);
}
} }

View File

@@ -1,47 +1,26 @@
<script> <script>
import { onMount } from 'svelte';
import { api } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte';
import Locations from './Locations.svelte'; import Locations from './Locations.svelte';
import ItemList from './ItemList.svelte'; import ItemList from './ItemList.svelte';
import ItemProps from './ItemProps.svelte'; import ItemProps from './ItemProps.svelte';
import { t } from '../../translations.svelte';
let companies = [
{
id: 1,
name: 'SRSoftware',
locations:{
Langenorla:{
Hauptgebäude:{
Obergeschoss:{
},
Untergeschoss:{
Einliegerwohnung:{
Bad:{
},
Küche:{
},
Wohnzimmer:{
}
},
Flur:{
},
Küche:{
},
Studio:{
}
},
Werkstatt:{
}
},
Nebengebäude:{
},
Grundstück:{
}
}
}
},
{ id: 2, name: 'Alrauna', locations: {} } let top_level = $state(null);
]
async function load(){
const url = api('stock/locations/of_user')
const res = await fetch(url,{credentials:'include'});
if (res.ok){
top_level = await res.json();
yikes();
} else error(res);
}
onMount(load);
</script> </script>
<h2>{t('Stock')}</h2> <h2>{t('Stock')}</h2>
@@ -49,11 +28,14 @@
<tbody> <tbody>
<tr> <tr>
<td class="locations"> <td class="locations">
{#each companies as company} {#if top_level}
<h3>{company.name}</h3> {#each top_level as realm,idx}
<h3>{realm.name}</h3>
<Locations locations={company.locations} /> {#if realm.locations}
<Locations locations={realm.locations} />
{/if}
{/each} {/each}
{/if}
</td> </td>
<td class="items"> <td class="items">
<ItemList /> <ItemList />

View File

@@ -2,6 +2,8 @@
import { t } from '../../translations.svelte'; import { t } from '../../translations.svelte';
let { locations } = $props(); let { locations } = $props();
console.log(locations);
</script> </script>
<ul> <ul>
@@ -10,10 +12,12 @@
<span class="symbol"></span> {t('add_object',{object:'location'})} <span class="symbol"></span> {t('add_object',{object:'location'})}
</a> </a>
</li> </li>
{#each Object.entries(locations) as [k,v]} {#each locations as location}
<li> <li>
{k} {location.name}
<svelte:self locations={v} /> {#if location.locations}
<svelte:self locations={location.locations} />
{/if}
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@@ -14,7 +14,7 @@ import static de.srsoftware.umbrella.core.model.Status.PREDEFINED;
import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE; import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static de.srsoftware.umbrella.core.model.Permission.OWNER;
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.Path; import de.srsoftware.tools.Path;

View File

@@ -2,16 +2,16 @@
package de.srsoftware.umbrella.stock; package de.srsoftware.umbrella.stock;
public class Constants { public class Constants {
public static final String PARENT_LOCATION_ID = "parent_location_id";
private Constants(){} private Constants(){}
public static final String CONFIG_DATABASE = "umbrella.modules.stock.database"; public static final String CONFIG_DATABASE = "umbrella.modules.stock.database";
public static final String ITEM_ID = "item_id"; public static final String ITEM_ID = "item_id";
public static final String OWNER = "owner"; public static final String LOCATIONS = "locations";
public static final String OF_USER = "of_user";
public static final String PROPERTY_ID = "prop_id"; public static final String PROPERTY_ID = "prop_id";
public static final String TABLE_ITEMS = "items"; public static final String TABLE_ITEMS = "items";
public static final String TABLE_ITEM_PROPERTIES = "item_props"; public static final String TABLE_ITEM_PROPERTIES = "item_props";
public static final String TABLE_LOCATIONS = "locations"; public static final String TABLE_LOCATIONS = "locations";
public static final String TABLE_PROPERTIES = "properties"; public static final String TABLE_PROPERTIES = "properties";
} }

View File

@@ -2,6 +2,8 @@
package de.srsoftware.umbrella.stock; package de.srsoftware.umbrella.stock;
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.isNull;
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.*;
@@ -11,11 +13,13 @@ import static java.lang.System.Logger.Level.ERROR;
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.text.MessageFormat.format;
import de.srsoftware.tools.Tuple;
import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Company;
import de.srsoftware.umbrella.core.model.Item; import de.srsoftware.umbrella.core.model.Item;
import de.srsoftware.umbrella.core.model.Location; import de.srsoftware.umbrella.core.model.Location;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.sql.Connection; import java.sql.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
@@ -61,7 +65,6 @@ public class SqliteDb extends BaseDb implements StockDb {
dropTokenTable(); dropTokenTable();
case 2: case 2:
transformTables(); transformTables();
replaceLocationsTable();
} }
return setCurrentVersion(3); return setCurrentVersion(3);
} }
@@ -78,6 +81,12 @@ public class SqliteDb extends BaseDb implements StockDb {
db.prepareStatement(sql).execute(); db.prepareStatement(sql).execute();
} }
private void createIntermediatePropsTable() throws SQLException { // create intermediate table
var sql = "CREATE TABLE IF NOT EXISTS item_props_temp ( {0} LONG NOT NULL, {1} LONG NOT NULL, {2} LONG NOT NULL, {3} VARCHAR(255) NOT NULL, PRIMARY KEY({0}, {1}, {2}))";
sql = format(sql, OWNER, ITEM_ID, PROPERTY_ID, VALUE);
db.prepareStatement(sql).execute();
}
private void createItemsTable() { private void createItemsTable() {
try { try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL, {3} TEXT, {4} VARCHAR(255))"; var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL, {3} TEXT, {4} VARCHAR(255))";
@@ -136,16 +145,44 @@ public class SqliteDb extends BaseDb implements StockDb {
return List.of(); return List.of();
} }
@Override
public Collection<Location> listCompanyLocations(Company company) {
try {
var rs = select(ALL).from(TABLE_LOCATIONS).where(OWNER,equal(-company.id())).where(PARENT_LOCATION_ID,isNull()).exec(db);
var list = new ArrayList<Location>();
while (rs.next()) list.add(Location.of(rs));
rs.close();
return list;
} catch (SQLException e){
throw databaseException("Failed to load locations for user {0}",company.name());
}
}
@Override
public Collection<Location> listUserLocations(UmbrellaUser user) {
try {
var rs = select(ALL).from(TABLE_LOCATIONS).where(OWNER,equal(user.id())).where(PARENT_LOCATION_ID,isNull()).exec(db);
var list = new ArrayList<Location>();
while (rs.next()) list.add(Location.of(rs));
rs.close();
return list;
} catch (SQLException e){
throw databaseException("Failed to load locations for user {0}",user.name());
}
}
private void transformTables(){ private void transformTables(){
try { try {
db.setAutoCommit(false); db.setAutoCommit(false);
createIntermediateLocationTable(); createIntermediateLocationTable();
createIntermediateItemsTable(); createIntermediateItemsTable();
createIntermediatePropsTable();
var oldLocationIdsToNew = transformLocations(); var oldLocationIdsToNew = transformLocations();
transformItems(oldLocationIdsToNew); transformItems(oldLocationIdsToNew);
transformProperties();
replaceLocationsTable();
replaceItemsTable();
replaceItemPropsTable();
db.setAutoCommit(true); db.setAutoCommit(true);
} catch (Exception e) { } catch (Exception e) {
try { try {
@@ -157,15 +194,19 @@ public class SqliteDb extends BaseDb implements StockDb {
} }
} }
private void replaceLocationsTable() { private void replaceItemsTable() throws SQLException {
try { db.prepareStatement(format("DROP TABLE {0}",TABLE_ITEMS)).execute();
db.setAutoCommit(false); db.prepareStatement(format("ALTER TABLE {0} RENAME TO {1}","items_temp",TABLE_ITEMS)).execute();
db.prepareStatement(format("DROP TABLE {0}",TABLE_LOCATIONS)).execute(); }
db.prepareStatement(format("ALTER TABLE {0} RENAME TO {1}","locations_temp",TABLE_LOCATIONS)).execute();
db.setAutoCommit(true); private void replaceItemPropsTable() throws SQLException {
} catch (SQLException e){ db.prepareStatement(format("DROP TABLE {0}",TABLE_ITEM_PROPERTIES)).execute();
throw databaseException("Failed to replace locations table!"); db.prepareStatement(format("ALTER TABLE {0} RENAME TO {1}","item_props_temp",TABLE_ITEM_PROPERTIES)).execute();
} }
private void replaceLocationsTable() throws SQLException {
db.prepareStatement(format("DROP TABLE {0}",TABLE_LOCATIONS)).execute();
db.prepareStatement(format("ALTER TABLE {0} RENAME TO {1}","locations_temp",TABLE_LOCATIONS)).execute();
} }
private void transformItems(Map<String, Long> oldLocationIdsToNew) throws SQLException { private void transformItems(Map<String, Long> oldLocationIdsToNew) throws SQLException {
@@ -224,4 +265,28 @@ public class SqliteDb extends BaseDb implements StockDb {
return oldToNew; return oldToNew;
} }
private void transformProperties() throws SQLException {
var rs = select(ALL).from(TABLE_ITEM_PROPERTIES).exec(db);
var insert = insertInto("item_props_temp",OWNER, ITEM_ID, PROPERTY_ID, VALUE);
while (rs.next()){
var oldItemId = rs.getString(ITEM_ID);
var parts = oldItemId.split(":");
var owner = 0L;
var itemId = 0L;
try {
owner = Long.parseLong(parts[1]);
itemId = Long.parseLong(parts[2]);
} catch (NumberFormatException e){
throw databaseException("Expected item id to be of format ss:dd:dd, but encountered \"{0}\"",oldItemId);
}
var ownerIsCompany = switch (parts[0]){
case "company" -> true;
case "user" -> false;
case null, default -> throw databaseException("Expected item id to start with 'company:' or 'user:', encountered \"{0}\"",oldItemId);
};
insert.values(ownerIsCompany?-owner:owner, itemId, rs.getLong(PROPERTY_ID), rs.getString(VALUE));
}
rs.close();
insert.execute(db).close();
}
} }

View File

@@ -2,16 +2,27 @@
package de.srsoftware.umbrella.stock; package de.srsoftware.umbrella.stock;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect; 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.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.missingFieldException;
import static de.srsoftware.umbrella.stock.Constants.CONFIG_DATABASE; import static de.srsoftware.umbrella.stock.Constants.*;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration; import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry; import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.StockService; import de.srsoftware.umbrella.core.api.StockService;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import java.util.Collection; import de.srsoftware.umbrella.core.model.Location;
import java.util.List; import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.io.IOException;
import java.util.*;
public class StockApi extends BaseHandler implements StockService { public class StockApi extends BaseHandler implements StockService {
@@ -24,6 +35,51 @@ public class StockApi extends BaseHandler implements StockService {
ModuleRegistry.add(this); ModuleRegistry.add(this);
} }
@Override
public boolean doGet(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 LOCATIONS -> getLocations(path,user.get(),ex);
case null, default -> doGet(path,ex);
};
} catch (UmbrellaException e){
return send(ex,e);
}
}
private boolean getLocations(Path path, UmbrellaUser user, HttpExchange ex) throws IOException {
var head = path.pop();
return switch (head){
case OF_USER -> getUserLocations(user,ex);
case null, default -> super.doGet(path,ex);
};
}
private boolean getUserLocations(UmbrellaUser user, HttpExchange ex) throws IOException {
var result = new ArrayList<Object>();
var userLocations = stockDb.listUserLocations(user);
result.add(Map.of(
ID, user.id(),
NAME,user.name(),
LOCATIONS,userLocations.stream().map(Location::toMap).toList()));
var companies = companyService().listCompaniesOf(user);
companies.values().stream().sorted(Comparator.comparing(a -> a.name().toLowerCase())).forEach(company -> {
var locations = stockDb.listCompanyLocations(company);
result.add(Map.of(
ID, company.id(),
NAME,company.name(),
LOCATIONS,locations.stream().sorted(Comparator.comparing(a -> a.name().toLowerCase())).map(Location::toMap).toList()));
});
return sendContent(ex, result);
}
@Override @Override
public Collection<Object> redefineMe(long company_id) { public Collection<Object> redefineMe(long company_id) {
return List.of(); return List.of();

View File

@@ -2,11 +2,18 @@
package de.srsoftware.umbrella.stock; package de.srsoftware.umbrella.stock;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Company;
import de.srsoftware.umbrella.core.model.Item; import de.srsoftware.umbrella.core.model.Item;
import de.srsoftware.umbrella.core.model.Location; import de.srsoftware.umbrella.core.model.Location;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Collection; import java.util.Collection;
public interface StockDb { public interface StockDb {
Collection<Item> listItems(long companyId) throws UmbrellaException; Collection<Item> listItems(long companyId) throws UmbrellaException;
Collection<Location> listLocations(long companyId); Collection<Location> listLocations(long companyId);
Collection<Location> listUserLocations(UmbrellaUser userId);
Collection<Location> listCompanyLocations(Company company);
} }

View File

@@ -2,6 +2,7 @@
package de.srsoftware.umbrella.task; package de.srsoftware.umbrella.task;
import static de.srsoftware.tools.Optionals.is0; import static de.srsoftware.tools.Optionals.is0;
import static de.srsoftware.umbrella.core.model.Permission.OWNER;
import static de.srsoftware.tools.Optionals.isSet; 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.*;