Browse Source

implemented function to return estimated times of company.

Therefore task and project module had to be created and partially implemented
feature/document
Stephan Richter 4 months ago
parent
commit
bac04ac047
  1. 5
      backend/build.gradle.kts
  2. 8
      backend/src/main/java/de/srsoftware/umbrella/backend/Application.java
  3. 4
      build.gradle.kts
  4. 3
      company/build.gradle.kts
  5. 3
      contact/build.gradle.kts
  6. 2
      core/build.gradle.kts
  7. 3
      core/src/main/java/de/srsoftware/umbrella/core/Constants.java
  8. 12
      core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java
  9. 56
      core/src/main/java/de/srsoftware/umbrella/core/model/Project.java
  10. 4
      documents/build.gradle.kts
  11. 3
      documents/src/main/java/de/srsoftware/umbrella/documents/model/Document.java
  12. 11
      frontend/src/Components/Item.svelte
  13. 34
      frontend/src/routes/document/EstimateList.svelte
  14. 18
      frontend/src/routes/document/ItemList.svelte
  15. 26
      frontend/src/routes/document/PositionSelector.svelte
  16. 8
      frontend/src/routes/document/View.svelte
  17. 3
      items/build.gradle.kts
  18. 7
      items/src/main/java/de/srsoftware/umbrella/items/Constants.java
  19. 36
      items/src/main/java/de/srsoftware/umbrella/items/Item.java
  20. 41
      items/src/main/java/de/srsoftware/umbrella/items/ItemApi.java
  21. 4
      items/src/main/java/de/srsoftware/umbrella/items/ItemDb.java
  22. 25
      items/src/main/java/de/srsoftware/umbrella/items/SqliteDb.java
  23. 4
      legacy/build.gradle.kts
  24. 4
      messages/build.gradle.kts
  25. 5
      project/build.gradle.kts
  26. 10
      project/src/main/java/de/srsoftware/umbrella/project/Constants.java
  27. 10
      project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java
  28. 79
      project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java
  29. 41
      project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java
  30. 4
      settings.gradle.kts
  31. 6
      task/build.gradle.kts
  32. 19
      task/src/main/java/de/srsoftware/umbrella/task/Constants.java
  33. 38
      task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java
  34. 54
      task/src/main/java/de/srsoftware/umbrella/task/Task.java
  35. 8
      task/src/main/java/de/srsoftware/umbrella/task/TaskDb.java
  36. 94
      task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java
  37. 6
      translations/src/main/resources/de.json
  38. 4
      user/build.gradle.kts
  39. 1
      web/build.gradle.kts

5
backend/build.gradle.kts

@ -19,14 +19,13 @@ dependencies{ @@ -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 {

8
backend/src/main/java/de/srsoftware/umbrella/backend/Application.java

@ -17,6 +17,8 @@ import de.srsoftware.umbrella.legacy.LegacyApi; @@ -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 { @@ -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);

4
build.gradle.kts

@ -40,8 +40,12 @@ subprojects { @@ -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")
}

3
company/build.gradle.kts

@ -2,8 +2,5 @@ description = "Umbrella : Companies" @@ -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")
}

3
contact/build.gradle.kts

@ -2,7 +2,4 @@ description = "Umbrella : Documents" @@ -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")
}

2
core/build.gradle.kts

@ -11,8 +11,6 @@ repositories { @@ -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")

3
core/src/main/java/de/srsoftware/umbrella/core/Constants.java

@ -48,12 +48,13 @@ public class Constants { @@ -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";

12
core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java

@ -0,0 +1,12 @@ @@ -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<Project> listProjects(long companyId,boolean includeClosed) throws UmbrellaException;
CompanyService companyService();
}

56
core/src/main/java/de/srsoftware/umbrella/core/model/Project.java

@ -0,0 +1,56 @@ @@ -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<String, Object> 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
);
}
}

4
documents/build.gradle.kts

@ -3,9 +3,5 @@ description = "Umbrella : Documents" @@ -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")
}

3
documents/src/main/java/de/srsoftware/umbrella/documents/model/Document.java

@ -9,11 +9,10 @@ import static de.srsoftware.umbrella.documents.Constants.FIELD_CUSTOMER; @@ -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;

