- go.mod module path (and every internal/vibetree import) inventree -> vibetree - cmd/inventree/ -> cmd/vibetree/, deploy/inventree.service -> vibetree.service - Default site name (seed data + the siteName template func's fallback) is now "vibetree", shown in the navbar, <title>, and login page - Session cookie renamed inventree_session -> vibetree_session - Default DB path data/inventree.db -> data/vibetree.db, CLI/log text, Dockerfile, docker-compose.yml, README, .gitignore/.dockerignore all updated to match Existing installs: this changes the session cookie name (users will be signed out once) and the default DB filename (point -db at your existing file, or rename it, to keep your data).
100 lines
2.2 KiB
Go
100 lines
2.2 KiB
Go
package statuses
|
|
|
|
import (
|
|
"context"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"vibetree/internal/models"
|
|
)
|
|
|
|
type ImportResult struct {
|
|
Created int
|
|
Updated int
|
|
Errors []string
|
|
}
|
|
|
|
// Import creates or updates statuses from CSV data with "Name" and "Color"
|
|
// columns (case-insensitive header names, Color optional). Existing
|
|
// statuses are matched and updated by name (case-insensitive); everything
|
|
// else is created.
|
|
func (s *Store) Import(ctx context.Context, r io.Reader) (*ImportResult, error) {
|
|
existing, err := s.List(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
byName := map[string]int64{}
|
|
for _, st := range existing {
|
|
byName[strings.ToLower(st.Name)] = st.ID
|
|
}
|
|
|
|
cr := csv.NewReader(r)
|
|
cr.FieldsPerRecord = -1
|
|
header, err := cr.Read()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return &ImportResult{}, nil
|
|
}
|
|
return nil, fmt.Errorf("read CSV header: %w", err)
|
|
}
|
|
colIdx := map[string]int{}
|
|
for i, h := range header {
|
|
colIdx[strings.ToLower(strings.TrimSpace(h))] = i
|
|
}
|
|
get := func(row []string, key string) string {
|
|
idx, ok := colIdx[key]
|
|
if !ok || idx >= len(row) {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(row[idx])
|
|
}
|
|
|
|
result := &ImportResult{}
|
|
rowNum := 1
|
|
for {
|
|
row, err := cr.Read()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read CSV row %d: %w", rowNum, err)
|
|
}
|
|
rowNum++
|
|
|
|
name := get(row, "name")
|
|
if name == "" {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("row %d: name is required, skipped", rowNum))
|
|
continue
|
|
}
|
|
color := get(row, "color")
|
|
|
|
if id, ok := byName[strings.ToLower(name)]; ok {
|
|
st, err := s.Get(ctx, id)
|
|
if err != nil {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("row %d: %v", rowNum, err))
|
|
continue
|
|
}
|
|
if color != "" {
|
|
st.Color = color
|
|
}
|
|
if err := s.Update(ctx, st); err != nil {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("row %d: update failed: %v", rowNum, err))
|
|
continue
|
|
}
|
|
result.Updated++
|
|
continue
|
|
}
|
|
|
|
st := &models.Status{Name: name, Color: color}
|
|
if err := s.Create(ctx, st); err != nil {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("row %d: create failed: %v", rowNum, err))
|
|
continue
|
|
}
|
|
byName[strings.ToLower(name)] = st.ID
|
|
result.Created++
|
|
}
|
|
return result, nil
|
|
}
|