Browse Source

implemented time tracking by clicking symbol at task.

next: stop running time, display running time in header

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
feature/brute_force_protection
Stephan Richter 2 months ago
parent
commit
72375b82cf
  1. 16
      core/src/main/java/de/srsoftware/umbrella/core/model/Time.java
  2. 2
      frontend/src/App.svelte
  3. 11
      frontend/src/routes/task/ListTask.svelte
  4. 16
      frontend/src/routes/time/AddTask.svelte
  5. 17
      frontend/src/routes/time/Index.svelte
  6. 11
      task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java
  7. 35
      time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java
  8. 2
      time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java
  9. 24
      time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java
  10. 4
      web/src/main/resources/web/css/default.css

16
core/src/main/java/de/srsoftware/umbrella/core/model/Time.java

@ -21,7 +21,7 @@ public class Time implements Mappable{
private long id; private long id;
private final long userId; private final long userId;
private final String description, subject; private final String description, subject;
private final State state; private State state;
public enum State{ public enum State{
Started(10), Started(10),
@ -88,9 +88,10 @@ public class Time implements Mappable{
public static Time of(ResultSet rs) throws SQLException { public static Time of(ResultSet rs) throws SQLException {
var startTimestamp = rs.getLong(START_TIME); var startTimestamp = rs.getLong(START_TIME);
var start = startTimestamp == 0 ? null : dateTimeOf(startTimestamp); var start = startTimestamp == 0 ? null : LocalDateTime.ofEpochSecond(startTimestamp,0,UTC);
var endTimestamp = rs.getLong(END_TIME); var endTimestamp = rs.getLong(END_TIME);
var end = endTimestamp == 0 ? null : dateTimeOf(endTimestamp);
var end = endTimestamp == 0 ? null : LocalDateTime.ofEpochSecond(endTimestamp,0,UTC);
return new Time( return new Time(
rs.getLong(ID), rs.getLong(ID),
@ -120,8 +121,9 @@ public class Time implements Mappable{
return state; return state;
} }
public Time stop(LocalDateTime now) { public Time stop(LocalDateTime endTime) {
end = now; end = endTime;
state = State.Open;
return this; return this;
} }
@ -141,9 +143,9 @@ public class Time implements Mappable{
map.put(SUBJECT,subject); map.put(SUBJECT,subject);
map.put(DESCRIPTION,mapMarkdown(description)); map.put(DESCRIPTION,mapMarkdown(description));
map.put(START_TIME,start.toString().replace("T"," ")); map.put(START_TIME,start.toString().replace("T"," "));
map.put(END_TIME,end.toString().replace("T"," ")); map.put(END_TIME,end == null ? null : end.toString().replace("T"," "));
map.put(STATE,Map.of(STATUS_CODE,state.code,NAME,state.name())); map.put(STATE,Map.of(STATUS_CODE,state.code,NAME,state.name()));
map.put(DURATION,Duration.between(start,end).toMinutes()/60d); map.put(DURATION,end == null ? null : Duration.between(start,end).toMinutes()/60d);
map.put(TASK_IDS,taskIds); map.put(TASK_IDS,taskIds);
return map; return map;
} }

2
frontend/src/App.svelte

@ -28,7 +28,6 @@
import TagUses from "./routes/tags/TagUses.svelte"; import TagUses from "./routes/tags/TagUses.svelte";
import TaskList from "./routes/task/Index.svelte"; import TaskList from "./routes/task/Index.svelte";
import Times from "./routes/time/Index.svelte"; import Times from "./routes/time/Index.svelte";
import TimeTask from "./routes/time/AddTask.svelte";
import User from "./routes/user/User.svelte"; import User from "./routes/user/User.svelte";
import ViewDoc from "./routes/document/View.svelte"; import ViewDoc from "./routes/document/View.svelte";
import ViewPrj from "./routes/project/View.svelte"; import ViewPrj from "./routes/project/View.svelte";
@ -77,7 +76,6 @@
<Route path="/task/:id/edit" component={ViewTask} /> <Route path="/task/:id/edit" component={ViewTask} />
<Route path="/task/:id/view" component={ViewTask} /> <Route path="/task/:id/view" component={ViewTask} />
<Route path="/time" component={Times} /> <Route path="/time" component={Times} />
<Route path="/time/add_task/:task_id" component={TimeTask} />
<Route path="/user" component={User} /> <Route path="/user" component={User} />
<Route path="/user/create" component={EditUser} /> <Route path="/user/create" component={EditUser} />
<Route path="/user/login" component={User} /> <Route path="/user/login" component={User} />

11
frontend/src/routes/task/ListTask.svelte

@ -26,8 +26,15 @@
router.navigate(`/task/${task.id}/add_subtask`); router.navigate(`/task/${task.id}/add_subtask`);
} }
function addTime(){ async function addTime(){
router.navigate(`/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
if (resp.ok) {
const track = await resp.json();
console.log(track);
} else {
error = await resp.text();
}
} }
async function deleteTask(){ async function deleteTask(){

16
frontend/src/routes/time/AddTask.svelte

@ -1,16 +0,0 @@
<script>
import { onMount } from 'svelte';
import { api } from '../../urls.svelte.js';
import { t } from '../../translations.svelte.js';
let { task_id } = $props();
async function addTask(){
const url = api(`time/track_task/${task_id}`);
const resp = await fetch(url,{credentials:'include'}); // create new time or return time with assigned tasks
}
onMount(addTask)
</script>
{t('timetracking')}

17
frontend/src/routes/time/Index.svelte

@ -30,28 +30,29 @@
<span class="error">{error}</span> <span class="error">{error}</span>
{/if} {/if}
{#if times} {#if times}
<table> <table class="timetracks">
<thead> <thead>
</thead> </thead>
<tbody> <tbody>
{#each Object.entries(times) as [tid,time]} {#each Object.entries(times) as [tid,time]}
<tr> <tr>
<td> <td class="start_end">
{time.start_time}{time.end_time} {time.start_time}{#if time.end_time}{time.end_time}{/if}
</td> </td>
<td> <td class="duration">{#if time.duration}
{time.duration}&nbsp;h {time.duration.toFixed(3)}&nbsp;h
{/if}
</td> </td>
<td> <td class="subject">
{time.subject} {time.subject}
</td> </td>
<td> <td class="tasks">
{#each Object.entries(time.tasks) as [tid,task]} {#each Object.entries(time.tasks) as [tid,task]}
<a href="#" onclick={e => openTask(tid)}>{task}</a> <a href="#" onclick={e => openTask(tid)}>{task}</a>
{/each} {/each}
</td> </td>
<td> <td class="state">
{t("state_"+time.state.name.toLowerCase())} {t("state_"+time.state.name.toLowerCase())}
</td> </td>
</tr> </tr>

11
task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java

@ -216,10 +216,19 @@ public class TaskModule extends BaseHandler implements TaskService {
return taskDb.listTasks(List.of(projectId)); return taskDb.listTasks(List.of(projectId));
} }
private Task loadTaskOrNull(long taskId){
try {
return taskDb.load(taskId);
} catch (UmbrellaException e){
LOG.log(WARNING,e.getMessage());
return null;
}
}
@Override @Override
public HashMap<Long, Task> load(Collection<Long> taskIds) { public HashMap<Long, Task> load(Collection<Long> taskIds) {
try { try {
var map = taskIds.stream().map(taskDb::load).collect(Collectors.toMap(Task::id, t -> t)); var map = taskIds.stream().map(this::loadTaskOrNull).filter(Objects::nonNull).collect(Collectors.toMap(Task::id, t -> t));
return new HashMap<>(map); return new HashMap<>(map);
} catch (Exception e){ } catch (Exception e){
throw new UmbrellaException(e.getMessage()); throw new UmbrellaException(e.getMessage());

35
time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java

@ -133,20 +133,27 @@ CREATE TABLE IF NOT EXISTS {0} (
} }
@Override @Override
public Time save(Time track) throws SQLException { public Time save(Time track) throws UmbrellaException {
if (track.id() == 0){ // create new try {
var rs = insertInto(TABLE_TASK_TIMES,USER_ID,SUBJECT,DESCRIPTION,START_TIME,END_TIME,STATE) if (track.id() == 0) { // create new
.values(track.userId(),track.subject(),track.description(),track.startSecond(),track.endSecond(),track.state().code()) var rs = insertInto(TABLE_TIMES, USER_ID, SUBJECT, DESCRIPTION, START_TIME, END_TIME, STATE)
.execute(db) .values(track.userId(), track.subject(), track.description(), track.startSecond(), track.endSecond(), track.state().code())
.getGeneratedKeys(); .execute(db)
if (rs.next()) track.setId(rs.getLong(1)); .getGeneratedKeys();
rs.close(); if (rs.next()) track.setId(rs.getLong(1));
} else { // update rs.close();
replaceInto(TABLE_TASK_TIMES,ID,USER_ID,SUBJECT,DESCRIPTION,START_TIME,END_TIME,STATE) } else { // update
.values(track.id(),track.userId(),track.subject(),track.description(),track.startSecond(),track.endSecond(),track.state().code()) replaceInto(TABLE_TIMES, ID, USER_ID, SUBJECT, DESCRIPTION, START_TIME, END_TIME, STATE)
.execute(db).close(); .values(track.id(), track.userId(), track.subject(), track.description(), track.startSecond(), track.endSecond(), track.state().code())
.execute(db).close();
}
var query = replaceInto(TABLE_TASK_TIMES,TIME_ID,TASK_ID);
for (var taskId : track.taskIds()) query.values(track.id(),taskId);
query.execute(db).close();
return track;
} catch (SQLException e){
LOG.log(ERROR,"Failed to write time to DB",e);
throw UmbrellaException.databaseException("Failed to write time to DB");
} }
// TODO: save tasks
return track;
} }
} }

2
time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java

@ -13,5 +13,5 @@ public interface TimeDb {
HashMap<Long,Time> listUserTimes(long userId, boolean showClosed); HashMap<Long,Time> listUserTimes(long userId, boolean showClosed);
Time save(Time track) throws SQLException; Time save(Time track) throws UmbrellaException;
} }

24
time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java

@ -93,13 +93,21 @@ public class TimeModule extends BaseHandler implements TimeService {
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw invalidFieldException(TASK_ID,"long value"); throw invalidFieldException(TASK_ID,"long value");
} }
var now = LocalDateTime.now(); var now = LocalDateTime.now().withSecond(0).withNano(0);
timeDb.listUserTimes(user.id(),false).values()
.stream().filter(time -> time.state() == Started) var opt = timeDb.listUserTimes(user.id(), false).values()
.sorted(Comparator.comparing(Time::start)) .stream()
.findFirst() .filter(time -> time.state() == Started)
.map(running -> running.stop(now)) .max(Comparator.comparing(Time::start));
.ifPresent(timeDb::save); if (opt.isPresent()){
var startedTime = opt.get();
if (startedTime.taskIds().contains(task.id())) {
// if the time started last already belongs to the task, there is nothing left to do
return sendContent(ex,startedTime);
}
timeDb.save(startedTime.stop(now));
}
var track = new Time(0,user.id(),task.name(),task.description(),now,null,Started,List.of(task.id())); var track = new Time(0,user.id(),task.name(),task.description(),now,null,Started,List.of(task.id()));
timeDb.save(track); timeDb.save(track);
@ -120,7 +128,7 @@ public class TimeModule extends BaseHandler implements TimeService {
var time = entry.getValue(); var time = entry.getValue();
var map = time.toMap(); var map = time.toMap();
map.remove(TASK_IDS); map.remove(TASK_IDS);
map.put(TASKS,time.taskIds().stream().collect(Collectors.toMap(tid -> tid, tid -> tasks.get(tid).name()))); map.put(TASKS,time.taskIds().stream().map(tasks::get).filter(Objects::nonNull).collect(toMap(Task::id,Task::name)));
result.put(entry.getKey(),map); result.put(entry.getKey(),map);
} }
return sendContent(ex,result); return sendContent(ex,result);

4
web/src/main/resources/web/css/default.css

@ -290,4 +290,8 @@ nav > form{
li > a > p:nth-child(1){ li > a > p:nth-child(1){
display: inline; display: inline;
}
.timetracks .duration{
font-weight: bold;
} }
Loading…
Cancel
Save