11
frontend/src/Components/Item.svelte

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
<script>
import { t } from '../translations.svelte.js';
let { item, onclick } = $props();
</script>
<fieldset {onclick}>
<legend>{item.code} | {item.name}</legend>
<div>{@html item.description.rendered}</div>
<span>{item.unit_price/100} {item.currency} / {item.unit}</span>
</fieldset>

34
frontend/src/routes/document/EstimateList.svelte

@ -1,7 +1,39 @@ @@ -1,7 +1,39 @@
<script>
import { t } from '../../translations.svelte.js';
import { onMount } from 'svelte';
import EstimatedTime from '../../Components/Item.svelte';
let { company_id, onSelect = (item) => {} } = $props();
let items = $state(null);
let error = $state(null);
async function loadItems(){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/task/estimated_times`;
let data = { company_id: company_id };
const resp = await fetch(url,{
credentials:'include',
method: 'POST',
body: JSON.stringify(data)
});
if (resp.ok){
items = await resp.json();
} else {
error = await resp.body();
}
}
onMount(loadItems);
</script>
<div>
<h1>Estimated Times</h1>
<h1>{t('task.estimated_times')}</h1>
{#if error}
<span class="error">{error}</span>
{/if}
{#if items}
{#each items as item,id}
<EstimatedTime item={item} onclick={() => onSelect(item)} />
{/each}
{/if}
</div>

18
frontend/src/routes/document/ItemList.svelte

@ -1,12 +1,21 @@ @@ -1,12 +1,21 @@
<script>
import { t } from '../../translations.svelte.js';
import { onMount } from 'svelte';
import Item from '../../Components/Item.svelte';
let { company_id, onSelect = (item) => {} } = $props();
let items = $state(null);
let error = $state(null);
async function loadItems(){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/items/list`;
const resp = await fetch(url,{credentials:'include'});
let data = { company_id: company_id };
const resp = await fetch(url,{
credentials:'include',
method: 'POST',
body: JSON.stringify(data)
});
if (resp.ok){
items = await resp.json();
} else {
@ -19,8 +28,13 @@ @@ -19,8 +28,13 @@
</script>
<div>
<h1>Items</h1>
<h1>{t('items.items')}</h1>
{#if error}
<span class="error">{error}</span>
{/if}
{#if items}
{#each items as item,id}
<Item item={item} onclick={() => onSelect(item)} />
{/each}
{/if}
</div>

26
frontend/src/routes/document/PositionSelector.svelte

@ -4,20 +4,36 @@ @@ -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();
}
</script>
<style>
div{
position: fixed;
top: 0;
bottom: 0;
bottom: 20px;
left: 0;
right: 0;
background: rgba(0,0,0,0.8);
background: rgba(0,0,0,0.9);
padding: 10px;
overflow: auto;
}
span{
position: sticky;
top: 0;
}
</style>
@ -29,9 +45,9 @@ @@ -29,9 +45,9 @@
<button onclick={close}>{t('document.abort')}</button>
</span>
{#if select == 0}
<ItemList />
<ItemList company_id={doc.company.id} onSelect={itemSelected} />
{:else if select == 1}
<EstimateList />
<EstimateList company_id={doc.company.id} onSelect={estimateSelected} />
{:else}
<TimeList />
{/if}

8
frontend/src/routes/document/View.svelte

@ -63,6 +63,12 @@ @@ -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);
</script>
@ -186,5 +192,5 @@ @@ -186,5 +192,5 @@
{/if}
{#if position_select}
<PositionSelector close={() => position_select=false} {doc} />
<PositionSelector close={() => position_select=false} {doc} onSelect={addPosition} />
{/if}

3
items/build.gradle.kts

@ -2,7 +2,4 @@ description = "Umbrella : Items" @@ -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")
}

7
items/src/main/java/de/srsoftware/umbrella/items/Constants.java

@ -1,7 +1,12 @@ @@ -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";
}

36
items/src/main/java/de/srsoftware/umbrella/items/Item.java

@ -1,4 +1,38 @@ @@ -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<String, Object> 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);
}
}

41
items/src/main/java/de/srsoftware/umbrella/items/ItemApi.java

@ -1,36 +1,42 @@ @@ -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> token = SessionToken.from(ex).map(Token::of);
@ -38,7 +44,7 @@ public class ItemApi extends BaseHandler { @@ -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 { @@ -46,8 +52,17 @@ public class ItemApi extends BaseHandler {
}
}
private boolean listItems(HttpExchange ex, Optional<UmbrellaUser> 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);
}
}

