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 ab776d1..a9be200 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,7 +3,6 @@ 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 de.srsoftware.umbrella.core.exceptions.UmbrellaException; @@ -18,8 +17,8 @@ import org.json.JSONObject; public class Time implements Mappable{ private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private final HashSet taskIds = new HashSet<>(); - private LocalDateTime end; - private LocalDateTime start; + private long start; + private Long end; private long id; private final long userId; private String description; @@ -33,7 +32,7 @@ public class Time implements Mappable{ Complete(60), Cancelled(100); - private int code; + private final int code; State(int code){ this.code = code; @@ -55,7 +54,7 @@ public class Time implements Mappable{ } } - public Time(long id, long userId, String subject, String description, LocalDateTime start, LocalDateTime end, State state, Collection taskIds){ + public Time(long id, long userId, String subject, String description, long start, Long end, State state, Collection taskIds){ this.id=id; this.userId = userId; this.subject = subject; @@ -70,14 +69,10 @@ public class Time implements Mappable{ return description; } - public LocalDateTime end(){ + public Long end(){ return end; } - public Long endSecond(){ - return end == null ? null : end.toEpochSecond(UTC); - } - public long id(){ return id; } @@ -90,11 +85,10 @@ public class Time implements Mappable{ } public static Time of(ResultSet rs) throws SQLException { - var startTimestamp = rs.getLong(START_TIME); - var start = startTimestamp == 0 ? null : LocalDateTime.ofEpochSecond(startTimestamp,0,UTC); - var endTimestamp = rs.getLong(END_TIME); + var start = rs.getLong(START_TIME); + Long end = rs.getLong(END_TIME); - var end = endTimestamp == 0 ? null : LocalDateTime.ofEpochSecond(endTimestamp,0,UTC); + if (end == 0) end = null; return new Time( rs.getLong(ID), @@ -120,9 +114,9 @@ public class Time implements Mappable{ if (o instanceof JSONObject nested && nested.get(SOURCE) instanceof String src) o = src; if (o instanceof String d) description = d; } - if (json.has(START_TIME) && json.get(START_TIME) instanceof String st) start = parse(st); - if (json.has(END_TIME) && json.get(END_TIME) instanceof String e) end = parse(e); - if (end != null && end.isBefore(start)) throw UmbrellaException.invalidFieldException(END_TIME,"after start_time"); + if (json.has(START_TIME) && json.get(START_TIME) instanceof Number st) start = st.longValue(); + if (json.has(END_TIME) && json.get(END_TIME) instanceof Number e) end = e.longValue(); + if (end != null && end < start) throw UmbrellaException.invalidFieldException(END_TIME,"after start_time"); return this; } @@ -130,21 +124,18 @@ public class Time implements Mappable{ id = newValue; } - public LocalDateTime start(){ + public long start(){ return start; } - public Long startSecond(){ - return start == null ? null : start.toEpochSecond(UTC); - } + public State state(){ return state; } - public Time stop(LocalDateTime endTime) { - end = endTime.withSecond(0).withNano(0); - start = start.withSecond(0).withNano(0); + public Time stop(long endTime) { + end = endTime; state = State.Open; return this; } @@ -164,10 +155,10 @@ public class Time implements Mappable{ map.put(USER_ID,userId); map.put(SUBJECT,subject); map.put(DESCRIPTION,mapMarkdown(description)); - map.put(START_TIME,start.toString().replace("T"," ")); - map.put(END_TIME,end == null ? null : end.toString().replace("T"," ")); + map.put(START_TIME,start); + map.put(END_TIME,end); map.put(STATE,Map.of(STATUS_CODE,state.code,NAME,state.name())); - map.put(DURATION,end == null ? null : Duration.between(start,end).toMinutes()/60d); + map.put(DURATION,end == null ? null : (end - start)/3600d); map.put(TASK_IDS,taskIds); return map; } diff --git a/frontend/src/Components/Menu.svelte b/frontend/src/Components/Menu.svelte index 56446d1..b318214 100644 --- a/frontend/src/Components/Menu.svelte +++ b/frontend/src/Components/Menu.svelte @@ -2,10 +2,10 @@ import { onDestroy, onMount } from 'svelte'; import { useTinyRouter } from 'svelte-tiny-router'; -import { api } from '../urls.svelte.js'; import { logout, user } from '../user.svelte.js'; import { t } from '../translations.svelte.js'; -import { timetrack } from '../user.svelte.js'; + +import TimeRecorder from './TimeRecorder.svelte'; let key = $state(null); const router = useTinyRouter(); @@ -30,56 +30,12 @@ function go(path){ return false; } -function msToTime(ms) { - const days = Math.floor(ms / (24 * 60 * 60 * 1000)); - ms %= 24 * 60 * 60 * 1000; - const hours = Math.floor(ms / (60 * 60 * 1000)); - ms %= 60 * 60 * 1000; - const minutes = Math.floor(ms / (60 * 1000)); - ms %= 60 * 1000; - const seconds = Math.floor(ms / 1000); - - let daysStr = days > 0 ? padTo2Digits(days) + ':' : ''; - return `${daysStr}${padTo2Digits(hours)}:${padTo2Digits(minutes)}:${padTo2Digits(seconds)}`; -} - -function padTo2Digits(num) { - return num.toString().padStart(2, '0'); -} - async function search(e){ e.preventDefault(); router.navigate(`/search?key=${key}`); return false; } -async function stopTrack(){ - if (timetrack.running.id){ - const url = api(`time/${timetrack.running.id}/stop`); - const res = await fetch(url,{credentials:'include'}); - if (res.ok){ - timetrack.running = null; - timetrack.elapsed = null; - timetrack.start = null; - router.navigate('/time'); - } - } -} - -let interval = null; - -$effect(() => { - if (timetrack.running) { - console.log('effect!'); - timetrack.start = Date.parse(timetrack.running.start_time); - interval = setInterval(() => { timetrack.elapsed = msToTime(Date.now() - timetrack.start); },1000); - } else { - clearInterval(interval); - timetrack.elapsed = null; - interval = null; - } -}); - onMount(fetchModules); onDestroy(() => { @@ -113,9 +69,5 @@ onDestroy(() => { {#if user.name } {t('logout')} {/if} - {#if timetrack.running} - {timetrack.elapsed} {timetrack.running.subject} - - - {/if} + diff --git a/frontend/src/Components/TimeRecordEditor.svelte b/frontend/src/Components/TimeRecordEditor.svelte index 7eb1835..269091a 100644 --- a/frontend/src/Components/TimeRecordEditor.svelte +++ b/frontend/src/Components/TimeRecordEditor.svelte @@ -1,6 +1,8 @@ + + +{#if timetrack.running} + + {timetrack.running.subject} {#if timetrack.elapsed}({timetrack.elapsed}){/if} + + +{/if} \ No newline at end of file diff --git a/frontend/src/Components/TimeStampInput.svelte b/frontend/src/Components/TimeStampInput.svelte new file mode 100644 index 0000000..5aa43b8 --- /dev/null +++ b/frontend/src/Components/TimeStampInput.svelte @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/frontend/src/routes/task/ListTask.svelte b/frontend/src/routes/task/ListTask.svelte index 55f0ee4..ec38490 100644 --- a/frontend/src/routes/task/ListTask.svelte +++ b/frontend/src/routes/task/ListTask.svelte @@ -2,10 +2,11 @@ import { onMount } from 'svelte'; import { useTinyRouter } from 'svelte-tiny-router'; - import { dragged } from './dragndrop.svelte.js'; - import { api } from '../../urls.svelte.js'; - import { t } from '../../translations.svelte.js'; - import { timetrack } from '../../user.svelte.js'; + import { dragged } from './dragndrop.svelte'; + import { api } from '../../urls.svelte'; + import { t } from '../../translations.svelte'; + import { timetrack } from '../../user.svelte'; + import { now } from '../../time.svelte'; import TaskList from './TaskList.svelte'; import LineEditor from '../../Components/LineEditor.svelte'; @@ -29,11 +30,14 @@ async function addTime(){ 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', + method : 'POST', + body : now() + }); // create new time or return time with assigned tasks if (resp.ok) { const track = await resp.json(); timetrack.running = track; - console.log(track); } else { error = await resp.text(); } diff --git a/frontend/src/routes/time/Index.svelte b/frontend/src/routes/time/Index.svelte index 0907944..23cad1e 100644 --- a/frontend/src/routes/time/Index.svelte +++ b/frontend/src/routes/time/Index.svelte @@ -3,6 +3,7 @@ import { useTinyRouter } from 'svelte-tiny-router'; import { api } from '../../urls.svelte.js'; import { t } from '../../translations.svelte.js'; + import { display } from '../../time.svelte.js'; import TimeEditor from '../../Components/TimeRecordEditor.svelte'; @@ -10,7 +11,7 @@ let router = useTinyRouter(); let times = $state(null); let detail = $state(null); - let sortedTimes = $derived.by(() => Object.values(times).sort((b, a) => a.start_time.localeCompare(b.start_time))); + let sortedTimes = $derived.by(() => Object.values(times).sort((b, a) => a.start_time - b.start_time)); let selected = $state({}); let ranges = {}; let timeMap = $derived.by(calcYearMap); @@ -29,7 +30,7 @@ let monthIndex = 0; for (let idx in sortedTimes){ const time = sortedTimes[idx]; - const start = time.start_time; + const start = display(time.start_time); const year = start.substring(0,4); const month = start.substring(5,7); if (year != lastYear){ @@ -52,6 +53,7 @@ return result; } + async function loadTimes(){ const url = api('time'); const resp = await fetch(url,{credentials:'include'}); @@ -88,8 +90,6 @@ async function update(time){ const url = api(`time/${time.id}`); - time.start_time = time.start_time.split(':').slice(0,2).join(':'); - time.end_time = time.end_time.split(':').slice(0,2).join(':'); const res = await fetch(url,{ credentials:'include', method:'PATCH', @@ -137,13 +137,13 @@ {#each sortedTimes as time,line} {#if timeMap.years[line]} - toggleRange(time.start_time.substring(0,4))}> - {time.start_time.substring(0,4)} + toggleRange(display(time.start_time).substring(0,4))}> + {display(time.start_time).substring(0,4)} {/if} {#if timeMap.months[line]} - toggleRange(time.start_time.substring(0,7))}> - {time.start_time.substring(5,7)} + toggleRange(display(time.start_time).substring(0,7))}> + {display(time.start_time).substring(5,7)} {/if} {#if detail == time.id} @@ -152,7 +152,7 @@ {:else} toggleSelect(time.id)}> - {time.start_time}{#if time.end_time}{time.end_time}{/if} + {display(time.start_time)}{#if time.end_time}{display(time.end_time)}{/if} {detail = time.id}}> {#if time.duration} diff --git a/frontend/src/time.svelte.js b/frontend/src/time.svelte.js new file mode 100644 index 0000000..e1f13d6 --- /dev/null +++ b/frontend/src/time.svelte.js @@ -0,0 +1,34 @@ +export function display(timestamp){ + const date = new Date(timestamp * 1000); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}`; +} + +function padTo2Digits(num) { + return num.toString().padStart(2, '0'); +} + +export function msToTime(ms) { + const totalSeconds = Math.floor(ms / 1000); + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + let timestring = ''; + if (days) timestring += days.toString().padStart(2, '0') + ":"; + if (days||hours) timestring += hours.toString().padStart(2, '0') + ":"; + return timestring + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); +} + +export function now(){ + return Math.floor(Date.now()/1000); +} + +export function to_secs(datetime){ + return Math.floor(new Date(datetime).getTime()/1000); +} \ No newline at end of file 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 2de695d..e040063 100644 --- a/time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java +++ b/time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java @@ -152,14 +152,14 @@ CREATE TABLE IF NOT EXISTS {0} ( try { if (track.id() == 0) { // create new var rs = insertInto(TABLE_TIMES, USER_ID, SUBJECT, DESCRIPTION, START_TIME, END_TIME, STATE) - .values(track.userId(), track.subject(), track.description(), track.startSecond(), track.endSecond(), track.state().code()) + .values(track.userId(), track.subject(), track.description(), track.start(), track.end(), track.state().code()) .execute(db) .getGeneratedKeys(); if (rs.next()) track.setId(rs.getLong(1)); rs.close(); } else { // update replaceInto(TABLE_TIMES, ID, USER_ID, SUBJECT, DESCRIPTION, START_TIME, END_TIME, STATE) - .values(track.id(), track.userId(), track.subject(), track.description(), track.startSecond(), track.endSecond(), track.state().code()) + .values(track.id(), track.userId(), track.subject(), track.description(), track.start(), track.end(), track.state().code()) .execute(db).close(); } var query = replaceInto(TABLE_TASK_TIMES,TIME_ID,TASK_ID); 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 7e05ab2..97ff2b6 100644 --- a/time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java +++ b/time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java @@ -21,25 +21,13 @@ 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.*; public class TimeModule extends BaseHandler implements TimeService { - private class ExtendedTime extends Time{ - - - private final Collection tasks; - - public ExtendedTime(long id, long userId, String subject, String description, LocalDateTime start, LocalDateTime end, State state, Collection tasks) { - super(id, userId, subject, description, start, end, state, tasks.stream().map(Task::id).toList()); - this.tasks = tasks; - } - - } - private final TimeDb timeDb; - public TimeModule(ModuleRegistry registry, Configuration config) throws UmbrellaException { super(registry); var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); @@ -55,16 +43,9 @@ public class TimeModule extends BaseHandler implements TimeService { if (user.isEmpty()) return unauthorized(ex); var head = path.pop(); return switch (head) { - case TRACK_TASK -> trackTask(user.get(),path,ex); case STARTED -> getStartedTime(user.get(),ex); case null -> getUserTimes(user.get(),ex); - default -> { - try { - long timeId = Long.parseLong(head); - if (STOP.equals(path.pop())) yield getStoppedTask(user.get(),timeId,ex); - } catch (Exception ignored) {} - yield super.doGet(path,ex); - } + default -> super.doGet(path,ex); }; } catch (UmbrellaException e){ return send(ex,e); @@ -72,8 +53,14 @@ public class TimeModule extends BaseHandler implements TimeService { } private boolean getStoppedTask(UmbrellaUser user, long timeId, HttpExchange ex) throws IOException { + long now; + try { + now = Long.parseLong(body(ex)); + } catch (NumberFormatException e) { + throw unprocessable("request body does not contain a timestamp!"); + } var time = timeDb.load(timeId); - timeDb.save(time.stop(LocalDateTime.now())); + timeDb.save(time.stop(now)); return sendContent(ex,time); } @@ -104,7 +91,14 @@ public class TimeModule extends BaseHandler implements TimeService { var head = path.pop(); return switch (head) { case LIST -> listTimes(ex,user.get()); - default -> super.doPost(path,ex); + case TRACK_TASK -> trackTask(user.get(),path,ex); + default -> { + try { + long timeId = Long.parseLong(head); + if (STOP.equals(path.pop())) yield getStoppedTask(user.get(),timeId,ex); + } catch (Exception ignored) {} + yield super.doGet(path,ex); + } }; } catch (UmbrellaException e){ return send(ex,e); @@ -136,7 +130,13 @@ public class TimeModule extends BaseHandler implements TimeService { } catch (NumberFormatException e) { throw invalidFieldException(TASK_ID,"long value"); } - var now = LocalDateTime.now().withNano(0); + + long now; + try { + now = Long.parseLong(body(ex)); + } catch (NumberFormatException e) { + throw unprocessable("request body does not contain a timestamp!"); + } var opt = getStartedTime(user); if (opt.isPresent()){ @@ -156,9 +156,7 @@ public class TimeModule extends BaseHandler implements TimeService { private boolean getStartedTime(UmbrellaUser user, HttpExchange ex) throws IOException { var startedTime = getStartedTime(user); - if (startedTime.isPresent()){ - return sendContent(ex,startedTime.get()); - } + if (startedTime.isPresent()) return sendContent(ex,startedTime.get()); return send(ex,UmbrellaException.notFound("no started time")); }