package commands import ( "fmt" "time" punchctx "git.tjp.lol/punchcard/internal/context" "git.tjp.lol/punchcard/internal/database" "git.tjp.lol/punchcard/internal/reports" "github.com/spf13/cobra" ) func NewReportCmd() *cobra.Command { cmd := &cobra.Command{ Use: "report", Short: "Generate reports from tracked time", Long: "Generate various types of reports (invoices, timesheets, etc.) from tracked time data.", } cmd.AddCommand(NewReportInvoiceCmd()) cmd.AddCommand(NewReportTimesheetCmd()) cmd.AddCommand(NewReportUnifiedCmd()) return cmd } func NewReportInvoiceCmd() *cobra.Command { cmd := &cobra.Command{ Use: "invoice", Short: "Generate a PDF invoice", Long: `Generate a PDF invoice from tracked time. Either --client or --project must be specified. Examples: # Generate invoice for last month (default) punch report invoice -c "Acme Corp" # Generate invoice for last week punch report invoice -c "Acme Corp" -d "last week" # Generate invoice for this month punch report invoice -c "Acme Corp" -d "this month" # Generate invoice for a specific month (most recent February) punch report invoice -c "Acme Corp" -d "february" # Generate invoice for month and year punch report invoice -c "Acme Corp" -d "july 2023" # Generate invoice for custom date range punch report invoice -c "Acme Corp" -d "2025-06-01 to 2025-06-30"`, RunE: func(cmd *cobra.Command, args []string) error { return runInvoiceCommand(cmd, args) }, } cmd.Flags().StringP("client", "c", "", "Generate invoice for specific client") cmd.Flags().StringP("project", "p", "", "Generate invoice for specific project") cmd.Flags().StringP("dates", "d", "last month", "Date range ('this week', 'this month', 'last week', 'last month', month names like 'february', 'month year' like 'july 2023', or 'YYYY-MM-DD to YYYY-MM-DD')") cmd.Flags().StringP("output", "o", "", "Output file path (default: auto-generated filename)") return cmd } func runInvoiceCommand(cmd *cobra.Command, args []string) error { // Get flag values clientName, _ := cmd.Flags().GetString("client") projectName, _ := cmd.Flags().GetString("project") dateStr, _ := cmd.Flags().GetString("dates") outputPath, _ := cmd.Flags().GetString("output") // Validate flags if clientName == "" && projectName == "" { return fmt.Errorf("either --client or --project must be specified") } if clientName != "" && projectName != "" { return fmt.Errorf("--client and --project are mutually exclusive") } // Parse date range dateRange, err := reports.ParseDateRange(dateStr) if err != nil { return fmt.Errorf("invalid date range: %w", err) } // Get database connection q := punchctx.GetDB(cmd.Context()) if q == nil { var err error q, err = database.GetDB() if err != nil { return fmt.Errorf("failed to connect to database: %w", err) } } // Create report parameters params := reports.ReportParams{ ClientName: clientName, ProjectName: projectName, DateRange: dateRange, OutputPath: outputPath, } // Generate invoice using high-level API var result *reports.ReportResult if projectName == "" { result, err = reports.GenerateClientInvoice(cmd.Context(), q, params) } else { result, err = reports.GenerateProjectInvoice(cmd.Context(), q, params) } if err != nil { return err } fmt.Printf("Invoice generated successfully: %s\n", result.OutputPath) fmt.Printf("Total hours: %.2f\n", result.TotalHours) fmt.Printf("Total amount: $%.2f\n", result.TotalAmount) return nil } func NewReportTimesheetCmd() *cobra.Command { cmd := &cobra.Command{ Use: "timesheet", Short: "Generate a PDF timesheet", Long: `Generate a PDF timesheet report from tracked time. Either --client or --project must be specified. Examples: # Generate timesheet for last month (default) punch report timesheet -c "Acme Corp" # Generate timesheet for last week punch report timesheet -c "Acme Corp" -d "last week" # Generate timesheet for this month punch report timesheet -c "Acme Corp" -d "this month" # Generate timesheet for a specific month (most recent February) punch report timesheet -c "Acme Corp" -d "february" # Generate timesheet for month and year punch report timesheet -c "Acme Corp" -d "july 2023" # Generate timesheet for custom date range punch report timesheet -c "Acme Corp" -d "2025-06-01 to 2025-06-30"`, RunE: func(cmd *cobra.Command, args []string) error { return runTimesheetCommand(cmd, args) }, } cmd.Flags().StringP("client", "c", "", "Generate timesheet for specific client") cmd.Flags().StringP("project", "p", "", "Generate timesheet for specific project") cmd.Flags().StringP("dates", "d", "last month", "Date range ('this week', 'this month', 'last week', 'last month', month names like 'february', 'month year' like 'july 2023', or 'YYYY-MM-DD to YYYY-MM-DD')") cmd.Flags().StringP("output", "o", "", "Output file path (default: auto-generated filename)") cmd.Flags().StringP("timezone", "t", "Local", "Timezone for displaying times (e.g., 'America/New_York', 'UTC', or 'Local')") return cmd } func runTimesheetCommand(cmd *cobra.Command, args []string) error { // Get flag values clientName, _ := cmd.Flags().GetString("client") projectName, _ := cmd.Flags().GetString("project") dateStr, _ := cmd.Flags().GetString("dates") outputPath, _ := cmd.Flags().GetString("output") timezone, _ := cmd.Flags().GetString("timezone") // Validate flags if clientName == "" && projectName == "" { return fmt.Errorf("either --client or --project must be specified") } if clientName != "" && projectName != "" { return fmt.Errorf("--client and --project are mutually exclusive") } // Parse date range dateRange, err := reports.ParseDateRange(dateStr) if err != nil { return fmt.Errorf("invalid date range: %w", err) } // Parse timezone var loc *time.Location if timezone == "Local" { loc = time.Local } else { loc, err = time.LoadLocation(timezone) if err != nil { return fmt.Errorf("invalid timezone '%s': %w", timezone, err) } } // Get database connection q := punchctx.GetDB(cmd.Context()) if q == nil { var err error q, err = database.GetDB() if err != nil { return fmt.Errorf("failed to connect to database: %w", err) } } // Create report parameters params := reports.ReportParams{ ClientName: clientName, ProjectName: projectName, DateRange: dateRange, OutputPath: outputPath, Timezone: loc, } // Generate timesheet using high-level API var result *reports.ReportResult if projectName == "" { result, err = reports.GenerateClientTimesheet(cmd.Context(), q, params) } else { result, err = reports.GenerateProjectTimesheet(cmd.Context(), q, params) } if err != nil { return err } fmt.Printf("Timesheet generated successfully: %s\n", result.OutputPath) fmt.Printf("Total hours: %.2f\n", result.TotalHours) fmt.Printf("Total entries: %d\n", result.TotalEntries) return nil } func NewReportUnifiedCmd() *cobra.Command { cmd := &cobra.Command{ Use: "unified", Short: "Generate a unified PDF report (invoice + timesheet)", Long: `Generate a unified PDF report combining invoice and timesheet on separate pages. Either --client or --project must be specified. Examples: # Generate unified report for last month (default) punch report unified -c "Acme Corp" # Generate unified report for last week punch report unified -c "Acme Corp" -d "last week" # Generate unified report for this month punch report unified -c "Acme Corp" -d "this month" # Generate unified report for a specific month (most recent February) punch report unified -c "Acme Corp" -d "february" # Generate unified report for month and year punch report unified -c "Acme Corp" -d "july 2023" # Generate unified report for custom date range punch report unified -c "Acme Corp" -d "2025-06-01 to 2025-06-30"`, RunE: func(cmd *cobra.Command, args []string) error { return runUnifiedCommand(cmd, args) }, } cmd.Flags().StringP("client", "c", "", "Generate unified report for specific client") cmd.Flags().StringP("project", "p", "", "Generate unified report for specific project") cmd.Flags().StringP("dates", "d", "last month", "Date range ('this week', 'this month', 'last week', 'last month', month names like 'february', 'month year' like 'july 2023', or 'YYYY-MM-DD to YYYY-MM-DD')") cmd.Flags().StringP("output", "o", "", "Output file path (default: auto-generated filename)") cmd.Flags().StringP("timezone", "t", "Local", "Timezone for displaying times (e.g., 'America/New_York', 'UTC', or 'Local')") return cmd } func runUnifiedCommand(cmd *cobra.Command, args []string) error { // Get flag values clientName, _ := cmd.Flags().GetString("client") projectName, _ := cmd.Flags().GetString("project") dateStr, _ := cmd.Flags().GetString("dates") outputPath, _ := cmd.Flags().GetString("output") timezone, _ := cmd.Flags().GetString("timezone") // Validate flags if clientName == "" && projectName == "" { return fmt.Errorf("either --client or --project must be specified") } if clientName != "" && projectName != "" { return fmt.Errorf("--client and --project are mutually exclusive") } // Parse date range dateRange, err := reports.ParseDateRange(dateStr) if err != nil { return fmt.Errorf("invalid date range: %w", err) } // Parse timezone var loc *time.Location if timezone == "Local" { loc = time.Local } else { loc, err = time.LoadLocation(timezone) if err != nil { return fmt.Errorf("invalid timezone '%s': %w", timezone, err) } } // Get database connection q := punchctx.GetDB(cmd.Context()) if q == nil { var err error q, err = database.GetDB() if err != nil { return fmt.Errorf("failed to connect to database: %w", err) } } // Create report parameters params := reports.ReportParams{ ClientName: clientName, ProjectName: projectName, DateRange: dateRange, OutputPath: outputPath, Timezone: loc, } // Generate unified report using high-level API var result *reports.ReportResult if projectName == "" { result, err = reports.GenerateClientUnifiedReport(cmd.Context(), q, params) } else { result, err = reports.GenerateProjectUnifiedReport(cmd.Context(), q, params) } if err != nil { return err } fmt.Printf("Unified report generated successfully: %s\n", result.OutputPath) fmt.Printf("Invoice total: $%.2f (%.0f hours)\n", result.TotalAmount, result.TotalHours) fmt.Printf("Timesheet total: %.2f hours (%d entries)\n", result.TotalHours, result.TotalEntries) return nil }