summaryrefslogtreecommitdiff
path: root/internal/tui/timer_box.go
blob: 2b0c34acb7b0b6e3cc379dcfd04f7dc59cef8f4b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package tui

import (
	"time"

	"git.tjp.lol/punchcard/internal/queries"

	"github.com/charmbracelet/lipgloss/v2"
)

// TimerInfo holds information about the current or most recent timer state
type TimerInfo struct {
	IsActive     bool
	EntryID      int64
	StartTime    time.Time
	Duration     time.Duration
	ClientID     int64
	ClientName   string
	ProjectID    *int64
	ProjectName  string
	Description  *string
	BillableRate *float64
}

func (ti *TimerInfo) setNames(clients []queries.Client, projects map[int64][]queries.Project) {
	for _, cl := range clients {
		if cl.ID == ti.ClientID {
			ti.ClientName = cl.Name
			break
		}
	}

	if ti.ProjectID == nil {
		return
	}
	for _, group := range projects {
		for _, proj := range group {
			if proj.ID == *ti.ProjectID {
				ti.ProjectName = proj.Name
				return
			}
		}
	}
}

// Box models for the three main components
type TimerBoxModel struct {
	timerInfo   TimerInfo
	currentTime time.Time
}

// NewTimerBoxModel creates a new timer box model
func NewTimerBoxModel() TimerBoxModel {
	return TimerBoxModel{
		currentTime: time.Now(),
	}
}

// View renders the timer box
func (m TimerBoxModel) View(width, height int, isSelected bool) string {
	var content string

	if m.timerInfo.IsActive {
		content = m.renderActiveTimer()
	} else {
		content = m.renderInactiveTimer()
	}

	// Apply box styling
	var style lipgloss.Style
	if isSelected {
		style = selectedBoxStyle
	} else if m.timerInfo.IsActive {
		style = activeBoxStyle
	} else {
		style = unselectedBoxStyle
	}

	return style.Width(width).Height(height).Render(content)
}

// renderActiveTimer renders the active timer display
func (m TimerBoxModel) renderActiveTimer() string {
	statusStyle := lipgloss.NewStyle().Foreground(colorTimerActive).Bold(true)
	content := statusStyle.Render("TRACKING") + "\n\n"

	// Timer duration - big and bold
	dur := FormatDuration(m.currentTime.Sub(m.timerInfo.StartTime))
	content += activeTimerStyle.Render(dur) + "\n\n"

	// Project/Client info
	dimLabel := lipgloss.NewStyle().Foreground(colorDimmed)
	if m.timerInfo.ProjectName != "" {
		content += dimLabel.Render(m.timerInfo.ClientName+" /") + "\n"
		content += m.timerInfo.ProjectName + "\n"
	} else {
		content += m.timerInfo.ClientName + "\n"
	}

	// Start time
	localStartTime := m.timerInfo.StartTime.Local()
	content += dimLabel.Render("since " + localStartTime.Format("3:04 PM"))

	return content
}

// renderInactiveTimer renders the inactive timer display
func (m TimerBoxModel) renderInactiveTimer() string {
	statusStyle := lipgloss.NewStyle().Foreground(colorDimmed)
	content := statusStyle.Render("IDLE") + "\n\n"

	if m.timerInfo.EntryID == 0 {
		content += inactiveTimerStyle.Render("No entries yet.\nPunch in to start.")
		return content
	}

	content += inactiveTimerStyle.Render(FormatDuration(m.timerInfo.Duration)) + "\n\n"

	dimLabel := lipgloss.NewStyle().Foreground(colorDimmed)
	if m.timerInfo.ProjectName != "" {
		content += dimLabel.Render(m.timerInfo.ClientName+" /") + "\n"
		content += inactiveTimerStyle.Render(m.timerInfo.ProjectName)
	} else {
		content += inactiveTimerStyle.Render(m.timerInfo.ClientName)
	}

	return content
}

func (m TimerBoxModel) activeTime() time.Duration {
	if !m.timerInfo.IsActive {
		return 0
	}
	return m.currentTime.Sub(m.timerInfo.StartTime)
}