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 @@
				@@ -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 @@
				@@ -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 @@
				@@ -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 @@
				@@ -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> | 
				
			||||
@ -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"; | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -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); | 
				
			||||
	} | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -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; | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -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"); | 
				
			||||
		} | 
				
			||||
	} | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -0,0 +1,5 @@
				@@ -0,0 +1,5 @@
					 | 
				
			||||
description = "Umbrella : Projects" | 
				
			||||
 | 
				
			||||
dependencies{ | 
				
			||||
    implementation(project(":core")) | 
				
			||||
} | 
				
			||||
@ -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"; | 
				
			||||
 | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -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; | 
				
			||||
} | 
				
			||||
@ -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); | 
				
			||||
	} | 
				
			||||
} | 
				
			||||
@ -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"); | 
				
			||||
		} | 
				
			||||
	} | 
				
			||||
} | 
				
			||||
@ -0,0 +1,6 @@
				@@ -0,0 +1,6 @@
					 | 
				
			||||
description = "Umbrella : Tasks" | 
				
			||||
 | 
				
			||||
dependencies{ | 
				
			||||
    implementation(project(":core")) | 
				
			||||
    implementation(project(":project")) | 
				
			||||
} | 
				
			||||
@ -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"; | 
				
			||||
} | 
				
			||||
@ -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"); | 
				
			||||
		} | 
				
			||||
	} | 
				
			||||
} | 
				
			||||
@ -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; | 
				
			||||
	} | 
				
			||||
} | 
				
			||||
@ -0,0 +1,8 @@
				@@ -0,0 +1,8 @@
					 | 
				
			||||
/* © SRSoftware 2025 */ | 
				
			||||
package de.srsoftware.umbrella.task; | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
public interface TaskDb { | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -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; | 
				
			||||
	} | 
				
			||||
} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue