Merge branch 'feat/extend-info-display-for-expenses'

# Conflicts:
#	internal/handler/space.go
This commit is contained in:
juancwu 2026-02-07 15:07:06 -05:00
commit 697b8879dd
No known key found for this signature in database
7 changed files with 531 additions and 24 deletions

View file

@ -38,6 +38,20 @@ func NewSpaceHandler(ss *service.SpaceService, ts *service.TagService, sls *serv
}
}
// getExpenseForSpace fetches an expense and verifies it belongs to the given space.
func (h *SpaceHandler) getExpenseForSpace(w http.ResponseWriter, spaceID, expenseID string) *model.Expense {
exp, err := h.expenseService.GetExpense(expenseID)
if err != nil {
http.Error(w, "Expense not found", http.StatusNotFound)
return nil
}
if exp.SpaceID != spaceID {
http.Error(w, "Not Found", http.StatusNotFound)
return nil
}
return exp
}
// getListForSpace fetches a shopping list and verifies it belongs to the given space.
// Returns the list on success, or writes an error response and returns nil.
func (h *SpaceHandler) getListForSpace(w http.ResponseWriter, spaceID, listID string) *model.ShoppingList {
@ -408,7 +422,7 @@ func (h *SpaceHandler) ExpensesPage(w http.ResponseWriter, r *http.Request) {
return
}
expenses, err := h.expenseService.GetExpensesForSpace(spaceID)
expenses, err := h.expenseService.GetExpensesWithTagsForSpace(spaceID)
if err != nil {
slog.Error("failed to get expenses for space", "error", err, "space_id", spaceID)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@ -582,7 +596,146 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) {
return
}
ui.Render(w, r, pages.ExpenseCreatedResponse(newExpense, balance))
// Build tags for the newly created expense
tagsMap, _ := h.expenseService.GetTagsByExpenseIDs([]string{newExpense.ID})
newExpenseWithTags := &model.ExpenseWithTags{
Expense: *newExpense,
Tags: tagsMap[newExpense.ID],
}
ui.Render(w, r, pages.ExpenseCreatedResponse(spaceID, newExpenseWithTags, balance))
}
func (h *SpaceHandler) UpdateExpense(w http.ResponseWriter, r *http.Request) {
spaceID := r.PathValue("spaceID")
expenseID := r.PathValue("expenseID")
if h.getExpenseForSpace(w, spaceID, expenseID) == nil {
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
description := r.FormValue("description")
amountStr := r.FormValue("amount")
typeStr := r.FormValue("type")
dateStr := r.FormValue("date")
tagNames := r.Form["tags"]
if description == "" || amountStr == "" || typeStr == "" || dateStr == "" {
http.Error(w, "All fields are required.", http.StatusBadRequest)
return
}
amountFloat, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
http.Error(w, "Invalid amount format.", http.StatusBadRequest)
return
}
amountCents := int(amountFloat * 100)
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
http.Error(w, "Invalid date format.", http.StatusBadRequest)
return
}
expenseType := model.ExpenseType(typeStr)
if expenseType != model.ExpenseTypeExpense && expenseType != model.ExpenseTypeTopup {
http.Error(w, "Invalid transaction type.", http.StatusBadRequest)
return
}
// Tag processing (same as CreateExpense)
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 == "" || 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 from expense form", "error", err, "tag_name", tagName)
continue
}
finalTagIDs = append(finalTagIDs, newTag.ID)
existingTagsMap[tagName] = newTag.ID
}
processedTags[tagName] = true
}
dto := service.UpdateExpenseDTO{
ID: expenseID,
SpaceID: spaceID,
Description: description,
Amount: amountCents,
Type: expenseType,
Date: date,
TagIDs: finalTagIDs,
}
updatedExpense, err := h.expenseService.UpdateExpense(dto)
if err != nil {
slog.Error("failed to update expense", "error", err)
http.Error(w, "Failed to update expense.", http.StatusInternalServerError)
return
}
tagsMap, _ := h.expenseService.GetTagsByExpenseIDs([]string{updatedExpense.ID})
expWithTags := &model.ExpenseWithTags{
Expense: *updatedExpense,
Tags: tagsMap[updatedExpense.ID],
}
balance, err := h.expenseService.GetBalanceForSpace(spaceID)
if err != nil {
slog.Error("failed to get balance after update", "error", err, "space_id", spaceID)
}
ui.Render(w, r, pages.ExpenseUpdatedResponse(spaceID, expWithTags, balance))
}
func (h *SpaceHandler) DeleteExpense(w http.ResponseWriter, r *http.Request) {
spaceID := r.PathValue("spaceID")
expenseID := r.PathValue("expenseID")
if h.getExpenseForSpace(w, spaceID, expenseID) == nil {
return
}
if err := h.expenseService.DeleteExpense(expenseID, spaceID); err != nil {
slog.Error("failed to delete expense", "error", err, "expense_id", expenseID)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
balance, err := h.expenseService.GetBalanceForSpace(spaceID)
if err != nil {
slog.Error("failed to get balance after delete", "error", err, "space_id", spaceID)
}
ui.Render(w, r, expense.BalanceCard(spaceID, balance, true))
}
func (h *SpaceHandler) CreateInvite(w http.ResponseWriter, r *http.Request) {
@ -661,14 +814,14 @@ func (h *SpaceHandler) GetBalanceCard(w http.ResponseWriter, r *http.Request) {
func (h *SpaceHandler) GetExpensesList(w http.ResponseWriter, r *http.Request) {
spaceID := r.PathValue("spaceID")
expenses, err := h.expenseService.GetExpensesForSpace(spaceID)
expenses, err := h.expenseService.GetExpensesWithTagsForSpace(spaceID)
if err != nil {
slog.Error("failed to get expenses", "error", err, "space_id", spaceID)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
ui.Render(w, r, pages.ExpensesListContent(expenses))
ui.Render(w, r, pages.ExpensesListContent(spaceID, expenses))
}
func (h *SpaceHandler) GetShoppingListItems(w http.ResponseWriter, r *http.Request) {