Files
task-master/internal/ticket/time.go
T
skrueckenandCursor 98d2253f44
CI / test-and-build (push) Failing after 1m19s
CI / demo (push) Skipped
Add YouTrack pull/push sync with project settings and time tracking.
Wire manual Y menu and CLI sync for issues and work items, persist
per-project credentials, and keep local edits until an explicit push.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-24 00:17:51 +02:00

117 lines
3.0 KiB
Go

package ticket
import (
"encoding/json"
"fmt"
"time"
)
const (
SourceTimer = "timer"
SourceManual = "manual"
)
const (
// SourceYouTrackTime marks time entries imported from YouTrack work items.
SourceYouTrackTime = "youtrack"
)
// 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"`
// ExternalID is the YouTrack work-item id when synced.
ExternalID string `json:"externalId,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"`
ExternalID string `json:"externalId,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,
ExternalID: e.ExternalID,
})
}
// 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"`
ExternalID string `json:"externalId,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
e.ExternalID = a.ExternalID
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)
}
}