chore: database

This commit is contained in:
2025-12-11 16:29:13 -07:00
parent 94f9b292f1
commit 23b4b964ed
21 changed files with 1017 additions and 220 deletions

View File

@@ -5,7 +5,7 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
cmd = "task gen && go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []

4
app.go
View File

@@ -9,6 +9,7 @@ import (
"strings"
cfg "flexsupport/internal/config"
db "flexsupport/internal/domain"
"flexsupport/internal/lib/logger"
"flexsupport/internal/router"
)
@@ -32,7 +33,8 @@ func App(ctx context.Context, stdout io.Writer, getenv func(string, string) stri
default:
log = slog.New(slog.NewJSONHandler(stdout, logOptions))
}
r := router.NewRouter(log)
r := router.NewRouter(log, config)
db.NewDB(ctx, config.DatabaseUrl)
fmt.Println("Starting server on :8080")
return http.ListenAndServe(":8080", r)

View File

@@ -16,6 +16,7 @@ type Config struct {
VerboseLogging bool `mapstructure:"VERBOSE_LOGGING"`
Environment Env `mapstructure:"ENVIRONMENT"`
Domain string `mapstructure:"DOMAIN"`
DatabaseUrl string `mapstructure:"DATABASE_URL"`
}
func New(getenv func(string, string) string) *Config {
@@ -24,6 +25,7 @@ func New(getenv func(string, string) string) *Config {
VerboseLogging: getenv("VERBOSE_LOGGING", "false") == "true",
Environment: Env(getenv("ENVIRONMENT", "development")),
Domain: getenv("DOMAIN", "http://localhost:8080"),
DatabaseUrl: getenv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"),
}
return cfg
}

View File

