summaryrefslogtreecommitdiff
path: root/internal/commands/status.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/commands/status.go')
-rw-r--r--internal/commands/status.go379
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
+}