4
items/src/main/java/de/srsoftware/umbrella/items/ItemDb.java

@ -1,7 +1,9 @@ @@ -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<Item> list();
Collection<Item> list(long companyId) throws UmbrellaException;
}

25
items/src/main/java/de/srsoftware/umbrella/items/SqliteDb.java

@ -1,11 +1,36 @@ @@ -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<Item> list(long companyId) throws UmbrellaException {
try {
var items = new HashSet<Item>();
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");
}
}
}

4
legacy/build.gradle.kts

@ -3,11 +3,7 @@ description = "Umbrella : Legacy API" @@ -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")
}

4
messages/build.gradle.kts

@ -3,11 +3,7 @@ description = "Umbrella : Message subsystem" @@ -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")
}

5
project/build.gradle.kts

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
description = "Umbrella : Projects"
dependencies{
implementation(project(":core"))
}

10
project/src/main/java/de/srsoftware/umbrella/project/Constants.java

@ -0,0 +1,10 @@ @@ -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";
}

10
project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java

@ -0,0 +1,10 @@ @@ -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<Project> list(long companyId, boolean includeClosed) throws UmbrellaException;
}

79
project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java

@ -0,0 +1,79 @@ @@ -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> 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<Project> 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);
}
}

41
project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java

@ -0,0 +1,41 @@ @@ -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<Project> list(long companyId, boolean includeClosed) throws UmbrellaException {
try {
var items = new HashSet<Project>();
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");
}
}
}

4
settings.gradle.kts

@ -13,4 +13,6 @@ include("company") @@ -13,4 +13,6 @@ include("company")
include("contact")
include("markdown")
include("items")
include("project")
include("items")
include("task")

6
task/build.gradle.kts

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
description = "Umbrella : Tasks"
dependencies{
implementation(project(":core"))
implementation(project(":project"))
}

19
task/src/main/java/de/srsoftware/umbrella/task/Constants.java

@ -0,0 +1,19 @@ @@ -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";
}

38
task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java

@ -0,0 +1,38 @@ @@ -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<Task> listTasks(List<Long> projectIds) throws UmbrellaException {
try {
var rs = select("*").from(TABLE_TASKS).where(PROJECT_ID, in(projectIds.toArray())).exec(db);
var list = new HashSet<Task>();
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");
}
}
}

54
task/src/main/java/de/srsoftware/umbrella/task/Task.java

@ -0,0 +1,54 @@ @@ -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<String, Object> toMap() {
var map = new HashMap<String,Object>();
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;
}
}

8
task/src/main/java/de/srsoftware/umbrella/task/TaskDb.java

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.task;
public interface TaskDb {
}

94
task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java

@ -0,0 +1,94 @@ @@ -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> 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<Long,Map<String,Object>>();
taskList.stream().filter(task -> !is0(task.estimatedTime())).forEach(task -> placeInTree(task,tree,map));
var result = new ArrayList<Map<String,Object>>();
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<String,Object> placeInTree(Task task, HashMap<Long, Map<String,Object>> tree, Map<Long, Task> map) {
var taskMap = task.toMap();
if (task.parentTaskId() != null){
Task parent = map.get(task.parentTaskId());
var trunk = placeInTree(parent,tree,map);
ArrayList<Object> children = (ArrayList<Object>) trunk.computeIfAbsent(CHILDREN, k -> new ArrayList<Object>());
children.add(taskMap);
return taskMap;
}
tree.put(task.id(),taskMap);
return taskMap;
}
}

6
translations/src/main/resources/de.json

@ -57,6 +57,9 @@ @@ -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 @@ @@ -91,6 +94,9 @@
"404": "Seite nicht gefunden",
"501": "Nicht implementiert"
},
"task": {
"estimated_times": "geschätzte Zeiten"
},
"user" : {
"actions": "Aktionen",
"abort": "abbrechen",

4
user/build.gradle.kts

@ -3,11 +3,7 @@ description = "Umbrella : User" @@ -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")
}

1
web/build.gradle.kts

@ -2,7 +2,6 @@ description = "Umbrella : Web" @@ -2,7 +2,6 @@ description = "Umbrella : Web"
dependencies{
implementation(project(":core"))
implementation("de.srsoftware:tools.optionals:1.0.0")
}
tasks.processResources {

Loading…
Cancel
Save