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>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package ticket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
reHourMin = regexp.MustCompile(`(?i)^(\d+)\s*h(?:ours?)?\s*(\d+)\s*m(?:ins?)?$`)
|
||||
reHourOnly = regexp.MustCompile(`(?i)^(\d+(?:\.\d+)?)\s*h(?:ours?)?$`)
|
||||
reMinOnly = regexp.MustCompile(`(?i)^(\d+)\s*m(?:ins?)?$`)
|
||||
reSecOnly = regexp.MustCompile(`(?i)^(\d+)\s*s(?:ecs?)?$`)
|
||||
reColon = regexp.MustCompile(`^(\d+):(\d{1,2})$`)
|
||||
)
|
||||
|
||||
// ParseDurationFlexible parses user durations like 1h30m, 90m, 1.5h, 1:30.
|
||||
func ParseDurationFlexible(s string) (time.Duration, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0, fmt.Errorf("duration required")
|
||||
}
|
||||
// Prefer Go's parser when it works (e.g. 1h30m, 90m).
|
||||
if d, err := time.ParseDuration(strings.ToLower(s)); err == nil && d > 0 {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
if m := reHourMin.FindStringSubmatch(s); m != nil {
|
||||
h, _ := strconv.Atoi(m[1])
|
||||
min, _ := strconv.Atoi(m[2])
|
||||
return time.Duration(h)*time.Hour + time.Duration(min)*time.Minute, nil
|
||||
}
|
||||
if m := reHourOnly.FindStringSubmatch(s); m != nil {
|
||||
h, err := strconv.ParseFloat(m[1], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid duration %q", s)
|
||||
}
|
||||
return time.Duration(h * float64(time.Hour)), nil
|
||||
}
|
||||
if m := reMinOnly.FindStringSubmatch(s); m != nil {
|
||||
min, _ := strconv.Atoi(m[1])
|
||||
return time.Duration(min) * time.Minute, nil
|
||||
}
|
||||
if m := reSecOnly.FindStringSubmatch(s); m != nil {
|
||||
sec, _ := strconv.Atoi(m[1])
|
||||
return time.Duration(sec) * time.Second, nil
|
||||
}
|
||||
if m := reColon.FindStringSubmatch(s); m != nil {
|
||||
h, _ := strconv.Atoi(m[1])
|
||||
min, _ := strconv.Atoi(m[2])
|
||||
if min >= 60 {
|
||||
return 0, fmt.Errorf("invalid minutes in %q", s)
|
||||
}
|
||||
return time.Duration(h)*time.Hour + time.Duration(min)*time.Minute, nil
|
||||
}
|
||||
return 0, fmt.Errorf("invalid duration %q (try 1h30m, 90m, 1.5h)", s)
|
||||
}
|
||||
Reference in New Issue
Block a user