|
|
|
|
@ -11,6 +11,50 @@
@@ -11,6 +11,50 @@
|
|
|
|
|
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(() => { |
|
|
|
|
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; |
|
|
|
|
console.log(result); |
|
|
|
|
return result; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
function calcYearMap(){ |
|
|
|
|
let result = Object.values(times).length; |
|
|
|
|
console.log({result:result}); |
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadTimes(){ |
|
|
|
|
const url = api('time'); |
|
|
|
|
@ -26,6 +70,26 @@
@@ -26,6 +70,26 @@
|
|
|
|
|
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(':'); |
|
|
|
|
@ -53,6 +117,12 @@
@@ -53,6 +117,12 @@
|
|
|
|
|
|
|
|
|
|
<style> |
|
|
|
|
td { vertical-align: top; } |
|
|
|
|
.year, .month{ |
|
|
|
|
border: 1px solid; |
|
|
|
|
} |
|
|
|
|
.selected td:not(.year):not(.month){ |
|
|
|
|
background: navy; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
|
|
|
|
|
<h1>{t('timetracking')}</h1> |
|
|
|
|
@ -63,6 +133,8 @@
@@ -63,6 +133,8 @@
|
|
|
|
|
<table class="timetracks"> |
|
|
|
|
<thead> |
|
|
|
|
<tr> |
|
|
|
|
<th>{t('year')}</th> |
|
|
|
|
<th>{t('month')}</th> |
|
|
|
|
<th>{t('start_end')}</th> |
|
|
|
|
<th>{t('duration')}</th> |
|
|
|
|
<th>{t('subject')}</th> |
|
|
|
|
@ -71,35 +143,44 @@
@@ -71,35 +143,44 @@
|
|
|
|
|
</tr> |
|
|
|
|
</thead> |
|
|
|
|
<tbody> |
|
|
|
|
{#each sortedTimes as time} |
|
|
|
|
{#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} |
|
|
|
|
<tr> |
|
|
|
|
<td colspan="5"> |
|
|
|
|
<TimeEditor record={time} onSet={update} /> |
|
|
|
|
</td> |
|
|
|
|
</tr> |
|
|
|
|
{:else} |
|
|
|
|
<tr onclick={e => {detail = time.id}}> |
|
|
|
|
<td class="start_end"> |
|
|
|
|
<td class="start_end" onclick={e => toggleSelect(time.id)}> |
|
|
|
|
{time.start_time}{#if time.end_time}…{time.end_time}{/if} |
|
|
|
|
</td> |
|
|
|
|
<td class="duration">{#if time.duration} |
|
|
|
|
<td class="duration" onclick={e => {detail = time.id}}> |
|
|
|
|
{#if time.duration} |
|
|
|
|
{time.duration.toFixed(3)} h |
|
|
|
|
{/if} |
|
|
|
|
</td> |
|
|
|
|
<td class="subject"> |
|
|
|
|
<td class="subject" onclick={e => {detail = time.id}}> |
|
|
|
|
{time.subject} |
|
|
|
|
</td> |
|
|
|
|
<td class="tasks"> |
|
|
|
|
<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"> |
|
|
|
|
<td class="state" onclick={e => {detail = time.id}}> |
|
|
|
|
{t("state_"+time.state.name.toLowerCase())} |
|
|
|
|
</td> |
|
|
|
|
</tr> |
|
|
|
|
{/if} |
|
|
|
|
</tr> |
|
|
|
|
{/each} |
|
|
|
|
</tbody> |
|
|
|
|
</table> |
|
|
|
|
|