diff options
Diffstat (limited to 'internal/commands/status.go')
-rw-r--r-- | internal/commands/status.go | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/internal/commands/status.go b/internal/commands/status.go new file mode 100644 index 0000000..626b258 --- /dev/null +++ b/internal/commands/status.go @@ -0,0 +1,379 @@ +package commands + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strconv" + "time" + + punchctx "punchcard/internal/context" + "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 +} |