feat: add tag combobox to make tagging expenses easier

This commit is contained in:
juancwu 2026-02-17 21:13:46 +00:00
commit ef37360da7
9 changed files with 671 additions and 106 deletions

View file

@ -5,7 +5,6 @@ import (
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
@ -697,7 +696,9 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) {
return
}
ui.Render(w, r, pages.ExpenseCreatedResponse(spaceID, expenses, balance, totalAllocated, 1, totalPages))
// Re-fetch tags (may have been auto-created)
refreshedTags, _ := h.tagService.GetTagsForSpace(spaceID)
ui.Render(w, r, pages.ExpenseCreatedResponse(spaceID, expenses, balance, totalAllocated, refreshedTags, 1, totalPages))
// OOB-swap the item selector with fresh data (items may have been deleted/checked)
listsWithItems, err := h.listService.GetListsWithUncheckedItems(spaceID)
@ -832,7 +833,8 @@ func (h *SpaceHandler) UpdateExpense(w http.ResponseWriter, r *http.Request) {
balance -= totalAllocated
methods, _ := h.methodService.GetMethodsForSpace(spaceID)
ui.Render(w, r, pages.ExpenseUpdatedResponse(spaceID, expWithTagsAndMethod, balance, totalAllocated, methods))
updatedTags, _ := h.tagService.GetTagsForSpace(spaceID)
ui.Render(w, r, pages.ExpenseUpdatedResponse(spaceID, expWithTagsAndMethod, balance, totalAllocated, methods, updatedTags))
}
func (h *SpaceHandler) DeleteExpense(w http.ResponseWriter, r *http.Request) {
@ -978,7 +980,8 @@ func (h *SpaceHandler) GetExpensesList(w http.ResponseWriter, r *http.Request) {
}
methods, _ := h.methodService.GetMethodsForSpace(spaceID)
ui.Render(w, r, pages.ExpensesListContent(spaceID, expenses, methods, page, totalPages))
paginatedTags, _ := h.tagService.GetTagsForSpace(spaceID)
ui.Render(w, r, pages.ExpensesListContent(spaceID, expenses, methods, paginatedTags, page, totalPages))
}
func (h *SpaceHandler) GetShoppingListItems(w http.ResponseWriter, r *http.Request) {
@ -1779,16 +1782,17 @@ func (h *SpaceHandler) CreateRecurringExpense(w http.ResponseWriter, r *http.Req
}
// Fetch tags/method for the response
spaceTags, _ := h.tagService.GetTagsForSpace(spaceID)
tagsMap, _ := h.recurringService.GetRecurringExpensesWithTagsAndMethodsForSpace(spaceID)
for _, item := range tagsMap {
if item.ID == re.ID {
ui.Render(w, r, recurring.RecurringItem(spaceID, item, nil))
ui.Render(w, r, recurring.RecurringItem(spaceID, item, nil, spaceTags))
return
}
}
// Fallback: render without tags
ui.Render(w, r, recurring.RecurringItem(spaceID, &model.RecurringExpenseWithTagsAndMethod{RecurringExpense: *re}, nil))
ui.Render(w, r, recurring.RecurringItem(spaceID, &model.RecurringExpenseWithTagsAndMethod{RecurringExpense: *re}, nil, spaceTags))
}
func (h *SpaceHandler) UpdateRecurringExpense(w http.ResponseWriter, r *http.Request) {
@ -1892,16 +1896,17 @@ func (h *SpaceHandler) UpdateRecurringExpense(w http.ResponseWriter, r *http.Req
}
// Build response with tags/method
updateSpaceTags, _ := h.tagService.GetTagsForSpace(spaceID)
tagsMapResult, _ := h.recurringService.GetRecurringExpensesWithTagsAndMethodsForSpace(spaceID)
for _, item := range tagsMapResult {
if item.ID == updated.ID {
methods, _ := h.methodService.GetMethodsForSpace(spaceID)
ui.Render(w, r, recurring.RecurringItem(spaceID, item, methods))
ui.Render(w, r, recurring.RecurringItem(spaceID, item, methods, updateSpaceTags))
return
}
}
ui.Render(w, r, recurring.RecurringItem(spaceID, &model.RecurringExpenseWithTagsAndMethod{RecurringExpense: *updated}, nil))
ui.Render(w, r, recurring.RecurringItem(spaceID, &model.RecurringExpenseWithTagsAndMethod{RecurringExpense: *updated}, nil, updateSpaceTags))
}
func (h *SpaceHandler) DeleteRecurringExpense(w http.ResponseWriter, r *http.Request) {
@ -1943,20 +1948,63 @@ func (h *SpaceHandler) ToggleRecurringExpense(w http.ResponseWriter, r *http.Req
return
}
toggleSpaceTags, _ := h.tagService.GetTagsForSpace(spaceID)
tagsMapResult, _ := h.recurringService.GetRecurringExpensesWithTagsAndMethodsForSpace(spaceID)
for _, item := range tagsMapResult {
if item.ID == updated.ID {
methods, _ := h.methodService.GetMethodsForSpace(spaceID)
ui.Render(w, r, recurring.RecurringItem(spaceID, item, methods))
ui.Render(w, r, recurring.RecurringItem(spaceID, item, methods, toggleSpaceTags))
return
}
}
ui.Render(w, r, recurring.RecurringItem(spaceID, &model.RecurringExpenseWithTagsAndMethod{RecurringExpense: *updated}, nil))
ui.Render(w, r, recurring.RecurringItem(spaceID, &model.RecurringExpenseWithTagsAndMethod{RecurringExpense: *updated}, nil, toggleSpaceTags))
}
// --- Budgets ---
// processTagNames normalizes tag names, deduplicates them, and resolves them
// to tag IDs. Tags that don't exist are auto-created.
func (h *SpaceHandler) processTagNames(spaceID string, tagNames []string) ([]string, error) {
existingTags, err := h.tagService.GetTagsForSpace(spaceID)
if err != nil {
return nil, err
}
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 {
newTag, err := h.tagService.CreateTag(spaceID, tagName, nil)
if err != nil {
slog.Error("failed to create new tag", "error", err, "tag_name", tagName)
continue
}
finalTagIDs = append(finalTagIDs, newTag.ID)
existingTagsMap[tagName] = newTag.ID
}
processedTags[tagName] = true
}
return finalTagIDs, nil
}
func (h *SpaceHandler) getBudgetForSpace(w http.ResponseWriter, spaceID, budgetID string) *model.Budget {
budget, err := h.budgetService.GetBudget(budgetID)
if err != nil {
@ -2004,24 +2052,26 @@ func (h *SpaceHandler) CreateBudget(w http.ResponseWriter, r *http.Request) {
return
}
tagIDsStr := r.FormValue("tag_ids")
tagNames := r.Form["tags"]
amountStr := r.FormValue("amount")
periodStr := r.FormValue("period")
startDateStr := r.FormValue("start_date")
endDateStr := r.FormValue("end_date")
var tagIDs []string
if tagIDsStr != "" {
for _, id := range strings.Split(tagIDsStr, ",") {
id = strings.TrimSpace(id)
if id != "" {
tagIDs = append(tagIDs, id)
}
}
if len(tagNames) == 0 || amountStr == "" || periodStr == "" || startDateStr == "" {
ui.RenderError(w, r, "All required fields must be provided.", http.StatusUnprocessableEntity)
return
}
if len(tagIDs) == 0 || amountStr == "" || periodStr == "" || startDateStr == "" {
ui.RenderError(w, r, "All required fields must be provided.", http.StatusUnprocessableEntity)
tagIDs, err := h.processTagNames(spaceID, tagNames)
if err != nil {
slog.Error("failed to process tag names", "error", err, "space_id", spaceID)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if len(tagIDs) == 0 {
ui.RenderError(w, r, "At least one valid tag is required.", http.StatusUnprocessableEntity)
return
}
@ -2082,24 +2132,26 @@ func (h *SpaceHandler) UpdateBudget(w http.ResponseWriter, r *http.Request) {
return
}
tagIDsStr := r.FormValue("tag_ids")
tagNames := r.Form["tags"]
amountStr := r.FormValue("amount")
periodStr := r.FormValue("period")
startDateStr := r.FormValue("start_date")
endDateStr := r.FormValue("end_date")
var tagIDs []string
if tagIDsStr != "" {
for _, id := range strings.Split(tagIDsStr, ",") {
id = strings.TrimSpace(id)
if id != "" {
tagIDs = append(tagIDs, id)
}
}
if len(tagNames) == 0 || amountStr == "" || periodStr == "" || startDateStr == "" {
ui.RenderError(w, r, "All required fields must be provided.", http.StatusUnprocessableEntity)
return
}
if len(tagIDs) == 0 || amountStr == "" || periodStr == "" || startDateStr == "" {
ui.RenderError(w, r, "All required fields must be provided.", http.StatusUnprocessableEntity)
tagIDs, err := h.processTagNames(spaceID, tagNames)
if err != nil {
slog.Error("failed to process tag names", "error", err, "space_id", spaceID)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if len(tagIDs) == 0 {
ui.RenderError(w, r, "At least one valid tag is required.", http.StatusUnprocessableEntity)
return
}

View file

@ -13,7 +13,7 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/label"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/paymentmethod"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/radio"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/tagsinput"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/tagcombobox"
)
type AddExpenseFormProps struct {
@ -139,16 +139,11 @@ templ AddExpenseForm(props AddExpenseFormProps) {
@label.Label(label.Props{For: "new-expense-tags"}) {
Tags (Optional)
}
<datalist id="available-tags">
for _, tag := range props.Tags {
<option value={ tag.Name }></option>
}
</datalist>
@tagsinput.TagsInput(tagsinput.Props{
@tagcombobox.TagCombobox(tagcombobox.Props{
ID: "new-expense-tags",
Name: "tags",
Placeholder: "Add tags (press enter)",
Attributes: templ.Attributes{"list": "available-tags"},
Tags: props.Tags,
Placeholder: "Search or create tags...",
})
</div>
// Payment Method
@ -163,7 +158,7 @@ templ AddExpenseForm(props AddExpenseFormProps) {
</form>
}
templ EditExpenseForm(spaceID string, exp *model.ExpenseWithTagsAndMethod, methods []*model.PaymentMethod) {
templ EditExpenseForm(spaceID string, exp *model.ExpenseWithTagsAndMethod, methods []*model.PaymentMethod, tags []*model.Tag) {
{{ editDialogID := "edit-expense-" + exp.ID }}
{{ tagValues := make([]string, len(exp.Tags)) }}
for i, t := range exp.Tags {
@ -248,11 +243,12 @@ templ EditExpenseForm(spaceID string, exp *model.ExpenseWithTagsAndMethod, metho
@label.Label(label.Props{For: "edit-tags-" + exp.ID}) {
Tags (Optional)
}
@tagsinput.TagsInput(tagsinput.Props{
@tagcombobox.TagCombobox(tagcombobox.Props{
ID: "edit-tags-" + exp.ID,
Name: "tags",
Value: tagValues,
Placeholder: "Add tags (press enter)",
Tags: tags,
Placeholder: "Search or create tags...",
})
</div>
// Payment Method

View file

@ -14,7 +14,7 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/paymentmethod"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/radio"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/selectbox"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/tagsinput"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/tagcombobox"
)
func frequencyLabel(f model.Frequency) string {
@ -34,7 +34,7 @@ func frequencyLabel(f model.Frequency) string {
}
}
templ RecurringItem(spaceID string, re *model.RecurringExpenseWithTagsAndMethod, methods []*model.PaymentMethod) {
templ RecurringItem(spaceID string, re *model.RecurringExpenseWithTagsAndMethod, methods []*model.PaymentMethod, tags []*model.Tag) {
{{ editDialogID := "edit-recurring-" + re.ID }}
{{ delDialogID := "del-recurring-" + re.ID }}
<div id={ "recurring-" + re.ID } class="p-4 flex justify-between items-start gap-2">
@ -113,7 +113,7 @@ templ RecurringItem(spaceID string, re *model.RecurringExpenseWithTagsAndMethod,
Update the details of this recurring transaction.
}
}
@EditRecurringForm(spaceID, re, methods)
@EditRecurringForm(spaceID, re, methods, tags)
}
}
// Delete button
@ -269,16 +269,11 @@ templ AddRecurringForm(spaceID string, tags []*model.Tag, methods []*model.Payme
@label.Label(label.Props{For: "recurring-tags"}) {
Tags
}
<datalist id="recurring-available-tags">
for _, t := range tags {
<option value={ t.Name }></option>
}
</datalist>
@tagsinput.TagsInput(tagsinput.Props{
@tagcombobox.TagCombobox(tagcombobox.Props{
ID: "recurring-tags",
Name: "tags",
Placeholder: "Add tags (press enter)",
Attributes: templ.Attributes{"list": "recurring-available-tags"},
Tags: tags,
Placeholder: "Search or create tags...",
})
</div>
// Payment Method
@ -291,7 +286,7 @@ templ AddRecurringForm(spaceID string, tags []*model.Tag, methods []*model.Payme
</form>
}
templ EditRecurringForm(spaceID string, re *model.RecurringExpenseWithTagsAndMethod, methods []*model.PaymentMethod) {
templ EditRecurringForm(spaceID string, re *model.RecurringExpenseWithTagsAndMethod, methods []*model.PaymentMethod, tags []*model.Tag) {
{{ editDialogID := "edit-recurring-" + re.ID }}
{{ tagValues := make([]string, len(re.Tags)) }}
for i, t := range re.Tags {
@ -422,11 +417,12 @@ templ EditRecurringForm(spaceID string, re *model.RecurringExpenseWithTagsAndMet
@label.Label(label.Props{For: "edit-recurring-tags-" + re.ID}) {
Tags
}
@tagsinput.TagsInput(tagsinput.Props{
@tagcombobox.TagCombobox(tagcombobox.Props{
ID: "edit-recurring-tags-" + re.ID,
Name: "tags",
Value: tagValues,
Placeholder: "Add tags (press enter)",
Tags: tags,
Placeholder: "Search or create tags...",
})
</div>
// Payment Method

View file

@ -0,0 +1,158 @@
package tagcombobox
import (
"strings"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
"git.juancwu.dev/juancwu/budgit/internal/utils"
)
type Props struct {
ID string
Name string // form field name (default "tags")
Value []string // pre-selected tag names
Tags []*model.Tag // all available tags in the space
Placeholder string
HasError bool
Disabled bool
Form string // associate with external form
}
func (p Props) fieldName() string {
if p.Name != "" {
return p.Name
}
return "tags"
}
func (p Props) isSelected(tagName string) bool {
lower := strings.ToLower(tagName)
for _, v := range p.Value {
if strings.ToLower(v) == lower {
return true
}
}
return false
}
templ TagCombobox(props Props) {
<div
id={ props.ID + "-container" }
class={
utils.TwMerge(
"relative w-full",
),
}
data-tagcombobox
data-tagcombobox-name={ props.fieldName() }
data-tagcombobox-form={ props.Form }
>
// Main input area styled like tagsinput
<div
class={
utils.TwMerge(
"flex items-center flex-wrap gap-2 p-2 rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none cursor-text",
"dark:bg-input/30",
"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
utils.If(props.Disabled, "opacity-50 cursor-not-allowed"),
"w-full min-h-[38px]",
utils.If(props.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
),
}
data-tagcombobox-input-area
>
// Selected tag chips
<div class="flex items-center flex-wrap gap-2" data-tagcombobox-chips>
for _, val := range props.Value {
@badge.Badge(badge.Props{
Attributes: templ.Attributes{"data-tagcombobox-chip": val},
}) {
<span>{ val }</span>
<button
type="button"
class="ml-1 text-current hover:text-destructive disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
disabled?={ props.Disabled }
data-tagcombobox-remove={ val }
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
}
}
</div>
// Text input for searching/typing
<input
type="text"
id={ props.ID }
class="border-0 shadow-none focus-visible:ring-0 focus-visible:outline-none h-auto py-0 px-0 bg-transparent rounded-none min-h-0 disabled:opacity-100 dark:bg-transparent flex-1 min-w-[80px] text-sm"
placeholder={ props.Placeholder }
disabled?={ props.Disabled }
autocomplete="off"
data-tagcombobox-text-input
/>
</div>
// Dropdown
<div
class="absolute z-50 mt-1 w-full rounded-md border bg-popover text-popover-foreground shadow-md hidden max-h-60 overflow-y-auto"
data-tagcombobox-dropdown
>
for _, tag := range props.Tags {
<div
class={
"flex items-center gap-2 px-3 py-2 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground",
templ.KV("font-medium", props.isSelected(tag.Name)),
}
data-tagcombobox-item={ tag.Name }
>
<svg
xmlns="http://www.w3.org/2000/svg"
class={
"h-4 w-4 shrink-0",
templ.KV("opacity-100", props.isSelected(tag.Name)),
templ.KV("opacity-0", !props.isSelected(tag.Name)),
}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
data-tagcombobox-check
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path>
</svg>
if tag.Color != nil {
<span class="inline-block w-3 h-3 rounded-full shrink-0" style={ "background-color: " + *tag.Color }></span>
}
<span data-tagcombobox-item-label>{ tag.Name }</span>
</div>
}
// "Create new" option (hidden by default, shown when typing something new)
<div
class="hidden items-center gap-2 px-3 py-2 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground text-muted-foreground"
data-tagcombobox-create
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"></path>
</svg>
<span>Create "<span data-tagcombobox-create-label></span>"</span>
</div>
</div>
// Hidden inputs for form submission
<div data-tagcombobox-hidden-inputs>
for _, val := range props.Value {
<input
type="hidden"
name={ props.fieldName() }
value={ val }
if props.Form != "" {
form={ props.Form }
}
/>
}
</div>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/tagcombobox.js") }></script>
}

View file

@ -13,6 +13,7 @@ import "git.juancwu.dev/juancwu/budgit/internal/ui/components/datepicker"
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/progress"
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/selectbox"
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/tagsinput"
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/tagcombobox"
import "fmt"
import "time"
@ -53,6 +54,7 @@ templ Base(props ...SEOProps) {
@progress.Script()
@tagsinput.Script()
@selectbox.Script()
@tagcombobox.Script()
// Site-wide enhancements
@themeScript()
// Must run before body to prevent flash

View file

@ -11,7 +11,7 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/label"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/radio"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/selectbox"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/tagcombobox"
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
)
@ -37,15 +37,6 @@ func progressBarColor(status model.BudgetStatus) string {
}
}
func budgetTagSelected(tags []*model.Tag, tagID string) bool {
for _, t := range tags {
if t.ID == tagID {
return true
}
}
return false
}
templ SpaceBudgetsPage(space *model.Space, budgets []*model.BudgetWithSpent, tags []*model.Tag) {
@layouts.Space("Budgets", space) {
<div class="space-y-4">
@ -190,23 +181,17 @@ templ AddBudgetForm(spaceID string, tags []*model.Tag) {
class="space-y-4"
>
@csrf.Token()
// Tag selector (multi-select)
// Tag selector
<div>
@label.Label(label.Props{}) {
@label.Label(label.Props{For: "budget-tags"}) {
Tags
}
@selectbox.SelectBox(selectbox.Props{ID: "budget-tags", Multiple: true}) {
@selectbox.Trigger(selectbox.TriggerProps{Name: "tag_ids", Multiple: true, ShowPills: true, SelectedCountText: "{n} tags selected"}) {
@selectbox.Value(selectbox.ValueProps{Placeholder: "Select tags..."})
}
@selectbox.Content() {
for _, t := range tags {
@selectbox.Item(selectbox.ItemProps{Value: t.ID}) {
{ t.Name }
}
}
}
}
@tagcombobox.TagCombobox(tagcombobox.Props{
ID: "budget-tags",
Name: "tags",
Tags: tags,
Placeholder: "Search or create tags...",
})
</div>
// Amount
<div>
@ -290,6 +275,10 @@ templ AddBudgetForm(spaceID string, tags []*model.Tag) {
templ EditBudgetForm(spaceID string, b *model.BudgetWithSpent, tags []*model.Tag) {
{{ editDialogID := "edit-budget-" + b.ID }}
{{ budgetTagNames := make([]string, len(b.Tags)) }}
for i, t := range b.Tags {
{{ budgetTagNames[i] = t.Name }}
}
<form
hx-patch={ fmt.Sprintf("/app/spaces/%s/budgets/%s", spaceID, b.ID) }
hx-target="#budgets-list-wrapper"
@ -298,23 +287,18 @@ templ EditBudgetForm(spaceID string, b *model.BudgetWithSpent, tags []*model.Tag
class="space-y-4"
>
@csrf.Token()
// Tag selector (multi-select with pre-selected tags)
// Tag selector
<div>
@label.Label(label.Props{}) {
@label.Label(label.Props{For: "edit-budget-tags-" + b.ID}) {
Tags
}
@selectbox.SelectBox(selectbox.Props{ID: "edit-budget-tags-" + b.ID, Multiple: true}) {
@selectbox.Trigger(selectbox.TriggerProps{Name: "tag_ids", Multiple: true, ShowPills: true, SelectedCountText: "{n} tags selected"}) {
@selectbox.Value(selectbox.ValueProps{Placeholder: "Select tags..."})
}
@selectbox.Content() {
for _, t := range tags {
@selectbox.Item(selectbox.ItemProps{Value: t.ID, Selected: budgetTagSelected(b.Tags, t.ID)}) {
{ t.Name }
}
}
}
}
@tagcombobox.TagCombobox(tagcombobox.Props{
ID: "edit-budget-tags-" + b.ID,
Name: "tags",
Value: budgetTagNames,
Tags: tags,
Placeholder: "Search or create tags...",
})
</div>
// Amount
<div>

View file

@ -48,21 +48,21 @@ templ SpaceExpensesPage(space *model.Space, expenses []*model.ExpenseWithTagsAnd
// List of expenses
<div class="border rounded-lg">
<div id="expenses-list-wrapper">
@ExpensesListContent(space.ID, expenses, methods, currentPage, totalPages)
@ExpensesListContent(space.ID, expenses, methods, tags, currentPage, totalPages)
</div>
</div>
</div>
}
}
templ ExpensesListContent(spaceID string, expenses []*model.ExpenseWithTagsAndMethod, methods []*model.PaymentMethod, currentPage, totalPages int) {
templ ExpensesListContent(spaceID string, expenses []*model.ExpenseWithTagsAndMethod, methods []*model.PaymentMethod, tags []*model.Tag, currentPage, totalPages int) {
<h2 class="text-lg font-semibold p-4">History</h2>
<div id="expenses-list" class="divide-y">
if len(expenses) == 0 {
<p class="p-4 text-sm text-muted-foreground">No expenses recorded yet.</p>
}
for _, exp := range expenses {
@ExpenseListItem(spaceID, exp, methods)
@ExpenseListItem(spaceID, exp, methods, tags)
}
</div>
if totalPages > 1 {
@ -109,7 +109,7 @@ templ ExpensesListContent(spaceID string, expenses []*model.ExpenseWithTagsAndMe
}
}
templ ExpenseListItem(spaceID string, exp *model.ExpenseWithTagsAndMethod, methods []*model.PaymentMethod) {
templ ExpenseListItem(spaceID string, exp *model.ExpenseWithTagsAndMethod, methods []*model.PaymentMethod, tags []*model.Tag) {
<div id={ "expense-" + exp.ID } class="p-4 flex justify-between items-start gap-2">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5">
@ -166,7 +166,7 @@ templ ExpenseListItem(spaceID string, exp *model.ExpenseWithTagsAndMethod, metho
Update the details of this transaction.
}
}
@expense.EditExpenseForm(spaceID, exp, methods)
@expense.EditExpenseForm(spaceID, exp, methods, tags)
}
}
// Delete button
@ -208,12 +208,12 @@ templ ExpenseListItem(spaceID string, exp *model.ExpenseWithTagsAndMethod, metho
</div>
}
templ ExpenseCreatedResponse(spaceID string, expenses []*model.ExpenseWithTagsAndMethod, balance int, allocated int, currentPage, totalPages int) {
@ExpensesListContent(spaceID, expenses, nil, currentPage, totalPages)
templ ExpenseCreatedResponse(spaceID string, expenses []*model.ExpenseWithTagsAndMethod, balance int, allocated int, tags []*model.Tag, currentPage, totalPages int) {
@ExpensesListContent(spaceID, expenses, nil, tags, currentPage, totalPages)
@expense.BalanceCard(spaceID, balance, allocated, true)
}
templ ExpenseUpdatedResponse(spaceID string, exp *model.ExpenseWithTagsAndMethod, balance int, allocated int, methods []*model.PaymentMethod) {
@ExpenseListItem(spaceID, exp, methods)
templ ExpenseUpdatedResponse(spaceID string, exp *model.ExpenseWithTagsAndMethod, balance int, allocated int, methods []*model.PaymentMethod, tags []*model.Tag) {
@ExpenseListItem(spaceID, exp, methods, tags)
@expense.BalanceCard(exp.SpaceID, balance, allocated, true)
}

View file

@ -38,7 +38,7 @@ templ SpaceRecurringPage(space *model.Space, recs []*model.RecurringExpenseWithT
<p class="p-4 text-sm text-muted-foreground">No recurring transactions set up yet.</p>
}
for _, re := range recs {
@recurring.RecurringItem(space.ID, re, methods)
@recurring.RecurringItem(space.ID, re, methods, tags)
}
</div>
</div>