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,72 @@
|
||||
package ticket_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"task/internal/ticket"
|
||||
)
|
||||
|
||||
func TestAggregateByProject(t *testing.T) {
|
||||
entries := []ticket.TimeEntry{
|
||||
{TicketID: "a", ProjectID: "p1", Duration: time.Hour},
|
||||
{TicketID: "b", ProjectID: "p1", Duration: 30 * time.Minute},
|
||||
{TicketID: "c", ProjectID: "p2", Duration: 15 * time.Minute},
|
||||
}
|
||||
rows := ticket.Aggregate(entries, nil, ticket.GroupByProject)
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("rows=%d want 2", len(rows))
|
||||
}
|
||||
if rows[0].Key != "p1" || rows[0].Duration != 90*time.Minute {
|
||||
t.Fatalf("first row: %+v", rows[0])
|
||||
}
|
||||
if ticket.TotalDuration(entries) != 105*time.Minute {
|
||||
t.Fatalf("total=%v", ticket.TotalDuration(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateByStatusAndTicket(t *testing.T) {
|
||||
tickets := map[string]ticket.Ticket{
|
||||
"a": {ID: "a", Title: "Alpha", Status: ticket.StatusTodo},
|
||||
"b": {ID: "b", Title: "Beta", Status: ticket.StatusDone},
|
||||
}
|
||||
entries := []ticket.TimeEntry{
|
||||
{TicketID: "a", Duration: time.Hour},
|
||||
{TicketID: "b", Duration: 20 * time.Minute},
|
||||
{TicketID: "a", Duration: 10 * time.Minute},
|
||||
}
|
||||
|
||||
byStatus := ticket.Aggregate(entries, tickets, ticket.GroupByStatus)
|
||||
if len(byStatus) != 2 {
|
||||
t.Fatalf("status rows=%d", len(byStatus))
|
||||
}
|
||||
|
||||
byTicket := ticket.Aggregate(entries, tickets, ticket.GroupByTicket)
|
||||
if len(byTicket) != 2 {
|
||||
t.Fatalf("ticket rows=%d", len(byTicket))
|
||||
}
|
||||
if byTicket[0].Label != "Alpha" || byTicket[0].Duration != 70*time.Minute {
|
||||
t.Fatalf("top ticket: %+v", byTicket[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateByDay(t *testing.T) {
|
||||
d1 := time.Date(2026, 10, 18, 10, 0, 0, 0, time.Local)
|
||||
d2 := time.Date(2026, 10, 19, 11, 0, 0, 0, time.Local)
|
||||
entries := []ticket.TimeEntry{
|
||||
{Start: d1, Duration: time.Hour},
|
||||
{Start: d2, Duration: 30 * time.Minute},
|
||||
{Start: d1, Duration: 15 * time.Minute},
|
||||
}
|
||||
rows := ticket.Aggregate(entries, nil, ticket.GroupByDay)
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("day rows=%d", len(rows))
|
||||
}
|
||||
// Newest day first.
|
||||
if rows[0].Key != "2026-10-19" {
|
||||
t.Fatalf("first day key=%q", rows[0].Key)
|
||||
}
|
||||
if rows[1].Duration != 75*time.Minute {
|
||||
t.Fatalf("older day dur=%v", rows[1].Duration)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user