OpenSource Projekt-Management-Software
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

178 lines
5.8 KiB

<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte.js';
import { t } from '../../translations.svelte.js';
import TimeEditor from '../../Components/TimeRecordEditor.svelte';
let error = $state(null);
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 selected = $state({});
let ranges = {};
let timeMap = $derived.by(calcYearMap);
let selectionSum = $derived(sortedTimes.filter(time => selected[time.id]).map(time => time.duration).reduce((acc, a) => acc + a, 0));
function calcYearMap(){
let result = {
months : {},
years : {}
}
let lastYear = null;
let lastMonth = null;
let yearCount = 0;
let monthCount = 0;
let yearIndex = 0;
let monthIndex = 0;
for (let idx in sortedTimes){
const time = sortedTimes[idx];
const start = time.start_time;
const year = start.substring(0,4);
const month = start.substring(5,7);
if (year != lastYear){
lastYear = year;
if (yearCount) result.years[yearIndex] = yearCount;
yearCount = 0;
yearIndex = idx;
}
yearCount +=1;
if (month != lastMonth){
lastMonth = month;
if (monthCount) result.months[monthIndex] = monthCount;
monthCount = 0;
monthIndex = idx;
}
monthCount +=1;
}
if (yearCount) result.years[yearIndex] = yearCount;
if (monthCount) result.months[monthIndex] = monthCount;
return result;
}
async function loadTimes(){
const url = api('time');
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
times = await resp.json();
} else {
error = await resp.text();
}
}
function openTask(tid){
router.navigate(`task/${tid}/view`);
}
function toggleRange(range){
let affected = sortedTimes.filter(time => time.start_time.startsWith(range));
if (ranges[range]){
delete ranges[range];
for (let time of affected){
if (selected[time.id]) delete selected[time.id]
}
} else {
for (let time of affected) selected[time.id] = true;
ranges[range] = true;
}
}
function toggleSelect(time_id){
detail = null;
if (selected[time_id]) {
delete selected[time_id]
} else selected[time_id] = true;
}
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',
body:JSON.stringify(time)
});
if (res.ok){
let json = await res.json();
let id = json.id;
for (let key of Object.keys(json)) times[id][key] = json[key];
detail = null;
error = null;
return true;
} else {
error = await res.text();
return false;
}
}
onMount(loadTimes);
</script>
<h1>{t('timetracking')}</h1>
{#if error}
<span class="error">{error}</span>
{/if}
{#if times}
{#if selectionSum}
<div class="timetracks sum">
{t('sum_of_records')}: <span>{selectionSum.toFixed(3)}&nbsp;{t('hours')}</span>
</div>
{/if}
<table class="timetracks">
<thead>
<tr>
<th>{t('year')}</th>
<th>{t('month')}</th>
<th>{t('start')}{t('end')}</th>
<th>{t('duration')}</th>
<th>{t('subject')}</th>
<th>{t('tasks')}</th>
<th>{t('state')}</th>
</tr>
</thead>
<tbody>
{#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>
{/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>
{/if}
{#if detail == time.id}
<td colspan="5">
<TimeEditor record={time} onSet={update} />
</td>
{:else}
<td class="start_end" onclick={e => toggleSelect(time.id)}>
{time.start_time}{#if time.end_time}{time.end_time}{/if}
</td>
<td class="duration" onclick={e => {detail = time.id}}>
{#if time.duration}
{time.duration.toFixed(3)}&nbsp;h
{/if}
</td>
<td class="subject" onclick={e => {detail = time.id}}>
{time.subject}
</td>
<td class="tasks" onclick={e => {detail = time.id}}>
{#each Object.entries(time.tasks) as [tid,task]}
<a href="#" onclick={e => openTask(tid)}>{task}</a>
{/each}
</td>
<td class="state" onclick={e => {detail = time.id}}>
{t("state_"+time.state.name.toLowerCase())}
</td>
{/if}
</tr>
{/each}
</tbody>
</table>
{/if}