diff --git a/internal/handler/space.go b/internal/handler/space.go index be913f9..66bce3c 100644 --- a/internal/handler/space.go +++ b/internal/handler/space.go @@ -342,7 +342,7 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) { amountStr := r.FormValue("amount") typeStr := r.FormValue("type") dateStr := r.FormValue("date") - tagIDs := r.Form["tags"] // For multi-select + tagNames := r.Form["tags"] // Contains tag names // --- Validation & Conversion --- if description == "" || amountStr == "" || typeStr == "" || dateStr == "" { @@ -369,6 +369,49 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) { return } + // --- Tag Processing --- + existingTags, err := h.tagService.GetTagsForSpace(spaceID) + if err != nil { + slog.Error("failed to get tags for space", "error", err, "space_id", spaceID) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + existingTagsMap := make(map[string]string) + for _, t := range existingTags { + existingTagsMap[t.Name] = t.ID + } + + var finalTagIDs []string + processedTags := make(map[string]bool) + + for _, rawTagName := range tagNames { + tagName := service.NormalizeTagName(rawTagName) + if tagName == "" { + continue + } + if processedTags[tagName] { + continue + } + + if id, exists := existingTagsMap[tagName]; exists { + finalTagIDs = append(finalTagIDs, id) + } else { + // Create new tag + newTag, err := h.tagService.CreateTag(spaceID, tagName, nil) + if err != nil { + slog.Error("failed to create new tag from expense form", "error", err, "tag_name", tagName) + // Continue processing other tags? Or fail? + // Let's continue, maybe the tag was invalid or duplicate race condition. + // If duplicate race condition, we could try fetching it again, but for now simple log. + continue + } + finalTagIDs = append(finalTagIDs, newTag.ID) + existingTagsMap[tagName] = newTag.ID // Update map in case repeated in this request (though processedTags handles that) + } + processedTags[tagName] = true + } + // --- DTO Creation & Service Call --- dto := service.CreateExpenseDTO{ SpaceID: spaceID, @@ -377,7 +420,7 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) { Amount: amountCents, Type: expenseType, Date: date, - TagIDs: tagIDs, + TagIDs: finalTagIDs, ItemIDs: []string{}, // TODO: Add item IDs from form } diff --git a/internal/service/tag.go b/internal/service/tag.go index 9485e1a..ca164cd 100644 --- a/internal/service/tag.go +++ b/internal/service/tag.go @@ -18,8 +18,12 @@ func NewTagService(tagRepo repository.TagRepository) *TagService { return &TagService{tagRepo: tagRepo} } +func NormalizeTagName(name string) string { + return strings.ToLower(strings.TrimSpace(name)) +} + func (s *TagService) CreateTag(spaceID, name string, color *string) (*model.Tag, error) { - name = strings.TrimSpace(name) + name = NormalizeTagName(name) if name == "" { return nil, fmt.Errorf("tag name cannot be empty") } @@ -51,7 +55,7 @@ func (s *TagService) GetTagByID(id string) (*model.Tag, error) { } func (s *TagService) UpdateTag(id, name string, color *string) (*model.Tag, error) { - name = strings.TrimSpace(name) + name = NormalizeTagName(name) if name == "" { return nil, fmt.Errorf("tag name cannot be empty") } diff --git a/internal/ui/components/expense/expense.templ b/internal/ui/components/expense/expense.templ index e9c483c..ea84858 100644 --- a/internal/ui/components/expense/expense.templ +++ b/internal/ui/components/expense/expense.templ @@ -6,6 +6,7 @@ import ( "git.juancwu.dev/juancwu/budgit/internal/ui/components/button" "git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf" "git.juancwu.dev/juancwu/budgit/internal/ui/components/input" + "git.juancwu.dev/juancwu/budgit/internal/ui/components/tagsinput" "time" ) @@ -28,56 +29,53 @@ templ AddExpenseForm(space *model.Space, tags []*model.Tag, lists []*model.Shopp Top-up - // Description
@input.Input(input.Props{ - Name: "description", - ID: "description", + Name: "description", + ID: "description", Attributes: templ.Attributes{"required": "true"}, })
- // Amount
@input.Input(input.Props{ - Name: "amount", - ID: "amount", - Type: "number", + Name: "amount", + ID: "amount", + Type: "number", Attributes: templ.Attributes{"step": "0.01", "required": "true"}, })
- // Date
@input.Input(input.Props{ - Name: "date", - ID: "date", - Type: "date", - Value: time.Now().Format("2006-01-02"), + Name: "date", + ID: "date", + Type: "date", + Value: time.Now().Format("2006-01-02"), Attributes: templ.Attributes{"required": "true"}, })
- // Tags - if len(tags) > 0 { -
- - -
- } - +
+ + + for _, tag := range tags { + + } + + @tagsinput.TagsInput(tagsinput.Props{ + Name: "tags", + Placeholder: "Add tags (press enter)", + Attributes: templ.Attributes{"list": "available-tags"}, + }) +
// TODO: Shopping list items selector -
- @button.Button(button.Props{ Type: button.TypeSubmit }) { + @button.Button(button.Props{Type: button.TypeSubmit}) { Save Transaction }
diff --git a/internal/ui/components/tagsinput/tagsinput.templ b/internal/ui/components/tagsinput/tagsinput.templ index 5099e47..f8daedd 100644 --- a/internal/ui/components/tagsinput/tagsinput.templ +++ b/internal/ui/components/tagsinput/tagsinput.templ @@ -26,6 +26,9 @@ templ TagsInput(props ...Props) { if len(props) > 0 { {{ p = props[0] }} } + if p.ID == "" { + {{ p.ID = utils.RandomID() }} + }