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:
+92
-11
@@ -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>
|
||||
|
||||
@@ -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 }) }),
|
||||
|
||||
Reference in New Issue
Block a user