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:
Skruecken
2026-10-19 11:05:10 +02:00
co-authored by Cursor
parent 41feba11de
commit bfc1cbd4d9
23 changed files with 2149 additions and 94 deletions
+21 -12
View File
@@ -3,6 +3,7 @@ package views
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
@@ -25,12 +26,13 @@ type TicketListStyles struct {
// TicketList is a custom viewport list of ticket cards.
type TicketList struct {
Items []ticket.Ticket
Cursor int
Offset int
Width int
Height int
Styles TicketListStyles
Items []ticket.Ticket
Cursor int
Offset int
Width int
Height int
Styles TicketListStyles
SpentByID map[string]time.Duration // optional; shown on selected card
}
// SetSize updates the viewport dimensions.
@@ -124,12 +126,16 @@ func (l *TicketList) renderCard(t ticket.Ticket, selected bool) string {
var content string
if selected {
// Expanded: headline + glamour-rendered description (fixed dark style).
// Expanded: headline + optional spent + glamour description.
content = headerLine
if spent, ok := l.SpentByID[t.ID]; ok && spent > 0 {
content += "\n" + lipgloss.NewStyle().Faint(true).Render(
"spent: "+ticket.FormatDurationHuman(spent),
)
}
desc := strings.TrimSpace(t.Description)
if desc != "" {
content = headerLine + "\n" + RenderMarkdown(desc, innerWidth)
} else {
content = headerLine
content += "\n" + RenderMarkdown(desc, innerWidth)
}
} else {
content = headerLine
@@ -190,8 +196,11 @@ func (l *TicketList) estimateHeight(index int) int {
if index < 0 || index >= len(l.Items) {
return CollapsedCardHeight
}
if index == l.Cursor && strings.TrimSpace(l.Items[index].Description) != "" {
return lipgloss.Height(l.renderCard(l.Items[index], true))
if index == l.Cursor {
t := l.Items[index]
if strings.TrimSpace(t.Description) != "" || (l.SpentByID != nil && l.SpentByID[t.ID] > 0) {
return lipgloss.Height(l.renderCard(t, true))
}
}
return CollapsedCardHeight
}