diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java index a5d49b7..21b27b1 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java @@ -133,7 +133,9 @@ public class Constants { public static final String OFFSET = "offset"; 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 PASS = "pass"; public static final String PASSWORD = "password"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Location.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Location.java index 47452a1..db04e22 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Location.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Location.java @@ -1,13 +1,16 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import java.sql.ResultSet; -import java.sql.SQLException; +import de.srsoftware.tools.Mappable; 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 boolean ownerIsCompany = false; private long id; @@ -37,8 +40,12 @@ public class Location { return name; } - public static Location of(ResultSet rs){ - return null; + public static Location of(ResultSet rs) throws SQLException { + 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(){ return parentLocationId; } + + @Override + public Map toMap() { + return Map.of( + ownerIsCompany ? COMPANY : USER,owner, + ID,id, + NAME,name, + DESCRIPTION,description); + } } diff --git a/frontend/src/routes/stock/Index.svelte b/frontend/src/routes/stock/Index.svelte index e7db7a5..86214a8 100644 --- a/frontend/src/routes/stock/Index.svelte +++ b/frontend/src/routes/stock/Index.svelte @@ -1,47 +1,26 @@

{t('Stock')}

@@ -49,11 +28,14 @@ - {#each companies as company} -

{company.name}

- - + {#if top_level} + {#each top_level as realm,idx} +

{realm.name}

+ {#if realm.locations} + + {/if} {/each} + {/if} diff --git a/frontend/src/routes/stock/Locations.svelte b/frontend/src/routes/stock/Locations.svelte index a4d3ef1..c58a815 100644 --- a/frontend/src/routes/stock/Locations.svelte +++ b/frontend/src/routes/stock/Locations.svelte @@ -2,6 +2,8 @@ import { t } from '../../translations.svelte'; let { locations } = $props(); + + console.log(locations);
    @@ -10,10 +12,12 @@ {t('add_object',{object:'location'})} - {#each Object.entries(locations) as [k,v]} + {#each locations as location}
  • - {k} - + {location.name} + {#if location.locations} + + {/if}
  • {/each}
\ No newline at end of file diff --git a/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java index 94972e3..dcb7f1c 100644 --- a/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java +++ b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java @@ -14,7 +14,7 @@ import static de.srsoftware.umbrella.core.model.Status.PREDEFINED; import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE; import static java.lang.Boolean.TRUE; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; - +import static de.srsoftware.umbrella.core.model.Permission.OWNER; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.configuration.Configuration; import de.srsoftware.tools.Path; diff --git a/stock/src/main/java/de/srsoftware/umbrella/stock/Constants.java b/stock/src/main/java/de/srsoftware/umbrella/stock/Constants.java index 5bd6407..873b436 100644 --- a/stock/src/main/java/de/srsoftware/umbrella/stock/Constants.java +++ b/stock/src/main/java/de/srsoftware/umbrella/stock/Constants.java @@ -2,16 +2,16 @@ package de.srsoftware.umbrella.stock; public class Constants { - public static final String PARENT_LOCATION_ID = "parent_location_id"; private Constants(){} public static final String CONFIG_DATABASE = "umbrella.modules.stock.database"; 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 TABLE_ITEMS = "items"; public static final String TABLE_ITEM_PROPERTIES = "item_props"; public static final String TABLE_LOCATIONS = "locations"; public static final String TABLE_PROPERTIES = "properties"; -} +} \ No newline at end of file diff --git a/stock/src/main/java/de/srsoftware/umbrella/stock/SqliteDb.java b/stock/src/main/java/de/srsoftware/umbrella/stock/SqliteDb.java index 941c04e..ac67ff6 100644 --- a/stock/src/main/java/de/srsoftware/umbrella/stock/SqliteDb.java +++ b/stock/src/main/java/de/srsoftware/umbrella/stock/SqliteDb.java @@ -2,6 +2,8 @@ package de.srsoftware.umbrella.stock; 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.SelectQuery.ALL; 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.text.MessageFormat.format; -import de.srsoftware.tools.Tuple; import de.srsoftware.umbrella.core.BaseDb; 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.Location; +import de.srsoftware.umbrella.core.model.UmbrellaUser; + import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -61,7 +65,6 @@ public class SqliteDb extends BaseDb implements StockDb { dropTokenTable(); case 2: transformTables(); - replaceLocationsTable(); } return setCurrentVersion(3); } @@ -78,6 +81,12 @@ public class SqliteDb extends BaseDb implements StockDb { 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() { try { 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(); } + @Override + public Collection 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(); + 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 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(); + 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(){ try { db.setAutoCommit(false); createIntermediateLocationTable(); createIntermediateItemsTable(); + createIntermediatePropsTable(); var oldLocationIdsToNew = transformLocations(); transformItems(oldLocationIdsToNew); - - - + transformProperties(); + replaceLocationsTable(); + replaceItemsTable(); + replaceItemPropsTable(); db.setAutoCommit(true); } catch (Exception e) { try { @@ -157,15 +194,19 @@ public class SqliteDb extends BaseDb implements StockDb { } } - private void replaceLocationsTable() { - try { - db.setAutoCommit(false); - 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); - } catch (SQLException e){ - throw databaseException("Failed to replace locations table!"); - } + private void replaceItemsTable() throws SQLException { + db.prepareStatement(format("DROP TABLE {0}",TABLE_ITEMS)).execute(); + db.prepareStatement(format("ALTER TABLE {0} RENAME TO {1}","items_temp",TABLE_ITEMS)).execute(); + } + + private void replaceItemPropsTable() throws SQLException { + db.prepareStatement(format("DROP TABLE {0}",TABLE_ITEM_PROPERTIES)).execute(); + 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 oldLocationIdsToNew) throws SQLException { @@ -224,4 +265,28 @@ public class SqliteDb extends BaseDb implements StockDb { 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(); + } } diff --git a/stock/src/main/java/de/srsoftware/umbrella/stock/StockApi.java b/stock/src/main/java/de/srsoftware/umbrella/stock/StockApi.java index 853d6cd..c39e110 100644 --- a/stock/src/main/java/de/srsoftware/umbrella/stock/StockApi.java +++ b/stock/src/main/java/de/srsoftware/umbrella/stock/StockApi.java @@ -2,16 +2,27 @@ package de.srsoftware.umbrella.stock; 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.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.tools.Path; +import de.srsoftware.tools.SessionToken; 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 java.util.Collection; -import java.util.List; +import de.srsoftware.umbrella.core.model.Location; +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 { @@ -24,6 +35,51 @@ public class StockApi extends BaseHandler implements StockService { ModuleRegistry.add(this); } + @Override + public boolean doGet(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + Optional 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(); + 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 public Collection redefineMe(long company_id) { return List.of(); diff --git a/stock/src/main/java/de/srsoftware/umbrella/stock/StockDb.java b/stock/src/main/java/de/srsoftware/umbrella/stock/StockDb.java index 9169d7d..592c115 100644 --- a/stock/src/main/java/de/srsoftware/umbrella/stock/StockDb.java +++ b/stock/src/main/java/de/srsoftware/umbrella/stock/StockDb.java @@ -2,11 +2,18 @@ package de.srsoftware.umbrella.stock; 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.Location; +import de.srsoftware.umbrella.core.model.UmbrellaUser; + import java.util.Collection; public interface StockDb { Collection listItems(long companyId) throws UmbrellaException; Collection listLocations(long companyId); + + Collection listUserLocations(UmbrellaUser userId); + + Collection listCompanyLocations(Company company); } diff --git a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java index 8137e91..37f83b9 100644 --- a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java +++ b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java @@ -2,6 +2,7 @@ package de.srsoftware.umbrella.task; 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.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.Constants.*;