From 01fad04c9be92af153ed82cfd61fee49fe283d61 Mon Sep 17 00:00:00 2001 From: T Date: Tue, 2 Sep 2025 11:17:44 -0600 Subject: history pane: summary line with active filters and total duration --- internal/tui/app.go | 2 +- internal/tui/history_box.go | 98 +++++++++++++++++++++++++++++++++++++++++++-- 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, " - ") +} -- cgit v1.2.3