Files
task-master/internal/ticket/time.go
T
SkrueckenandCursor bfc1cbd4d9 Add ticket time tracking with reports and entry management.
Support start/stop timer, manual durations, grouped reports, and per-ticket edit/delete of time entries with undo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 11:05:10 +02:00

106 lines
2.6 KiB
Go

package ticket
import (
"encoding/json"
"fmt"
"time"
)
const (
SourceTimer = "timer"
SourceManual = "manual"
)
// TimeEntry is a recorded block of work on a ticket.
type TimeEntry struct {
ID string `json:"id"`
TicketID string `json:"ticketId"`
ProjectID string `json:"projectId"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Duration time.Duration `json:"duration"`
Source string `json:"source"`
Note string `json:"note,omitempty"`
}
// MarshalJSON encodes Duration as nanoseconds for stable JSON.
func (e TimeEntry) MarshalJSON() ([]byte, error) {
type alias struct {
ID string `json:"id"`
TicketID string `json:"ticketId"`
ProjectID string `json:"projectId"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Duration int64 `json:"duration"`
Source string `json:"source"`
Note string `json:"note,omitempty"`
}
return json.Marshal(alias{
ID: e.ID,
TicketID: e.TicketID,
ProjectID: e.ProjectID,
Start: e.Start,
End: e.End,
Duration: int64(e.Duration),
Source: e.Source,
Note: e.Note,
})
}
// UnmarshalJSON decodes Duration from nanoseconds.
func (e *TimeEntry) UnmarshalJSON(data []byte) error {
type alias struct {
ID string `json:"id"`
TicketID string `json:"ticketId"`
ProjectID string `json:"projectId"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Duration int64 `json:"duration"`
Source string `json:"source"`
Note string `json:"note,omitempty"`
}
var a alias
if err := json.Unmarshal(data, &a); err != nil {
return err
}
e.ID = a.ID
e.TicketID = a.TicketID
e.ProjectID = a.ProjectID
e.Start = a.Start
e.End = a.End
e.Duration = time.Duration(a.Duration)
e.Source = a.Source
e.Note = a.Note
return nil
}
// ActiveTimer is a running stopwatch persisted separately until stop.
type ActiveTimer struct {
TicketID string `json:"ticketId"`
ProjectID string `json:"projectId"`
StartedAt time.Time `json:"startedAt"`
}
// FormatDurationHuman returns a compact duration like 2h15m or 45m.
func FormatDurationHuman(d time.Duration) string {
if d < 0 {
d = 0
}
d = d.Round(time.Second)
h := d / time.Hour
m := (d % time.Hour) / time.Minute
s := (d % time.Minute) / time.Second
switch {
case h > 0 && m > 0:
return fmt.Sprintf("%dh%dm", h, m)
case h > 0:
return fmt.Sprintf("%dh", h)
case m > 0 && s > 0 && m < 5:
return fmt.Sprintf("%dm%ds", m, s)
case m > 0:
return fmt.Sprintf("%dm", m)
default:
return fmt.Sprintf("%ds", s)
}
}