package commands import ( "context" "database/sql" "errors" "fmt" "strconv" "time" punchctx "git.tjp.lol/punchcard/internal/context" "git.tjp.lol/punchcard/internal/queries" "github.com/spf13/cobra" ) func NewStatusCmd() *cobra.Command { cmd := &cobra.Command{ Use: "status", Aliases: []string{"st"}, Short: "Show current status and summaries", Long: `Show the current status including: - Current week work summary by project and client - Current month work summary by project and client - Active timer status (if any) - Clients and projects list (use --clients/-c or --projects/-p to show only one type)`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { q := punchctx.GetDB(cmd.Context()) if q == nil { return fmt.Errorf("database not available in context") } ctx := cmd.Context() // Get active timer status first activeEntry, err := q.GetActiveTimeEntry(ctx) var hasActiveTimer bool if err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("failed to check for active timer: %w", err) } hasActiveTimer = (err == nil) // Display active timer status if hasActiveTimer { duration := time.Since(activeEntry.StartTime) cmd.Printf("šŸ”“ Active Timer (running for %v)\n", duration.Round(time.Second)) // Get client info client, err := findClient(ctx, q, strconv.FormatInt(activeEntry.ClientID, 10)) if err != nil { cmd.Printf(" Client: ID %d (error getting name: %v)\n", activeEntry.ClientID, err) } else { clientInfo := client.Name if client.BillableRate.Valid { rateInDollars := float64(client.BillableRate.Int64) / 100.0 clientInfo += fmt.Sprintf(" ($%.2f/hr)", rateInDollars) } cmd.Printf(" Client: %s\n", clientInfo) } // Get project info if exists if activeEntry.ProjectID.Valid { project, err := findProject(ctx, q, strconv.FormatInt(activeEntry.ProjectID.Int64, 10)) if err != nil { cmd.Printf(" Project: ID %d (error getting name: %v)\n", activeEntry.ProjectID.Int64, err) } else { projectInfo := project.Name if project.BillableRate.Valid { rateInDollars := float64(project.BillableRate.Int64) / 100.0 projectInfo += fmt.Sprintf(" ($%.2f/hr)", rateInDollars) } cmd.Printf(" Project: %s\n", projectInfo) } } else { cmd.Printf(" Project: (none)\n") } // Show description if exists if activeEntry.Description.Valid { cmd.Printf(" Description: %s\n", activeEntry.Description.String) } else { cmd.Printf(" Description: (none)\n") } // Show billable rate if it exists on the time entry if activeEntry.BillableRate.Valid { rateInDollars := float64(activeEntry.BillableRate.Int64) / 100.0 cmd.Printf(" Billable Rate: $%.2f/hr\n", rateInDollars) } cmd.Printf("\n") } else { cmd.Printf("⚪ No active timer\n") // Try to show the most recent time entry (will be completed since no active timer) recentEntry, err := q.GetMostRecentTimeEntry(ctx) if err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("failed to get most recent time entry: %w", err) } if err == nil { // Display the most recent entry duration := recentEntry.EndTime.Time.Sub(recentEntry.StartTime) cmd.Printf("\nšŸ“ Most Recent Entry\n") // Get client info client, err := findClient(ctx, q, strconv.FormatInt(recentEntry.ClientID, 10)) if err != nil { cmd.Printf(" Client: ID %d (error getting name: %v)\n", recentEntry.ClientID, err) } else { clientInfo := client.Name if client.BillableRate.Valid { rateInDollars := float64(client.BillableRate.Int64) / 100.0 clientInfo += fmt.Sprintf(" ($%.2f/hr)", rateInDollars) } cmd.Printf(" Client: %s\n", clientInfo) } // Get project info if exists if recentEntry.ProjectID.Valid { project, err := findProject(ctx, q, strconv.FormatInt(recentEntry.ProjectID.Int64, 10)) if err != nil { cmd.Printf(" Project: ID %d (error getting name: %v)\n", recentEntry.ProjectID.Int64, err) } else { projectInfo := project.Name if project.BillableRate.Valid { rateInDollars := float64(project.BillableRate.Int64) / 100.0 projectInfo += fmt.Sprintf(" ($%.2f/hr)", rateInDollars) } cmd.Printf(" Project: %s\n", projectInfo) } } else { cmd.Printf(" Project: (none)\n") } // Show description if exists if recentEntry.Description.Valid { cmd.Printf(" Description: %s\n", recentEntry.Description.String) } else { cmd.Printf(" Description: (none)\n") } // Show billable rate if it exists on the time entry if recentEntry.BillableRate.Valid { rateInDollars := float64(recentEntry.BillableRate.Int64) / 100.0 cmd.Printf(" Billable Rate: $%.2f/hr\n", rateInDollars) } // Show time information cmd.Printf(" Started: %s\n", recentEntry.StartTime.Format("Jan 2, 2006 at 3:04 PM")) cmd.Printf(" Ended: %s\n", recentEntry.EndTime.Time.Format("Jan 2, 2006 at 3:04 PM")) cmd.Printf(" Duration: %v\n", duration.Round(time.Minute)) } cmd.Printf("\n") } // Display clients and projects showClients, _ := cmd.Flags().GetBool("clients") showProjects, _ := cmd.Flags().GetBool("projects") if err := displayClientsAndProjects(ctx, cmd, q, showClients, showProjects); err != nil { return fmt.Errorf("failed to display clients and projects: %w", err) } // Display current week summary if err := displayWeekSummary(ctx, cmd, q); err != nil { return fmt.Errorf("failed to display week summary: %w", err) } // Display current month summary if err := displayMonthSummary(ctx, cmd, q); err != nil { return fmt.Errorf("failed to display month summary: %w", err) } return nil }, } cmd.Flags().BoolP("clients", "c", false, "Show clients list") cmd.Flags().BoolP("projects", "p", false, "Show projects list") return cmd } func displayClientsAndProjects(ctx context.Context, cmd *cobra.Command, q *queries.Queries, showClients, showProjects bool) error { if showClients && showProjects { cmd.Printf("šŸ“‹ Clients & Projects\n") } else if showClients { cmd.Printf("šŸ‘„ Clients\n") } else if showProjects { cmd.Printf("šŸ“ Projects\n") } clients, err := q.ListAllClients(ctx) if err != nil { return fmt.Errorf("failed to get clients: %w", err) } projects, err := q.ListAllProjects(ctx) if err != nil { return fmt.Errorf("failed to get projects: %w", err) } if len(clients) == 0 { cmd.Printf(" No clients found\n\n") return nil } // Group projects by client projectsByClient := make(map[int64][]queries.ListAllProjectsRow) for _, project := range projects { projectsByClient[project.ClientID] = append(projectsByClient[project.ClientID], project) } if showClients && showProjects { // Show clients with their projects nested for _, client := range clients { email := "" if client.Email.Valid { email = fmt.Sprintf(" <%s>", client.Email.String) } rate := "" if client.BillableRate.Valid { rateInDollars := float64(client.BillableRate.Int64) / 100.0 rate = fmt.Sprintf(" - $%.2f/hr", rateInDollars) } cmd.Printf(" • %s%s (ID: %d)%s\n", client.Name, email, client.ID, rate) clientProjects := projectsByClient[client.ID] if len(clientProjects) == 0 { cmd.Printf(" └── (no projects)\n") } else { for i, project := range clientProjects { prefix := "ā”œā”€ā”€" if i == len(clientProjects)-1 { prefix = "└──" } rate := "" if project.BillableRate.Valid { rateInDollars := float64(project.BillableRate.Int64) / 100.0 rate = fmt.Sprintf(" - $%.2f/hr", rateInDollars) } cmd.Printf(" %s %s (ID: %d)%s\n", prefix, project.Name, project.ID, rate) } } } } else if showClients { // Show only clients for _, client := range clients { email := "" if client.Email.Valid { email = fmt.Sprintf(" <%s>", client.Email.String) } rate := "" if client.BillableRate.Valid { rateInDollars := float64(client.BillableRate.Int64) / 100.0 rate = fmt.Sprintf(" - $%.2f/hr", rateInDollars) } cmd.Printf(" • %s%s (ID: %d)%s\n", client.Name, email, client.ID, rate) } } else if showProjects { // Show only projects with their client names for _, project := range projects { rate := "" if project.BillableRate.Valid { rateInDollars := float64(project.BillableRate.Int64) / 100.0 rate = fmt.Sprintf(" - $%.2f/hr", rateInDollars) } cmd.Printf(" • %s (Client: %s, ID: %d)%s\n", project.Name, project.ClientName, project.ID, rate) } } cmd.Printf("\n") return nil } func displayWeekSummary(ctx context.Context, cmd *cobra.Command, q *queries.Queries) error { cmd.Printf("šŸ“… This Week\n") weekSummary, err := q.GetWeekSummaryByProject(ctx) if err != nil { return fmt.Errorf("failed to get week summary: %w", err) } if len(weekSummary) == 0 { cmd.Printf(" No time entries this week\n\n") return nil } // Group by client and calculate totals clientTotals := make(map[int64]time.Duration) currentClientID := int64(-1) for _, row := range weekSummary { duration := time.Duration(row.TotalSeconds) * time.Second clientTotals[row.ClientID] += duration if row.ClientID != currentClientID { if currentClientID != -1 { // Print client total cmd.Printf(" Total: %v\n", clientTotals[currentClientID].Round(time.Minute)) } cmd.Printf(" • %s:\n", row.ClientName) currentClientID = row.ClientID } projectName := "(no project)" if row.ProjectName.Valid { projectName = row.ProjectName.String } cmd.Printf(" - %s: %v\n", projectName, duration.Round(time.Minute)) } // Print final client total if currentClientID != -1 { cmd.Printf(" Total: %v\n", clientTotals[currentClientID].Round(time.Minute)) } // Print grand total var grandTotal time.Duration for _, total := range clientTotals { grandTotal += total } cmd.Printf(" WEEK TOTAL: %v\n\n", grandTotal.Round(time.Minute)) return nil } func displayMonthSummary(ctx context.Context, cmd *cobra.Command, q *queries.Queries) error { cmd.Printf("šŸ“Š This Month\n") monthSummary, err := q.GetMonthSummaryByProject(ctx) if err != nil { return fmt.Errorf("failed to get month summary: %w", err) } if len(monthSummary) == 0 { cmd.Printf(" No time entries this month\n\n") return nil } // Group by client and calculate totals clientTotals := make(map[int64]time.Duration) currentClientID := int64(-1) for _, row := range monthSummary { duration := time.Duration(row.TotalSeconds) * time.Second clientTotals[row.ClientID] += duration if row.ClientID != currentClientID { if currentClientID != -1 { // Print client total cmd.Printf(" Total: %v\n", clientTotals[currentClientID].Round(time.Minute)) } cmd.Printf(" • %s:\n", row.ClientName) currentClientID = row.ClientID } projectName := "(no project)" if row.ProjectName.Valid { projectName = row.ProjectName.String } cmd.Printf(" - %s: %v\n", projectName, duration.Round(time.Minute)) } // Print final client total if currentClientID != -1 { cmd.Printf(" Total: %v\n", clientTotals[currentClientID].Round(time.Minute)) } // Print grand total var grandTotal time.Duration for _, total := range clientTotals { grandTotal += total } cmd.Printf(" MONTH TOTAL: %v\n\n", grandTotal.Round(time.Minute)) return nil }