summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/commands/report.go440
-rw-r--r--internal/reports/api.go500
-rw-r--r--internal/tui/app.go44
-rw-r--r--internal/tui/commands.go59
-rw-r--r--internal/tui/form.go77
-rw-r--r--internal/tui/keys.go5
-rw-r--r--internal/tui/modal.go13
7 files changed, 725 insertions, 413 deletions
diff --git a/internal/commands/report.go b/internal/commands/report.go
index bcc0e5f..30972b4 100644
--- a/internal/commands/report.go
+++ b/internal/commands/report.go
@@ -1,15 +1,11 @@
package commands
import (
- "context"
- "database/sql"
"fmt"
- "path/filepath"
"time"
punchctx "punchcard/internal/context"
"punchcard/internal/database"
- "punchcard/internal/queries"
"punchcard/internal/reports"
"github.com/spf13/cobra"
@@ -97,147 +93,32 @@ func runInvoiceCommand(cmd *cobra.Command, args []string) error {
}
}
- // 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)
+ // Create report parameters
+ params := reports.ReportParams{
+ ClientName: clientName,
+ ProjectName: projectName,
+ DateRange: dateRange,
+ OutputPath: outputPath,
}
- // Convert to absolute path
- outputPath, err = filepath.Abs(outputPath)
- if err != nil {
- return fmt.Errorf("failed to resolve output path: %w", err)
+ // 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)
}
-
- // 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)
+ return 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)
+ 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 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 {
cmd := &cobra.Command{
Use: "timesheet",
@@ -319,120 +200,33 @@ func runTimesheetCommand(cmd *cobra.Command, args []string) error {
}
}
- // Generate timesheet based on client or project
- var timesheetData *reports.TimesheetData
- if clientName != "" {
- timesheetData, err = generateClientTimesheet(q, clientName, dateRange, loc)
- } else {
- timesheetData, err = generateProjectTimesheet(q, projectName, dateRange, loc)
- }
- if err != nil {
- return err
- }
-
- // Generate output filename if not specified
- if outputPath == "" {
- outputPath = reports.GenerateDefaultTimesheetFilename(timesheetData.ClientName, timesheetData.ProjectName, dateRange)
+ // Create report parameters
+ params := reports.ReportParams{
+ ClientName: clientName,
+ ProjectName: projectName,
+ DateRange: dateRange,
+ OutputPath: outputPath,
+ Timezone: loc,
}
- // Convert to absolute path
- outputPath, err = filepath.Abs(outputPath)
- if err != nil {
- return fmt.Errorf("failed to resolve output path: %w", err)
+ // 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)
}
-
- // Generate PDF
- err = reports.GenerateTimesheetPDF(timesheetData, outputPath)
if err != nil {
- return fmt.Errorf("failed to generate timesheet PDF: %w", err)
+ return err
}
- fmt.Printf("Timesheet generated successfully: %s\n", outputPath)
- fmt.Printf("Total hours: %.2f\n", timesheetData.TotalHours)
- fmt.Printf("Total entries: %d\n", len(timesheetData.Entries))
+ 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 generateClientTimesheet(q *queries.Queries, clientName string, dateRange reports.DateRange, loc *time.Location) (*reports.TimesheetData, 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)
- }
-
- // Get timesheet data
- entries, err := q.GetTimesheetDataByClient(context.Background(), queries.GetTimesheetDataByClientParams{
- ClientID: client.ID,
- StartTime: dateRange.Start,
- EndTime: dateRange.End,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to get timesheet 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.GenerateTimesheetData(entries, client.ID, client.Name, "", contractor, dateRange, loc)
-}
-
-func generateProjectTimesheet(q *queries.Queries, projectName string, dateRange reports.DateRange, loc *time.Location) (*reports.TimesheetData, error) {
- // Find project
- 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)
- }
-
- // Get timesheet data
- entries, err := q.GetTimesheetDataByProject(context.Background(), queries.GetTimesheetDataByProjectParams{
- ProjectID: project.ID,
- StartTime: dateRange.Start,
- EndTime: dateRange.End,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to get timesheet 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.GenerateTimesheetData(entries, client.ID, client.Name, projectName, contractor, dateRange, loc)
-}
-
func NewReportUnifiedCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "unified",
@@ -514,163 +308,29 @@ func runUnifiedCommand(cmd *cobra.Command, args []string) error {
}
}
- // Generate unified report based on client or project
- var unifiedData *reports.UnifiedReportData
- if clientName != "" {
- unifiedData, err = generateClientUnified(q, clientName, dateRange, loc)
- } else {
- unifiedData, err = generateProjectUnified(q, projectName, dateRange, loc)
- }
- if err != nil {
- return err
- }
-
- // Generate output filename if not specified
- if outputPath == "" {
- outputPath = reports.GenerateDefaultUnifiedFilename(unifiedData.InvoiceData.ClientName, unifiedData.InvoiceData.ProjectName, dateRange)
+ // Create report parameters
+ params := reports.ReportParams{
+ ClientName: clientName,
+ ProjectName: projectName,
+ DateRange: dateRange,
+ OutputPath: outputPath,
+ Timezone: loc,
}
- // Convert to absolute path
- outputPath, err = filepath.Abs(outputPath)
- if err != nil {
- return fmt.Errorf("failed to resolve output path: %w", err)
+ // 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)
}
-
- // Generate PDF
- err = reports.GenerateUnifiedPDF(unifiedData, outputPath)
if err != nil {
- return fmt.Errorf("failed to generate unified PDF: %w", err)
- }
-
- // Record invoice in database
- if _, err := q.CreateInvoice(cmd.Context(), queries.CreateInvoiceParams{
- Year: int64(unifiedData.InvoiceData.DateRange.Start.Year()),
- Month: int64(unifiedData.InvoiceData.DateRange.Start.Month()),
- Number: unifiedData.InvoiceData.InvoiceNumber,
- ClientID: unifiedData.InvoiceData.ClientID,
- TotalAmount: int64(unifiedData.InvoiceData.TotalAmount * 100),
- }); err != nil {
- return fmt.Errorf("failed to record invoice in database: %w", err)
+ return err
}
- fmt.Printf("Unified report generated successfully: %s\n", outputPath)
- fmt.Printf("Invoice total: $%.2f (%d hours)\n", unifiedData.InvoiceData.TotalAmount, int(unifiedData.InvoiceData.TotalHours))
- fmt.Printf("Timesheet total: %.2f hours (%d entries)\n", unifiedData.TimesheetData.TotalHours, len(unifiedData.TimesheetData.Entries))
+ 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
}
-
-func generateClientUnified(q *queries.Queries, clientName string, dateRange reports.DateRange, loc *time.Location) (*reports.UnifiedReportData, 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)
- }
-
- // Get invoice number
- 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 data for both invoice and timesheet
- invoiceEntries, 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)
- }
-
- timesheetEntries, err := q.GetTimesheetDataByClient(context.Background(), queries.GetTimesheetDataByClientParams{
- ClientID: client.ID,
- StartTime: dateRange.Start,
- EndTime: dateRange.End,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to get timesheet data: %w", err)
- }
-
- if len(invoiceEntries) == 0 || len(timesheetEntries) == 0 {
- return nil, fmt.Errorf("no completed time entries found for client %s in the specified date range", client.Name)
- }
-
- return reports.GenerateUnifiedReportData(invoiceEntries, client.ID, client.Name, "", contractor, highestNumber+1, dateRange, loc)
-}
-
-func generateProjectUnified(q *queries.Queries, projectName string, dateRange reports.DateRange, loc *time.Location) (*reports.UnifiedReportData, error) {
- // Find project
- 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)
- }
-
- // Get invoice number
- 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 data for both invoice and timesheet
- invoiceEntries, 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)
- }
-
- timesheetEntries, err := q.GetTimesheetDataByProject(context.Background(), queries.GetTimesheetDataByProjectParams{
- ProjectID: project.ID,
- StartTime: dateRange.Start,
- EndTime: dateRange.End,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to get timesheet data: %w", err)
- }
-
- if len(invoiceEntries) == 0 || len(timesheetEntries) == 0 {
- return nil, fmt.Errorf("no completed time entries found for project %s in the specified date range", projectName)
- }
-
- return reports.GenerateUnifiedReportData(invoiceEntries, client.ID, client.Name, projectName, contractor, highestNumber+1, dateRange, loc)
-}
diff --git a/internal/reports/api.go b/internal/reports/api.go
new file mode 100644
index 0000000..0db8672
--- /dev/null
+++ b/internal/reports/api.go
@@ -0,0 +1,500 @@
+package reports
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "path/filepath"
+ "time"
+
+ "punchcard/internal/queries"
+)
+
+type ReportParams struct {
+ ClientName string
+ ProjectName string
+ DateRange DateRange
+ OutputPath string
+ Timezone *time.Location
+}
+
+type ReportResult struct {
+ OutputPath string
+ TotalHours float64
+ TotalAmount float64
+ TotalEntries int
+}
+
+func GenerateInvoice(ctx context.Context, q *queries.Queries, params ReportParams) (*ReportResult, error) {
+ if params.ProjectName == "" {
+ return GenerateClientInvoice(ctx, q, params)
+ }
+ return GenerateProjectInvoice(ctx, q, params)
+}
+
+func GenerateTimesheet(ctx context.Context, q *queries.Queries, params ReportParams) (*ReportResult, error) {
+ if params.ProjectName == "" {
+ return GenerateClientTimesheet(ctx, q, params)
+ }
+ return GenerateProjectTimesheet(ctx, q, params)
+}
+
+func GenerateUnifiedReport(ctx context.Context, q *queries.Queries, params ReportParams) (*ReportResult, error) {
+ if params.ProjectName == "" {
+ return GenerateClientUnifiedReport(ctx, q, params)
+ }
+ return GenerateProjectUnifiedReport(ctx, q, params)
+}
+
+func GenerateClientInvoice(ctx context.Context, q *queries.Queries, params ReportParams) (*ReportResult, error) {
+ client, err := findClientByName(ctx, q, params.ClientName)
+ if err != nil {
+ return nil, fmt.Errorf("client not found: %s", params.ClientName)
+ }
+
+ contractor, err := q.GetContractor(ctx)
+ 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(ctx, queries.GetHighestInvoiceNumberParams{
+ Year: int64(params.DateRange.Start.Year()),
+ Month: int64(params.DateRange.Start.Month()),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get highest invoice number: %w", err)
+ }
+
+ entries, err := q.GetInvoiceDataByClient(ctx, queries.GetInvoiceDataByClientParams{
+ ClientID: client.ID,
+ StartTime: params.DateRange.Start,
+ EndTime: params.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)
+ }
+
+ invoiceData, err := GenerateInvoiceData(entries, client.ID, client.Name, "", contractor, highestNumber+1, params.DateRange)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate invoice data: %w", err)
+ }
+
+ outputPath := params.OutputPath
+ if outputPath == "" {
+ outputPath = GenerateDefaultInvoiceFilename(invoiceData.ClientName, invoiceData.ProjectName, params.DateRange)
+ }
+
+ outputPath, err = filepath.Abs(outputPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve output path: %w", err)
+ }
+
+ if err := GenerateInvoicePDF(invoiceData, outputPath); err != nil {
+ return nil, fmt.Errorf("failed to generate invoice PDF: %w", err)
+ }
+
+ if err := RecordInvoice(q,
+ int64(invoiceData.DateRange.Start.Year()),
+ int64(invoiceData.DateRange.Start.Month()),
+ invoiceData.InvoiceNumber,
+ invoiceData.ClientID,
+ int64(invoiceData.TotalAmount*100),
+ ); err != nil {
+ return nil, err
+ }
+
+ return &ReportResult{
+ OutputPath: outputPath,
+ TotalHours: invoiceData.TotalHours,
+ TotalAmount: invoiceData.TotalAmount,
+ TotalEntries: len(invoiceData.LineItems),
+ }, nil
+}
+
+func GenerateProjectInvoice(ctx context.Context, q *queries.Queries, params ReportParams) (*ReportResult, error) {
+ project, err := findProjectByName(ctx, q, params.ProjectName)
+ if err != nil {
+ return nil, fmt.Errorf("failed to find project: %w", err)
+ }
+
+ clients, err := q.FindClient(ctx, 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]
+
+ contractor, err := q.GetContractor(ctx)
+ 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(ctx, queries.GetHighestInvoiceNumberParams{
+ Year: int64(params.DateRange.Start.Year()),
+ Month: int64(params.DateRange.Start.Month()),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get highest invoice number: %w", err)
+ }
+
+ entries, err := q.GetInvoiceDataByProject(ctx, queries.GetInvoiceDataByProjectParams{
+ ProjectID: project.ID,
+ StartTime: params.DateRange.Start,
+ EndTime: params.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", params.ProjectName)
+ }
+
+ invoiceData, err := GenerateInvoiceData(entries, client.ID, client.Name, params.ProjectName, contractor, highestNumber+1, params.DateRange)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate invoice data: %w", err)
+ }
+
+ outputPath := params.OutputPath
+ if outputPath == "" {
+ outputPath = GenerateDefaultInvoiceFilename(invoiceData.ClientName, invoiceData.ProjectName, params.DateRange)
+ }
+
+ outputPath, err = filepath.Abs(outputPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve output path: %w", err)
+ }
+
+ if err := GenerateInvoicePDF(invoiceData, outputPath); err != nil {
+ return nil, fmt.Errorf("failed to generate invoice PDF: %w", err)
+ }
+
+ if err := RecordInvoice(q,
+ int64(invoiceData.DateRange.Start.Year()),
+ int64(invoiceData.DateRange.Start.Month()),
+ invoiceData.InvoiceNumber,
+ invoiceData.ClientID,
+ int64(invoiceData.TotalAmount*100),
+ ); err != nil {
+ return nil, err
+ }
+
+ return &ReportResult{
+ OutputPath: outputPath,
+ TotalHours: invoiceData.TotalHours,
+ TotalAmount: invoiceData.TotalAmount,
+ TotalEntries: len(invoiceData.LineItems),
+ }, nil
+}
+
+func GenerateClientTimesheet(ctx context.Context, q *queries.Queries, params ReportParams) (*ReportResult, error) {
+ client, err := findClientByName(ctx, q, params.ClientName)
+ if err != nil {
+ return nil, fmt.Errorf("client not found: %s", params.ClientName)
+ }
+
+ contractor, err := q.GetContractor(ctx)
+ 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)
+ }
+
+ entries, err := q.GetTimesheetDataByClient(ctx, queries.GetTimesheetDataByClientParams{
+ ClientID: client.ID,
+ StartTime: params.DateRange.Start,
+ EndTime: params.DateRange.End,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get timesheet 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)
+ }
+
+ timesheetData, err := GenerateTimesheetData(entries, client.ID, client.Name, "", contractor, params.DateRange, params.Timezone)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate timesheet data: %w", err)
+ }
+
+ outputPath := params.OutputPath
+ if outputPath == "" {
+ outputPath = GenerateDefaultTimesheetFilename(timesheetData.ClientName, timesheetData.ProjectName, params.DateRange)
+ }
+
+ outputPath, err = filepath.Abs(outputPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve output path: %w", err)
+ }
+
+ if err := GenerateTimesheetPDF(timesheetData, outputPath); err != nil {
+ return nil, fmt.Errorf("failed to generate timesheet PDF: %w", err)
+ }
+
+ return &ReportResult{
+ OutputPath: outputPath,
+ TotalHours: timesheetData.TotalHours,
+ TotalAmount: 0, // Timesheets don't have amounts
+ TotalEntries: len(timesheetData.Entries),
+ }, nil
+}
+
+func GenerateProjectTimesheet(ctx context.Context, q *queries.Queries, params ReportParams) (*ReportResult, error) {
+ project, err := findProjectByName(ctx, q, params.ProjectName)
+ if err != nil {
+ return nil, fmt.Errorf("failed to find project: %w", err)
+ }
+
+ clients, err := q.FindClient(ctx, 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]
+
+ contractor, err := q.GetContractor(ctx)
+ 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)
+ }
+
+ entries, err := q.GetTimesheetDataByProject(ctx, queries.GetTimesheetDataByProjectParams{
+ ProjectID: project.ID,
+ StartTime: params.DateRange.Start,
+ EndTime: params.DateRange.End,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get timesheet data: %w", err)
+ }
+
+ if len(entries) == 0 {
+ return nil, fmt.Errorf("no completed time entries found for project %s in the specified date range", params.ProjectName)
+ }
+
+ timesheetData, err := GenerateTimesheetData(entries, client.ID, client.Name, params.ProjectName, contractor, params.DateRange, params.Timezone)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate timesheet data: %w", err)
+ }
+
+ outputPath := params.OutputPath
+ if outputPath == "" {
+ outputPath = GenerateDefaultTimesheetFilename(timesheetData.ClientName, timesheetData.ProjectName, params.DateRange)
+ }
+
+ outputPath, err = filepath.Abs(outputPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve output path: %w", err)
+ }
+
+ if err := GenerateTimesheetPDF(timesheetData, outputPath); err != nil {
+ return nil, fmt.Errorf("failed to generate timesheet PDF: %w", err)
+ }
+
+ return &ReportResult{
+ OutputPath: outputPath,
+ TotalHours: timesheetData.TotalHours,
+ TotalAmount: 0, // Timesheets don't have amounts
+ TotalEntries: len(timesheetData.Entries),
+ }, nil
+}
+
+func GenerateClientUnifiedReport(ctx context.Context, q *queries.Queries, params ReportParams) (*ReportResult, error) {
+ client, err := findClientByName(ctx, q, params.ClientName)
+ if err != nil {
+ return nil, fmt.Errorf("client not found: %s", params.ClientName)
+ }
+
+ contractor, err := q.GetContractor(ctx)
+ 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(ctx, queries.GetHighestInvoiceNumberParams{
+ Year: int64(params.DateRange.Start.Year()),
+ Month: int64(params.DateRange.Start.Month()),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get highest invoice number: %w", err)
+ }
+
+ invoiceEntries, err := q.GetInvoiceDataByClient(ctx, queries.GetInvoiceDataByClientParams{
+ ClientID: client.ID,
+ StartTime: params.DateRange.Start,
+ EndTime: params.DateRange.End,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get invoice data: %w", err)
+ }
+
+ if len(invoiceEntries) == 0 {
+ return nil, fmt.Errorf("no completed time entries found for client %s in the specified date range", client.Name)
+ }
+
+ unifiedData, err := GenerateUnifiedReportData(invoiceEntries, client.ID, client.Name, "", contractor, highestNumber+1, params.DateRange, params.Timezone)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate unified report data: %w", err)
+ }
+
+ outputPath := params.OutputPath
+ if outputPath == "" {
+ outputPath = GenerateDefaultUnifiedFilename(unifiedData.InvoiceData.ClientName, unifiedData.InvoiceData.ProjectName, params.DateRange)
+ }
+
+ outputPath, err = filepath.Abs(outputPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve output path: %w", err)
+ }
+
+ if err := GenerateUnifiedPDF(unifiedData, outputPath); err != nil {
+ return nil, fmt.Errorf("failed to generate unified PDF: %w", err)
+ }
+
+ if err := RecordInvoice(q,
+ int64(unifiedData.InvoiceData.DateRange.Start.Year()),
+ int64(unifiedData.InvoiceData.DateRange.Start.Month()),
+ unifiedData.InvoiceData.InvoiceNumber,
+ unifiedData.InvoiceData.ClientID,
+ int64(unifiedData.InvoiceData.TotalAmount*100),
+ ); err != nil {
+ return nil, err
+ }
+
+ return &ReportResult{
+ OutputPath: outputPath,
+ TotalHours: unifiedData.InvoiceData.TotalHours,
+ TotalAmount: unifiedData.InvoiceData.TotalAmount,
+ TotalEntries: len(unifiedData.TimesheetData.Entries),
+ }, nil
+}
+
+func GenerateProjectUnifiedReport(ctx context.Context, q *queries.Queries, params ReportParams) (*ReportResult, error) {
+ project, err := findProjectByName(ctx, q, params.ProjectName)
+ if err != nil {
+ return nil, fmt.Errorf("failed to find project: %w", err)
+ }
+
+ clients, err := q.FindClient(ctx, 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]
+
+ contractor, err := q.GetContractor(ctx)
+ 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(ctx, queries.GetHighestInvoiceNumberParams{
+ Year: int64(params.DateRange.Start.Year()),
+ Month: int64(params.DateRange.Start.Month()),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get highest invoice number: %w", err)
+ }
+
+ invoiceEntries, err := q.GetInvoiceDataByProject(ctx, queries.GetInvoiceDataByProjectParams{
+ ProjectID: project.ID,
+ StartTime: params.DateRange.Start,
+ EndTime: params.DateRange.End,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get invoice data: %w", err)
+ }
+
+ if len(invoiceEntries) == 0 {
+ return nil, fmt.Errorf("no completed time entries found for project %s in the specified date range", params.ProjectName)
+ }
+
+ unifiedData, err := GenerateUnifiedReportData(invoiceEntries, client.ID, client.Name, params.ProjectName, contractor, highestNumber+1, params.DateRange, params.Timezone)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate unified report data: %w", err)
+ }
+
+ outputPath := params.OutputPath
+ if outputPath == "" {
+ outputPath = GenerateDefaultUnifiedFilename(unifiedData.InvoiceData.ClientName, unifiedData.InvoiceData.ProjectName, params.DateRange)
+ }
+
+ outputPath, err = filepath.Abs(outputPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve output path: %w", err)
+ }
+
+ if err := GenerateUnifiedPDF(unifiedData, outputPath); err != nil {
+ return nil, fmt.Errorf("failed to generate unified PDF: %w", err)
+ }
+
+ if err := RecordInvoice(q,
+ int64(unifiedData.InvoiceData.DateRange.Start.Year()),
+ int64(unifiedData.InvoiceData.DateRange.Start.Month()),
+ unifiedData.InvoiceData.InvoiceNumber,
+ unifiedData.InvoiceData.ClientID,
+ int64(unifiedData.InvoiceData.TotalAmount*100),
+ ); err != nil {
+ return nil, err
+ }
+
+ return &ReportResult{
+ OutputPath: outputPath,
+ TotalHours: unifiedData.InvoiceData.TotalHours,
+ TotalAmount: unifiedData.InvoiceData.TotalAmount,
+ TotalEntries: len(unifiedData.TimesheetData.Entries),
+ }, nil
+}
+
+// Helper functions for finding clients and projects
+func findClientByName(ctx context.Context, q *queries.Queries, clientName string) (queries.Client, error) {
+ clients, err := q.FindClient(ctx, queries.FindClientParams{
+ ID: 0,
+ Name: clientName,
+ })
+ if err != nil {
+ return queries.Client{}, err
+ }
+ if len(clients) == 0 {
+ return queries.Client{}, sql.ErrNoRows
+ }
+ return clients[0], nil
+}
+
+func findProjectByName(ctx context.Context, q *queries.Queries, projectName string) (queries.Project, error) {
+ projects, err := q.FindProject(ctx, queries.FindProjectParams{
+ ID: 0,
+ Name: projectName,
+ })
+ if err != nil {
+ return queries.Project{}, err
+ }
+ if len(projects) == 0 {
+ return queries.Project{}, sql.ErrNoRows
+ }
+ return projects[0], nil
+}
+
diff --git a/internal/tui/app.go b/internal/tui/app.go
index 433a5ad..4b31c38 100644
--- a/internal/tui/app.go
+++ b/internal/tui/app.go
@@ -205,6 +205,11 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.historyBox.filter = HistoryFilter(msg)
cmds = append(cmds, m.refreshCmd)
+ case openReportModal:
+ if m.selectedBox == HistoryBox && m.historyBox.viewLevel == HistoryLevelSummary {
+ m.openReportModal()
+ }
+
}
return m, tea.Batch(cmds...)
@@ -252,10 +257,10 @@ func (m *AppModel) openHistoryFilterModal() {
var dateRangeStr string
if filter.EndDate == nil {
// Use "since <date>" format for open-ended ranges
- dateRangeStr = fmt.Sprintf("since %s", filter.StartDate.Format("2006-01-02"))
+ dateRangeStr = fmt.Sprintf("since %s", filter.StartDate.Format(time.DateOnly))
} else {
// Use "YYYY-MM-DD to YYYY-MM-DD" format for bounded ranges
- dateRangeStr = fmt.Sprintf("%s to %s", filter.StartDate.Format("2006-01-02"), filter.EndDate.Format("2006-01-02"))
+ dateRangeStr = fmt.Sprintf("%s to %s", filter.StartDate.Format(time.DateOnly), filter.EndDate.Format(time.DateOnly))
}
m.modalBox.form.fields[0].SetValue(dateRangeStr)
@@ -282,6 +287,41 @@ func (m *AppModel) openHistoryFilterModal() {
}
}
+func (m *AppModel) openReportModal() {
+ m.modalBox.activate(ModalTypeGenerateReport, 0, *m)
+ m.modalBox.form.fields[0].Focus()
+
+ filter := m.historyBox.filter
+
+ var dateRangeStr string
+ if filter.EndDate == nil {
+ dateRangeStr = fmt.Sprintf("since %s", filter.StartDate.Format(time.DateOnly))
+ } else {
+ dateRangeStr = fmt.Sprintf("%s to %s", filter.StartDate.Format(time.DateOnly), filter.EndDate.Format(time.DateOnly))
+ }
+ m.modalBox.form.fields[1].SetValue(dateRangeStr)
+
+ if filter.ClientID != nil {
+ for _, client := range m.projectsBox.clients {
+ if client.ID == *filter.ClientID {
+ m.modalBox.form.fields[2].SetValue(client.Name)
+ break
+ }
+ }
+ }
+
+ if filter.ProjectID != nil {
+ for _, clientProjects := range m.projectsBox.projects {
+ for _, project := range clientProjects {
+ if project.ID == *filter.ProjectID {
+ m.modalBox.form.fields[3].SetValue(project.Name)
+ break
+ }
+ }
+ }
+ }
+}
+
// View renders the app
func (m AppModel) View() string {
if m.width == 0 || m.height == 0 {
diff --git a/internal/tui/commands.go b/internal/tui/commands.go
index aa5cc79..9a836c5 100644
--- a/internal/tui/commands.go
+++ b/internal/tui/commands.go
@@ -2,8 +2,13 @@ package tui
import (
"context"
+ "fmt"
+ "strings"
+ "time"
"punchcard/internal/actions"
+ "punchcard/internal/queries"
+ "punchcard/internal/reports"
tea "github.com/charmbracelet/bubbletea"
)
@@ -22,6 +27,7 @@ type (
openCreateClientModal struct{}
openCreateProjectModal struct{}
openHistoryFilterModal struct{}
+ openReportModal struct{}
updateHistoryFilter HistoryFilter
)
@@ -113,3 +119,56 @@ func createProjectModal() tea.Cmd {
func createHistoryFilterModal() tea.Cmd {
return func() tea.Msg { return openHistoryFilterModal{} }
}
+
+func createReportModal() tea.Cmd {
+ return func() tea.Msg { return openReportModal{} }
+}
+
+func generateReport(m *ModalBoxModel, am AppModel) tea.Cmd {
+ return func() tea.Msg {
+ form := &m.form
+
+ dateRange, err := reports.ParseDateRange(form.fields[1].Value())
+ if err != nil {
+ form.fields[1].Err = fmt.Errorf("invalid date range: %v", err)
+ return reOpenModal()
+ }
+
+ var tz *time.Location
+ tzstr := form.fields[5].Value()
+ if tzstr == "" {
+ tz = time.Local
+ } else {
+ zone, err := time.LoadLocation(tzstr)
+ if err != nil {
+ form.fields[5].Err = err
+ return reOpenModal()
+ }
+ tz = zone
+ }
+
+ var genFunc func(context.Context, *queries.Queries, reports.ReportParams) (*reports.ReportResult, error)
+ switch strings.ToLower(form.fields[0].Value()) {
+ case "invoice":
+ genFunc = reports.GenerateInvoice
+ case "timesheet":
+ genFunc = reports.GenerateTimesheet
+ case "unified":
+ genFunc = reports.GenerateUnifiedReport
+ }
+
+ params := reports.ReportParams{
+ ClientName: form.fields[2].Value(),
+ ProjectName: form.fields[3].Value(),
+ DateRange: dateRange,
+ OutputPath: form.fields[4].Value(),
+ Timezone: tz,
+ }
+ if _, err := genFunc(context.Background(), am.queries, params); err != nil {
+ form.err = err
+ return reOpenModal()
+ }
+
+ return nil
+ }
+}
diff --git a/internal/tui/form.go b/internal/tui/form.go
index 3a1e5f6..d70fb8b 100644
--- a/internal/tui/form.go
+++ b/internal/tui/form.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strconv"
+ "strings"
"time"
"punchcard/internal/reports"
@@ -19,6 +20,7 @@ const (
noSuggestions suggestionType = iota
suggestClients
suggestProjects
+ suggestReportType
)
type FormField struct {
@@ -100,6 +102,22 @@ func newDateRangeField(label string) FormField {
return f
}
+func newReportTypeField(label string) FormField {
+ f := FormField{
+ Model: textinput.New(),
+ label: label,
+ suggestions: suggestReportType,
+ }
+ f.Validate = func(s string) error {
+ switch strings.ToLower(s) {
+ case "invoice", "timesheet", "unified":
+ return nil
+ }
+ return errors.New("pick one of invoice, timesheet, or unified")
+ }
+ return f
+}
+
type Form struct {
fields []FormField
selIdx int
@@ -113,8 +131,8 @@ func NewForm(fields []FormField) Form {
return Form{fields: fields}
}
-func (ff Form) Error() error {
- for _, field := range ff.fields {
+func (f Form) Error() error {
+ for _, field := range f.fields {
if field.Err != nil {
return field.Err
}
@@ -169,44 +187,58 @@ func NewHistoryFilterForm() Form {
return form
}
-func (ff Form) Update(msg tea.Msg) (Form, tea.Cmd) {
+func NewGenerateReportForm() Form {
+ form := NewForm([]FormField{
+ newReportTypeField("Report Type"),
+ newDateRangeField("Date Range"),
+ {Model: textinput.New(), label: "Client", suggestions: suggestClients},
+ {Model: textinput.New(), label: "Project (optional)", suggestions: suggestProjects},
+ {Model: textinput.New(), label: "Output Path (optional)"},
+ {Model: textinput.New(), label: "Timezone (optional)"},
+ })
+ form.SelectedStyle = &modalFocusedInputStyle
+ form.UnselectedStyle = &modalBlurredInputStyle
+ return form
+}
+
+func (f Form) Update(msg tea.Msg) (Form, tea.Cmd) {
if msg, ok := msg.(tea.KeyMsg); ok {
switch msg.String() {
case "tab":
- ff.fields[ff.selIdx].Blur()
- ff.selIdx = (ff.selIdx + 1) % len(ff.fields)
- return ff, ff.fields[ff.selIdx].Focus()
+ f.fields[f.selIdx].Blur()
+ f.selIdx = (f.selIdx + 1) % len(f.fields)
+ return f, f.fields[f.selIdx].Focus()
case "shift+tab":
- ff.fields[ff.selIdx].Blur()
- ff.selIdx--
- if ff.selIdx < 0 {
- ff.selIdx += len(ff.fields)
+ f.fields[f.selIdx].Blur()
+ f.selIdx--
+ if f.selIdx < 0 {
+ f.selIdx += len(f.fields)
}
- return ff, ff.fields[ff.selIdx].Focus()
+ return f, f.fields[f.selIdx].Focus()
}
}
- field, cmd := ff.fields[ff.selIdx].Update(msg)
- ff.fields[ff.selIdx] = field
- return ff, cmd
+ field, cmd := f.fields[f.selIdx].Update(msg)
+ f.fields[f.selIdx] = field
+ return f, cmd
}
-func (ff Form) View() string {
+func (f Form) View() string {
content := ""
- if ff.err != nil {
- content += errorStyle.Render(ff.err.Error()) + "\n\n"
+ if f.err != nil {
+ content += errorStyle.Render(f.err.Error()) + "\n\n"
}
- for i, field := range ff.fields {
+ for i, field := range f.fields {
if i > 0 {
content += "\n\n"
}
content += field.label + ":\n"
- style := ff.UnselectedStyle
- if i == ff.selIdx {
- style = ff.SelectedStyle
+ style := f.UnselectedStyle
+ if i == f.selIdx {
+ style = f.SelectedStyle
}
if style != nil {
content += style.Render(field.View())
@@ -241,6 +273,9 @@ func (f *Form) SetSuggestions(m AppModel) {
}
ff.SetSuggestions(projNames)
ff.ShowSuggestions = true
+ case suggestReportType:
+ ff.SetSuggestions([]string{"Invoice", "Timesheet", "Unified"})
+ ff.ShowSuggestions = true
}
}
}
diff --git a/internal/tui/keys.go b/internal/tui/keys.go
index 3a7242d..69ce223 100644
--- a/internal/tui/keys.go
+++ b/internal/tui/keys.go
@@ -174,6 +174,11 @@ var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map
Description: func(AppModel) string { return "Bottom" },
Result: func(*AppModel) tea.Cmd { return changeSelectionToBottom() },
},
+ "R": KeyBinding{
+ Key: "R",
+ Description: func(AppModel) string { return "Report" },
+ Result: func(*AppModel) tea.Cmd { return createReportModal() },
+ },
},
ScopeHistoryBoxDetails: {
"j": KeyBinding{
diff --git a/internal/tui/modal.go b/internal/tui/modal.go
index badc658..7660243 100644
--- a/internal/tui/modal.go
+++ b/internal/tui/modal.go
@@ -23,6 +23,7 @@ const (
ModalTypeDeleteConfirmation
ModalTypeEntry
ModalTypeHistoryFilter
+ ModalTypeGenerateReport
)
func (mt ModalType) newForm() Form {
@@ -35,6 +36,8 @@ func (mt ModalType) newForm() Form {
return NewProjectForm()
case ModalTypeHistoryFilter:
return NewHistoryFilterForm()
+ case ModalTypeGenerateReport:
+ return NewGenerateReportForm()
}
return Form{}
@@ -91,6 +94,8 @@ func (m ModalBoxModel) Render() string {
return m.RenderFormModal("📂 Project")
case ModalTypeHistoryFilter:
return m.RenderFormModal("🔍 History Filter")
+ case ModalTypeGenerateReport:
+ return m.RenderFormModal("📄 Generate Report")
default: // REMOVE ME
return "DEFAULT CONTENT"
}
@@ -274,6 +279,14 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd {
// Return filter update message
return func() tea.Msg { return updateHistoryFilter(newFilter) }
+
+ case ModalTypeGenerateReport:
+ if err := m.form.Error(); err != nil {
+ return reOpenModal()
+ }
+
+ return generateReport(m, am)
+
}
return nil