summaryrefslogtreecommitdiff
path: root/internal/commands/report.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/commands/report.go')
-rw-r--r--internal/commands/report.go207
1 files changed, 203 insertions, 4 deletions
diff --git a/internal/commands/report.go b/internal/commands/report.go
index eaa1b49..25e0483 100644
--- a/internal/commands/report.go
+++ b/internal/commands/report.go
@@ -1,7 +1,15 @@
package commands
import (
+ "context"
+ "database/sql"
"fmt"
+ "path/filepath"
+
+ punchctx "punchcard/internal/context"
+ "punchcard/internal/database"
+ "punchcard/internal/queries"
+ "punchcard/internal/reports"
"github.com/spf13/cobra"
)
@@ -20,15 +28,206 @@ func NewReportCmd() *cobra.Command {
}
func NewReportInvoiceCmd() *cobra.Command {
- return &cobra.Command{
+ cmd := &cobra.Command{
Use: "invoice",
Short: "Generate a PDF invoice",
- Long: "Generate a PDF invoice from tracked time.",
+ 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 custom date range
+ punch report invoice -c "Acme Corp" -d "2025-06-01 to 2025-06-30"
+
+ # Generate invoice for specific project
+ punch report invoice -p "Website Redesign" -d "2025-01-01 to 2025-01-31"`,
RunE: func(cmd *cobra.Command, args []string) error {
- fmt.Println("Invoice generation (placeholder)")
- return nil
+ 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 ('last week', 'last month', 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)
+ }
+ }
+
+ // Generate invoice based on client or project
+ var invoiceData *reports.InvoiceData
+ if clientName != "" {
+ invoiceData, err = generateClientInvoice(q, clientName, dateRange)
+ } else {
+ invoiceData, err = generateProjectInvoice(q, projectName, dateRange)
+ }
+ if err != nil {
+ return err
+ }
+
+ // Generate output filename if not specified
+ if outputPath == "" {
+ outputPath = reports.GenerateDefaultInvoiceFilename(invoiceData.ClientName, invoiceData.ProjectName, dateRange)
+ }
+
+ // Convert to absolute path
+ outputPath, err = filepath.Abs(outputPath)
+ if err != nil {
+ return fmt.Errorf("failed to resolve output path: %w", err)
+ }
+
+ // Generate PDF
+ err = reports.GenerateInvoicePDF(invoiceData, outputPath)
+ if err != nil {
+ return fmt.Errorf("failed to generate invoice PDF: %w", err)
+ }
+
+ if _, err := q.CreateInvoice(cmd.Context(), queries.CreateInvoiceParams{
+ Year: int64(invoiceData.DateRange.Start.Year()),
+ Month: int64(invoiceData.DateRange.Start.Month()),
+ Number: invoiceData.InvoiceNumber,
+ ClientID: invoiceData.ClientID,
+ TotalAmount: int64(invoiceData.TotalAmount * 100),
+ }); err != nil {
+ return fmt.Errorf("failed to record invoice in database: %w", err)
+ }
+
+ fmt.Printf("Invoice generated successfully: %s\n", outputPath)
+ fmt.Printf("Total hours: %.2f\n", invoiceData.TotalHours)
+ fmt.Printf("Total amount: $%.2f\n", invoiceData.TotalAmount)
+
+ return nil
+}
+
+func generateClientInvoice(q *queries.Queries, clientName string, dateRange reports.DateRange) (*reports.InvoiceData, error) {
+ // Find client
+ client, err := findClient(context.Background(), q, clientName)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, fmt.Errorf("client not found: %s", clientName)
+ }
+ return nil, fmt.Errorf("failed to find client: %w", err)
+ }
+
+ // Get contractor data
+ contractor, err := q.GetContractor(context.Background())
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, fmt.Errorf("no contractor information found - please add contractor details first")
+ }
+ return nil, fmt.Errorf("failed to get contractor information: %w", err)
+ }
+
+ highestNumber, err := q.GetHighestInvoiceNumber(context.Background(), queries.GetHighestInvoiceNumberParams{
+ Year: int64(dateRange.Start.Year()),
+ Month: int64(dateRange.Start.Month()),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get highest invoice number: %w", err)
+ }
+
+ // Get invoice data
+ entries, err := q.GetInvoiceDataByClient(context.Background(), queries.GetInvoiceDataByClientParams{
+ ClientID: client.ID,
+ StartTime: dateRange.Start,
+ EndTime: dateRange.End,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get invoice data: %w", err)
+ }
+
+ if len(entries) == 0 {
+ return nil, fmt.Errorf("no completed time entries found for client %s in the specified date range", client.Name)
+ }
+
+ return reports.GenerateInvoiceData(entries, client.ID, client.Name, "", contractor, highestNumber+1, dateRange)
+}
+
+func generateProjectInvoice(q *queries.Queries, projectName string, dateRange reports.DateRange) (*reports.InvoiceData, error) {
+ // We need to find the project, but we need the client info too
+ // Let's first find all projects with this name
+ project, err := findProject(context.Background(), q, projectName)
+ if err != nil {
+ return nil, fmt.Errorf("failed to find project: %w", err)
+ }
+
+ // Get client info
+ clients, err := q.FindClient(context.Background(), queries.FindClientParams{
+ ID: project.ClientID,
+ Name: "",
+ })
+ if err != nil || len(clients) == 0 {
+ return nil, fmt.Errorf("failed to find client for project")
+ }
+ client := clients[0]
+
+ // Get contractor data
+ contractor, err := q.GetContractor(context.Background())
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, fmt.Errorf("no contractor information found - please add contractor details first")
+ }
+ return nil, fmt.Errorf("failed to get contractor information: %w", err)
+ }
+
+ highestNumber, err := q.GetHighestInvoiceNumber(context.Background(), queries.GetHighestInvoiceNumberParams{
+ Year: int64(dateRange.Start.Year()),
+ Month: int64(dateRange.Start.Month()),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get highest invoice number: %w", err)
+ }
+
+ // Get invoice data
+ entries, err := q.GetInvoiceDataByProject(context.Background(), queries.GetInvoiceDataByProjectParams{
+ ProjectID: project.ID,
+ StartTime: dateRange.Start,
+ EndTime: dateRange.End,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get invoice data: %w", err)
+ }
+
+ if len(entries) == 0 {
+ return nil, fmt.Errorf("no completed time entries found for project %s in the specified date range", projectName)
+ }
+
+ return reports.GenerateInvoiceData(entries, client.ID, client.Name, projectName, contractor, highestNumber+1, dateRange)
}
func NewReportTimesheetCmd() *cobra.Command {