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) } }