Merge branch 'module/timetracking'
This commit is contained in:
@@ -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 }
|
||||
<a onclick={logout}>{t('logout')}</a>
|
||||
{/if}
|
||||
{#if timetrack.running}
|
||||
<span class="timetracking">{timetrack.elapsed} {timetrack.running.subject}
|
||||
<button onclick={stopTrack} title={t('stop')} class="symbol"></button>
|
||||
</span>
|
||||
{/if}
|
||||
<TimeRecorder />
|
||||
</nav>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import { display } from '../time.svelte.js';
|
||||
import { t } from '../translations.svelte.js';
|
||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||
import TimeStampInput from './TimeStampInput.svelte';
|
||||
|
||||
let { record = null, onSet = time => {} } = $props();
|
||||
|
||||
@@ -20,11 +22,11 @@
|
||||
</label>
|
||||
<label>
|
||||
{t('start')}
|
||||
<input type="datetime-local" bind:value={record.start_time} />
|
||||
<TimeStampInput bind:timestamp={record.start_time} />
|
||||
</label>
|
||||
<label>
|
||||
{t('end')}
|
||||
<input type="datetime-local" bind:value={record.end_time} />
|
||||
<TimeStampInput bind:timestamp={record.end_time} />
|
||||
</label>
|
||||
<label>
|
||||
{t('description')}
|
||||
|
||||
55
frontend/src/Components/TimeRecorder.svelte
Normal file
55
frontend/src/Components/TimeRecorder.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script>
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api } from '../urls.svelte';
|
||||
import { t } from '../translations.svelte';
|
||||
import { msToTime, now } from '../time.svelte';
|
||||
import { timetrack } from '../user.svelte';
|
||||
|
||||
let interval = null;
|
||||
const router = useTinyRouter();
|
||||
|
||||
async function stopTrack(){
|
||||
if (timetrack.running.id){
|
||||
const url = api(`time/${timetrack.running.id}/stop`);
|
||||
const resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'POST',
|
||||
body : now()
|
||||
});
|
||||
if (resp.ok){
|
||||
timetrack.running = null;
|
||||
timetrack.elapsed = null;
|
||||
timetrack.start = null;
|
||||
go();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function go(){
|
||||
router.navigate('/time');
|
||||
}
|
||||
|
||||
function updateElapsed(){
|
||||
timetrack.elapsed = msToTime(Date.now() - timetrack.start);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (timetrack.running) {
|
||||
timetrack.start = new Date(timetrack.running.start_time*1000);
|
||||
interval = setInterval(updateElapsed,1000);
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
timetrack.elapsed = null;
|
||||
interval = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{#if timetrack.running}
|
||||
<span class="timetracking">
|
||||
<span onclick={go} >{timetrack.running.subject} {#if timetrack.elapsed}({timetrack.elapsed}){/if}</span>
|
||||
<button onclick={stopTrack} title={t('stop')} class="symbol"></button>
|
||||
</span>
|
||||
{/if}
|
||||
16
frontend/src/Components/TimeStampInput.svelte
Normal file
16
frontend/src/Components/TimeStampInput.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import { display, to_secs } from '../time.svelte.js';
|
||||
let { timestamp = $bindable() } = $props();
|
||||
|
||||
let datetime = $state(null);
|
||||
|
||||
$effect(() => {
|
||||
datetime = display(timestamp);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
timestamp = to_secs(datetime);
|
||||
});
|
||||
</script>
|
||||
|
||||
<input type="datetime-local" bind:value={datetime} />
|
||||
@@ -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,7 +30,11 @@
|
||||
|
||||
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;
|
||||
|
||||
@@ -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}
|
||||
<tr class={selected[time.id]?'selected':''}>
|
||||
{#if timeMap.years[line]}
|
||||
<td class="year" rowspan={timeMap.years[line]} onclick={e => toggleRange(time.start_time.substring(0,4))}>
|
||||
{time.start_time.substring(0,4)}
|
||||
<td class="year" rowspan={timeMap.years[line]} onclick={e => toggleRange(display(time.start_time).substring(0,4))}>
|
||||
{display(time.start_time).substring(0,4)}
|
||||
</td>
|
||||
{/if}
|
||||
{#if timeMap.months[line]}
|
||||
<td class="month" rowspan={timeMap.months[line]} onclick={e => toggleRange(time.start_time.substring(0,7))}>
|
||||
{time.start_time.substring(5,7)}
|
||||
<td class="month" rowspan={timeMap.months[line]} onclick={e => toggleRange(display(time.start_time).substring(0,7))}>
|
||||
{display(time.start_time).substring(5,7)}
|
||||
</td>
|
||||
{/if}
|
||||
{#if detail == time.id}
|
||||
@@ -152,7 +152,7 @@
|
||||
</td>
|
||||
{:else}
|
||||
<td class="start_end" onclick={e => toggleSelect(time.id)}>
|
||||
{time.start_time}{#if time.end_time}<wbr>…<wbr>{time.end_time}{/if}
|
||||
{display(time.start_time)}{#if time.end_time}<wbr>…<wbr>{display(time.end_time)}{/if}
|
||||
</td>
|
||||
<td class="duration" onclick={e => {detail = time.id}}>
|
||||
{#if time.duration}
|
||||
|
||||
34
frontend/src/time.svelte.js
Normal file
34
frontend/src/time.svelte.js
Normal file
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user