summaryrefslogtreecommitdiff
path: root/internal/tui/shared.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui/shared.go')
-rw-r--r--internal/tui/shared.go225
1 files changed, 225 insertions, 0 deletions
diff --git a/internal/tui/shared.go b/internal/tui/shared.go
new file mode 100644
index 0000000..77b282d
--- /dev/null
+++ b/internal/tui/shared.go
@@ -0,0 +1,225 @@
+package tui
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "fmt"
+ "time"
+
+ "punchcard/internal/queries"
+
+ "github.com/charmbracelet/lipgloss/v2"
+)
+
+var (
+ // Styles for the TUI
+ topBarInactiveStyle = lipgloss.NewStyle().
+ Background(lipgloss.Color("21")).
+ Foreground(lipgloss.Color("230")).
+ Padding(0, 1)
+
+ bottomBarStyle = lipgloss.NewStyle().
+ Background(lipgloss.Color("238")).
+ Foreground(lipgloss.Color("252")).
+ Padding(0, 1)
+
+ // Box styles
+ selectedBoxStyle = lipgloss.NewStyle().
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(lipgloss.Color("62")).
+ Padding(1, 2)
+
+ unselectedBoxStyle = lipgloss.NewStyle().
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(lipgloss.Color("238")).
+ Padding(1, 2)
+
+ activeTimerStyle = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("196")).
+ Bold(true)
+
+ inactiveTimerStyle = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("246"))
+)
+
+// FormatDuration formats a duration in a human-readable way
+func FormatDuration(d time.Duration) string {
+ d = d.Round(time.Second)
+ hours := int(d.Hours())
+ minutes := int(d.Minutes()) % 60
+ seconds := int(d.Seconds()) % 60
+
+ if hours > 0 {
+ return fmt.Sprintf("%dh %02dm", hours, minutes)
+ }
+ if minutes > 0 {
+ return fmt.Sprintf("%dm %02ds", minutes, seconds)
+ }
+ return fmt.Sprintf("%ds", seconds)
+}
+
+// GetTimeStats retrieves today's and week's time statistics
+func GetTimeStats(ctx context.Context, q *queries.Queries) (TimeStats, error) {
+ var stats TimeStats
+
+ // Get today's total
+ todaySeconds, err := q.GetTodaySummary(ctx)
+ if err != nil && !errors.Is(err, sql.ErrNoRows) {
+ return stats, fmt.Errorf("failed to get today's summary: %w", err)
+ }
+ if err == nil {
+ stats.TodayTotal = time.Duration(todaySeconds) * time.Second
+ }
+
+ // Get week's total
+ weekSummary, err := q.GetWeekSummaryByProject(ctx)
+ if err != nil {
+ return stats, fmt.Errorf("failed to get week summary: %w", err)
+ }
+
+ var weekTotal time.Duration
+ for _, row := range weekSummary {
+ weekTotal += time.Duration(row.TotalSeconds) * time.Second
+ }
+ stats.WeekTotal = weekTotal
+
+ return stats, nil
+}
+
+// GetTimerInfo retrieves current timer information
+func GetTimerInfo(ctx context.Context, q *queries.Queries) (TimerInfo, error) {
+ var info TimerInfo
+
+ activeEntry, err := q.GetActiveTimeEntry(ctx)
+ if err != nil && !errors.Is(err, sql.ErrNoRows) {
+ return info, fmt.Errorf("failed to get active timer: %w", err)
+ }
+
+ if errors.Is(err, sql.ErrNoRows) {
+ // No active timer
+ return info, nil
+ }
+
+ // Active timer found
+ info.IsActive = true
+ info.StartTime = activeEntry.StartTime
+ info.Duration = time.Since(activeEntry.StartTime)
+
+ // Get client information
+ client, err := q.FindClient(ctx, queries.FindClientParams{
+ ID: activeEntry.ClientID,
+ Name: "",
+ })
+ if err == nil && len(client) > 0 {
+ info.ClientName = client[0].Name
+ if client[0].BillableRate.Valid {
+ rate := float64(client[0].BillableRate.Int64) / 100.0
+ info.BillableRate = &rate
+ }
+ }
+
+ // Get project information if exists
+ if activeEntry.ProjectID.Valid {
+ project, err := q.FindProject(ctx, queries.FindProjectParams{
+ ID: activeEntry.ProjectID.Int64,
+ Name: "",
+ })
+ if err == nil && len(project) > 0 {
+ info.ProjectName = project[0].Name
+ if project[0].BillableRate.Valid {
+ projectRate := float64(project[0].BillableRate.Int64) / 100.0
+ info.BillableRate = &projectRate
+ }
+ }
+ }
+
+ // Get description
+ if activeEntry.Description.Valid {
+ info.Description = activeEntry.Description.String
+ }
+
+ // Use entry-specific billable rate if set
+ if activeEntry.BillableRate.Valid {
+ entryRate := float64(activeEntry.BillableRate.Int64) / 100.0
+ info.BillableRate = &entryRate
+ }
+
+ return info, nil
+}
+
+// RenderTopBar renders the top bar with view name and time stats
+func RenderTopBar(viewName string, stats TimeStats, width int) string {
+ left := viewName
+ right := fmt.Sprintf("Today: %s | Week: %s",
+ FormatDuration(stats.TodayTotal),
+ FormatDuration(stats.WeekTotal))
+
+ // Use lipgloss to create left and right aligned content
+ leftStyle := lipgloss.NewStyle().Align(lipgloss.Left)
+ rightStyle := lipgloss.NewStyle().Align(lipgloss.Right)
+
+ // Calculate available width for content (minus padding)
+ contentWidth := width - 2 // Account for horizontal padding
+
+ // Create a layout with left and right content
+ content := lipgloss.JoinHorizontal(
+ lipgloss.Top,
+ leftStyle.Width(contentWidth/2).Render(left),
+ rightStyle.Width(contentWidth/2).Render(right),
+ )
+
+ return topBarInactiveStyle.Width(width).Render(content)
+}
+
+// RenderBottomBar renders the bottom bar with key bindings
+func RenderBottomBar(bindings []KeyBinding, width int) string {
+ var content string
+ for i, binding := range bindings {
+ if i > 0 {
+ content += " "
+ }
+ // Format key with bold and square brackets
+ keyStyle := lipgloss.NewStyle().Bold(true)
+ formattedKey := keyStyle.Render(fmt.Sprintf("[%s]", binding.Key))
+ content += fmt.Sprintf("%s %s", formattedKey, binding.Description)
+ }
+
+ return bottomBarStyle.Width(width).Render(content)
+}
+
+// GetAppData fetches all data needed for the TUI
+func GetAppData(ctx context.Context, q *queries.Queries) (TimerInfo, TimeStats, []queries.Client, []queries.ListAllProjectsRow, []queries.TimeEntry, error) {
+ // Get timer info
+ timerInfo, err := GetTimerInfo(ctx, q)
+ if err != nil {
+ return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get timer info: %w", err)
+ }
+
+ // Get time stats
+ stats, err := GetTimeStats(ctx, q)
+ if err != nil {
+ return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get time stats: %w", err)
+ }
+
+ // Get clients
+ clients, err := q.ListAllClients(ctx)
+ if err != nil {
+ return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get clients: %w", err)
+ }
+
+ // Get projects
+ projects, err := q.ListAllProjects(ctx)
+ if err != nil {
+ return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get projects: %w", err)
+ }
+
+ // Get recent entries
+ entries, err := q.GetRecentTimeEntries(ctx, 20)
+ if err != nil {
+ return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get recent entries: %w", err)
+ }
+
+ return timerInfo, stats, clients, projects, entries, nil
+}
+