working on time tracking implementation

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2025-08-26 15:33:31 +02:00
parent d0e3dc8b0c
commit 7a020048d8
6 changed files with 84 additions and 33 deletions

View File

@@ -3,19 +3,23 @@ package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Util.*; import static de.srsoftware.umbrella.core.Util.*;
import static java.time.ZoneOffset.UTC;
import de.srsoftware.tools.Mappable; import de.srsoftware.tools.Mappable;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*; import java.util.*;
public class Time implements Mappable{ public class Time implements Mappable{
private final Collection<Long> taskIds; private final Collection<Long> taskIds;
private final LocalDateTime end, start; private LocalDateTime end;
private final long id, userId; private final LocalDateTime start;
private long id;
private final long userId;
private final String description, subject; private final String description, subject;
private final State state; private final State state;
@@ -46,8 +50,9 @@ public class Time implements Mappable{
default -> throw new IllegalArgumentException(); default -> throw new IllegalArgumentException();
}; };
} }
}
}
public Time(long id, long userId, String subject, String description, LocalDateTime start, LocalDateTime end, State state, Collection<Long> taskIds){ public Time(long id, long userId, String subject, String description, LocalDateTime start, LocalDateTime end, State state, Collection<Long> taskIds){
this.id=id; this.id=id;
this.userId = userId; this.userId = userId;
@@ -58,6 +63,17 @@ public class Time implements Mappable{
this.state = state; this.state = state;
this.taskIds = taskIds; 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(){ public long id(){
return id; return id;
@@ -88,14 +104,31 @@ public class Time implements Mappable{
); );
} }
public void setId(long newValue) {
id = newValue;
}
public LocalDateTime start(){ public LocalDateTime start(){
return start; return start;
} }
public Long startSecond(){
return start == null ? null : start.toEpochSecond(UTC);
}
public State state(){ public State state(){
return state; return state;
} }
public Time stop(LocalDateTime now) {
end = now;
return this;
}
public String subject(){
return subject;
}
public Collection<Long> taskIds(){ public Collection<Long> taskIds(){
return taskIds; return taskIds;
} }
@@ -114,4 +147,8 @@ public class Time implements Mappable{
map.put(TASK_IDS,taskIds); map.put(TASK_IDS,taskIds);
return map; return map;
} }
public long userId(){
return userId;
}
} }

View File

@@ -6,7 +6,7 @@
let { task_id } = $props(); let { task_id } = $props();
async function addTask(){ 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 const resp = await fetch(url,{credentials:'include'}); // create new time or return time with assigned tasks
} }

View File

@@ -5,7 +5,7 @@ public class Constants {
private 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 CONFIG_DATABASE = "umbrella.modules.time.database";
public static final String TABLE_TASK_TIMES = "task_times"; 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 TASKS = "tasks";
public static final String TIME_ID = "time_id"; public static final String TIME_ID = "time_id";
public static final String TIMES = "times"; public static final String TIMES = "times";
public static final String CHILDREN = "children"; public static final String TRACK_TASK = "track_task";
} }

View File

@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.time;
import static de.srsoftware.tools.jdbc.Condition.*; import static de.srsoftware.tools.jdbc.Condition.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; 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.tools.jdbc.Query.select;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.model.Status.OPEN; 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 de.srsoftware.umbrella.time.Constants.*;
import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.ERROR;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import static java.time.ZoneOffset.UTC;
import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Time; import de.srsoftware.umbrella.core.model.Time;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.ZoneOffset;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -27,11 +30,6 @@ public class SqliteDb extends BaseDb implements TimeDb {
super(connection); super(connection);
} }
@Override
public Time createNew(long userId, String subject, String description) {
return null;
}
@Override @Override
protected int createTables() { protected int createTables() {
int currentVersion = createSettingsTable(); int currentVersion = createSettingsTable();
@@ -134,4 +132,19 @@ CREATE TABLE IF NOT EXISTS {0} (
throw new UmbrellaException("Failed to load times for task list"); 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
}
}
} }

View File

@@ -3,13 +3,15 @@ package de.srsoftware.umbrella.time;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Time; import de.srsoftware.umbrella.core.model.Time;
import java.sql.SQLException;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
public interface TimeDb { public interface TimeDb {
Time createNew(long userId, String subject, String description);
HashMap<Long,Time> listTimes(Collection<Long> taskIds, boolean showClosed) throws UmbrellaException; HashMap<Long,Time> listTimes(Collection<Long> taskIds, boolean showClosed) throws UmbrellaException;
HashMap<Long,Time> listUserTimes(long userId, boolean showClosed); HashMap<Long,Time> listUserTimes(long userId, boolean showClosed);
Time save(Time track) throws SQLException;
} }

View File

@@ -21,8 +21,8 @@ import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*; import de.srsoftware.umbrella.core.model.*;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*; import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class TimeModule extends BaseHandler implements TimeService { public class TimeModule extends BaseHandler implements TimeService {
@@ -57,7 +57,7 @@ public class TimeModule extends BaseHandler implements TimeService {
if (user.isEmpty()) return unauthorized(ex); if (user.isEmpty()) return unauthorized(ex);
var head = path.pop(); var head = path.pop();
return switch (head) { 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); case null -> getUserTimes(user.get(),ex);
default -> super.doGet(path,ex); default -> super.doGet(path,ex);
}; };
@@ -83,28 +83,27 @@ 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); if (path.empty()) throw missingFieldException(TASK_ID);
long taskId; Task task;
try { 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) { } catch (NumberFormatException e) {
throw invalidFieldException(TASK_ID,"long value"); throw invalidFieldException(TASK_ID,"long value");
} }
var taskIds = new HashSet<Long>(); var now = LocalDateTime.now();
var times = timeDb.listUserTimes(user.id(), false).values().stream() timeDb.listUserTimes(user.id(),false).values()
.filter(time -> time.state() == Started) .stream().filter(time -> time.state() == Started)
.peek(time -> taskIds.addAll(time.taskIds())) .sorted(Comparator.comparing(Time::start))
.collect(Collectors.toMap(Time::id, t -> t)); .findFirst()
taskIds.add(taskId); .map(running -> running.stop(now))
var tasks = taskService().load(taskIds); .ifPresent(timeDb::save);
if (times.isEmpty()){ var track = new Time(0,user.id(),task.name(),task.description(),now,null,Started,List.of(task.id()));
var task = tasks.get(taskId); timeDb.save(track);
if (task == null) throw UmbrellaException.notFound("Failed to find task with id = {0}",taskId);
var time = timeDb.createNew(user.id(),task.name(),task.description()); return sendContent(ex,track);
times.put(time.id(),time);
}
return sendContent(ex,Map.of(TIMES,times,TASKS,tasks));
} }
private boolean getUserTimes(UmbrellaUser user, HttpExchange ex) throws IOException { private boolean getUserTimes(UmbrellaUser user, HttpExchange ex) throws IOException {