From bac04ac047299323c830f39b9b391c84cc40011b Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sun, 13 Jul 2025 23:45:18 +0200 Subject: [PATCH] implemented function to return estimated times of company. Therefore task and project module had to be created and partially implemented --- backend/build.gradle.kts | 5 +- .../umbrella/backend/Application.java | 8 +- build.gradle.kts | 4 + company/build.gradle.kts | 3 - contact/build.gradle.kts | 3 - core/build.gradle.kts | 2 - .../srsoftware/umbrella/core/Constants.java | 3 +- .../umbrella/core/api/ProjectService.java | 12 +++ .../umbrella/core/model/Project.java | 56 +++++++++++ documents/build.gradle.kts | 4 - .../umbrella/documents/model/Document.java | 3 +- frontend/src/Components/Item.svelte | 11 +++ .../src/routes/document/EstimateList.svelte | 34 ++++++- frontend/src/routes/document/ItemList.svelte | 18 +++- .../routes/document/PositionSelector.svelte | 26 ++++- frontend/src/routes/document/View.svelte | 8 +- items/build.gradle.kts | 3 - .../srsoftware/umbrella/items/Constants.java | 7 +- .../de/srsoftware/umbrella/items/Item.java | 36 ++++++- .../de/srsoftware/umbrella/items/ItemApi.java | 41 +++++--- .../de/srsoftware/umbrella/items/ItemDb.java | 4 +- .../srsoftware/umbrella/items/SqliteDb.java | 25 +++++ legacy/build.gradle.kts | 4 - messages/build.gradle.kts | 4 - project/build.gradle.kts | 5 + .../umbrella/project/Constants.java | 10 ++ .../umbrella/project/ProjectDb.java | 10 ++ .../umbrella/project/ProjectModule.java | 79 ++++++++++++++++ .../srsoftware/umbrella/project/SqliteDb.java | 41 ++++++++ settings.gradle.kts | 4 +- task/build.gradle.kts | 6 ++ .../srsoftware/umbrella/task/Constants.java | 19 ++++ .../de/srsoftware/umbrella/task/SqliteDb.java | 38 ++++++++ .../de/srsoftware/umbrella/task/Task.java | 54 +++++++++++ .../de/srsoftware/umbrella/task/TaskDb.java | 8 ++ .../srsoftware/umbrella/task/TaskModule.java | 94 +++++++++++++++++++ translations/src/main/resources/de.json | 6 ++ user/build.gradle.kts | 4 - web/build.gradle.kts | 1 - 39 files changed, 642 insertions(+), 61 deletions(-) create mode 100644 core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java create mode 100644 core/src/main/java/de/srsoftware/umbrella/core/model/Project.java create mode 100644 frontend/src/Components/Item.svelte create mode 100644 project/build.gradle.kts create mode 100644 project/src/main/java/de/srsoftware/umbrella/project/Constants.java create mode 100644 project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java create mode 100644 project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java create mode 100644 project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java create mode 100644 task/build.gradle.kts create mode 100644 task/src/main/java/de/srsoftware/umbrella/task/Constants.java create mode 100644 task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java create mode 100644 task/src/main/java/de/srsoftware/umbrella/task/Task.java create mode 100644 task/src/main/java/de/srsoftware/umbrella/task/TaskDb.java create mode 100644 task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 849628f..1f2bcc0 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -19,14 +19,13 @@ dependencies{ implementation(project(":legacy")) implementation(project(":markdown")) implementation(project(":messages")) + implementation(project(":task")) + implementation(project(":project")) implementation(project(":translations")) implementation(project(":user")) implementation(project(":web")) - implementation("de.srsoftware:configuration.api:1.0.2") implementation("de.srsoftware:configuration.json:1.0.3") - implementation("de.srsoftware:tools.optionals:1.0.0") implementation("de.srsoftware:tools.slf4j2syslog:1.0.1") // this provides a slf4j implementation that forwards to System.Logger - implementation("de.srsoftware:tools.util:2.0.3") } tasks.jar { diff --git a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java index e154bbf..525d7a5 100644 --- a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java +++ b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java @@ -17,6 +17,8 @@ import de.srsoftware.umbrella.legacy.LegacyApi; import de.srsoftware.umbrella.markdown.MarkdownApi; import de.srsoftware.umbrella.message.MessageApi; import de.srsoftware.umbrella.message.MessageSystem; +import de.srsoftware.umbrella.project.ProjectModule; +import de.srsoftware.umbrella.task.TaskModule; import de.srsoftware.umbrella.translations.Translations; import de.srsoftware.umbrella.user.UserModule; import de.srsoftware.umbrella.web.WebHandler; @@ -58,16 +60,20 @@ public class Application { var userModule = new UserModule(config,messageSystem); var companyModule = new CompanyModule(config, userModule); var documentApi = new DocumentApi(companyModule, config); - var itemApi = new ItemApi(config,userModule); + var itemApi = new ItemApi(config,companyModule); var legacyApi = new LegacyApi(userModule.userDb(),config); var markdownApi = new MarkdownApi(userModule); var messageApi = new MessageApi(messageSystem); + var projectModule = new ProjectModule(config,companyModule); + var taskModule = new TaskModule(config,projectModule); var webHandler = new WebHandler(); documentApi .bindPath("/api/document") .on(server); itemApi .bindPath("/api/items") .on(server); markdownApi .bindPath("/api/markdown") .on(server); messageApi .bindPath("/api/messages") .on(server); + projectModule .bindPath("/api/project") .on(server); + taskModule .bindPath("/api/task") .on(server); translationModule.bindPath("/api/translations").on(server); userModule .bindPath("/api/user") .on(server); legacyApi .bindPath("/legacy") .on(server); diff --git a/build.gradle.kts b/build.gradle.kts index 58df713..2affcbd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,8 +40,12 @@ subprojects { dependencies { 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:1.3.2") implementation("de.srsoftware:tools.http:6.0.4") implementation("de.srsoftware:tools.logging:1.3.2") + implementation("de.srsoftware:tools.optionals:1.0.0") + implementation("de.srsoftware:tools.util:2.0.3") implementation("org.json:json:20240303") } diff --git a/company/build.gradle.kts b/company/build.gradle.kts index 7b676e3..3c67e9c 100644 --- a/company/build.gradle.kts +++ b/company/build.gradle.kts @@ -2,8 +2,5 @@ description = "Umbrella : Companies" dependencies{ implementation(project(":core")) - implementation("de.srsoftware:configuration.api:1.0.2") - implementation("de.srsoftware:tools.jdbc:1.3.2") - implementation("de.srsoftware:tools.util:2.0.3") } diff --git a/contact/build.gradle.kts b/contact/build.gradle.kts index e9883f5..b483152 100644 --- a/contact/build.gradle.kts +++ b/contact/build.gradle.kts @@ -2,7 +2,4 @@ description = "Umbrella : Documents" dependencies{ implementation(project(":core")) - implementation("de.srsoftware:configuration.api:1.0.2") - implementation("de.srsoftware:tools.jdbc:1.3.2") - implementation("de.srsoftware:tools.util:2.0.3") } \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index dd3160e..f8e0cea 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -11,8 +11,6 @@ repositories { dependencies { implementation("de.srsoftware:tools.mime:1.1.2") - implementation("de.srsoftware:tools.optionals:1.0.0") - implementation("de.srsoftware:tools.util:2.0.3") implementation("org.xerial:sqlite-jdbc:3.49.0.0") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") 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 d8c8edd..b69a44a 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java @@ -48,12 +48,13 @@ public class Constants { public static final String RENDERED = "rendered"; public static final String SENDER = "sender"; public static final String SETTINGS = "settings"; + public static final String SHOW_CLOSED = "show_closed"; public static final String SOURCE = "source"; public static final String STATE = "state"; + public static final String STATUS = "status"; public static final String STATUS_CODE = "code"; public static final String STRING = "string"; public static final String SUBJECT = "subject"; - public static final String TABLE_SETTINGS = "settings"; public static final String TEMPLATE = "template"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java new file mode 100644 index 0000000..5ea0734 --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java @@ -0,0 +1,12 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.api; + +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Project; +import java.util.Collection; + +public interface ProjectService { + public Collection listProjects(long companyId,boolean includeClosed) throws UmbrellaException; + + CompanyService companyService(); +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Project.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Project.java new file mode 100644 index 0000000..c0d4020 --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Project.java @@ -0,0 +1,56 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.model; + +import static de.srsoftware.umbrella.core.Constants.*; + +import de.srsoftware.tools.Mappable; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +public record Project(long id, String name, String description, Status status, long companyId, boolean showClosed) implements Mappable { + public enum Status{ + Open(10), + Started(20), + Pending(40), + Complete(60), + Cancelled(100); + + private int code; + + Status(int code){ + this.code = code; + } + + public int code(){ + return code; + } + + public static Status of(int code){ + return switch (code){ + case 10 -> Open; + case 20 -> Started; + case 40 -> Pending; + case 60 -> Complete; + case 100 -> Cancelled; + default -> throw new IllegalArgumentException(); + }; + } + } + + public static Project of(ResultSet rs) throws SQLException { + return new Project(rs.getLong(ID),rs.getString(NAME),rs.getString(DESCRIPTION),Status.of(rs.getInt(STATUS)),rs.getLong(COMPANY_ID),rs.getBoolean(SHOW_CLOSED)); + } + + @Override + public Map toMap() { + return Map.of( + ID,id, + NAME,name, + DESCRIPTION,description, + STATUS,Map.of(STATUS_CODE,status.code(), NAME,status.name()), + COMPANY_ID,companyId, + SHOW_CLOSED,showClosed + ); + } +} diff --git a/documents/build.gradle.kts b/documents/build.gradle.kts index a66e013..6552745 100644 --- a/documents/build.gradle.kts +++ b/documents/build.gradle.kts @@ -3,9 +3,5 @@ description = "Umbrella : Documents" dependencies{ implementation(project(":company")) implementation(project(":core")) - implementation("de.srsoftware:configuration.api:1.0.2") - implementation("de.srsoftware:tools.jdbc:1.3.2") implementation("de.srsoftware:tools.mime:1.1.2") - implementation("de.srsoftware:tools.optionals:1.0.0") - implementation("de.srsoftware:tools.util:2.0.3") } \ No newline at end of file diff --git a/documents/src/main/java/de/srsoftware/umbrella/documents/model/Document.java b/documents/src/main/java/de/srsoftware/umbrella/documents/model/Document.java index b88b8c4..950b107 100644 --- a/documents/src/main/java/de/srsoftware/umbrella/documents/model/Document.java +++ b/documents/src/main/java/de/srsoftware/umbrella/documents/model/Document.java @@ -9,11 +9,10 @@ import static de.srsoftware.umbrella.documents.Constants.FIELD_CUSTOMER; import static java.util.Optional.empty; import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import java.time.LocalDate; import java.util.*; import java.util.stream.Collectors; - -import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import org.json.JSONObject; diff --git a/frontend/src/Components/Item.svelte b/frontend/src/Components/Item.svelte new file mode 100644 index 0000000..f089580 --- /dev/null +++ b/frontend/src/Components/Item.svelte @@ -0,0 +1,11 @@ + + +
+ {item.code} | {item.name} +
{@html item.description.rendered}
+ {item.unit_price/100} {item.currency} / {item.unit} +
diff --git a/frontend/src/routes/document/EstimateList.svelte b/frontend/src/routes/document/EstimateList.svelte index abe99e1..9ed25f8 100644 --- a/frontend/src/routes/document/EstimateList.svelte +++ b/frontend/src/routes/document/EstimateList.svelte @@ -1,7 +1,39 @@
-

Estimated Times

+

{t('task.estimated_times')}

+ {#if error} + {error} + {/if} + {#if items} + {#each items as item,id} + onSelect(item)} /> + {/each} + {/if}
\ No newline at end of file diff --git a/frontend/src/routes/document/ItemList.svelte b/frontend/src/routes/document/ItemList.svelte index 47c584f..bce2242 100644 --- a/frontend/src/routes/document/ItemList.svelte +++ b/frontend/src/routes/document/ItemList.svelte @@ -1,12 +1,21 @@
-

Items

+

{t('items.items')}

{#if error} {error} {/if} + {#if items} + {#each items as item,id} + onSelect(item)} /> + {/each} + {/if}
\ No newline at end of file diff --git a/frontend/src/routes/document/PositionSelector.svelte b/frontend/src/routes/document/PositionSelector.svelte index 33472e8..4f46221 100644 --- a/frontend/src/routes/document/PositionSelector.svelte +++ b/frontend/src/routes/document/PositionSelector.svelte @@ -4,20 +4,36 @@ import ItemList from './ItemList.svelte'; import TimeList from './TimeList.svelte'; - let { close = () => {}, doc = $bindable({}) } = $props(); + let { close = () => {}, doc = $bindable({}), onSelect = (item) => {} } = $props(); let select = $state(0); + + function estimateSelected(estimate){ + onSelect({estimate:estimate}); + close(); + } + + function itemSelected(item){ + onSelect({item:item}); + close(); + } @@ -29,9 +45,9 @@ {#if select == 0} - + {:else if select == 1} - + {:else} {/if} diff --git a/frontend/src/routes/document/View.svelte b/frontend/src/routes/document/View.svelte index 2f50ddf..5ce0acb 100644 --- a/frontend/src/routes/document/View.svelte +++ b/frontend/src/routes/document/View.svelte @@ -63,6 +63,12 @@ } } + function addPosition(selected){ + let newPos = {}; + if (selected.item) newPos['item']=selected.item.id; + console.log(JSON.stringify({newPos:newPos})); + } + onMount(loadDoc); @@ -186,5 +192,5 @@ {/if} {#if position_select} - position_select=false} {doc} /> + position_select=false} {doc} onSelect={addPosition} /> {/if} \ No newline at end of file diff --git a/items/build.gradle.kts b/items/build.gradle.kts index 8ea554d..87abe3c 100644 --- a/items/build.gradle.kts +++ b/items/build.gradle.kts @@ -2,7 +2,4 @@ description = "Umbrella : Items" dependencies{ implementation(project(":core")) - implementation("de.srsoftware:configuration.api:1.0.2") - implementation("de.srsoftware:tools.jdbc:1.3.2") - implementation("de.srsoftware:tools.util:2.0.3") } \ No newline at end of file diff --git a/items/src/main/java/de/srsoftware/umbrella/items/Constants.java b/items/src/main/java/de/srsoftware/umbrella/items/Constants.java index fbb7032..1cf8423 100644 --- a/items/src/main/java/de/srsoftware/umbrella/items/Constants.java +++ b/items/src/main/java/de/srsoftware/umbrella/items/Constants.java @@ -1,7 +1,12 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.items; public class Constants { private Constants(){} - + public static final String CODE = "code"; public static final String CONFIG_DATABASE = "umbrella.modules.items.database"; + public static final String TABLE_ITEMS = "items"; + public static final String TAX = "tax"; + public static final String UNIT = "unit"; + public static final String UNIT_PRICE = "unit_price"; } diff --git a/items/src/main/java/de/srsoftware/umbrella/items/Item.java b/items/src/main/java/de/srsoftware/umbrella/items/Item.java index aaaa086..9678929 100644 --- a/items/src/main/java/de/srsoftware/umbrella/items/Item.java +++ b/items/src/main/java/de/srsoftware/umbrella/items/Item.java @@ -1,4 +1,38 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.items; -public class Item { +import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.Util.markdown; +import static de.srsoftware.umbrella.items.Constants.*; + +import de.srsoftware.tools.Mappable; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +public record Item(long id, long companyId, String code, String name, String description, String unit, long unitPrice, long tax) implements Mappable { + public static Item of(ResultSet rs) throws SQLException { + var id = rs.getLong(ID); + var companyId = rs.getLong(COMPANY_ID); + var code = rs.getString(CODE); + var name = rs.getString(NAME); + var desc = rs.getString(DESCRIPTION); + var unit = rs.getString(UNIT); + var unitPrice = rs.getLong(UNIT_PRICE); + var tax = rs.getInt(TAX); + + return new Item(id,companyId,code,name,desc,unit,unitPrice,tax); + } + + @Override + public Map toMap() { + return Map.of( + ID,id, + COMPANY_ID,companyId, + CODE,code,NAME,name, + DESCRIPTION,Map.of(SOURCE,description,RENDERED,markdown(description)), + UNIT,unit, + UNIT_PRICE,unitPrice, + TAX,tax); + } } diff --git a/items/src/main/java/de/srsoftware/umbrella/items/ItemApi.java b/items/src/main/java/de/srsoftware/umbrella/items/ItemApi.java index 67988d2..993286b 100644 --- a/items/src/main/java/de/srsoftware/umbrella/items/ItemApi.java +++ b/items/src/main/java/de/srsoftware/umbrella/items/ItemApi.java @@ -1,36 +1,42 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.items; +import static de.srsoftware.umbrella.core.ConnectionProvider.connect; +import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.Paths.LIST; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.forbidden; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static de.srsoftware.umbrella.items.Constants.CONFIG_DATABASE; + 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.api.CompanyService; import de.srsoftware.umbrella.core.api.UserService; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Token; import de.srsoftware.umbrella.core.model.UmbrellaUser; - import java.io.IOException; +import java.util.HashMap; import java.util.Optional; -import static de.srsoftware.umbrella.core.ConnectionProvider.connect; -import static de.srsoftware.umbrella.core.Paths.LIST; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; -import static de.srsoftware.umbrella.items.Constants.CONFIG_DATABASE; - public class ItemApi extends BaseHandler { private final ItemDb itemDb; + private final CompanyService companies; private final UserService users; - public ItemApi(Configuration config, UserService userService) throws UmbrellaException { + public ItemApi(Configuration config, CompanyService companyService) throws UmbrellaException { var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); itemDb = new SqliteDb(connect(dbFile)); - users = userService; + companies = companyService; + users = companies.userService(); } @Override - public boolean doGet(Path path, HttpExchange ex) throws IOException { + public boolean doPost(Path path, HttpExchange ex) throws IOException { addCors(ex); try { Optional token = SessionToken.from(ex).map(Token::of); @@ -38,7 +44,7 @@ public class ItemApi extends BaseHandler { if (user.isEmpty()) return unauthorized(ex); var head = path.pop(); return switch (head) { - case LIST -> listItems(ex,user); + case LIST -> listItems(ex,user.get()); default -> super.doGet(path,ex); }; } catch (UmbrellaException e){ @@ -46,8 +52,17 @@ public class ItemApi extends BaseHandler { } } - private boolean listItems(HttpExchange ex, Optional user) throws IOException { - var items = itemDb.list(); - return notImplemented(ex,"listItems",this); + private boolean listItems(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException { + var json = json(ex); + if (!(json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number cid)) throw missingFieldException(COMPANY_ID); + var companyId = cid.longValue(); + var company = companies.get(companyId); + if (!companies.membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); + var items = itemDb.list(companyId) + .stream() + .map(Item::toMap) + .map(HashMap::new) + .peek(map -> map.put(FIELD_CURRENCY,company.currency())); + return sendContent(ex,items); } } \ No newline at end of file diff --git a/items/src/main/java/de/srsoftware/umbrella/items/ItemDb.java b/items/src/main/java/de/srsoftware/umbrella/items/ItemDb.java index 18766e6..aea7262 100644 --- a/items/src/main/java/de/srsoftware/umbrella/items/ItemDb.java +++ b/items/src/main/java/de/srsoftware/umbrella/items/ItemDb.java @@ -1,7 +1,9 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.items; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import java.util.Collection; public interface ItemDb { - Collection list(); + Collection list(long companyId) throws UmbrellaException; } diff --git a/items/src/main/java/de/srsoftware/umbrella/items/SqliteDb.java b/items/src/main/java/de/srsoftware/umbrella/items/SqliteDb.java index b8487e9..cca7b11 100644 --- a/items/src/main/java/de/srsoftware/umbrella/items/SqliteDb.java +++ b/items/src/main/java/de/srsoftware/umbrella/items/SqliteDb.java @@ -1,11 +1,36 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.items; +import static de.srsoftware.tools.jdbc.Condition.equal; +import static de.srsoftware.tools.jdbc.Query.select; +import static de.srsoftware.umbrella.core.Constants.COMPANY_ID; +import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR; +import static de.srsoftware.umbrella.items.Constants.TABLE_ITEMS; + +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.HashSet; public class SqliteDb implements ItemDb{ + private final Connection db; public SqliteDb(Connection connection) { db = connection; } + + @Override + public Collection list(long companyId) throws UmbrellaException { + try { + var items = new HashSet(); + var rs = select("*").from(TABLE_ITEMS).where(COMPANY_ID, equal(companyId)).exec(db); + while (rs.next()) items.add(Item.of(rs)); + rs.close(); + return items; + } catch (SQLException e) { + throw new UmbrellaException(HTTP_SERVER_ERROR,"Failed to load items from database"); + } + } } diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts index ada7a6a..68df3be 100644 --- a/legacy/build.gradle.kts +++ b/legacy/build.gradle.kts @@ -3,11 +3,7 @@ description = "Umbrella : Legacy API" dependencies{ implementation(project(":core")) implementation(project(":user")) - implementation("de.srsoftware:configuration.api:1.0.2") - implementation("de.srsoftware:tools.jdbc:1.3.2") implementation("de.srsoftware:tools.mime:1.1.2") - implementation("de.srsoftware:tools.optionals:1.0.0") - implementation("de.srsoftware:tools.util:2.0.3") implementation("org.bitbucket.b_c:jose4j:0.9.6") implementation("org.xerial:sqlite-jdbc:3.49.0.0") } \ No newline at end of file diff --git a/messages/build.gradle.kts b/messages/build.gradle.kts index a76e7e2..db792b9 100644 --- a/messages/build.gradle.kts +++ b/messages/build.gradle.kts @@ -3,11 +3,7 @@ description = "Umbrella : Message subsystem" dependencies{ implementation(project(":core")) implementation("com.sun.mail:jakarta.mail:2.0.1") - implementation("de.srsoftware:configuration.api:1.0.2") - implementation("de.srsoftware:tools.jdbc:1.3.2") implementation("de.srsoftware:tools.mime:1.1.2") - implementation("de.srsoftware:tools.optionals:1.0.0") - implementation("de.srsoftware:tools.util:2.0.3") implementation("org.bitbucket.b_c:jose4j:0.9.6") implementation("org.xerial:sqlite-jdbc:3.49.0.0") } \ No newline at end of file diff --git a/project/build.gradle.kts b/project/build.gradle.kts new file mode 100644 index 0000000..dc6d52c --- /dev/null +++ b/project/build.gradle.kts @@ -0,0 +1,5 @@ +description = "Umbrella : Projects" + +dependencies{ + implementation(project(":core")) +} \ No newline at end of file diff --git a/project/src/main/java/de/srsoftware/umbrella/project/Constants.java b/project/src/main/java/de/srsoftware/umbrella/project/Constants.java new file mode 100644 index 0000000..0c7b22d --- /dev/null +++ b/project/src/main/java/de/srsoftware/umbrella/project/Constants.java @@ -0,0 +1,10 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.project; + +public class Constants { + private Constants(){} + public static final String CONFIG_DATABASE = "umbrella.modules.project.database"; + public static final String TABLE_PROJECTS = "projects"; + + +} diff --git a/project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java b/project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java new file mode 100644 index 0000000..ce7acee --- /dev/null +++ b/project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java @@ -0,0 +1,10 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.project; + +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Project; +import java.util.Collection; + +public interface ProjectDb { + Collection list(long companyId, boolean includeClosed) throws UmbrellaException; +} diff --git a/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java new file mode 100644 index 0000000..8097dd7 --- /dev/null +++ b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java @@ -0,0 +1,79 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.project; + +import static de.srsoftware.umbrella.core.ConnectionProvider.connect; +import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.Paths.LIST; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.forbidden; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE; + +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.api.CompanyService; +import de.srsoftware.umbrella.core.api.ProjectService; +import de.srsoftware.umbrella.core.api.UserService; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Project; +import de.srsoftware.umbrella.core.model.Token; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Optional; + +public class ProjectModule extends BaseHandler implements ProjectService { + + private final ProjectDb projectDb; + private final CompanyService companies; + private final UserService users; + + public ProjectModule(Configuration config, CompanyService companyService) throws UmbrellaException { + var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); + projectDb = new SqliteDb(connect(dbFile)); + companies = companyService; + users = companies.userService(); + } + + @Override + public CompanyService companyService() { + return companies; + } + + @Override + public boolean doPost(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + Optional token = SessionToken.from(ex).map(Token::of); + var user = users.loadUser(token); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + return switch (head) { + case LIST -> listItems(ex,user.get()); + default -> super.doGet(path,ex); + }; + } catch (UmbrellaException e){ + return send(ex,e); + } + } + + public Collection listProjects(long companyId, boolean includeClosed) throws UmbrellaException { + return projectDb.list(companyId, includeClosed); + } + + private boolean listItems(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException { + var json = json(ex); + if (!(json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number cid)) throw missingFieldException(COMPANY_ID); + var companyId = cid.longValue(); + var company = companies.get(companyId); + if (!companies.membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); + var items = listProjects(companyId,false) + .stream() + .map(Project::toMap) + .map(HashMap::new); + return sendContent(ex,items); + } +} \ No newline at end of file diff --git a/project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java b/project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java new file mode 100644 index 0000000..cdf96af --- /dev/null +++ b/project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java @@ -0,0 +1,41 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.project; + +import static de.srsoftware.tools.jdbc.Condition.equal; +import static de.srsoftware.tools.jdbc.Condition.lessThan; +import static de.srsoftware.tools.jdbc.Query.select; +import static de.srsoftware.umbrella.core.Constants.COMPANY_ID; +import static de.srsoftware.umbrella.core.Constants.STATUS; +import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR; +import static de.srsoftware.umbrella.project.Constants.TABLE_PROJECTS; + +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Project; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.HashSet; + +public class SqliteDb implements ProjectDb { + + private final Connection db; + + public SqliteDb(Connection connection) { + db = connection; + } + + @Override + public Collection list(long companyId, boolean includeClosed) throws UmbrellaException { + try { + var items = new HashSet(); + var query = select("*").from(TABLE_PROJECTS).where(COMPANY_ID, equal(companyId)); + if (!includeClosed) query = query.where(STATUS,lessThan(Project.Status.Complete.code())); + var rs = query.exec(db); + while (rs.next()) items.add(Project.of(rs)); + rs.close(); + return items; + } catch (SQLException e) { + throw new UmbrellaException(HTTP_SERVER_ERROR,"Failed to load items from database"); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7d7c659..b162c9e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,4 +13,6 @@ include("company") include("contact") include("markdown") -include("items") \ No newline at end of file +include("project") +include("items") +include("task") diff --git a/task/build.gradle.kts b/task/build.gradle.kts new file mode 100644 index 0000000..667ecb6 --- /dev/null +++ b/task/build.gradle.kts @@ -0,0 +1,6 @@ +description = "Umbrella : Tasks" + +dependencies{ + implementation(project(":core")) + implementation(project(":project")) +} \ No newline at end of file diff --git a/task/src/main/java/de/srsoftware/umbrella/task/Constants.java b/task/src/main/java/de/srsoftware/umbrella/task/Constants.java new file mode 100644 index 0000000..eb9487d --- /dev/null +++ b/task/src/main/java/de/srsoftware/umbrella/task/Constants.java @@ -0,0 +1,19 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.task; + +public class Constants { + private Constants(){} + + public static final String CONFIG_DATABASE = "umbrella.modules.task.database"; + public static final String CHILDREN = "children"; + public static final String DUE_DATE = "due_date"; + public static final String ESTIMATED_TIMES = "estimated_times"; + public static final String ESTIMATED_TIME = "estimated_time"; + public static final String EST_TIME = "est_time"; + public static final String NO_INDEX = "no_index"; + public static final String PARENT_TASK_ID = "parent_task_id"; + public static final String PROJECT_ID = "project_id"; + public static final String START_DATE = "start_date"; + public static final String TABLE_TASKS = "tasks"; + public static final String FIELD_TASKS = "tasks"; +} diff --git a/task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java b/task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java new file mode 100644 index 0000000..be575b8 --- /dev/null +++ b/task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java @@ -0,0 +1,38 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.task; + + +import static de.srsoftware.tools.jdbc.Condition.in; +import static de.srsoftware.tools.jdbc.Query.select; +import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR; +import static de.srsoftware.umbrella.task.Constants.PROJECT_ID; +import static de.srsoftware.umbrella.task.Constants.TABLE_TASKS; + +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + + +public class SqliteDb implements TaskDb { + + private final Connection db; + + public SqliteDb(Connection connection) { + db = connection; + } + + public Collection listTasks(List projectIds) throws UmbrellaException { + try { + var rs = select("*").from(TABLE_TASKS).where(PROJECT_ID, in(projectIds.toArray())).exec(db); + var list = new HashSet(); + while (rs.next()) list.add(Task.of(rs)); + rs.close(); + return list; + } catch (SQLException e) { + throw new UmbrellaException(HTTP_SERVER_ERROR,"Failed to load tasks for project ids"); + } + } +} diff --git a/task/src/main/java/de/srsoftware/umbrella/task/Task.java b/task/src/main/java/de/srsoftware/umbrella/task/Task.java new file mode 100644 index 0000000..92d153e --- /dev/null +++ b/task/src/main/java/de/srsoftware/umbrella/task/Task.java @@ -0,0 +1,54 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.task; + +import static de.srsoftware.tools.Optionals.nullIfEmpty; +import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.Constants.SHOW_CLOSED; +import static de.srsoftware.umbrella.core.Constants.STATUS; +import static de.srsoftware.umbrella.task.Constants.*; + +import de.srsoftware.tools.Mappable; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +public record Task(long id, long projectId, Long parentTaskId, String name, String description, int status, Double estimatedTime, LocalDate start, LocalDate dueDate,boolean showClosed, boolean noIndex) implements Mappable { + public static Task of(ResultSet rs) throws SQLException { + var estTime = rs.getDouble(EST_TIME); + var parentTaskId = rs.getLong(PARENT_TASK_ID); + var startDate = nullIfEmpty(rs.getString(START_DATE)); + var dueDate = nullIfEmpty(rs.getString(DUE_DATE)); + return new Task( + rs.getLong(ID), + rs.getLong(PROJECT_ID), + parentTaskId == 0d ? null : parentTaskId, + rs.getString(NAME), + rs.getString(DESCRIPTION), + rs.getInt(STATUS), + estTime == 0d ? null : estTime, + startDate != null ? LocalDate.parse(startDate) : null, + dueDate != null ? LocalDate.parse(dueDate) : null, + rs.getBoolean(SHOW_CLOSED), + rs.getBoolean(NO_INDEX) + ); + } + + @Override + public Map toMap() { + var map = new HashMap(); + map.put(ID, id); + map.put(PROJECT_ID, projectId); + map.put(PARENT_TASK_ID, parentTaskId); + map.put(NAME, name); + map.put(DESCRIPTION, description); + map.put(STATUS, status); + map.put(ESTIMATED_TIME, estimatedTime); + map.put(START_DATE,start); + map.put(DUE_DATE,dueDate); + map.put(SHOW_CLOSED,showClosed); + map.put(NO_INDEX,noIndex); + return map; + } +} diff --git a/task/src/main/java/de/srsoftware/umbrella/task/TaskDb.java b/task/src/main/java/de/srsoftware/umbrella/task/TaskDb.java new file mode 100644 index 0000000..837b5ee --- /dev/null +++ b/task/src/main/java/de/srsoftware/umbrella/task/TaskDb.java @@ -0,0 +1,8 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.task; + + + +public interface TaskDb { + +} diff --git a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java new file mode 100644 index 0000000..9e1be8c --- /dev/null +++ b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java @@ -0,0 +1,94 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.task; + +import static de.srsoftware.tools.Optionals.is0; +import static de.srsoftware.umbrella.core.ConnectionProvider.connect; +import static de.srsoftware.umbrella.core.Constants.COMPANY_ID; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.forbidden; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static de.srsoftware.umbrella.task.Constants.*; +import static java.util.Objects.isNull; +import static java.util.stream.Collectors.toMap; + +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.api.CompanyService; +import de.srsoftware.umbrella.core.api.ProjectService; +import de.srsoftware.umbrella.core.api.UserService; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Project; +import de.srsoftware.umbrella.core.model.Token; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +public class TaskModule extends BaseHandler { + + private final SqliteDb taskDb; + private final ProjectService projects; + private final UserService users; + private final CompanyService companies; + + public TaskModule(Configuration config, ProjectService projectService) throws UmbrellaException { + var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); + taskDb = new SqliteDb(connect(dbFile)); + projects = projectService; + companies = projectService.companyService(); + users = companies.userService(); + } + + @Override + public boolean doPost(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + Optional token = SessionToken.from(ex).map(Token::of); + var user = users.loadUser(token); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + return switch (head) { + case ESTIMATED_TIMES -> estimatedTimes(user.get(),ex); + default -> super.doGet(path,ex); + }; + } catch (UmbrellaException e){ + return send(ex,e); + } + } + + private boolean estimatedTimes(UmbrellaUser user, HttpExchange ex) throws IOException, UmbrellaException { + var json = json(ex); + if (!(json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number cid)) throw missingFieldException(COMPANY_ID); + var companyId = cid.longValue(); + var company = companies.get(companyId); + if (!companies.membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); + var projects = this.projects.listProjects(companyId,false); + var taskList = taskDb.listTasks(projects.stream().map(Project::id).toList()); + var map = taskList.stream().collect(toMap(Task::id, t -> t)); + var tree = new HashMap>(); + taskList.stream().filter(task -> !is0(task.estimatedTime())).forEach(task -> placeInTree(task,tree,map)); + var result = new ArrayList>(); + projects.forEach(project -> { + var projectMap = new HashMap<>(project.toMap()); + var children = tree.values().stream().filter(root -> project.id() == (Long)root.get(PROJECT_ID)).toList(); + projectMap.put(FIELD_TASKS,children); + result.add(projectMap); + }); + return sendContent(ex,result); + } + + private Map placeInTree(Task task, HashMap> tree, Map map) { + var taskMap = task.toMap(); + if (task.parentTaskId() != null){ + Task parent = map.get(task.parentTaskId()); + var trunk = placeInTree(parent,tree,map); + ArrayList children = (ArrayList) trunk.computeIfAbsent(CHILDREN, k -> new ArrayList()); + children.add(taskMap); + return taskMap; + } + tree.put(task.id(),taskMap); + return taskMap; + } +} diff --git a/translations/src/main/resources/de.json b/translations/src/main/resources/de.json index 1fb3b6e..8e198cc 100644 --- a/translations/src/main/resources/de.json +++ b/translations/src/main/resources/de.json @@ -57,6 +57,9 @@ "home" : { "Welcome" : "Willkommen, {0}" }, + "items": { + "items": "Artikel" + }, "login" : { "do_login" : "anmelden", "Email_or_Username": "Email oder Nutzername", @@ -91,6 +94,9 @@ "404": "Seite nicht gefunden", "501": "Nicht implementiert" }, + "task": { + "estimated_times": "geschätzte Zeiten" + }, "user" : { "actions": "Aktionen", "abort": "abbrechen", diff --git a/user/build.gradle.kts b/user/build.gradle.kts index 90250c5..5886712 100644 --- a/user/build.gradle.kts +++ b/user/build.gradle.kts @@ -3,11 +3,7 @@ description = "Umbrella : User" dependencies{ implementation(project(":core")) implementation(project(":messages")) - implementation("de.srsoftware:configuration.api:1.0.2") - implementation("de.srsoftware:tools.jdbc:1.3.2") implementation("de.srsoftware:tools.mime:1.1.2") - implementation("de.srsoftware:tools.optionals:1.0.0") - implementation("de.srsoftware:tools.util:2.0.3") implementation("org.bitbucket.b_c:jose4j:0.9.6") implementation("org.xerial:sqlite-jdbc:3.49.0.0") } \ No newline at end of file diff --git a/web/build.gradle.kts b/web/build.gradle.kts index d30f485..46fd175 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -2,7 +2,6 @@ description = "Umbrella : Web" dependencies{ implementation(project(":core")) - implementation("de.srsoftware:tools.optionals:1.0.0") } tasks.processResources {