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
+43 -21
View File
@@ -88,6 +88,7 @@ type Model struct {
timeEntries views.TimeEntriesView
timeEntriesTicket string // ticket ID for ModeTimeEntries
editingTimeEntry ticket.TimeEntry
timeEdit views.TimeEditForm
activeTimer *ticket.ActiveTimer
spentByID map[string]time.Duration
width int
@@ -207,8 +208,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch m.mode {
case ModeCreate, ModeSearch, ModeTimeManual, ModeTimeEdit:
case ModeCreate, ModeSearch, ModeTimeManual:
return m.updateInput(msg)
case ModeTimeEdit:
return m.updateTimeEdit(msg)
case ModeEditProject:
return m.updateEditProject(msg)
case ModeProjects:
@@ -238,8 +241,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m.mode {
case ModeProjects, ModeMoveTicket:
m.projectList, cmd = m.projectList.Update(msg)
case ModeCreate, ModeSearch, ModeTimeManual, ModeTimeEdit:
case ModeCreate, ModeSearch, ModeTimeManual:
m.input, cmd = m.input.Update(msg)
case ModeTimeEdit:
cmd = m.timeEdit.Update(msg)
case ModeEditProject:
cmd = m.projectEdit.Update(msg)
}
@@ -585,8 +590,6 @@ func (m Model) updateInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch {
case wasProject:
m.mode = ModeProjects
case m.mode == ModeTimeEdit:
m.mode = ModeTimeEntries
default:
m.mode = ModeList
}
@@ -608,16 +611,6 @@ func (m Model) updateInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, m.keys.Enter):
value := strings.TrimSpace(m.input.Value())
if m.mode == ModeTimeEdit {
if err := m.saveEditedTime(value); err != nil {
m.setError(err)
return m, nil
}
m.input.Blur()
m.mode = ModeTimeEntries
m.setStatus("time updated")
return m, nil
}
if m.mode == ModeTimeManual {
if err := m.addManualTime(value); err != nil {
m.setError(err)
@@ -708,10 +701,8 @@ func (m Model) updateTimeEntries(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
m.editingTimeEntry = e
m.timeEdit = views.NewTimeEditForm(e, m.width)
m.mode = ModeTimeEdit
m.input = views.NewTimeManualInput()
m.input.SetValue(ticket.FormatDurationHuman(e.Duration))
m.input.Focus()
return m, textinput.Blink
case key.Matches(msg, m.keys.Undo):
@@ -1717,18 +1708,49 @@ func (m *Model) deleteRemoteWorkItem(e ticket.TimeEntry) error {
return nil
}
func (m *Model) saveEditedTime(raw string) error {
func (m Model) updateTimeEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, m.keys.Escape):
m.mode = ModeTimeEntries
m.clearStatus()
return m, nil
case key.Matches(msg, m.keys.Tab), msg.Type == tea.KeyDown:
m.timeEdit.NextField()
return m, textinput.Blink
case key.Matches(msg, m.keys.ShiftTab), msg.Type == tea.KeyUp:
m.timeEdit.PrevField()
return m, textinput.Blink
case key.Matches(msg, m.keys.Enter):
m.clearStatus()
if err := m.saveEditedTime(); err != nil {
m.setError(err)
return m, nil
}
m.mode = ModeTimeEntries
m.setStatus("time updated")
return m, nil
}
cmd := m.timeEdit.Update(msg)
return m, cmd
}
func (m *Model) saveEditedTime() error {
if m.timeStore == nil {
return fmt.Errorf("time store not configured")
}
dur, err := ticket.ParseDurationFlexible(raw)
start, end, err := m.timeEdit.Parse()
if err != nil {
return err
}
before := m.editingTimeEntry
after := before
after.Duration = dur
after.End = after.Start.Add(dur)
after.Start = start
after.End = end
after.Duration = end.Sub(start)
if err := m.exec.Do(&command.UpdateTimeEntry{
Store: m.timeStore,
Before: before,