@@ -0,0 +1,159 @@
package db
import (
"context"
"embed"
"fmt"
"log/slog"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
)
//go:embed migrations/*.sql
var migrationFiles embed.FS
type Migration struct {
Version int64
Name string
SQL string
}
func (db *DB) getAppliedMigrations(ctx context.Context) (map[int64]bool, error) {
rows, err := db.QueryContext(ctx, "SELECT version FROM schema_migrations")
if err != nil {
return nil, err
}
defer func() {
err = rows.Close()
if err != nil {
slog.Error("Failed to close rows", "error", err)
return
}
}()
applied := make(map[int64]bool)
for rows.Next() {
var version int64
if err := rows.Scan(&version); err != nil {
return nil, err
}
applied[version] = true
}
slog.Info("Applied migrations", "count", len(applied))
return applied, rows.Err()
}
func (db *DB) runMigrations(ctx context.Context) error {
applied, err := db.getAppliedMigrations(ctx)
if err != nil {
return NewIgnorableError("failed to get applied migrations: " + err.Error())
}
migrations, err := loadMigrations()
if err != nil {
return NewIgnorableError("failed to load migrations: " + err.Error())
}
tx, err := db.Beginx()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
for _, migration := range migrations {
if applied[migration.Version] {
continue
}
slog.Info("Applying migration",
slog.Int64("version", migration.Version),
slog.String("name", migration.Name))
if _, err := tx.ExecContext(ctx, migration.SQL); err != nil {
_ = tx.Rollback()
return fmt.Errorf("failed to apply migration %d: %w", migration.Version, err)
}
insertSQL := "INSERT INTO schema_migrations (version, name) VALUES ($1, $2)"
_, err = tx.ExecContext(ctx, insertSQL, migration.Version, migration.Name)
if err != nil {
_ = tx.Rollback()
return fmt.Errorf("migration %d failed: %w", migration.Version, err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
func loadMigrations() ([]Migration, error) {
entries, err := migrationFiles.ReadDir("migrations")
if err != nil {
return nil, err
}
slog.Info("Loading migrations")
slog.Info("Found migrations", "count", len(entries))
migrations := make([]Migration, 0, len(entries))
for _, entry := range entries {
slog.Info(entry.Name())
if !strings.HasSuffix(entry.Name(), ".sql") {
slog.Info("Skipping migration", "name", entry.Name())
continue
}
parts := strings.SplitN(entry.Name(), "_", 2)
if len(parts) < 2 {
slog.Info("Skipping migration", "name", entry.Name())
continue
}
version, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
slog.Info("Skipping migration", "name", entry.Name())
continue
}
content, err := migrationFiles.ReadFile(filepath.Join("migrations", entry.Name()))
if err != nil {
slog.Error("Failed to read migration file", "name", entry.Name(), "error", err)
return nil, err
}
name := strings.TrimSuffix(parts[1], ".sql")
migrations = append(migrations, Migration{
Version: version,
Name: name,
SQL: string(content),
})
}
if len(migrations) == 0 {
return migrations, nil
}
sort.Slice(migrations, func(i, j int) bool {
return migrations[i].Version < migrations[j].Version
})
migrations = slices.Clip(migrations)
return migrations, nil
}
type IgnorableError struct {
msg string
}
func (ie IgnorableError) Error() string {
return ie.msg
}
func NewIgnorableError(message string) error {
return &IgnorableError{msg: message}
}
var ErrIgnorable = &IgnorableError{}

View File

@@ -1,3 +1,281 @@
CREATE TABLE IF NOT EXISTS users(
id TEXT NOT NULL PRIMARY KEY,
)
create extension if not exists citext;
create extension if not exists pgcrypto;
create table if not exists users (
id uuid primary key default gen_random_uuid(),
name text not null,
email citext unique not null,
email_verified boolean not null default false,
password_hash text,
is_system_admin boolean not null default false,
created_at timestamptz not null default now(),
last_login_at timestamptz
);
create table if not exists tenants (
id uuid primary key default gen_random_uuid(),
slug text not null unique,
name text not null,
logo_url text,
timezone text not null default 'UTC',
created_at timestamptz not null default now(),
onboarding_completed_at timestamptz
);
create table if not exists tenant_memberships (
tenant_id uuid not null references tenants (id) on delete cascade,
user_id uuid not null references users(id) on delete cascade,
status text not null default 'active', -- active/invited/disabled
created_at timestamptz not null default now(),
primary key (tenant_id, user_id)
);
create index on tenant_memberships (user_id);
create table if not exists roles (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants (id) on delete cascade,
name text not null,
is_system boolean not null default false,
unique (tenant_id, name)
);
create table if not exists permissions (
key text primary key, -- e.g. 'ticket.read', 'ticket.write', 'project.admin', etc.
description text
);
create table if not exists role_permissions (
role_id uuid not null references roles(id) on delete cascade,
permission_key text not null references permissions(key) on delete cascade,
primary key (role_id, permission_key)
);
create table if not exists membership_roles (
tenant_id uuid not null,
user_id uuid not null,
role_id uuid not null references roles(id) on delete cascade,
primary key (tenant_id, user_id, role_id),
foreign key (tenant_id, user_id) references tenant_memberships (tenant_id, user_id) on delete cascade
);
create table if not exists projects (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants (id) on delete cascade,
key text not null, -- IT, HR, etc
name text not null,
description text,
is_archived boolean not null default false,
created_at timestamptz not null default now(),
unique (tenant_id, key),
unique (tenant_id, name)
);
create index on projects (tenant_id);
create table if not exists project_portal_settings (
project_id uuid primary key references projects(id) on delete cascade,
enabled boolean not null default false
);
create table if not exists project_memberships (
tenant_id uuid not null references tenants(id) on delete cascade,
project_id uuid not null references projects(id) on delete cascade,
user_id uuid not null references users(id) on delete cascade,
created_at timestamptz not null default now(),
primary key (tenant_id, project_id, user_id)
);
create index on project_memberships (tenant_id, user_id);
create table if not exists request_types (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants(id) on delete cascade,
project_id uuid not null references projects(id) on delete cascade,
key text not null, -- "maintenance_request"
name text not null, -- "Maintenance Request"
description text,
is_archived boolean not null default false,
sort_order int not null default 0,
unique (tenant_id, project_id, key),
unique (tenant_id, project_id, id)
);
create index on request_types (tenant_id, project_id);
create table if not exists request_type_fields (
tenant_id uuid not null references tenants(id) on delete cascade,
request_type_id uuid not null references request_types(id) on delete cascade,
field_id uuid not null references custom_fields(id) on delete restrict,
sort_order int not null default 0,
-- per ticket type behavior
required boolean not null default false,
requester_visible boolean not null default true,
primary key (tenant_id, request_type_id, field_id)
);
create index on request_type_fields (tenant_id, request_type_id);
create table if not exists tickets (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants(id) on delete cascade,
project_id uuid not null references projects(id) on delete restrict,
request_type_id uuid not null,
ticket_number bigint not null,
title text not null,
description text,
status text not null,
priority text,
created_by_user_id uuid references users(id),
requester_user_id uuid references users(id),
assigned_to_user_id uuid references users(id),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
closed_at timestamptz,
unique (project_id, ticket_number),
constraint tickets_request_type_same_project_fk
foreign key (tenant_id, project_id, request_type_id)
references request_types (tenant_id, project_id, id)
on delete restrict
);
create index if not exists tickets_tenant_project_status_idx
on tickets (tenant_id, project_id, status);
create index if not exists tickets_tenant_requester_idx
on tickets (tenant_id, requester_user_id);
create index if not exists tickets_tenant_assigned_idx
on tickets (tenant_id, assigned_to_user_id);
create table if not exists custom_fields (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants(id) on delete cascade,
key text not null, -- stable programmatic key: "asset_tag"
name text not null, -- display name: "Asset Tag"
description text,
field_type text not null, -- 'text','textarea','number','bool','date','datetime',
-- 'select','multiselect','user','group' (future)
is_archived boolean not null default false,
created_at timestamptz not null default now(),
unique (tenant_id, key)
);
create index if not exists custom_fields_tenant_idx
on custom_fields (tenant_id);
create table if not exists custom_field_options (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants(id) on delete cascade,
field_id uuid not null references custom_fields(id) on delete cascade,
value text not null, -- stable value stored in ticket
label text not null, -- display label
sort_order int not null default 0,
is_archived boolean not null default false,
unique (field_id, value)
);
create index on custom_field_options (tenant_id, field_id);
create table if not exists ticket_field_values (
tenant_id uuid not null references tenants(id) on delete cascade,
ticket_id uuid not null references tickets(id) on delete cascade,
field_id uuid not null references custom_fields(id) on delete cascade,
value jsonb not null, -- e.g. {"text":"abc"} or {"option":"laptop"} or {"user_id":"..."}
updated_at timestamptz not null default now(),
primary key (tenant_id, ticket_id, field_id)
);
create index on ticket_field_values (tenant_id, field_id);
create index ticket_field_values_value_gin
on ticket_field_values using gin (value);
create table if not exists ticket_comments (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants(id) on delete cascade,
ticket_id uuid not null references tickets(id) on delete cascade,
author_user_id uuid references users(id),
body text not null,
is_internal boolean not null default false,
created_at timestamptz not null default now()
);
create index on ticket_comments (tenant_id, ticket_id, created_at);
create table if not exists ticket_events (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants(id) on delete cascade,
ticket_id uuid not null references tickets(id) on delete cascade,
actor_user_id uuid references users(id),
type text not null, -- 'status_changed', 'comment_added', etc
payload jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
);
create index on ticket_events (tenant_id, ticket_id, created_at);
create table if not exists tenant_settings (
tenant_id uuid primary key references tenants(id) on delete cascade,
public_portal_enabled boolean not null default false,
portal_title text,
portal_welcome_text text,
outbound_from_name text,
outbound_from_email citext
);
create table if not exists integrations (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants(id) on delete cascade,
integration_type text not null, -- 'shopify', 'oidc', 'webhook', 'smtp', etc
name text not null,
enabled boolean not null default false,
config jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
unique (tenant_id, integration_type, name)
);
create index on integrations (tenant_id, integration_type);
create table if not exists tenant_domains (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references tenants(id) on delete cascade,
hostname text not null unique, -- "acme.flexsupport.com" or "support.acme.com"
is_primary boolean not null default false,
created_at timestamptz not null default now()
);
create index if not exists tenant_domains_tenant_idx
on tenant_domains (tenant_id);
create table if not exists schema_migrations (
version integer primary key,
name text not null,
applied_at timestamptz not null default now()
);

View File

@@ -9,8 +9,8 @@ import (
// "flexsupport/ui/components/dropdown"
"flexsupport/ui/components/input"
"flexsupport/ui/components/label"
// "flexsupport/ui/components/popover"
// "flexsupport/ui/components/selectbox"
"flexsupport/ui/components/popover"
"flexsupport/ui/components/selectbox"
// "flexsupport/ui/components/textarea"
"flexsupport/ui/components/dialog"
// "flexsupport/ui/components/toast"
@@ -31,6 +31,8 @@ templ BaseLayout(contents templ.Component) {
@input.Script()
@label.Script()
@dialog.Script()
@selectbox.Script()
@popover.Script()
<script nonce={ templ.GetNonce(ctx) }>
(function() {
// Get current theme preference (system, light, or dark)

View File

@@ -17,8 +17,8 @@ import (
// "flexsupport/ui/components/dropdown"
"flexsupport/ui/components/input"
"flexsupport/ui/components/label"
// "flexsupport/ui/components/popover"
// "flexsupport/ui/components/selectbox"
"flexsupport/ui/components/popover"
"flexsupport/ui/components/selectbox"
// "flexsupport/ui/components/textarea"
"flexsupport/ui/components/dialog"
// "flexsupport/ui/components/toast"
@@ -63,6 +63,14 @@ func BaseLayout(contents templ.Component) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = selectbox.Script().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = popover.Script().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<script nonce=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@@ -70,7 +78,7 @@ func BaseLayout(contents templ.Component) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(templ.GetNonce(ctx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/layout/base.templ`, Line: 34, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/layout/base.templ`, Line: 36, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {

View File

@@ -0,0 +1,56 @@
package middleware
import (
"context"
"net/http"
)
type ctxKey string
const (
ctxTenantID ctxKey = "tenant_id"
ctxTenantSlug ctxKey = "tenant_slug"
)
type Tenant struct {
ID string
Slug string
Name string
}
type TenantResolver interface {
ResolveByHost(ctx context.Context, host string) (*Tenant, bool, error)
ResolveBySlug(ctx context.Context, slug string) (*Tenant, bool, error)
}
func TenantMiddleware(resolver TenantResolver, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := r.Host
tenant, ok, err := resolver.ResolveByHost(r.Context(), host)
if err != nil {
http.Error(w, "tenant lookup failed", http.StatusInternalServerError)
return
}
// // Optional: fallback to /t/{slug}/... (dev mode)
// if !ok {
// slug, ok2 := tenantSlugFromPath(r.URL.Path) // you implement
// if ok2 {
// tenant, ok, err = resolver.ResolveBySlug(r.Context(), slug)
// if err != nil {
// http.Error(w, "tenant lookup failed", http.StatusInternalServerError)
// return
// }
// }
// }
if !ok {
http.NotFound(w, r)
return
}
ctx := context.WithValue(r.Context(), ctxTenantID, tenant.ID)
ctx = context.WithValue(ctx, ctxTenantSlug, tenant.Slug)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -4,6 +4,7 @@ import (
"log/slog"
"net/http"
"flexsupport/internal/config"
mw "flexsupport/internal/middleware"
"flexsupport/static"
@@ -16,18 +17,29 @@ import (
"github.com/go-chi/chi/v5/middleware"
)
var AppDev string = "development"
func NewRouter(log *slog.Logger) *chi.Mux {
func NewRouter(log *slog.Logger, cfg *config.Config) *chi.Mux {
r := chi.NewMux()
// Dashboard
r.Handle("/assets/*",
disableCacheInDevMode(
http.StripPrefix("/assets/",
static.AssetRouter()),
),
)
r.Group(func(r chi.Router) {
r.Use(
middleware.Compress(5),
)
r.Handle("/assets/*",
disableCacheInDevMode(
http.StripPrefix("/assets/",
static.AssetRouter(cfg)),
cfg,
),
)
r.Handle("/public/*",
disableCacheInDevMode(
http.StripPrefix("/public/",
static.PublicRouter(cfg)),
cfg,
),
)
})
// Tickets
r.Group(func(r chi.Router) {
r.Use(
@@ -45,8 +57,8 @@ func NewRouter(log *slog.Logger) *chi.Mux {
return r
}
func disableCacheInDevMode(next http.Handler) http.Handler {
if AppDev == "development" {
func disableCacheInDevMode(next http.Handler, cfg *config.Config) http.Handler {
if cfg.Environment == config.PROD {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@@ -3,6 +3,8 @@ package dashboard
import (
"flexsupport/internal/models"
"flexsupport/ui/components/card"
"flexsupport/ui/components/button"
"flexsupport/ui/partials/tables"
"flexsupport/ui/partials/search"
)
@@ -138,19 +140,15 @@ templ Dashboard(tickets []models.Ticket, isMobile bool) {
</div>
if !isMobile {
<!-- Filters and Search -->
@card.Card() {
@card.Content() {
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
@search.SearchTickets("", "")
<a
href="/tickets/new"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
New Ticket
</a>
</div>
<div class="flex flex-row items-center my-2 justify-between gap-4">
@search.SearchTickets("", "")
@button.Button(button.Props{
Href: "/tickets/new",
Variant: button.VariantOutline,
}) {
New Ticket
}
}
</div>
}
@tables.TicketsTable(tickets, isMobile)
</div>

View File

@@ -11,6 +11,8 @@ import templruntime "github.com/a-h/templ/runtime"
import (
"flexsupport/internal/models"
"flexsupport/ui/components/card"
"flexsupport/ui/components/button"
"flexsupport/ui/partials/search"
"flexsupport/ui/partials/tables"
)
@@ -205,7 +207,11 @@ func Dashboard(tickets []models.Ticket, isMobile bool) templ.Component {
return templ_7745c5c3_Err
}
if !isMobile {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<!-- Filters and Search --> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<!-- Filters and Search --> <div class=\"flex flex-row items-center my-2 justify-between gap-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = search.SearchTickets("", "").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -221,39 +227,20 @@ func Dashboard(tickets []models.Ticket, isMobile bool) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var11 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = search.SearchTickets("", "").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"/tickets/new\" class=\"inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\">New Ticket</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = card.Content().Render(templ.WithChildren(ctx, templ_7745c5c3_Var11), templ_7745c5c3_Buffer)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "New Ticket")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = card.Card().Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
templ_7745c5c3_Err = button.Button(button.Props{
Href: "/tickets/new",
Variant: button.VariantOutline,
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

13
main.go
View File

@@ -4,7 +4,8 @@ import (
"context"
"fmt"
"os"
"os/exec"
// "os/exec"
"github.com/joho/godotenv"
)
@@ -14,9 +15,13 @@ var Environment = "development"
func init() {
os.Setenv("env", Environment)
if Environment == "development" {
// exec.Command("bunx", "tailwindcss", "-i", "./static/assets/css/input.css", "-o", "./static/assets/css/output.min.css", "-m").Run()
// exec.Command("go", "tool", "templ", "generate")
exec.Command("task", "gen").Run()
// // exec.Command("bunx", "tailwindcss", "-i", "./static/assets/css/input.css", "-o", "./static/assets/css/output.min.css", "-m").Run()
// // exec.Command("go", "tool", "templ", "generate")
// out, err := exec.Command("task", "gen").Output()
// if err != nil {
// panic(err)
// }
// fmt.Printf("%s\n", out)
err := godotenv.Load(".env")
if err != nil {
panic(err)

View File

@@ -2,9 +2,12 @@ package static
import (
"embed"
// "fmt"
"fmt"
"io/fs"
"net/http"
"flexsupport/internal/config"
// "fmt"
// "strings"
// "github.com/go-chi/chi/v5"
// "github.com/go-chi/chi/v5/middleware"
@@ -14,10 +17,13 @@ import (
//go:embed all:assets
var Static embed.FS
var AppDev string = "development"
//go:embed all:public
var Public embed.FS
func AssetRouter() http.Handler {
if AppDev == "development" {
func AssetRouter(cfg *config.Config) http.Handler {
if cfg.Environment == config.DEV {
fmt.Println("ASSETS DEV")
return http.FileServer(http.Dir("./static/assets"))
}
st, err := fs.Sub(Static, "assets")
@@ -28,6 +34,19 @@ func AssetRouter() http.Handler {
return handler
}
func PublicRouter(cfg *config.Config) http.Handler {
if cfg.Environment == config.DEV {
fmt.Println("PUBLIC DEV")
return http.FileServer(http.Dir("./static/public"))
}
st, err := fs.Sub(Public, "public")
if err != nil {
panic(err)
}
handler := http.FileServer(http.FS(st))
return handler
}
// func Handler() http.HandlerFunc {
// return func(w http.ResponseWriter, r *http.Request) {
// // Get the static filesystem

View File

@@ -3,13 +3,105 @@
@custom-variant dark (&:where(.dark, .dark *));
:root {
--background: oklch(0.9383 0.0042 236.4993);
--foreground: oklch(0.3211 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0.3211 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.3211 0 0);
--primary: oklch(0.6397 0.1720 36.4421);
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.9670 0.0029 264.5419);
--secondary-foreground: oklch(0.4461 0.0263 256.8018);
--muted: oklch(0.9846 0.0017 247.8389);
--muted-foreground: oklch(0.5510 0.0234 264.3637);
--accent: oklch(0.9119 0.0222 243.8174);
--accent-foreground: oklch(0.3791 0.1378 265.5222);
--destructive: oklch(0.6368 0.2078 25.3313);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0.9022 0.0052 247.8822);
--input: oklch(0.9700 0.0029 264.5420);
--ring: oklch(0.6397 0.1720 36.4421);
--sidebar: oklch(0.9030 0.0046 258.3257);
--sidebar-foreground: oklch(0.3211 0 0);
--sidebar-primary: oklch(0.6397 0.1720 36.4421);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.9119 0.0222 243.8174);
--sidebar-accent-foreground: oklch(0.3791 0.1378 265.5222);
--sidebar-border: oklch(0.9276 0.0058 264.5313);
--sidebar-ring: oklch(0.6397 0.1720 36.4421);
--font-sans: Inter, sans-serif;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.75rem;
--shadow-x: 0px;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: hsl(0 0% 0%);
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
.dark {
--background: oklch(0.2598 0.0306 262.6666);
--foreground: oklch(0.9219 0 0);
--card: oklch(0.3106 0.0301 268.6365);
--card-foreground: oklch(0.9219 0 0);
--popover: oklch(0.2900 0.0249 268.3986);
--popover-foreground: oklch(0.9219 0 0);
--primary: oklch(0.6397 0.1720 36.4421);
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.3095 0.0266 266.7132);
--secondary-foreground: oklch(0.9219 0 0);
--muted: oklch(0.3095 0.0266 266.7132);
--muted-foreground: oklch(0.7155 0 0);
--accent: oklch(0.3380 0.0589 267.5867);
--accent-foreground: oklch(0.8823 0.0571 254.1284);
--destructive: oklch(0.6368 0.2078 25.3313);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0.3843 0.0301 269.7337);
--input: oklch(0.3843 0.0301 269.7337);
--ring: oklch(0.6397 0.1720 36.4421);
--sidebar: oklch(0.3100 0.0283 267.7408);
--sidebar-foreground: oklch(0.9219 0 0);
--sidebar-primary: oklch(0.6397 0.1720 36.4421);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.3380 0.0589 267.5867);
--sidebar-accent-foreground: oklch(0.8823 0.0571 254.1284);
--sidebar-border: oklch(0.3843 0.0301 269.7337);
--sidebar-ring: oklch(0.6397 0.1720 36.4421);
--font-sans: Inter, sans-serif;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.75rem;
--shadow-x: 0px;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: hsl(0 0% 0%);
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.25);
}
@theme inline {
--breakpoint-3xl: 1600px;
--breakpoint-4xl: 2000px;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -25,54 +117,37 @@
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
:root {
--radius: 0.65rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
@layer base {
* {
@apply border-border outline-ring/50;

File diff suppressed because one or more lines are too long

View File

@@ -19,8 +19,8 @@ templ TicketRows(tickets []models.Ticket, isMobile bool) {
<a href={ ticketUrl } class="text-blue-600 hover:text-blue-900">{ ticket.ID }</a>
}
@table.Cell() {
<div class="text-sm font-medium text-primary-foreground">{ ticket.CustomerName }</div>
<div class="text-sm ">{ ticket.CustomerEmail }</div>
<div class="text-sm font-medium ">{ ticket.CustomerName }</div>
<div class="text-sm text-muted-foreground ">{ ticket.CustomerEmail }</div>
}
@table.Cell() {
{ ticket.ItemType }

View File

@@ -121,27 +121,27 @@ func TicketRows(tickets []models.Ticket, isMobile bool) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"text-sm font-medium text-primary-foreground\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"text-sm font-medium \">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(ticket.CustomerName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/rows/ticketRows.templ`, Line: 22, Col: 83}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/rows/ticketRows.templ`, Line: 22, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"text-sm \">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"text-sm text-muted-foreground \">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(ticket.CustomerEmail)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/rows/ticketRows.templ`, Line: 23, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/rows/ticketRows.templ`, Line: 23, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {

View File

@@ -1,10 +1,16 @@
package search
import "flexsupport/ui/components/input"
import (
"flexsupport/ui/components/input"
"flexsupport/ui/components/icon"
"flexsupport/ui/components/selectbox"
)
templ SearchTickets(term, status string) {
<span class="htmx-indicator">
<img src="/assets/icons/loader.svg" class="size-4 animate-spin" alt="Loading..."/> Loading...
@icon.LoaderCircle(icon.Props{Class: "animate-spin size-4"})
</span>
<div class="flex-1">
@input.Input(input.Props{
@@ -23,23 +29,55 @@ templ SearchTickets(term, status string) {
})
</div>
<div class="flex gap-2">
<select
name="status"
id="status"
class="block w-full pl-3 pr-10 py-2 text-base border-gray-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
hx-get="/tickets"
value={ status }
hx-trigger="change"
hx-target="#ticket-list"
hx-swap="innerHTML"
hx-include="#search"
>
<option value="">All Statuses</option>
<option value="new" selected={ status == "new" }>New</option>
<option value="in_progress" selected={ status == "in_progress" }>In Progress</option>
<option value="waiting_parts" selected={ status == "waiting_parts" }>Waiting for Parts</option>
<option value="ready" selected={ status == "ready" }>Ready for Pickup</option>
<option value="completed" selected={ status == "completed" }>Completed</option>
</select>
@selectbox.SelectBox() {
@selectbox.Trigger(selectbox.TriggerProps{
Name: "status",
ID: "status",
Attributes: templ.Attributes{
"hx-get": "/tickets",
"value": status,
"hx-trigger": "change",
"hx-target": "#ticket-list",
"hx-swap": "innerHTML",
"hx-include": "#search",
},
}) {
@selectbox.Value(selectbox.ValueProps{
Placeholder: "Filter by status...",
})
}
@selectbox.Content(selectbox.ContentProps{NoSearch: true}) {
@selectbox.Item(selectbox.ItemProps{
Value: "new",
Selected: status == "new",
}) {
New
}
@selectbox.Item(selectbox.ItemProps{
Value: "in_progress",
Selected: status == "in_progress",
}) {
In Progress
}
@selectbox.Item(selectbox.ItemProps{
Value: "waiting",
Selected: status == "waiting",
}) {
Waiting for Parts
}
@selectbox.Item(selectbox.ItemProps{
Value: "ready",
Selected: status == "ready",
}) {
Ready for Pickup
}
@selectbox.Item(selectbox.ItemProps{
Value: "completed",
Selected: status == "completed",
}) {
Completed
}
}
}
</div>
}

View File

@@ -8,7 +8,13 @@ package search
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "flexsupport/ui/components/input"
import (
"flexsupport/ui/components/input"
"flexsupport/ui/components/icon"
"flexsupport/ui/components/selectbox"
)
func SearchTickets(term, status string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
@@ -31,7 +37,15 @@ func SearchTickets(term, status string) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<span class=\"htmx-indicator\"><img src=\"/assets/icons/loader.svg\" class=\"size-4 animate-spin\" alt=\"Loading...\"> Loading...</span><div class=\"flex-1\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<span class=\"htmx-indicator\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = icon.LoaderCircle(icon.Props{Class: "animate-spin size-4"}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span><div class=\"flex-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -52,85 +66,227 @@ func SearchTickets(term, status string) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"flex gap-2\"><select name=\"status\" id=\"status\" class=\"block w-full pl-3 pr-10 py-2 text-base border-gray-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md\" hx-get=\"/tickets\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><div class=\"flex gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(status)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/search/tickets.templ`, Line: 31, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = selectbox.Value(selectbox.ValueProps{
Placeholder: "Filter by status...",
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = selectbox.Trigger(selectbox.TriggerProps{
Name: "status",
ID: "status",
Attributes: templ.Attributes{
"hx-get": "/tickets",
"value": status,
"hx-trigger": "change",
"hx-target": "#ticket-list",
"hx-swap": "innerHTML",
"hx-include": "#search",
},
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "New")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = selectbox.Item(selectbox.ItemProps{
Value: "new",
Selected: status == "new",
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var6 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "In Progress")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = selectbox.Item(selectbox.ItemProps{
Value: "in_progress",
Selected: status == "in_progress",
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var7 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Waiting for Parts")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = selectbox.Item(selectbox.ItemProps{
Value: "waiting",
Selected: status == "waiting",
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var7), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var8 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Ready for Pickup")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = selectbox.Item(selectbox.ItemProps{
Value: "ready",
Selected: status == "ready",
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var8), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var9 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Completed")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = selectbox.Item(selectbox.ItemProps{
Value: "completed",
Selected: status == "completed",
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var9), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = selectbox.Content(selectbox.ContentProps{NoSearch: true}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = selectbox.SelectBox().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-trigger=\"change\" hx-target=\"#ticket-list\" hx-swap=\"innerHTML\" hx-include=\"#search\"><option value=\"\">All Statuses</option> <option value=\"new\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(status == "new")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/search/tickets.templ`, Line: 38, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">New</option> <option value=\"in_progress\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(status == "in_progress")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/search/tickets.templ`, Line: 39, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">In Progress</option> <option value=\"waiting_parts\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(status == "waiting_parts")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/search/tickets.templ`, Line: 40, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">Waiting for Parts</option> <option value=\"ready\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(status == "ready")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/search/tickets.templ`, Line: 41, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">Ready for Pickup</option> <option value=\"completed\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(status == "completed")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/partials/search/tickets.templ`, Line: 42, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">Completed</option></select></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}