1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
package actions
import (
"context"
"database/sql"
"fmt"
"regexp"
"strconv"
"strings"
"git.tjp.lol/punchcard/internal/queries"
)
// CreateClient creates a new client with the given name and optional email/rate
func (a *actions) CreateClient(ctx context.Context, name, email string, billableRate *float64) (*queries.Client, error) {
// Parse name and email if name contains email format "Name <email>"
finalName, finalEmail := parseNameAndEmail(name, email)
var emailParam sql.NullString
if finalEmail != "" {
emailParam = sql.NullString{String: finalEmail, Valid: true}
}
var billableRateParam sql.NullInt64
if billableRate != nil {
billableRateParam = sql.NullInt64{Int64: int64(*billableRate * 100), Valid: true}
}
client, err := a.queries.CreateClient(ctx, queries.CreateClientParams{
Name: finalName,
Email: emailParam,
BillableRate: billableRateParam,
})
if err != nil {
return nil, fmt.Errorf("failed to create client: %w", err)
}
return &client, nil
}
func (a *actions) EditClient(ctx context.Context, id int64, name, email string, billableRate *float64) (*queries.Client, error) {
finalName, finalEmail := parseNameAndEmail(name, email)
var emailParam sql.NullString
if finalEmail != "" {
emailParam = sql.NullString{String: finalEmail, Valid: true}
}
var rateParam sql.NullInt64
if billableRate != nil {
rateParam = sql.NullInt64{Int64: int64(*billableRate * 100), Valid: true}
}
client, err := a.queries.UpdateClient(ctx, queries.UpdateClientParams{
Name: finalName,
Email: emailParam,
BillableRate: rateParam,
ID: id,
})
if err != nil {
return nil, fmt.Errorf("failed to update client: %w", err)
}
return &client, nil
}
// FindClient finds a client by name or ID
func (a *actions) FindClient(ctx context.Context, nameOrID string) (*queries.Client, error) {
// Parse as ID if possible, otherwise use 0
var idParam int64
if id, err := strconv.ParseInt(nameOrID, 10, 64); err == nil {
idParam = id
}
// Search by both ID and name
clients, err := a.queries.FindClient(ctx, queries.FindClientParams{
ID: idParam,
Name: nameOrID,
})
if err != nil {
return nil, fmt.Errorf("database error looking up client: %w", err)
}
// Check results
switch len(clients) {
case 0:
return nil, fmt.Errorf("%w: %s", ErrClientNotFound, nameOrID)
case 1:
return &clients[0], nil
default:
return nil, fmt.Errorf("%w: %s matches multiple clients", ErrAmbiguousClient, nameOrID)
}
}
// parseNameAndEmail handles parsing name and email from various input formats
func parseNameAndEmail(nameArg, emailArg string) (string, string) {
// If separate email provided, use it (but still check for embedded format)
finalEmail := emailArg
if finalEmail != "" {
if matches := emailAndNameRegex.FindStringSubmatch(finalEmail); matches != nil {
finalEmail = strings.TrimSpace(matches[2])
}
}
// Check if name contains embedded email format "Name <email@domain.com>"
finalName := nameArg
if matches := emailAndNameRegex.FindStringSubmatch(nameArg); matches != nil {
finalName = strings.TrimSpace(matches[1])
if finalEmail == "" {
finalEmail = strings.TrimSpace(matches[2])
}
}
return finalName, finalEmail
}
var emailAndNameRegex = regexp.MustCompile(`^(.+?)<([^>]+@[^>]+)>$`)
func (a *actions) ArchiveClient(ctx context.Context, id int64) error {
err := a.queries.ArchiveClient(ctx, id)
if err != nil {
return fmt.Errorf("failed to archive client: %w", err)
}
return nil
}
func (a *actions) UnarchiveClient(ctx context.Context, id int64) error {
err := a.queries.UnarchiveClient(ctx, id)
if err != nil {
return fmt.Errorf("failed to unarchive client: %w", err)
}
return nil
}
|