diff options
Diffstat (limited to 'internal/reports')
-rw-r--r-- | internal/reports/api.go | 3 | ||||
-rw-r--r-- | internal/reports/invoice.go | 2 | ||||
-rw-r--r-- | internal/reports/pdf.go | 20 | ||||
-rw-r--r-- | internal/reports/pdf_test.go | 7 | ||||
-rw-r--r-- | internal/reports/timesheet.go | 67 | ||||
-rw-r--r-- | internal/reports/timesheet_test.go | 17 | ||||
-rw-r--r-- | internal/reports/unified.go | 5 | ||||
-rw-r--r-- | internal/reports/unified_test.go | 148 |
8 files changed, 134 insertions, 135 deletions
diff --git a/internal/reports/api.go b/internal/reports/api.go index 0db8672..90b066b 100644 --- a/internal/reports/api.go +++ b/internal/reports/api.go @@ -7,7 +7,7 @@ import ( "path/filepath" "time" - "punchcard/internal/queries" + "git.tjp.lol/punchcard/internal/queries" ) type ReportParams struct { @@ -497,4 +497,3 @@ func findProjectByName(ctx context.Context, q *queries.Queries, projectName stri } return projects[0], nil } - diff --git a/internal/reports/invoice.go b/internal/reports/invoice.go index 73235d5..4ac5eb4 100644 --- a/internal/reports/invoice.go +++ b/internal/reports/invoice.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "punchcard/internal/queries" + "git.tjp.lol/punchcard/internal/queries" ) type InvoiceData struct { diff --git a/internal/reports/pdf.go b/internal/reports/pdf.go index 8434f07..b83436f 100644 --- a/internal/reports/pdf.go +++ b/internal/reports/pdf.go @@ -9,8 +9,8 @@ import ( "path/filepath" "time" - "punchcard/internal/queries" - "punchcard/templates" + "git.tjp.lol/punchcard/internal/queries" + "git.tjp.lol/punchcard/templates" ) // RecordInvoice records the invoice in the database after successful generation @@ -229,16 +229,16 @@ type UnifiedJSONData struct { ContractorName string `json:"contractor_name"` ContractorLabel string `json:"contractor_label"` ContractorEmail string `json:"contractor_email"` - + // Invoice-specific fields InvoiceNumber string `json:"invoice_number"` LineItems []LineItem `json:"line_items"` TotalAmount float64 `json:"total_amount"` - + // Timesheet-specific fields - Entries []TimesheetEntry `json:"entries"` - Timezone string `json:"timezone"` - + Entries []TimesheetEntry `json:"entries"` + Timezone string `json:"timezone"` + // Shared field with same value TotalHours float64 `json:"total_hours"` } @@ -268,7 +268,7 @@ func GenerateUnifiedPDF(unifiedData *UnifiedReportData, outputPath string) error ContractorLabel: unifiedData.InvoiceData.ContractorLabel, ContractorEmail: unifiedData.InvoiceData.ContractorEmail, TotalHours: unifiedData.InvoiceData.TotalHours, // Should match timesheet total - + // Invoice-specific fields InvoiceNumber: fmt.Sprintf("%04d-%02d-%03d", unifiedData.InvoiceData.DateRange.Start.Year(), @@ -277,7 +277,7 @@ func GenerateUnifiedPDF(unifiedData *UnifiedReportData, outputPath string) error ), LineItems: unifiedData.InvoiceData.LineItems, TotalAmount: unifiedData.InvoiceData.TotalAmount, - + // Timesheet-specific fields Entries: unifiedData.TimesheetData.Entries, Timezone: unifiedData.TimesheetData.Timezone, @@ -296,7 +296,7 @@ func GenerateUnifiedPDF(unifiedData *UnifiedReportData, outputPath string) error // Create unified template by combining invoice and timesheet templates unifiedTemplate := templates.InvoiceTemplate + "\n\n#pagebreak()\n\n" + templates.TimesheetTemplate - + // Write Typst template file typstFile := filepath.Join(tempDir, "unified.typ") if err := os.WriteFile(typstFile, []byte(unifiedTemplate), 0o644); err != nil { diff --git a/internal/reports/pdf_test.go b/internal/reports/pdf_test.go index b31112e..1f2e968 100644 --- a/internal/reports/pdf_test.go +++ b/internal/reports/pdf_test.go @@ -4,9 +4,10 @@ import ( "os" "os/exec" "path/filepath" - "punchcard/templates" "testing" "time" + + "git.tjp.lol/punchcard/templates" ) // Helper function for tests @@ -39,13 +40,13 @@ func TestTypstTemplateCompilation(t *testing.T) { } dataFile := filepath.Join(tempDir, "data.json") - if err := os.WriteFile(dataFile, testData, 0644); err != nil { + if err := os.WriteFile(dataFile, testData, 0o644); err != nil { t.Fatalf("Failed to write test data file: %v", err) } // Write Typst template to temp directory typstFile := filepath.Join(tempDir, "invoice.typ") - if err := os.WriteFile(typstFile, []byte(templates.InvoiceTemplate), 0644); err != nil { + if err := os.WriteFile(typstFile, []byte(templates.InvoiceTemplate), 0o644); err != nil { t.Fatalf("Failed to write Typst template: %v", err) } diff --git a/internal/reports/timesheet.go b/internal/reports/timesheet.go index a40d8ae..d9ad4b8 100644 --- a/internal/reports/timesheet.go +++ b/internal/reports/timesheet.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "punchcard/internal/queries" + "git.tjp.lol/punchcard/internal/queries" ) type TimesheetData struct { @@ -33,14 +33,14 @@ type TimesheetEntry struct { } type timesheetEntryData struct { - TimeEntryID int64 - StartTime time.Time - EndTime sql.NullTime - Description sql.NullString - ClientID int64 - ClientName string - ProjectID sql.NullInt64 - ProjectName sql.NullString + TimeEntryID int64 + StartTime time.Time + EndTime sql.NullTime + Description sql.NullString + ClientID int64 + ClientName string + ProjectID sql.NullInt64 + ProjectName sql.NullString DurationSeconds int64 } @@ -59,28 +59,28 @@ func GenerateTimesheetData( case []queries.GetTimesheetDataByClientRow: for _, entry := range e { timeEntries = append(timeEntries, timesheetEntryData{ - TimeEntryID: entry.TimeEntryID, - StartTime: entry.StartTime, - EndTime: entry.EndTime, - Description: entry.Description, - ClientID: entry.ClientID, - ClientName: entry.ClientName, - ProjectID: entry.ProjectID, - ProjectName: entry.ProjectName, + TimeEntryID: entry.TimeEntryID, + StartTime: entry.StartTime, + EndTime: entry.EndTime, + Description: entry.Description, + ClientID: entry.ClientID, + ClientName: entry.ClientName, + ProjectID: entry.ProjectID, + ProjectName: entry.ProjectName, DurationSeconds: entry.DurationSeconds, }) } case []queries.GetTimesheetDataByProjectRow: for _, entry := range e { timeEntries = append(timeEntries, timesheetEntryData{ - TimeEntryID: entry.TimeEntryID, - StartTime: entry.StartTime, - EndTime: entry.EndTime, - Description: entry.Description, - ClientID: entry.ClientID, - ClientName: entry.ClientName, - ProjectID: sql.NullInt64{Int64: entry.ProjectID, Valid: true}, - ProjectName: sql.NullString{String: entry.ProjectName, Valid: true}, + TimeEntryID: entry.TimeEntryID, + StartTime: entry.StartTime, + EndTime: entry.EndTime, + Description: entry.Description, + ClientID: entry.ClientID, + ClientName: entry.ClientName, + ProjectID: sql.NullInt64{Int64: entry.ProjectID, Valid: true}, + ProjectName: sql.NullString{String: entry.ProjectName, Valid: true}, DurationSeconds: entry.DurationSeconds, }) } @@ -134,27 +134,27 @@ func convertToTimesheetEntries(entries []timesheetEntryData, loc *time.Location) // Convert UTC times to specified timezone localStartTime := entry.StartTime.In(loc) localEndTime := entry.EndTime.Time.In(loc) - + // Format date as YYYY-MM-DD date := localStartTime.Format("2006-01-02") - + // Format times as HH:MM startTime := localStartTime.Format("15:04") endTime := localEndTime.Format("15:04") - + // Format duration as HH:MM duration := formatDuration(entry.DurationSeconds) - + // Calculate hours as decimal, rounded to nearest minute - totalMinutes := (entry.DurationSeconds + 30) / 60 // Round to nearest minute + totalMinutes := (entry.DurationSeconds + 30) / 60 // Round to nearest minute hours := float64(totalMinutes) / 60.0 - + // Get project name projectName := "" if entry.ProjectName.Valid { projectName = entry.ProjectName.String } - + // Get description description := "" if entry.Description.Valid { @@ -177,9 +177,8 @@ func convertToTimesheetEntries(entries []timesheetEntryData, loc *time.Location) func formatDuration(seconds int64) string { // Round to nearest minute - totalMinutes := (seconds + 30) / 60 // Add 30 seconds for rounding + totalMinutes := (seconds + 30) / 60 // Add 30 seconds for rounding hours := totalMinutes / 60 minutes := totalMinutes % 60 return fmt.Sprintf("%d:%02d", hours, minutes) } - diff --git a/internal/reports/timesheet_test.go b/internal/reports/timesheet_test.go index 8c0ac52..ed35c86 100644 --- a/internal/reports/timesheet_test.go +++ b/internal/reports/timesheet_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "punchcard/internal/queries" - "punchcard/templates" + "git.tjp.lol/punchcard/internal/queries" + "git.tjp.lol/punchcard/templates" ) func TestGenerateTimesheetData(t *testing.T) { @@ -217,7 +217,7 @@ func TestConvertToTimesheetEntries(t *testing.T) { entries: []timesheetEntryData{ { TimeEntryID: 2, - StartTime: mustParseTime("2025-07-10T18:00:00Z"), // 6:00 PM UTC + StartTime: mustParseTime("2025-07-10T18:00:00Z"), // 6:00 PM UTC EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T19:00:00Z"), Valid: true}, // 7:00 PM UTC Description: sql.NullString{String: "Meeting", Valid: true}, DurationSeconds: 3600, // 1 hour @@ -373,13 +373,13 @@ func TestTimesheetTypstTemplateCompilation(t *testing.T) { }` dataFile := filepath.Join(tempDir, "data.json") - if err := os.WriteFile(dataFile, []byte(testData), 0644); err != nil { + if err := os.WriteFile(dataFile, []byte(testData), 0o644); err != nil { t.Fatalf("Failed to write test data file: %v", err) } // Write Typst template to temp directory typstFile := filepath.Join(tempDir, "timesheet.typ") - if err := os.WriteFile(typstFile, []byte(templates.TimesheetTemplate), 0644); err != nil { + if err := os.WriteFile(typstFile, []byte(templates.TimesheetTemplate), 0o644); err != nil { t.Fatalf("Failed to write Typst template: %v", err) } @@ -502,12 +502,12 @@ func TestGenerateDefaultTimesheetFilename(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := GenerateDefaultTimesheetFilename(tt.clientName, tt.projectName, dateRange) - + // Check that the filename starts with the expected pattern if len(result) < len(tt.want) || result[:len(tt.want)] != tt.want { t.Errorf("GenerateDefaultTimesheetFilename() prefix = %s, want prefix %s", result, tt.want) } - + // Check that it ends with .pdf if result[len(result)-4:] != ".pdf" { t.Errorf("GenerateDefaultTimesheetFilename() should end with .pdf, got %s", result) @@ -538,4 +538,5 @@ func abs(x float64) float64 { return -x } return x -}
\ No newline at end of file +} + diff --git a/internal/reports/unified.go b/internal/reports/unified.go index a6eb8c4..07507ae 100644 --- a/internal/reports/unified.go +++ b/internal/reports/unified.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "punchcard/internal/queries" + "git.tjp.lol/punchcard/internal/queries" ) type UnifiedReportData struct { @@ -80,4 +80,5 @@ func GenerateUnifiedReportData( InvoiceData: invoiceData, TimesheetData: timesheetData, }, nil -}
\ No newline at end of file +} + diff --git a/internal/reports/unified_test.go b/internal/reports/unified_test.go index 64d0b3f..cf18350 100644 --- a/internal/reports/unified_test.go +++ b/internal/reports/unified_test.go @@ -5,10 +5,9 @@ import ( "testing" "time" - "punchcard/internal/queries" + "git.tjp.lol/punchcard/internal/queries" ) - func TestGenerateUnifiedReportData(t *testing.T) { tests := []struct { name string @@ -29,30 +28,30 @@ func TestGenerateUnifiedReportData(t *testing.T) { name: "client entries with UTC timezone", entries: []queries.GetInvoiceDataByClientRow{ { - TimeEntryID: 1, - StartTime: mustParseTime("2025-07-10T14:55:00Z"), - EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T18:05:00Z"), Valid: true}, - Description: sql.NullString{String: "GL closing", Valid: true}, - ClientID: 1, - ClientName: "Test Client", - ProjectID: sql.NullInt64{Int64: 1, Valid: true}, - ProjectName: sql.NullString{String: "Test Project", Valid: true}, - DurationSeconds: 11400, // 3:10 + TimeEntryID: 1, + StartTime: mustParseTime("2025-07-10T14:55:00Z"), + EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T18:05:00Z"), Valid: true}, + Description: sql.NullString{String: "GL closing", Valid: true}, + ClientID: 1, + ClientName: "Test Client", + ProjectID: sql.NullInt64{Int64: 1, Valid: true}, + ProjectName: sql.NullString{String: "Test Project", Valid: true}, + DurationSeconds: 11400, // 3:10 EntryBillableRate: sql.NullInt64{Int64: 150, Valid: true}, ClientBillableRate: sql.NullInt64{Int64: 150, Valid: true}, ProjectBillableRate: sql.NullInt64{Int64: 150, Valid: true}, RateSource: "entry", }, { - TimeEntryID: 2, - StartTime: mustParseTime("2025-07-10T18:42:00Z"), - EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T20:04:00Z"), Valid: true}, - Description: sql.NullString{String: "GL closing", Valid: true}, - ClientID: 1, - ClientName: "Test Client", - ProjectID: sql.NullInt64{Int64: 1, Valid: true}, - ProjectName: sql.NullString{String: "Test Project", Valid: true}, - DurationSeconds: 4920, // 1:22 + TimeEntryID: 2, + StartTime: mustParseTime("2025-07-10T18:42:00Z"), + EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T20:04:00Z"), Valid: true}, + Description: sql.NullString{String: "GL closing", Valid: true}, + ClientID: 1, + ClientName: "Test Client", + ProjectID: sql.NullInt64{Int64: 1, Valid: true}, + ProjectName: sql.NullString{String: "Test Project", Valid: true}, + DurationSeconds: 4920, // 1:22 EntryBillableRate: sql.NullInt64{Int64: 150, Valid: true}, ClientBillableRate: sql.NullInt64{Int64: 150, Valid: true}, ProjectBillableRate: sql.NullInt64{Int64: 150, Valid: true}, @@ -66,13 +65,13 @@ func TestGenerateUnifiedReportData(t *testing.T) { Label: "Software Development", Email: "travis@example.com", }, - invoiceNumber: 123, + invoiceNumber: 123, dateRange: DateRange{ Start: mustParseTime("2025-07-01T00:00:00Z"), End: mustParseTime("2025-07-31T23:59:59Z"), }, timezone: time.UTC, - wantEntries: 1, // Both entries have same rate so grouped together + wantEntries: 1, // Both entries have same rate so grouped together wantHours: 4.5333, // 16320 seconds / 3600 wantTotalAmount: 6.80, // 4.5333 * 1.50 }, @@ -80,14 +79,14 @@ func TestGenerateUnifiedReportData(t *testing.T) { name: "project entries with local timezone", entries: []queries.GetInvoiceDataByProjectRow{ { - TimeEntryID: 3, - StartTime: mustParseTime("2025-07-11T13:55:00Z"), - EndTime: sql.NullTime{Time: mustParseTime("2025-07-11T18:35:00Z"), Valid: true}, - Description: sql.NullString{String: "Development work", Valid: true}, - ClientID: 1, - ClientName: "Test Client", - ProjectID: 1, - ProjectName: "Test Project", + TimeEntryID: 3, + StartTime: mustParseTime("2025-07-11T13:55:00Z"), + EndTime: sql.NullTime{Time: mustParseTime("2025-07-11T18:35:00Z"), Valid: true}, + Description: sql.NullString{String: "Development work", Valid: true}, + ClientID: 1, + ClientName: "Test Client", + ProjectID: 1, + ProjectName: "Test Project", DurationSeconds: 16800, // 4:40 EntryBillableRate: sql.NullInt64{Int64: 125, Valid: true}, ClientBillableRate: sql.NullInt64{Int64: 125, Valid: true}, @@ -117,15 +116,15 @@ func TestGenerateUnifiedReportData(t *testing.T) { name: "entries with different timezone", entries: []queries.GetInvoiceDataByClientRow{ { - TimeEntryID: 4, - StartTime: mustParseTime("2025-07-15T00:09:00Z"), - EndTime: sql.NullTime{Time: mustParseTime("2025-07-15T00:13:00Z"), Valid: true}, - Description: sql.NullString{String: "Quick fix", Valid: true}, - ClientID: 1, - ClientName: "Test Client", - ProjectID: sql.NullInt64{Int64: 1, Valid: true}, - ProjectName: sql.NullString{String: "Test Project", Valid: true}, - DurationSeconds: 240, // 4 minutes + TimeEntryID: 4, + StartTime: mustParseTime("2025-07-15T00:09:00Z"), + EndTime: sql.NullTime{Time: mustParseTime("2025-07-15T00:13:00Z"), Valid: true}, + Description: sql.NullString{String: "Quick fix", Valid: true}, + ClientID: 1, + ClientName: "Test Client", + ProjectID: sql.NullInt64{Int64: 1, Valid: true}, + ProjectName: sql.NullString{String: "Test Project", Valid: true}, + DurationSeconds: 240, // 4 minutes EntryBillableRate: sql.NullInt64{Int64: 200, Valid: true}, ClientBillableRate: sql.NullInt64{Int64: 200, Valid: true}, ProjectBillableRate: sql.NullInt64{Int64: 200, Valid: true}, @@ -278,14 +277,14 @@ func TestUnifiedReportDataConsistency(t *testing.T) { // Test that unified report produces consistent data between invoice and timesheet components entries := []queries.GetInvoiceDataByClientRow{ { - TimeEntryID: 1, - StartTime: mustParseTime("2025-07-10T14:55:00Z"), - EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T18:05:00Z"), Valid: true}, - Description: sql.NullString{String: "Development work", Valid: true}, - ClientID: 1, - ClientName: "Test Client", - ProjectID: sql.NullInt64{Int64: 1, Valid: true}, - ProjectName: sql.NullString{String: "Test Project", Valid: true}, + TimeEntryID: 1, + StartTime: mustParseTime("2025-07-10T14:55:00Z"), + EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T18:05:00Z"), Valid: true}, + Description: sql.NullString{String: "Development work", Valid: true}, + ClientID: 1, + ClientName: "Test Client", + ProjectID: sql.NullInt64{Int64: 1, Valid: true}, + ProjectName: sql.NullString{String: "Test Project", Valid: true}, DurationSeconds: 11400, // 3:10 EntryBillableRate: sql.NullInt64{Int64: 150, Valid: true}, ClientBillableRate: sql.NullInt64{Int64: 150, Valid: true}, @@ -293,14 +292,14 @@ func TestUnifiedReportDataConsistency(t *testing.T) { RateSource: "entry", }, { - TimeEntryID: 2, - StartTime: mustParseTime("2025-07-10T18:42:00Z"), - EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T20:04:00Z"), Valid: true}, - Description: sql.NullString{String: "Code review", Valid: true}, - ClientID: 1, - ClientName: "Test Client", - ProjectID: sql.NullInt64{Int64: 1, Valid: true}, - ProjectName: sql.NullString{String: "Test Project", Valid: true}, + TimeEntryID: 2, + StartTime: mustParseTime("2025-07-10T18:42:00Z"), + EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T20:04:00Z"), Valid: true}, + Description: sql.NullString{String: "Code review", Valid: true}, + ClientID: 1, + ClientName: "Test Client", + ProjectID: sql.NullInt64{Int64: 1, Valid: true}, + ProjectName: sql.NullString{String: "Test Project", Valid: true}, DurationSeconds: 4920, // 1:22 EntryBillableRate: sql.NullInt64{Int64: 150, Valid: true}, ClientBillableRate: sql.NullInt64{Int64: 150, Valid: true}, @@ -330,7 +329,6 @@ func TestUnifiedReportDataConsistency(t *testing.T) { dateRange, time.UTC, ) - if err != nil { t.Fatalf("GenerateUnifiedReportData() error = %v", err) } @@ -385,15 +383,15 @@ func TestUnifiedReportEntryTypeConversion(t *testing.T) { name: "client entries conversion", entries: []queries.GetInvoiceDataByClientRow{ { - TimeEntryID: 1, - StartTime: mustParseTime("2025-07-10T14:55:00Z"), - EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T18:05:00Z"), Valid: true}, - Description: sql.NullString{String: "Work", Valid: true}, - ClientID: 1, - ClientName: "Test Client", - ProjectID: sql.NullInt64{Int64: 1, Valid: true}, - ProjectName: sql.NullString{String: "Test Project", Valid: true}, - DurationSeconds: 11400, + TimeEntryID: 1, + StartTime: mustParseTime("2025-07-10T14:55:00Z"), + EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T18:05:00Z"), Valid: true}, + Description: sql.NullString{String: "Work", Valid: true}, + ClientID: 1, + ClientName: "Test Client", + ProjectID: sql.NullInt64{Int64: 1, Valid: true}, + ProjectName: sql.NullString{String: "Test Project", Valid: true}, + DurationSeconds: 11400, EntryBillableRate: sql.NullInt64{Int64: 150, Valid: true}, ClientBillableRate: sql.NullInt64{Int64: 150, Valid: true}, ProjectBillableRate: sql.NullInt64{Int64: 150, Valid: true}, @@ -406,14 +404,14 @@ func TestUnifiedReportEntryTypeConversion(t *testing.T) { name: "project entries conversion", entries: []queries.GetInvoiceDataByProjectRow{ { - TimeEntryID: 2, - StartTime: mustParseTime("2025-07-11T13:55:00Z"), - EndTime: sql.NullTime{Time: mustParseTime("2025-07-11T18:35:00Z"), Valid: true}, - Description: sql.NullString{String: "Work", Valid: true}, - ClientID: 1, - ClientName: "Test Client", - ProjectID: 1, - ProjectName: "Test Project", + TimeEntryID: 2, + StartTime: mustParseTime("2025-07-11T13:55:00Z"), + EndTime: sql.NullTime{Time: mustParseTime("2025-07-11T18:35:00Z"), Valid: true}, + Description: sql.NullString{String: "Work", Valid: true}, + ClientID: 1, + ClientName: "Test Client", + ProjectID: 1, + ProjectName: "Test Project", DurationSeconds: 16800, EntryBillableRate: sql.NullInt64{Int64: 125, Valid: true}, ClientBillableRate: sql.NullInt64{Int64: 125, Valid: true}, @@ -526,7 +524,6 @@ func TestUnifiedReportEmptyEntries(t *testing.T) { dateRange, time.UTC, ) - if err != nil { t.Errorf("GenerateUnifiedReportData() error = %v", err) return @@ -559,4 +556,5 @@ func TestUnifiedReportEmptyEntries(t *testing.T) { } }) } -}
\ No newline at end of file +} + |