summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-08-13 16:48:58 -0600
committerT <t@tjp.lol>2025-08-13 16:49:20 -0600
commit7d0d21ba8663ab7ff777a06f4b113337fa717ff3 (patch)
treef06d8691fd13bee6ebccfc17a08c599bd5710705
parent5c3554c7e49abe263faf54c61e435ba1d5202d27 (diff)
go module in git.tjp.lol
-rw-r--r--README.md6
-rw-r--r--cmd/punch/main.go4
-rw-r--r--go.mod4
-rw-r--r--internal/actions/actions.go2
-rw-r--r--internal/actions/clients.go2
-rw-r--r--internal/actions/projects.go2
-rw-r--r--internal/actions/timer.go2
-rw-r--r--internal/commands/add_client.go4
-rw-r--r--internal/commands/add_client_test.go2
-rw-r--r--internal/commands/add_project.go4
-rw-r--r--internal/commands/add_project_test.go2
-rw-r--r--internal/commands/billable_rate_test.go2
-rw-r--r--internal/commands/helpers.go8
-rw-r--r--internal/commands/import.go6
-rw-r--r--internal/commands/import_test.go2
-rw-r--r--internal/commands/in.go4
-rw-r--r--internal/commands/in_test.go2
-rw-r--r--internal/commands/out.go4
-rw-r--r--internal/commands/out_test.go2
-rw-r--r--internal/commands/report.go6
-rw-r--r--internal/commands/report_coalescing_test.go4
-rw-r--r--internal/commands/root.go4
-rw-r--r--internal/commands/set.go7
-rw-r--r--internal/commands/status.go4
-rw-r--r--internal/commands/status_test.go2
-rw-r--r--internal/commands/test_utils.go6
-rw-r--r--internal/commands/tui.go4
-rw-r--r--internal/context/db.go3
-rw-r--r--internal/database/db.go2
-rw-r--r--internal/reports/api.go3
-rw-r--r--internal/reports/invoice.go2
-rw-r--r--internal/reports/pdf.go20
-rw-r--r--internal/reports/pdf_test.go7
-rw-r--r--internal/reports/timesheet.go67
-rw-r--r--internal/reports/timesheet_test.go17
-rw-r--r--internal/reports/unified.go5
-rw-r--r--internal/reports/unified_test.go148
-rw-r--r--internal/tui/app.go2
-rw-r--r--internal/tui/commands.go6
-rw-r--r--internal/tui/form.go6
-rw-r--r--internal/tui/history_box.go2
-rw-r--r--internal/tui/modal.go6
-rw-r--r--internal/tui/projects_box.go2
-rw-r--r--internal/tui/shared.go2
-rw-r--r--internal/tui/timer_box.go2
45 files changed, 201 insertions, 202 deletions
diff --git a/README.md b/README.md
index ac54f5f..af05474 100644
--- a/README.md
+++ b/README.md
@@ -65,14 +65,14 @@ Reports are generated using Typst and compiled to PDF automatically.
If you have Go installed:
```bash
-go install git.tjp.lol/punchcard.git
+go install git.tjp.lol/punchcard/cmd/punch@latest
```
Or build from source:
```bash
-git clone git.tjp.lol/punchcard.git
+git clone https://git.tjp.lol/punchcard.git
cd punchcard
-go build -o punch cmd/punch/main.go
+go install ./cmd/punch
```
## Requirements
diff --git a/cmd/punch/main.go b/cmd/punch/main.go
index c98b970..6cf3fa3 100644
--- a/cmd/punch/main.go
+++ b/cmd/punch/main.go
@@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"os"
- "punchcard/internal/commands"
+ "git.tjp.lol/punchcard/internal/commands"
)
func main() {
@@ -18,4 +18,4 @@ func main() {
// Default error handling
os.Exit(1)
}
-} \ No newline at end of file
+}
diff --git a/go.mod b/go.mod
index 9f0013b..3b14e71 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,11 @@
-module punchcard
+module git.tjp.lol/punchcard
go 1.24.4
require (
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.6
+ github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3
github.com/spf13/cobra v1.9.1
modernc.org/sqlite v1.38.2
@@ -14,7 +15,6 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
- github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
diff --git a/internal/actions/actions.go b/internal/actions/actions.go
index d3e1460..61d007b 100644
--- a/internal/actions/actions.go
+++ b/internal/actions/actions.go
@@ -3,7 +3,7 @@ package actions
import (
"context"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
// Actions provides high-level business operations for time tracking
diff --git a/internal/actions/clients.go b/internal/actions/clients.go
index c71ef4a..1a99d59 100644
--- a/internal/actions/clients.go
+++ b/internal/actions/clients.go
@@ -8,7 +8,7 @@ import (
"strconv"
"strings"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
// CreateClient creates a new client with the given name and optional email/rate
diff --git a/internal/actions/projects.go b/internal/actions/projects.go
index 21f5ef5..4cb4638 100644
--- a/internal/actions/projects.go
+++ b/internal/actions/projects.go
@@ -6,7 +6,7 @@ import (
"fmt"
"strconv"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
// CreateProject creates a new project for the specified client
diff --git a/internal/actions/timer.go b/internal/actions/timer.go
index 9f29c51..a7e7bbb 100644
--- a/internal/actions/timer.go
+++ b/internal/actions/timer.go
@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
// PunchIn starts a timer for the specified client/project
diff --git a/internal/commands/add_client.go b/internal/commands/add_client.go
index 98aec3d..8b1bb0d 100644
--- a/internal/commands/add_client.go
+++ b/internal/commands/add_client.go
@@ -3,8 +3,8 @@ package commands
import (
"fmt"
- "punchcard/internal/actions"
- "punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/actions"
+ "git.tjp.lol/punchcard/internal/context"
"github.com/spf13/cobra"
)
diff --git a/internal/commands/add_client_test.go b/internal/commands/add_client_test.go
index 23c6e71..0373002 100644
--- a/internal/commands/add_client_test.go
+++ b/internal/commands/add_client_test.go
@@ -4,7 +4,7 @@ import (
"context"
"testing"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
func TestAddClientCommand(t *testing.T) {
diff --git a/internal/commands/add_project.go b/internal/commands/add_project.go
index 1ed42db..6aede2a 100644
--- a/internal/commands/add_project.go
+++ b/internal/commands/add_project.go
@@ -3,8 +3,8 @@ package commands
import (
"fmt"
- "punchcard/internal/actions"
- punchctx "punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/actions"
+ punchctx "git.tjp.lol/punchcard/internal/context"
"github.com/spf13/cobra"
)
diff --git a/internal/commands/add_project_test.go b/internal/commands/add_project_test.go
index c41bd5c..1857d9e 100644
--- a/internal/commands/add_project_test.go
+++ b/internal/commands/add_project_test.go
@@ -5,7 +5,7 @@ import (
"database/sql"
"testing"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
func TestAddProjectCommand(t *testing.T) {
diff --git a/internal/commands/billable_rate_test.go b/internal/commands/billable_rate_test.go
index 335eb07..228e29d 100644
--- a/internal/commands/billable_rate_test.go
+++ b/internal/commands/billable_rate_test.go
@@ -6,7 +6,7 @@ import (
"testing"
"time"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
func TestTimeEntryBillableRateCoalescing(t *testing.T) {
diff --git a/internal/commands/helpers.go b/internal/commands/helpers.go
index a0c572b..cf2747d 100644
--- a/internal/commands/helpers.go
+++ b/internal/commands/helpers.go
@@ -3,8 +3,9 @@ package commands
import (
"context"
"errors"
- "punchcard/internal/actions"
- "punchcard/internal/queries"
+
+ "git.tjp.lol/punchcard/internal/actions"
+ "git.tjp.lol/punchcard/internal/queries"
)
// ErrNoActiveTimer is returned when trying to punch out but no timer is active
@@ -29,4 +30,5 @@ func findProject(ctx context.Context, q *queries.Queries, projectRef string) (qu
return queries.Project{}, err
}
return *project, nil
-} \ No newline at end of file
+}
+
diff --git a/internal/commands/import.go b/internal/commands/import.go
index acbb0d6..28862b2 100644
--- a/internal/commands/import.go
+++ b/internal/commands/import.go
@@ -9,9 +9,9 @@ import (
"strings"
"time"
- punchctx "punchcard/internal/context"
- "punchcard/internal/database"
- "punchcard/internal/queries"
+ punchctx "git.tjp.lol/punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/database"
+ "git.tjp.lol/punchcard/internal/queries"
"github.com/spf13/cobra"
)
diff --git a/internal/commands/import_test.go b/internal/commands/import_test.go
index ed59f92..6907b23 100644
--- a/internal/commands/import_test.go
+++ b/internal/commands/import_test.go
@@ -9,7 +9,7 @@ import (
"testing"
"time"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
func TestImportCommand(t *testing.T) {
diff --git a/internal/commands/in.go b/internal/commands/in.go
index e7847f6..8c5025a 100644
--- a/internal/commands/in.go
+++ b/internal/commands/in.go
@@ -3,8 +3,8 @@ package commands
import (
"fmt"
- "punchcard/internal/actions"
- punchctx "punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/actions"
+ punchctx "git.tjp.lol/punchcard/internal/context"
"github.com/spf13/cobra"
)
diff --git a/internal/commands/in_test.go b/internal/commands/in_test.go
index eac70a2..7d11a29 100644
--- a/internal/commands/in_test.go
+++ b/internal/commands/in_test.go
@@ -6,7 +6,7 @@ import (
"strings"
"testing"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
func TestInCommand(t *testing.T) {
diff --git a/internal/commands/out.go b/internal/commands/out.go
index 1355f3d..a10bb6e 100644
--- a/internal/commands/out.go
+++ b/internal/commands/out.go
@@ -5,8 +5,8 @@ import (
"fmt"
"time"
- "punchcard/internal/actions"
- punchctx "punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/actions"
+ punchctx "git.tjp.lol/punchcard/internal/context"
"github.com/spf13/cobra"
)
diff --git a/internal/commands/out_test.go b/internal/commands/out_test.go
index aeb2359..03a9b73 100644
--- a/internal/commands/out_test.go
+++ b/internal/commands/out_test.go
@@ -6,7 +6,7 @@ import (
"errors"
"testing"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
func TestOutCommand(t *testing.T) {
diff --git a/internal/commands/report.go b/internal/commands/report.go
index 30972b4..530a237 100644
--- a/internal/commands/report.go
+++ b/internal/commands/report.go
@@ -4,9 +4,9 @@ import (
"fmt"
"time"
- punchctx "punchcard/internal/context"
- "punchcard/internal/database"
- "punchcard/internal/reports"
+ punchctx "git.tjp.lol/punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/database"
+ "git.tjp.lol/punchcard/internal/reports"
"github.com/spf13/cobra"
)
diff --git a/internal/commands/report_coalescing_test.go b/internal/commands/report_coalescing_test.go
index 8f4d221..fd0172c 100644
--- a/internal/commands/report_coalescing_test.go
+++ b/internal/commands/report_coalescing_test.go
@@ -6,7 +6,7 @@ import (
"testing"
"time"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
func TestReportRateCoalescing(t *testing.T) {
@@ -248,4 +248,4 @@ func TestReportRateCoalescingWithoutProject(t *testing.T) {
if entry.EntryBillableRate.Valid {
t.Errorf("Expected entry billable rate to be NULL when no explicit override, got %d", entry.EntryBillableRate.Int64)
}
-} \ No newline at end of file
+}
diff --git a/internal/commands/root.go b/internal/commands/root.go
index f3611ac..9ac0790 100644
--- a/internal/commands/root.go
+++ b/internal/commands/root.go
@@ -4,8 +4,8 @@ import (
"context"
"database/sql"
- punchctx "punchcard/internal/context"
- "punchcard/internal/database"
+ punchctx "git.tjp.lol/punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/database"
"github.com/spf13/cobra"
)
diff --git a/internal/commands/set.go b/internal/commands/set.go
index 32f3b96..b6e6cb5 100644
--- a/internal/commands/set.go
+++ b/internal/commands/set.go
@@ -7,9 +7,9 @@ import (
"strconv"
"strings"
- punchctx "punchcard/internal/context"
- "punchcard/internal/database"
- "punchcard/internal/queries"
+ punchctx "git.tjp.lol/punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/database"
+ "git.tjp.lol/punchcard/internal/queries"
"github.com/spf13/cobra"
)
@@ -330,4 +330,3 @@ func setContractorValues(q *queries.Queries, updates map[string]string) error {
return nil
}
-
diff --git a/internal/commands/status.go b/internal/commands/status.go
index 626b258..0f3dc29 100644
--- a/internal/commands/status.go
+++ b/internal/commands/status.go
@@ -8,8 +8,8 @@ import (
"strconv"
"time"
- punchctx "punchcard/internal/context"
- "punchcard/internal/queries"
+ punchctx "git.tjp.lol/punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/queries"
"github.com/spf13/cobra"
)
diff --git a/internal/commands/status_test.go b/internal/commands/status_test.go
index d7c2ac8..ef80e2d 100644
--- a/internal/commands/status_test.go
+++ b/internal/commands/status_test.go
@@ -6,7 +6,7 @@ import (
"strings"
"testing"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
func TestStatusCommand(t *testing.T) {
diff --git a/internal/commands/test_utils.go b/internal/commands/test_utils.go
index 282e472..c53578f 100644
--- a/internal/commands/test_utils.go
+++ b/internal/commands/test_utils.go
@@ -6,9 +6,9 @@ import (
"database/sql"
"testing"
- punchctx "punchcard/internal/context"
- "punchcard/internal/database"
- "punchcard/internal/queries"
+ punchctx "git.tjp.lol/punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/database"
+ "git.tjp.lol/punchcard/internal/queries"
)
func setupTestDB(t *testing.T) (*queries.Queries, func()) {
diff --git a/internal/commands/tui.go b/internal/commands/tui.go
index 529e937..774e982 100644
--- a/internal/commands/tui.go
+++ b/internal/commands/tui.go
@@ -3,8 +3,8 @@ package commands
import (
"fmt"
- punchctx "punchcard/internal/context"
- "punchcard/internal/tui"
+ punchctx "git.tjp.lol/punchcard/internal/context"
+ "git.tjp.lol/punchcard/internal/tui"
"github.com/spf13/cobra"
)
diff --git a/internal/context/db.go b/internal/context/db.go
index a9f53d3..044dc28 100644
--- a/internal/context/db.go
+++ b/internal/context/db.go
@@ -3,7 +3,7 @@ package context
import (
"context"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
type dbContextKey struct{}
@@ -20,4 +20,3 @@ func GetDB(ctx context.Context) *queries.Queries {
}
return nil
}
-
diff --git a/internal/database/db.go b/internal/database/db.go
index f699d14..27fb472 100644
--- a/internal/database/db.go
+++ b/internal/database/db.go
@@ -7,7 +7,7 @@ import (
"os"
"path/filepath"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
_ "modernc.org/sqlite"
)
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
+}
+
diff --git a/internal/tui/app.go b/internal/tui/app.go
index 4b31c38..f434034 100644
--- a/internal/tui/app.go
+++ b/internal/tui/app.go
@@ -5,7 +5,7 @@ import (
"fmt"
"time"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss/v2"
diff --git a/internal/tui/commands.go b/internal/tui/commands.go
index 9a836c5..657b3f5 100644
--- a/internal/tui/commands.go
+++ b/internal/tui/commands.go
@@ -6,9 +6,9 @@ import (
"strings"
"time"
- "punchcard/internal/actions"
- "punchcard/internal/queries"
- "punchcard/internal/reports"
+ "git.tjp.lol/punchcard/internal/actions"
+ "git.tjp.lol/punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/reports"
tea "github.com/charmbracelet/bubbletea"
)
diff --git a/internal/tui/form.go b/internal/tui/form.go
index d70fb8b..09db989 100644
--- a/internal/tui/form.go
+++ b/internal/tui/form.go
@@ -7,7 +7,7 @@ import (
"strings"
"time"
- "punchcard/internal/reports"
+ "git.tjp.lol/punchcard/internal/reports"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
@@ -104,8 +104,8 @@ func newDateRangeField(label string) FormField {
func newReportTypeField(label string) FormField {
f := FormField{
- Model: textinput.New(),
- label: label,
+ Model: textinput.New(),
+ label: label,
suggestions: suggestReportType,
}
f.Validate = func(s string) error {
diff --git a/internal/tui/history_box.go b/internal/tui/history_box.go
index 01ea59b..b4cccf2 100644
--- a/internal/tui/history_box.go
+++ b/internal/tui/history_box.go
@@ -6,7 +6,7 @@ import (
"strconv"
"time"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
"github.com/charmbracelet/bubbles/viewport"
"github.com/charmbracelet/lipgloss/v2"
diff --git a/internal/tui/modal.go b/internal/tui/modal.go
index 7660243..77b3fa9 100644
--- a/internal/tui/modal.go
+++ b/internal/tui/modal.go
@@ -7,9 +7,9 @@ import (
"strconv"
"time"
- "punchcard/internal/actions"
- "punchcard/internal/queries"
- "punchcard/internal/reports"
+ "git.tjp.lol/punchcard/internal/actions"
+ "git.tjp.lol/punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/reports"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss/v2"
diff --git a/internal/tui/projects_box.go b/internal/tui/projects_box.go
index 9c62d4d..3bf44b5 100644
--- a/internal/tui/projects_box.go
+++ b/internal/tui/projects_box.go
@@ -4,7 +4,7 @@ import (
"fmt"
"strconv"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
"github.com/charmbracelet/lipgloss/v2"
)
diff --git a/internal/tui/shared.go b/internal/tui/shared.go
index 7ef7772..c79560a 100644
--- a/internal/tui/shared.go
+++ b/internal/tui/shared.go
@@ -8,7 +8,7 @@ import (
"slices"
"time"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
"github.com/charmbracelet/lipgloss/v2"
)
diff --git a/internal/tui/timer_box.go b/internal/tui/timer_box.go
index 09a42c7..891004a 100644
--- a/internal/tui/timer_box.go
+++ b/internal/tui/timer_box.go
@@ -4,7 +4,7 @@ import (
"fmt"
"time"
- "punchcard/internal/queries"
+ "git.tjp.lol/punchcard/internal/queries"
)
// TimerInfo holds information about the current or most recent timer state