Allow editing time entry start and end in TUI and web.

Duration is derived from the range so timer sessions can be corrected after the fact.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-27 22:59:40 +01:00
co-authored by Cursor
parent afbf86dcf2
commit 79b0821837
10 changed files with 479 additions and 40 deletions
+92 -11
View File
@@ -43,6 +43,9 @@
let memberUserId = $state('')
let memberRole = $state('member')
let timeMinutes = $state(30)
let editingTimeId = $state('')
let editTimeStart = $state('')
let editTimeEnd = $state('')
let tokenName = $state('tui')
let tokenScope = $state('write')
@@ -67,6 +70,64 @@
return `${m}m`
}
function toLocalInput(iso) {
if (!iso) return ''
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return ''
const pad = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
}
async function refreshTicketTime() {
if (!selected) return
const all = await api.listTime(projectId)
ticketTime = all.filter((e) => e.ticketId === selected.id)
}
function beginEditTime(e) {
editingTimeId = e.id
editTimeStart = toLocalInput(e.start)
editTimeEnd = toLocalInput(e.end || (e.start && e.duration
? new Date(new Date(e.start).getTime() + Number(e.duration)).toISOString()
: ''))
}
function cancelEditTime() {
editingTimeId = ''
editTimeStart = ''
editTimeEnd = ''
}
async function saveTimeEntry() {
if (!selected || !editingTimeId) return
if (!editTimeStart || !editTimeEnd) {
error = 'Start and end are required'
return
}
const start = new Date(editTimeStart)
const end = new Date(editTimeEnd)
if (!(end > start)) {
error = 'End must be after start'
return
}
error = ''
await api.patchTime(projectId, editingTimeId, {
start: start.toISOString(),
end: end.toISOString(),
})
cancelEditTime()
await refreshTicketTime()
msg = 'Time updated'
}
async function removeTimeEntry(eid) {
if (!confirm('Delete time entry?')) return
await api.deleteTime(projectId, eid)
if (editingTimeId === eid) cancelEditTime()
await refreshTicketTime()
msg = 'Time deleted'
}
async function doLogin() {
error = ''
try {
@@ -182,6 +243,7 @@
editTitle = t.title
editDesc = t.description || ''
editStatus = t.status
cancelEditTime()
revisions = await api.listRevisions(projectId, t.id)
const all = await api.listTime(projectId)
ticketTime = all.filter((e) => e.ticketId === t.id)
@@ -226,8 +288,7 @@
durationNs: timeMinutes * 60 * 1e9,
source: 'manual',
})
const all = await api.listTime(projectId)
ticketTime = all.filter((e) => e.ticketId === selected.id)
await refreshTicketTime()
msg = `Booked ${timeMinutes}m`
}
@@ -239,8 +300,7 @@
await api.startTimer(selected.id, projectId)
}
await refreshTimer()
const all = await api.listTime(projectId)
ticketTime = all.filter((e) => e.ticketId === selected.id)
await refreshTicketTime()
}
async function openReport() {
@@ -570,16 +630,37 @@
<button onclick={bookTime}>Book</button>
</div>
<table>
<thead><tr><th>When</th><th>Duration</th><th>Source</th></tr></thead>
<thead><tr><th>When</th><th>Duration</th><th>Source</th><th></th></tr></thead>
<tbody>
{#each ticketTime as e}
<tr>
<td class="muted">{e.start ? new Date(e.start).toLocaleString() : '—'}</td>
<td>{formatDuration(e.duration)}</td>
<td>{e.source}</td>
</tr>
{#if editingTimeId === e.id}
<tr>
<td colspan="4">
<div class="row" style="flex-wrap: wrap; gap: 0.5rem;">
<label class="row" style="width: auto;">Start
<input type="datetime-local" bind:value={editTimeStart} style="width: auto;" />
</label>
<label class="row" style="width: auto;">End
<input type="datetime-local" bind:value={editTimeEnd} style="width: auto;" />
</label>
<button class="primary" onclick={saveTimeEntry}>Save</button>
<button onclick={cancelEditTime}>Cancel</button>
</div>
</td>
</tr>
{:else}
<tr>
<td class="muted">{e.start ? new Date(e.start).toLocaleString() : '—'}</td>
<td>{formatDuration(e.duration)}</td>
<td>{e.source}</td>
<td class="row" style="width: auto; justify-content: flex-end;">
<button onclick={() => beginEditTime(e)}>Edit</button>
<button class="danger" onclick={() => removeTimeEntry(e.id)}>Delete</button>
</td>
</tr>
{/if}
{:else}
<tr><td colspan="3" class="muted">No time yet.</td></tr>
<tr><td colspan="4" class="muted">No time yet.</td></tr>
{/each}
</tbody>
</table>
+4
View File
@@ -76,6 +76,10 @@ export const api = {
listTime: (projectId) => request(`/projects/${projectId}/time`),
createTime: (projectId, body) =>
request(`/projects/${projectId}/time`, { method: 'POST', body: JSON.stringify(body) }),
patchTime: (projectId, eid, body) =>
request(`/projects/${projectId}/time/${eid}`, { method: 'PATCH', body: JSON.stringify(body) }),
deleteTime: (projectId, eid) =>
request(`/projects/${projectId}/time/${eid}`, { method: 'DELETE' }),
getTimer: () => request('/timer'),
startTimer: (ticketId, projectId) =>
request('/timer/start', { method: 'POST', body: JSON.stringify({ ticketId, projectId }) }),