Browse Source
Therefore task and project module had to be created and partially implementedfeature/document
39 changed files with 642 additions and 61 deletions
@ -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(); |
||||||
|
} |
||||||
@ -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 |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -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> |
||||||
@ -1,7 +1,39 @@ |
|||||||
<script> |
<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> |
</script> |
||||||
|
|
||||||
<div> |
<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> |
</div> |
||||||
@ -1,7 +1,12 @@ |
|||||||
|
/* © SRSoftware 2025 */ |
||||||
package de.srsoftware.umbrella.items; |
package de.srsoftware.umbrella.items; |
||||||
|
|
||||||
public class Constants { |
public class Constants { |
||||||
private Constants(){} |
private Constants(){} |
||||||
|
public static final String CODE = "code"; |
||||||
public static final String CONFIG_DATABASE = "umbrella.modules.items.database"; |
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"; |
||||||
} |
} |
||||||
|
|||||||
@ -1,4 +1,38 @@ |
|||||||
|
/* © SRSoftware 2025 */ |
||||||
package de.srsoftware.umbrella.items; |
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); |
||||||
|
} |
||||||
} |
} |
||||||
|
|||||||
@ -1,7 +1,9 @@ |
|||||||
|
/* © SRSoftware 2025 */ |
||||||
package de.srsoftware.umbrella.items; |
package de.srsoftware.umbrella.items; |
||||||
|
|
||||||
|
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; |
||||||
import java.util.Collection; |
import java.util.Collection; |
||||||
|
|
||||||
public interface ItemDb { |
public interface ItemDb { |
||||||
Collection<Item> list(); |
Collection<Item> list(long companyId) throws UmbrellaException; |
||||||
} |
} |
||||||
|
|||||||
@ -1,11 +1,36 @@ |
|||||||
|
/* © SRSoftware 2025 */ |
||||||
package de.srsoftware.umbrella.items; |
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.Connection; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.HashSet; |
||||||
|
|
||||||
public class SqliteDb implements ItemDb{ |
public class SqliteDb implements ItemDb{ |
||||||
|
|
||||||
private final Connection db; |
private final Connection db; |
||||||
|
|
||||||
public SqliteDb(Connection connection) { |
public SqliteDb(Connection connection) { |
||||||
db = 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"); |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,5 @@ |
|||||||
|
description = "Umbrella : Projects" |
||||||
|
|
||||||
|
dependencies{ |
||||||
|
implementation(project(":core")) |
||||||
|
} |
||||||
@ -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"; |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
description = "Umbrella : Tasks" |
||||||
|
|
||||||
|
dependencies{ |
||||||
|
implementation(project(":core")) |
||||||
|
implementation(project(":project")) |
||||||
|
} |
||||||
@ -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"; |
||||||
|
} |
||||||
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
/* © SRSoftware 2025 */ |
||||||
|
package de.srsoftware.umbrella.task; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public interface TaskDb { |
||||||
|
|
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue