summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-09-02 11:17:44 -0600
committerT <t@tjp.lol>2025-09-02 11:37:19 -0600
commit01fad04c9be92af153ed82cfd61fee49fe283d61 (patch)
treeed6bd6d4fb93172a8b77b346d797c6dd5ad150a7
parentadd7c1a8126733dd86282f443dc53127888c06af (diff)
history pane: summary line with active filters and total duration
-rw-r--r--internal/tui/app.go2
-rw-r--r--internal/tui/history_box.go98
2 files changed, 96 insertions, 4 deletions
diff --git a/internal/tui/app.go b/internal/tui/app.go
index 6bcc77d..6031adb 100644
--- a/internal/tui/app.go
+++ b/internal/tui/app.go
@@ -407,7 +407,7 @@ func (m AppModel) View() string {
timerBox := m.timerBox.View(timerBoxWidth, timerBoxHeight, m.selectedBox == TimerBox)
projectsBox := m.projectsBox.View(projectsBoxWidth, projectsBoxHeight, m.selectedBox == ProjectsBox)
- historyBox := m.historyBox.View(historyBoxWidth, historyBoxHeight, m.selectedBox == HistoryBox, m.timerBox)
+ historyBox := m.historyBox.View(historyBoxWidth, historyBoxHeight, m.selectedBox == HistoryBox, m.timerBox, m.projectsBox.clients, m.projectsBox.projects)
leftColumn := lipgloss.JoinVertical(lipgloss.Left, timerBox, projectsBox)
mainContent := lipgloss.JoinHorizontal(lipgloss.Top, leftColumn, historyBox)
diff --git a/internal/tui/history_box.go b/internal/tui/history_box.go
index b4cccf2..769843f 100644
--- a/internal/tui/history_box.go
+++ b/internal/tui/history_box.go
@@ -4,6 +4,7 @@ import (
"fmt"
"slices"
"strconv"
+ "strings"
"time"
"git.tjp.lol/punchcard/internal/queries"
@@ -43,6 +44,9 @@ type HistoryBoxModel struct {
entries map[HistorySummaryKey][]queries.TimeEntry
detailSelection int
+
+ // Total duration of all entries in current filter, excluding active timer
+ totalDuration time.Duration
}
// HistorySummaryItem represents a date + client/project combination with total duration
@@ -118,6 +122,7 @@ func (m *HistoryBoxModel) regenerateSummaries(
return HistorySummaryKey{dateOnly(entry.StartTime.Local()), entry.ClientID, projectID}
})
+ m.totalDuration = 0
for key, entries := range m.entries {
var totalDur time.Duration = 0
for _, entry := range entries {
@@ -145,6 +150,7 @@ func (m *HistoryBoxModel) regenerateSummaries(
}
m.summaryItems = append(m.summaryItems, item)
+ m.totalDuration += totalDur
}
slices.SortFunc(m.summaryItems, func(a, b HistorySummaryItem) int {
@@ -174,7 +180,7 @@ func (m *HistoryBoxModel) regenerateSummaries(
}
// View renders the history box
-func (m HistoryBoxModel) View(width, height int, isSelected bool, timer TimerBoxModel) string {
+func (m HistoryBoxModel) View(width, height int, isSelected bool, timer TimerBoxModel, clients []queries.Client, projects map[int64][]queries.Project) string {
var content string
if len(m.entries) == 0 {
@@ -183,7 +189,7 @@ func (m HistoryBoxModel) View(width, height int, isSelected bool, timer TimerBox
} else {
switch m.viewLevel {
case HistoryLevelSummary:
- content = m.renderSummaryView(timer)
+ content = m.renderSummaryView(timer, clients, projects)
case HistoryLevelDetails:
content = m.renderDetailsView(timer)
}
@@ -219,6 +225,11 @@ func (m HistoryBoxModel) selectionHeight() int {
func (m HistoryBoxModel) summarySelectionHeight() int {
height := 1 // "Recent History" title line
+
+ if len(m.summaryItems) > 0 {
+ height += 3 // 2 newlines + filter info line
+ }
+
var date *time.Time
for i, item := range m.summaryItems {
if date == nil || !date.Equal(item.Date) {
@@ -257,12 +268,18 @@ var (
selectedActiveEntryStyle = lipgloss.NewStyle().Background(lipgloss.Color("196")).Foreground(lipgloss.Color("230"))
descriptionStyle = lipgloss.NewStyle()
activeDescriptionStyle = lipgloss.NewStyle().Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
+ filterInfoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("248"))
)
// renderSummaryView renders the summary view (level 1) with date headers and client/project summaries
-func (m HistoryBoxModel) renderSummaryView(timer TimerBoxModel) string {
+func (m HistoryBoxModel) renderSummaryView(timer TimerBoxModel, clients []queries.Client, projects map[int64][]queries.Project) string {
content := titleStyle.Render("📝 Recent History")
+ if len(m.summaryItems) > 0 {
+ filterInfo := m.formatFilterInfo(clients, projects, timer)
+ content += "\n\n" + filterInfoStyle.Render(filterInfo)
+ }
+
var activeKey HistorySummaryKey
if timer.timerInfo.IsActive {
activeKey = HistorySummaryKey{
@@ -522,3 +539,78 @@ func (m *HistoryBoxModel) recheckBounds() {
}
}
}
+
+// formatFilterInfo formats the filter criteria line showing client/project, time range, and total duration
+func (m HistoryBoxModel) formatFilterInfo(clients []queries.Client, projects map[int64][]queries.Project, timer TimerBoxModel) string {
+ var parts []string
+
+ if m.filter.ClientID != nil {
+ clientName := ""
+ for _, client := range clients {
+ if client.ID == *m.filter.ClientID {
+ clientName = client.Name
+ break
+ }
+ }
+
+ if m.filter.ProjectID != nil {
+ projectName := ""
+ if clientProjects, ok := projects[*m.filter.ClientID]; ok {
+ for _, project := range clientProjects {
+ if project.ID == *m.filter.ProjectID {
+ projectName = project.Name
+ break
+ }
+ }
+ }
+ parts = append(parts, fmt.Sprintf("%s / %s", clientName, projectName))
+ } else {
+ parts = append(parts, clientName)
+ }
+ }
+
+ if m.filter.EndDate != nil {
+ parts = append(parts, fmt.Sprintf("%s to %s",
+ m.filter.StartDate.Format(time.DateOnly),
+ m.filter.EndDate.Format(time.DateOnly)))
+ } else {
+ parts = append(parts, fmt.Sprintf("since %s", m.filter.StartDate.Format(time.DateOnly)))
+ }
+
+ // Start with cached total (excluding active timer), then add active timer if it matches filter
+ totalDur := m.totalDuration
+ if timer.timerInfo.IsActive {
+ // Check if active timer matches current filter criteria
+ matchesFilter := true
+
+ // Check client filter
+ if m.filter.ClientID != nil && *m.filter.ClientID != timer.timerInfo.ClientID {
+ matchesFilter = false
+ }
+
+ // Check project filter
+ if matchesFilter && m.filter.ProjectID != nil {
+ if timer.timerInfo.ProjectID == nil || *timer.timerInfo.ProjectID != *m.filter.ProjectID {
+ matchesFilter = false
+ }
+ }
+
+ // Check date filter
+ if matchesFilter {
+ startTime := timer.timerInfo.StartTime.Local()
+ if startTime.Before(m.filter.StartDate) {
+ matchesFilter = false
+ }
+ if matchesFilter && m.filter.EndDate != nil && startTime.After(*m.filter.EndDate) {
+ matchesFilter = false
+ }
+ }
+
+ if matchesFilter {
+ totalDur += timer.currentTime.Sub(timer.timerInfo.StartTime)
+ }
+ }
+ parts = append(parts, FormatDuration(totalDur))
+
+ return strings.Join(parts, " - ")
+}