diff options
author | T <t@tjp.lol> | 2025-08-13 15:25:23 -0600 |
---|---|---|
committer | T <t@tjp.lol> | 2025-08-13 16:02:05 -0600 |
commit | e9e6eb4e456ee53da5a6ef743251410d4d3d8381 (patch) | |
tree | d722145a5f7a3dd07623e96045078983b3a14e4c /internal/reports | |
parent | d6781f3e5b431057c23b2deaa943f273699e37f5 (diff) |
report generation modal
Diffstat (limited to 'internal/reports')
-rw-r--r-- | internal/reports/api.go | 500 |
1 files changed, 500 insertions, 0 deletions
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 +} + |