diff --git a/cmd/server/main.go b/cmd/server/main.go index 7bbcc0a..0a59816 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -51,7 +51,7 @@ func main() { }) srv := &http.Server{ - Addr: ":" + cfg.Port, + Addr: cfg.Host + ":" + cfg.Port, Handler: finalHandler, } diff --git a/internal/handler/space.go b/internal/handler/space.go index dd55e66..f5ed049 100644 --- a/internal/handler/space.go +++ b/internal/handler/space.go @@ -3,6 +3,7 @@ package handler import ( "log/slog" "net/http" + "strconv" "strings" "time" @@ -193,11 +194,76 @@ func (h *spaceHandler) SpaceAccountPage(w http.ResponseWriter, r *http.Request) return } + recent, err := h.transactionService.ListByAccount(accountID, 5, 0) + if err != nil { + slog.Error("failed to load recent transactions", "error", err, "account_id", accountID) + recent = nil + } + ui.Render(w, r, pages.SpaceAccountPage(pages.SpaceAccountPageProps{ - SpaceID: spaceID, - AccountID: accountID, - AccountName: account.Name, - AccountBalance: account.Balance, + SpaceID: spaceID, + AccountID: accountID, + AccountName: account.Name, + AccountBalance: account.Balance, + RecentTransactions: recent, + })) +} + +func (h *spaceHandler) SpaceAccountTransactionsPage(w http.ResponseWriter, r *http.Request) { + spaceID := r.PathValue("spaceID") + accountID := r.PathValue("accountID") + + account, err := h.accountService.GetAccount(accountID) + if err != nil { + slog.Error("failed to load account", "error", err, "account_id", accountID) + ui.Render(w, r, pages.NotFound()) + return + } + if account.SpaceID != spaceID { + ui.Render(w, r, pages.NotFound()) + return + } + + const perPage = 25 + page := 1 + if p := strings.TrimSpace(r.URL.Query().Get("page")); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } + + total, err := h.transactionService.CountByAccount(accountID) + if err != nil { + slog.Error("failed to count transactions", "error", err, "account_id", accountID) + ui.RenderError(w, r, "Failed to load transactions", http.StatusInternalServerError) + return + } + + totalPages := (total + perPage - 1) / perPage + if totalPages < 1 { + totalPages = 1 + } + if page > totalPages { + page = totalPages + } + + offset := (page - 1) * perPage + txns, err := h.transactionService.ListByAccount(accountID, perPage, offset) + if err != nil { + slog.Error("failed to load transactions", "error", err, "account_id", accountID) + ui.RenderError(w, r, "Failed to load transactions", http.StatusInternalServerError) + return + } + + ui.Render(w, r, pages.SpaceAccountTransactionsPage(pages.SpaceAccountTransactionsPageProps{ + SpaceID: spaceID, + AccountID: accountID, + AccountName: account.Name, + Transactions: txns, + CurrentPage: page, + TotalPages: totalPages, + TotalCount: total, + PerPage: perPage, })) } diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go index 11c8ab4..e8bdfe6 100644 --- a/internal/repository/transaction.go +++ b/internal/repository/transaction.go @@ -10,6 +10,8 @@ import ( type TransactionRepository interface { CreateBillAtomic(t *model.Transaction, newBalance decimal.Decimal, categoryID *string) error + ListByAccount(accountID string, limit, offset int) ([]*model.Transaction, error) + CountByAccount(accountID string) (int, error) } type transactionRepository struct { @@ -51,3 +53,26 @@ func (r *transactionRepository) CreateBillAtomic(t *model.Transaction, newBalanc return nil }) } + +func (r *transactionRepository) ListByAccount(accountID string, limit, offset int) ([]*model.Transaction, error) { + query := ` + SELECT id, value, type, account_id, title, description, occurred_at, created_at, updated_at + FROM transactions + WHERE account_id = $1 + ORDER BY occurred_at DESC, created_at DESC + LIMIT $2 OFFSET $3; + ` + txns := []*model.Transaction{} + if err := r.db.Select(&txns, query, accountID, limit, offset); err != nil { + return nil, err + } + return txns, nil +} + +func (r *transactionRepository) CountByAccount(accountID string) (int, error) { + var count int + if err := r.db.Get(&count, `SELECT COUNT(*) FROM transactions WHERE account_id = $1;`, accountID); err != nil { + return 0, err + } + return count, nil +} diff --git a/internal/routes/routes.go b/internal/routes/routes.go index dc7b4c3..ae86ccc 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -95,6 +95,7 @@ func SetupRoutes(a *app.App) http.Handler { g.SubGroup("/accounts/{accountID}", func(g *router.Group) { g.Get("/overview", spaceH.SpaceAccountPage).Name("page.app.spaces.space.accounts.account.overview") + g.Get("/transactions", spaceH.SpaceAccountTransactionsPage).Name("page.app.spaces.space.accounts.account.transactions") g.Get("/bills/create", spaceH.SpaceCreateBillPage).Name("page.app.spaces.space.accounts.account.bills.create") g.Post("/bills/create", spaceH.HandleCreateBill).Name("action.app.spaces.space.accounts.account.bills.create") }) diff --git a/internal/service/transaction.go b/internal/service/transaction.go index f69572b..fd2caab 100644 --- a/internal/service/transaction.go +++ b/internal/service/transaction.go @@ -89,6 +89,28 @@ func (s *TransactionService) PayBill(input PayBillInput) (*model.Transaction, er return txn, nil } +func (s *TransactionService) ListByAccount(accountID string, limit, offset int) ([]*model.Transaction, error) { + if limit <= 0 { + limit = 25 + } + if offset < 0 { + offset = 0 + } + txns, err := s.transactionRepo.ListByAccount(accountID, limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list transactions: %w", err) + } + return txns, nil +} + +func (s *TransactionService) CountByAccount(accountID string) (int, error) { + count, err := s.transactionRepo.CountByAccount(accountID) + if err != nil { + return 0, fmt.Errorf("failed to count transactions: %w", err) + } + return count, nil +} + func (s *TransactionService) ListCategories() ([]*model.Category, error) { categories, err := s.categoryRepo.All() if err != nil { diff --git a/internal/ui/blocks/transaction_list.templ b/internal/ui/blocks/transaction_list.templ new file mode 100644 index 0000000..ef61397 --- /dev/null +++ b/internal/ui/blocks/transaction_list.templ @@ -0,0 +1,56 @@ +package blocks + +import "git.juancwu.dev/juancwu/budgit/internal/model" +import "git.juancwu.dev/juancwu/budgit/internal/ui/components/icon" +import "git.juancwu.dev/juancwu/budgit/internal/ui/utils" + +templ TransactionList(txns []*model.Transaction) { + if len(txns) == 0 { +
{ t.Title }
+{ t.OccurredAt.Format("Jan 2, 2006") }
++ { sign }${ utils.FormatDecimalWithThousands(t.Value.StringFixedBank(2)) } +
+ if t.Description != nil && *t.Description != "" { +{ *t.Description }
+ } ++ All activity for { props.AccountName }. +
+