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>
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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(){
|
||||||
|
|||||||
@@ -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')}
|
|
||||||
@@ -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} h
|
{time.duration.toFixed(3)} 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>
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user