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