package tui import ( "fmt" "time" "punchcard/internal/queries" ) // 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 style := unselectedBoxStyle if isSelected { style = selectedBoxStyle } return style.Width(width).Height(height).Render(content) } // renderActiveTimer renders the active timer display func (m TimerBoxModel) renderActiveTimer() string { content := titleStyle.Render("⏱ Active Timer") + "\n\n" // Timer duration timerLine := fmt.Sprintf("Duration: %s", FormatDuration(m.currentTime.Sub(m.timerInfo.StartTime))) content += activeTimerStyle.Render(timerLine) + "\n\n" // Project/Client info if m.timerInfo.ProjectName != "" { projectLine := fmt.Sprintf("Project: %s / %s", m.timerInfo.ClientName, m.timerInfo.ProjectName) content += projectLine + "\n" } else { clientLine := fmt.Sprintf("Client: %s", m.timerInfo.ClientName) content += clientLine + "\n" } // Start time (convert from UTC to local) localStartTime := m.timerInfo.StartTime.Local() startLine := fmt.Sprintf("Started: %s", localStartTime.Format("3:04 PM")) content += startLine + "\n" // Description if available if m.timerInfo.Description != nil { content += "\n" descLine := fmt.Sprintf("Description: %s", *m.timerInfo.Description) content += descLine + "\n" } // Billable rate if available if m.timerInfo.BillableRate != nil { rateLine := fmt.Sprintf("Rate: $%.2f/hr", *m.timerInfo.BillableRate) content += rateLine + "\n" } return content } // renderInactiveTimer renders the inactive timer display func (m TimerBoxModel) renderInactiveTimer() string { content := titleStyle.Render("⚪ Last Timer (Inactive)") + "\n\n" timerLine := fmt.Sprintf("Duration: %s", FormatDuration(m.timerInfo.Duration)) content += inactiveTimerStyle.Render(timerLine) + "\n\n" if m.timerInfo.ProjectName != "" { content += inactiveTimerStyle.Render(fmt.Sprintf("Project: %s / %s", m.timerInfo.ClientName, m.timerInfo.ProjectName)) + "\n" } else { content += inactiveTimerStyle.Render(fmt.Sprintf("Client: %s", m.timerInfo.ClientName)) + "\n" } content += inactiveTimerStyle.Render(fmt.Sprintf("Started: %s", m.timerInfo.StartTime.Local().Format("3:04 PM"))) + "\n" if m.timerInfo.Description != nil { content += "\n" + inactiveTimerStyle.Render(fmt.Sprintf("Description: %s", *m.timerInfo.Description)) + "\n" } if m.timerInfo.BillableRate != nil { content += inactiveTimerStyle.Render(fmt.Sprintf("Rate: $%.2f/hr", *m.timerInfo.BillableRate)) + "\n" } return content } func (m TimerBoxModel) activeTime() time.Duration { if !m.timerInfo.IsActive { return 0 } return m.currentTime.Sub(m.timerInfo.StartTime) }