17 changed files with 330 additions and 25 deletions
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
/* © SRSoftware 2025 */ |
||||
package de.srsoftware.umbrella.core.api; |
||||
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; |
||||
import de.srsoftware.umbrella.core.model.Task; |
||||
import java.util.Collection; |
||||
|
||||
public interface TaskService { |
||||
CompanyService companyService(); |
||||
Collection<Task> listCompanyTasks(long companyId) throws UmbrellaException; |
||||
ProjectService projectService(); |
||||
|
||||
UserService userService(); |
||||
|
||||
} |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
/* © SRSoftware 2025 */ |
||||
package de.srsoftware.umbrella.core.api; |
||||
|
||||
public interface TimeService { |
||||
} |
||||
@ -1,12 +1,9 @@
@@ -1,12 +1,9 @@
|
||||
/* © SRSoftware 2025 */ |
||||
package de.srsoftware.umbrella.task; |
||||
package de.srsoftware.umbrella.core.model; |
||||
|
||||
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.core.Util.markdown; |
||||
import static de.srsoftware.umbrella.task.Constants.*; |
||||
|
||||
import de.srsoftware.tools.Mappable; |
||||
import java.sql.ResultSet; |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
/* © SRSoftware 2025 */ |
||||
package de.srsoftware.umbrella.core.model; |
||||
|
||||
import de.srsoftware.tools.Mappable; |
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.*; |
||||
import static de.srsoftware.umbrella.core.Util.dateTimeOf; |
||||
|
||||
import java.sql.ResultSet; |
||||
import java.sql.SQLException; |
||||
import java.time.LocalDateTime; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
public record Time(long id, long userId, String subject, String description, LocalDateTime start, LocalDateTime end, State state, Set<Long> taskIds) implements Mappable { |
||||
public enum State{ |
||||
Started(10), |
||||
Open(20), |
||||
Pending(40), |
||||
Complete(60), |
||||
Cancelled(100); |
||||
|
||||
private int code; |
||||
|
||||
State(int code){ |
||||
this.code = code; |
||||
} |
||||
|
||||
public int code(){ |
||||
return code; |
||||
} |
||||
|
||||
public static State of(int code){ |
||||
return switch (code){ |
||||
case 10 -> Started; |
||||
case 20 -> Open; |
||||
case 40 -> Pending; |
||||
case 60 -> Complete; |
||||
case 100 -> Cancelled; |
||||
default -> throw new IllegalArgumentException(); |
||||
}; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public Map<String, Object> toMap() { |
||||
var map = new HashMap<String,Object>(); |
||||
map.put(ID,id); |
||||
map.put(USER_ID,userId); |
||||
map.put(SUBJECT,subject); |
||||
map.put(DESCRIPTION,description); |
||||
map.put(START_TIME,start); |
||||
map.put(END_TIME,end); |
||||
map.put(STATE,Map.of(STATUS_CODE,state.code,NAME,state.name())); |
||||
return map; |
||||
} |
||||
|
||||
public static Time of(ResultSet rs) throws SQLException { |
||||
var startTimestamp = rs.getLong(START_TIME); |
||||
var start = startTimestamp == 0 ? null : dateTimeOf(startTimestamp); |
||||
var endTimestamp = rs.getLong(END_TIME); |
||||
var end = endTimestamp == 0 ? null : dateTimeOf(endTimestamp); |
||||
|
||||
return new Time( |
||||
rs.getLong(ID), |
||||
rs.getLong(USER_ID), |
||||
rs.getString(SUBJECT), |
||||
rs.getString(DESCRIPTION), |
||||
start, |
||||
end, |
||||
State.of(rs.getInt(STATE)), |
||||
new HashSet<>() |
||||
); |
||||
} |
||||
} |
||||
@ -1,18 +1,17 @@
@@ -1,18 +1,17 @@
|
||||
rootProject.name = "Umbrella25" |
||||
|
||||
include("backend") |
||||
include("company") |
||||
include("contact") |
||||
include("core") |
||||
include("documents") |
||||
include("legacy") |
||||
include("items") |
||||
include("messages") |
||||
include("translations") |
||||
include("user") |
||||
include("web") |
||||
|
||||
include("company") |
||||
include("contact") |
||||
|
||||
include("markdown") |
||||
include("project") |
||||
include("items") |
||||
include("task") |
||||
include("time") |
||||
include("translations") |
||||
include("user") |
||||
include("web") |
||||
|
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
description = "Umbrella : Timetracking" |
||||
|
||||
dependencies{ |
||||
implementation(project(":core")) |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
/* © SRSoftware 2025 */ |
||||
package de.srsoftware.umbrella.time; |
||||
|
||||
public class Constants { |
||||
private Constants(){} |
||||
|
||||
|
||||
public static final String CONFIG_DATABASE = "umbrella.modules.time.database"; |
||||
|
||||
public static final String TABLE_TASK_TIMES = "task_times"; |
||||
public static final String TABLE_TIMES = "times"; |
||||
public static final String TASK_ID = "task_id"; |
||||
public static final String TASKS = "tasks"; |
||||
public static final String TIME_ID = "time_id"; |
||||
public static final String TIMES = "times"; |
||||
public static final String CHILDREN = "children"; |
||||
|
||||
} |
||||
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
/* © SRSoftware 2025 */ |
||||
package de.srsoftware.umbrella.time; |
||||
|
||||
import static de.srsoftware.tools.jdbc.Condition.in; |
||||
import static de.srsoftware.tools.jdbc.Query.select; |
||||
import static de.srsoftware.umbrella.core.Constants.ID; |
||||
import static de.srsoftware.umbrella.time.Constants.*; |
||||
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; |
||||
import de.srsoftware.umbrella.core.model.Time; |
||||
import java.sql.Connection; |
||||
import java.sql.SQLException; |
||||
import java.util.Collection; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
|
||||
public class SqliteDb implements TimeDb { |
||||
|
||||
private final Connection db; |
||||
|
||||
public SqliteDb(Connection connection) { |
||||
db = connection; |
||||
} |
||||
|
||||
@Override |
||||
public Collection<Time> listTimes(Collection<Long> taskIds) throws UmbrellaException { |
||||
try { |
||||
var rs = select("*").from(TABLE_TASK_TIMES).where(TASK_ID,in(taskIds.toArray())).exec(db); |
||||
var mapFromTimesToTasks = new HashMap<Long,HashSet<Long>>(); |
||||
while (rs.next()){ |
||||
var timeId = rs.getLong(TIME_ID); |
||||
var taskId = rs.getLong(TASK_ID); |
||||
mapFromTimesToTasks.computeIfAbsent(timeId, k -> new HashSet<>()).add(taskId); |
||||
} |
||||
rs.close(); |
||||
rs = select("*").from(TABLE_TIMES).where(ID,in(mapFromTimesToTasks.keySet().toArray())).exec(db); |
||||
var times = new HashSet<Time>(); |
||||
while (rs.next()) { |
||||
var time = Time.of(rs); |
||||
time.taskIds().addAll(mapFromTimesToTasks.get(time.id())); |
||||
times.add(time); |
||||
} |
||||
rs.close(); |
||||
return times; |
||||
} catch (SQLException e) { |
||||
throw new UmbrellaException("Failed to load times for task list"); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
/* © SRSoftware 2025 */ |
||||
package de.srsoftware.umbrella.time; |
||||
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; |
||||
import de.srsoftware.umbrella.core.model.Time; |
||||
import java.util.Collection; |
||||
import java.util.List; |
||||
|
||||
public interface TimeDb { |
||||
Collection<Time> listTimes(Collection<Long> taskIds) throws UmbrellaException; |
||||
} |
||||
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
/* © SRSoftware 2025 */ |
||||
package de.srsoftware.umbrella.time; |
||||
|
||||
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.missingFieldException; |
||||
import static de.srsoftware.umbrella.time.Constants.*; |
||||
|
||||
import com.sun.net.httpserver.HttpExchange; |
||||
import de.srsoftware.configuration.Configuration; |
||||
import de.srsoftware.tools.Path; |
||||
import de.srsoftware.tools.SessionToken; |
||||
import de.srsoftware.umbrella.core.BaseHandler; |
||||
import de.srsoftware.umbrella.core.api.*; |
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; |
||||
import de.srsoftware.umbrella.core.model.*; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.*; |
||||
import java.util.stream.Collectors; |
||||
|
||||
public class TimeModule extends BaseHandler implements TimeService { |
||||
private final UserService users; |
||||
private final TimeDb timeDb; |
||||
private final TaskService tasks; |
||||
private final CompanyService companies; |
||||
private final ProjectService projects; |
||||
|
||||
public TimeModule(Configuration config, TaskService taskService) throws UmbrellaException { |
||||
companies = taskService.companyService(); |
||||
projects = taskService.projectService(); |
||||
tasks = taskService; |
||||
users = tasks.userService(); |
||||
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); |
||||
timeDb = new SqliteDb(connect(dbFile)); |
||||
} |
||||
|
||||
@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 -> listTimes(ex,user.get()); |
||||
default -> super.doPost(path,ex); |
||||
}; |
||||
} catch (UmbrellaException e){ |
||||
return send(ex,e); |
||||
} |
||||
} |
||||
|
||||
private boolean listTimes(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); |
||||
long companyId = cid.longValue(); |
||||
if (!companies.membership(companyId,user.id())) throw UmbrellaException.forbidden("You are not a member of compayn {0}",companyId); |
||||
var projectMap = projects.listProjects(companyId,false).stream().collect(Collectors.toMap(Project::id,p -> p)); |
||||
var taskMap = tasks.listCompanyTasks(companyId).stream().collect(Collectors.toMap(Task::id,Task::toMap)); |
||||
var timesList = timeDb.listTimes(taskMap.keySet()); |
||||
var tasksWithTime = new HashMap<Long,Map<String,Object>>(); |
||||
var tree = new HashMap<Long,Map<String,Object>>(); |
||||
for (var time : timesList) { |
||||
if (time.state().code() >= 60) continue; |
||||
var timeMap = time.toMap(); |
||||
for (var taskId : time.taskIds()) { |
||||
var task = tasksWithTime.computeIfAbsent(taskId, k -> taskMap.get(taskId)); |
||||
@SuppressWarnings("unchecked") |
||||
HashMap<Long,Map<String, Object>> taskTimes = (HashMap<Long,Map<String, Object>>) task.computeIfAbsent(TIMES, k -> new HashMap<Long,Map<String, Object>>()); |
||||
taskTimes.put(time.id(),timeMap); |
||||
while (task.get(PARENT_TASK_ID) instanceof Long parentTaskId){ |
||||
var parentTask = taskMap.get(parentTaskId); |
||||
@SuppressWarnings("unchecked") |
||||
HashMap<Long,Map<String, Object>> children = (HashMap<Long,Map<String, Object>>) parentTask.computeIfAbsent(CHILDREN, k -> new HashMap<Long,Map<String, Object>>()); |
||||
children.put(taskId,task); |
||||
task = parentTask; |
||||
taskId = parentTaskId; |
||||
} |
||||
if (task.get(PROJECT_ID) instanceof Long projectId){ |
||||
var project = tree.computeIfAbsent(projectId,k -> new HashMap<>(projectMap.get(projectId).toMap())); |
||||
@SuppressWarnings("unchecked") |
||||
HashMap<Long,Map<String,Object>> projectTasks = (HashMap<Long,Map<String, Object>>) project.computeIfAbsent(TASKS, k -> new HashMap<Long,Map<String,Object>>()); |
||||
projectTasks.put(taskId,task); |
||||
} |
||||
} |
||||
} |
||||
return sendContent(ex,tree); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue