first commit
All checks were successful
CI / backend (push) Successful in 13m19s
CI / frontend (push) Successful in 11m3s
CI / docker (push) Successful in 1m58s

This commit is contained in:
Cauê Faleiros
2026-06-03 16:31:42 -03:00
commit 8c7e5fbbe4
92 changed files with 18226 additions and 0 deletions

View File

@@ -0,0 +1,201 @@
package clients
import (
"context"
"errors"
"regexp"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var ErrNotFound = errors.New("client not found")
type Store struct {
db *pgxpool.Pool
}
func NewStore(db *pgxpool.Pool) Store {
return Store{db: db}
}
func (s Store) List(ctx context.Context) ([]Client, error) {
rows, err := s.db.Query(ctx, `
SELECT id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at
FROM clients
ORDER BY name ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
clients := []Client{}
for rows.Next() {
client, err := scanClient(rows)
if err != nil {
return nil, err
}
clients = append(clients, client)
}
return clients, rows.Err()
}
func (s Store) ListForUser(ctx context.Context, userID string) ([]Client, error) {
rows, err := s.db.Query(ctx, `
SELECT clients.id::text, clients.name, clients.slug, clients.status, clients.website_url, clients.instagram_url, clients.facebook_url, clients.linkedin_url, clients.color, clients.notes, clients.created_at, clients.updated_at
FROM clients
INNER JOIN client_users ON client_users.client_id = clients.id
WHERE client_users.user_id = $1::uuid
ORDER BY clients.name ASC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
clients := []Client{}
for rows.Next() {
client, err := scanClient(rows)
if err != nil {
return nil, err
}
clients = append(clients, client)
}
return clients, rows.Err()
}
func (s Store) UserCanAccess(ctx context.Context, userID string, clientID string) (bool, error) {
var exists bool
err := s.db.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1
FROM client_users
WHERE user_id = $1::uuid
AND client_id = $2::uuid
)
`, userID, clientID).Scan(&exists)
return exists, err
}
func (s Store) FindByID(ctx context.Context, id string) (Client, error) {
row := s.db.QueryRow(ctx, `
SELECT id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at
FROM clients
WHERE id = $1
`, id)
return scanClient(row)
}
func (s Store) Create(ctx context.Context, name string) (Client, error) {
slug := slugify(name)
row := s.db.QueryRow(ctx, `
INSERT INTO clients (name, slug, status)
VALUES ($1, $2, 'active')
RETURNING id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at
`, strings.TrimSpace(name), slug)
return scanClient(row)
}
func (s Store) Update(ctx context.Context, id string, name string, status string, websiteURL string, instagramURL string, facebookURL string, linkedinURL string, color string, notes string) (Client, error) {
row := s.db.QueryRow(ctx, `
UPDATE clients
SET
name = COALESCE(NULLIF($2, ''), name),
slug = CASE WHEN NULLIF($2, '') IS NULL THEN slug ELSE $3 END,
status = COALESCE(NULLIF($4, ''), status),
website_url = $5,
instagram_url = $6,
facebook_url = $7,
linkedin_url = $8,
color = COALESCE(NULLIF($9, ''), color),
notes = $10,
updated_at = now()
WHERE id = $1
RETURNING id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at
`, id, strings.TrimSpace(name), slugify(name), strings.TrimSpace(status), strings.TrimSpace(websiteURL), strings.TrimSpace(instagramURL), strings.TrimSpace(facebookURL), strings.TrimSpace(linkedinURL), strings.TrimSpace(color), strings.TrimSpace(notes))
return scanClient(row)
}
func (s Store) Archive(ctx context.Context, id string) (Client, error) {
row := s.db.QueryRow(ctx, `
UPDATE clients
SET status = 'archived', updated_at = now()
WHERE id = $1
RETURNING id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at
`, id)
return scanClient(row)
}
func (s Store) Delete(ctx context.Context, id string) error {
result, err := s.db.Exec(ctx, `
DELETE FROM clients
WHERE id = $1
`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
type scanner interface {
Scan(dest ...any) error
}
func scanClient(row scanner) (Client, error) {
var client Client
err := row.Scan(
&client.ID,
&client.Name,
&client.Slug,
&client.Status,
&client.WebsiteURL,
&client.InstagramURL,
&client.FacebookURL,
&client.LinkedinURL,
&client.Color,
&client.Notes,
&client.CreatedAt,
&client.UpdatedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return Client{}, ErrNotFound
}
if err != nil {
return Client{}, err
}
return client, nil
}
func slugify(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
replacements := map[string]string{
"á": "a", "à": "a", "â": "a", "ã": "a", "ä": "a",
"é": "e", "è": "e", "ê": "e", "ë": "e",
"í": "i", "ì": "i", "î": "i", "ï": "i",
"ó": "o", "ò": "o", "ô": "o", "õ": "o", "ö": "o",
"ú": "u", "ù": "u", "û": "u", "ü": "u",
"ç": "c",
}
for from, to := range replacements {
value = strings.ReplaceAll(value, from, to)
}
value = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString(value, "-")
value = strings.Trim(value, "-")
if value == "" {
return "cliente"
}
return value
}