From 7a020048d866146081a0b2dc87e0fffed1cac29b Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 26 Aug 2025 15:33:31 +0200 Subject: [PATCH] working on time tracking implementation Signed-off-by: Stephan Richter --- .../srsoftware/umbrella/core/model/Time.java | 43 +++++++++++++++++-- frontend/src/routes/time/AddTask.svelte | 2 +- .../srsoftware/umbrella/time/Constants.java | 4 +- .../de/srsoftware/umbrella/time/SqliteDb.java | 23 +++++++--- .../de/srsoftware/umbrella/time/TimeDb.java | 6 ++- .../srsoftware/umbrella/time/TimeModule.java | 39 ++++++++--------- 6 files changed, 84 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Time.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Time.java index 43f1bc9..a268493 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Time.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Time.java @@ -3,19 +3,23 @@ package de.srsoftware.umbrella.core.model; import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Util.*; +import static java.time.ZoneOffset.UTC; import de.srsoftware.tools.Mappable; import java.sql.ResultSet; import java.sql.SQLException; import java.time.Duration; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.*; public class Time implements Mappable{ private final Collection taskIds; - private final LocalDateTime end, start; - private final long id, userId; + private LocalDateTime end; + private final LocalDateTime start; + private long id; + private final long userId; private final String description, subject; private final State state; @@ -46,8 +50,9 @@ public class Time implements Mappable{ default -> throw new IllegalArgumentException(); }; } - } + + } public Time(long id, long userId, String subject, String description, LocalDateTime start, LocalDateTime end, State state, Collection taskIds){ this.id=id; this.userId = userId; @@ -58,6 +63,17 @@ public class Time implements Mappable{ this.state = state; this.taskIds = taskIds; } + public String description(){ + return description; + } + + public LocalDateTime end(){ + return end; + } + + public Long endSecond(){ + return end == null ? null : end.toEpochSecond(UTC); + } public long id(){ return id; @@ -88,14 +104,31 @@ public class Time implements Mappable{ ); } + public void setId(long newValue) { + id = newValue; + } + public LocalDateTime start(){ return start; } + public Long startSecond(){ + return start == null ? null : start.toEpochSecond(UTC); + } + public State state(){ return state; } + public Time stop(LocalDateTime now) { + end = now; + return this; + } + + public String subject(){ + return subject; + } + public Collection taskIds(){ return taskIds; } @@ -114,4 +147,8 @@ public class Time implements Mappable{ map.put(TASK_IDS,taskIds); return map; } + + public long userId(){ + return userId; + } } diff --git a/frontend/src/routes/time/AddTask.svelte b/frontend/src/routes/time/AddTask.svelte index 9718fc2..54c7121 100644 --- a/frontend/src/routes/time/AddTask.svelte +++ b/frontend/src/routes/time/AddTask.svelte @@ -6,7 +6,7 @@ let { task_id } = $props(); async function addTask(){ - const url = api(`time/add_task/${task_id}`); + const url = api(`time/track_task/${task_id}`); const resp = await fetch(url,{credentials:'include'}); // create new time or return time with assigned tasks } diff --git a/time/src/main/java/de/srsoftware/umbrella/time/Constants.java b/time/src/main/java/de/srsoftware/umbrella/time/Constants.java index 91d249e..ef4d943 100644 --- a/time/src/main/java/de/srsoftware/umbrella/time/Constants.java +++ b/time/src/main/java/de/srsoftware/umbrella/time/Constants.java @@ -5,7 +5,7 @@ public class Constants { private Constants(){} - public static final String ADD_TASK = "add_task"; + public static final String CHILDREN = "children"; public static final String CONFIG_DATABASE = "umbrella.modules.time.database"; public static final String TABLE_TASK_TIMES = "task_times"; @@ -14,6 +14,6 @@ public class Constants { 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"; + public static final String TRACK_TASK = "track_task"; } diff --git a/time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java b/time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java index 6552dd2..5f94067 100644 --- a/time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java +++ b/time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java @@ -3,6 +3,7 @@ package de.srsoftware.umbrella.time; import static de.srsoftware.tools.jdbc.Condition.*; import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; +import static de.srsoftware.tools.jdbc.Query.insertInto; import static de.srsoftware.tools.jdbc.Query.select; import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.model.Status.OPEN; @@ -10,12 +11,14 @@ import static de.srsoftware.umbrella.core.model.Time.State.Complete; import static de.srsoftware.umbrella.time.Constants.*; import static java.lang.System.Logger.Level.ERROR; import static java.text.MessageFormat.format; +import static java.time.ZoneOffset.UTC; import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Time; import java.sql.Connection; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -27,11 +30,6 @@ public class SqliteDb extends BaseDb implements TimeDb { super(connection); } - @Override - public Time createNew(long userId, String subject, String description) { - return null; - } - @Override protected int createTables() { int currentVersion = createSettingsTable(); @@ -134,4 +132,19 @@ CREATE TABLE IF NOT EXISTS {0} ( throw new UmbrellaException("Failed to load times for task list"); } } + + @Override + public Time save(Time track) throws SQLException { + if (track.id() == 0){ // create new + var rs = insertInto(TABLE_TASK_TIMES,USER_ID,SUBJECT,DESCRIPTION,START_TIME,END_TIME,STATE) + .values(track.userId(),track.subject(),track.description(),track.startSecond(),track.endSecond(),track.state().code()) + .execute(db) + .getGeneratedKeys(); + if (rs.next()) track.setId(rs.getLong(1)); + rs.close(); + return track; + } else { // update +// TODO: update + } + } } diff --git a/time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java b/time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java index 05d1963..a7a7427 100644 --- a/time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java +++ b/time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java @@ -3,13 +3,15 @@ package de.srsoftware.umbrella.time; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Time; + +import java.sql.SQLException; import java.util.Collection; import java.util.HashMap; public interface TimeDb { - Time createNew(long userId, String subject, String description); - HashMap listTimes(Collection taskIds, boolean showClosed) throws UmbrellaException; HashMap listUserTimes(long userId, boolean showClosed); + + Time save(Time track) throws SQLException; } diff --git a/time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java b/time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java index 86ea140..82ac129 100644 --- a/time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java +++ b/time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java @@ -21,8 +21,8 @@ import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.*; import java.io.IOException; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; public class TimeModule extends BaseHandler implements TimeService { @@ -57,7 +57,7 @@ public class TimeModule extends BaseHandler implements TimeService { if (user.isEmpty()) return unauthorized(ex); var head = path.pop(); return switch (head) { - case ADD_TASK -> getAddTask(user.get(),path,ex); + case TRACK_TASK -> trackTask(user.get(),path,ex); case null -> getUserTimes(user.get(),ex); default -> super.doGet(path,ex); }; @@ -83,31 +83,30 @@ public class TimeModule extends BaseHandler implements TimeService { } } - private boolean getAddTask(UmbrellaUser user, Path path, HttpExchange ex) throws IOException { + private boolean trackTask(UmbrellaUser user, Path path, HttpExchange ex) throws IOException { if (path.empty()) throw missingFieldException(TASK_ID); - long taskId; + Task task; try { - taskId = Long.parseLong(path.pop()); + var taskId = Long.parseLong(path.pop()); + task = taskService().load(List.of(taskId)).get(taskId); + if (task == null) throw UmbrellaException.notFound("Failed to load task with id = {0}",taskId); } catch (NumberFormatException e) { throw invalidFieldException(TASK_ID,"long value"); } - var taskIds = new HashSet(); - var times = timeDb.listUserTimes(user.id(), false).values().stream() - .filter(time -> time.state() == Started) - .peek(time -> taskIds.addAll(time.taskIds())) - .collect(Collectors.toMap(Time::id, t -> t)); - taskIds.add(taskId); - var tasks = taskService().load(taskIds); - if (times.isEmpty()){ - var task = tasks.get(taskId); - if (task == null) throw UmbrellaException.notFound("Failed to find task with id = {0}",taskId); - var time = timeDb.createNew(user.id(),task.name(),task.description()); - times.put(time.id(),time); - } - return sendContent(ex,Map.of(TIMES,times,TASKS,tasks)); + var now = LocalDateTime.now(); + timeDb.listUserTimes(user.id(),false).values() + .stream().filter(time -> time.state() == Started) + .sorted(Comparator.comparing(Time::start)) + .findFirst() + .map(running -> running.stop(now)) + .ifPresent(timeDb::save); + var track = new Time(0,user.id(),task.name(),task.description(),now,null,Started,List.of(task.id())); + timeDb.save(track); + + return sendContent(ex,track); } - private boolean getUserTimes(UmbrellaUser user, HttpExchange ex) throws IOException { + private boolean getUserTimes(UmbrellaUser user, HttpExchange ex) throws IOException { Set taskIds = new HashSet<>(); Map projects = projectService().listUserProjects(user.id(), true); for (var pid : projects.keySet()) taskIds.addAll(taskService().listProjectTasks(pid).keySet());