feat: paginate expenses history
This commit is contained in:
parent
c2f91f9c33
commit
153eaca676
5 changed files with 237 additions and 100 deletions
|
|
@ -382,7 +382,12 @@ func (h *SpaceHandler) ExpensesPage(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
expenses, err := h.expenseService.GetExpensesWithTagsForSpace(spaceID)
|
page := 1
|
||||||
|
if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && p > 0 {
|
||||||
|
page = p
|
||||||
|
}
|
||||||
|
|
||||||
|
expenses, totalPages, err := h.expenseService.GetExpensesWithTagsForSpacePaginated(spaceID, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to get expenses for space", "error", err, "space_id", spaceID)
|
slog.Error("failed to get expenses for space", "error", err, "space_id", spaceID)
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
|
@ -410,7 +415,7 @@ func (h *SpaceHandler) ExpensesPage(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Render(w, r, pages.SpaceExpensesPage(space, expenses, balance, tags, listsWithItems))
|
ui.Render(w, r, pages.SpaceExpensesPage(space, expenses, balance, tags, listsWithItems, page, totalPages))
|
||||||
|
|
||||||
if r.URL.Query().Get("created") == "true" {
|
if r.URL.Query().Get("created") == "true" {
|
||||||
ui.Render(w, r, toast.Toast(toast.Props{
|
ui.Render(w, r, toast.Toast(toast.Props{
|
||||||
|
|
@ -525,7 +530,7 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) {
|
||||||
ItemIDs: itemIDs,
|
ItemIDs: itemIDs,
|
||||||
}
|
}
|
||||||
|
|
||||||
newExpense, err := h.expenseService.CreateExpense(dto)
|
_, err = h.expenseService.CreateExpense(dto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to create expense", "error", err)
|
slog.Error("failed to create expense", "error", err)
|
||||||
http.Error(w, "Failed to create expense.", http.StatusInternalServerError)
|
http.Error(w, "Failed to create expense.", http.StatusInternalServerError)
|
||||||
|
|
@ -556,14 +561,23 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build tags for the newly created expense
|
// Return the full paginated list for page 1 so the new expense appears
|
||||||
tagsMap, _ := h.expenseService.GetTagsByExpenseIDs([]string{newExpense.ID})
|
expenses, totalPages, err := h.expenseService.GetExpensesWithTagsForSpacePaginated(spaceID, 1)
|
||||||
newExpenseWithTags := &model.ExpenseWithTags{
|
if err != nil {
|
||||||
Expense: *newExpense,
|
slog.Error("failed to get paginated expenses after create", "error", err, "space_id", spaceID)
|
||||||
Tags: tagsMap[newExpense.ID],
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Render(w, r, pages.ExpenseCreatedResponse(spaceID, newExpenseWithTags, balance))
|
ui.Render(w, r, pages.ExpenseCreatedResponse(spaceID, expenses, balance, 1, totalPages))
|
||||||
|
|
||||||
|
// OOB-swap the item selector with fresh data (items may have been deleted/checked)
|
||||||
|
listsWithItems, err := h.listService.GetListsWithUncheckedItems(spaceID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to refresh lists with items after create", "error", err, "space_id", spaceID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui.Render(w, r, expense.ItemSelectorSection(listsWithItems, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SpaceHandler) UpdateExpense(w http.ResponseWriter, r *http.Request) {
|
func (h *SpaceHandler) UpdateExpense(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -774,14 +788,19 @@ func (h *SpaceHandler) GetBalanceCard(w http.ResponseWriter, r *http.Request) {
|
||||||
func (h *SpaceHandler) GetExpensesList(w http.ResponseWriter, r *http.Request) {
|
func (h *SpaceHandler) GetExpensesList(w http.ResponseWriter, r *http.Request) {
|
||||||
spaceID := r.PathValue("spaceID")
|
spaceID := r.PathValue("spaceID")
|
||||||
|
|
||||||
expenses, err := h.expenseService.GetExpensesWithTagsForSpace(spaceID)
|
page := 1
|
||||||
|
if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && p > 0 {
|
||||||
|
page = p
|
||||||
|
}
|
||||||
|
|
||||||
|
expenses, totalPages, err := h.expenseService.GetExpensesWithTagsForSpacePaginated(spaceID, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to get expenses", "error", err, "space_id", spaceID)
|
slog.Error("failed to get expenses", "error", err, "space_id", spaceID)
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Render(w, r, pages.ExpensesListContent(spaceID, expenses))
|
ui.Render(w, r, pages.ExpensesListContent(spaceID, expenses, page, totalPages))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SpaceHandler) GetShoppingListItems(w http.ResponseWriter, r *http.Request) {
|
func (h *SpaceHandler) GetShoppingListItems(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ type ExpenseRepository interface {
|
||||||
Create(expense *model.Expense, tagIDs []string, itemIDs []string) error
|
Create(expense *model.Expense, tagIDs []string, itemIDs []string) error
|
||||||
GetByID(id string) (*model.Expense, error)
|
GetByID(id string) (*model.Expense, error)
|
||||||
GetBySpaceID(spaceID string) ([]*model.Expense, error)
|
GetBySpaceID(spaceID string) ([]*model.Expense, error)
|
||||||
|
GetBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.Expense, error)
|
||||||
|
CountBySpaceID(spaceID string) (int, error)
|
||||||
GetExpensesByTag(spaceID string, fromDate, toDate time.Time) ([]*model.TagExpenseSummary, error)
|
GetExpensesByTag(spaceID string, fromDate, toDate time.Time) ([]*model.TagExpenseSummary, error)
|
||||||
GetTagsByExpenseIDs(expenseIDs []string) (map[string][]*model.Tag, error)
|
GetTagsByExpenseIDs(expenseIDs []string) (map[string][]*model.Tag, error)
|
||||||
Update(expense *model.Expense, tagIDs []string) error
|
Update(expense *model.Expense, tagIDs []string) error
|
||||||
|
|
@ -91,6 +93,22 @@ func (r *expenseRepository) GetBySpaceID(spaceID string) ([]*model.Expense, erro
|
||||||
return expenses, nil
|
return expenses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *expenseRepository) GetBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.Expense, error) {
|
||||||
|
var expenses []*model.Expense
|
||||||
|
query := `SELECT * FROM expenses WHERE space_id = $1 ORDER BY date DESC, created_at DESC LIMIT $2 OFFSET $3;`
|
||||||
|
err := r.db.Select(&expenses, query, spaceID, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return expenses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *expenseRepository) CountBySpaceID(spaceID string) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := r.db.Get(&count, `SELECT COUNT(*) FROM expenses WHERE space_id = $1;`, spaceID)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *expenseRepository) GetExpensesByTag(spaceID string, fromDate, toDate time.Time) ([]*model.TagExpenseSummary, error) {
|
func (r *expenseRepository) GetExpensesByTag(spaceID string, fromDate, toDate time.Time) ([]*model.TagExpenseSummary, error) {
|
||||||
var summaries []*model.TagExpenseSummary
|
var summaries []*model.TagExpenseSummary
|
||||||
query := `
|
query := `
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ type UpdateExpenseDTO struct {
|
||||||
TagIDs []string
|
TagIDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ExpensesPerPage = 25
|
||||||
|
|
||||||
type ExpenseService struct {
|
type ExpenseService struct {
|
||||||
expenseRepo repository.ExpenseRepository
|
expenseRepo repository.ExpenseRepository
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +123,49 @@ func (s *ExpenseService) GetExpensesWithTagsForSpace(spaceID string) ([]*model.E
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ExpenseService) GetExpensesWithTagsForSpacePaginated(spaceID string, page int) ([]*model.ExpenseWithTags, int, error) {
|
||||||
|
total, err := s.expenseRepo.CountBySpaceID(spaceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (total + ExpensesPerPage - 1) / ExpensesPerPage
|
||||||
|
if totalPages < 1 {
|
||||||
|
totalPages = 1
|
||||||
|
}
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if page > totalPages {
|
||||||
|
page = totalPages
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * ExpensesPerPage
|
||||||
|
expenses, err := s.expenseRepo.GetBySpaceIDPaginated(spaceID, ExpensesPerPage, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]string, len(expenses))
|
||||||
|
for i, e := range expenses {
|
||||||
|
ids[i] = e.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsMap, err := s.expenseRepo.GetTagsByExpenseIDs(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]*model.ExpenseWithTags, len(expenses))
|
||||||
|
for i, e := range expenses {
|
||||||
|
result[i] = &model.ExpenseWithTags{
|
||||||
|
Expense: *e,
|
||||||
|
Tags: tagsMap[e.ID],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, totalPages, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ExpenseService) GetExpense(id string) (*model.Expense, error) {
|
func (s *ExpenseService) GetExpense(id string) (*model.Expense, error) {
|
||||||
return s.expenseRepo.GetByID(id)
|
return s.expenseRepo.GetByID(id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ func (p AddExpenseFormProps) formAttrs() templ.Attributes {
|
||||||
}
|
}
|
||||||
return templ.Attributes{
|
return templ.Attributes{
|
||||||
"hx-post": "/app/spaces/" + p.Space.ID + "/expenses",
|
"hx-post": "/app/spaces/" + p.Space.ID + "/expenses",
|
||||||
"hx-target": "#expenses-list",
|
"hx-target": "#expenses-list-wrapper",
|
||||||
"hx-swap": "afterbegin",
|
"hx-swap": "innerHTML",
|
||||||
"_": closeScript,
|
"_": closeScript,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,87 +143,7 @@ templ AddExpenseForm(props AddExpenseFormProps) {
|
||||||
})
|
})
|
||||||
</div>
|
</div>
|
||||||
// Shopping list items selector
|
// Shopping list items selector
|
||||||
<div id="item-selector-section">
|
@ItemSelectorSection(props.ListsWithItems, false)
|
||||||
@label.Label(label.Props{}) {
|
|
||||||
Link Shopping List Items
|
|
||||||
}
|
|
||||||
if len(props.ListsWithItems) == 0 {
|
|
||||||
<p class="text-sm text-muted-foreground">No unchecked items available.</p>
|
|
||||||
} else {
|
|
||||||
<div class="max-h-48 overflow-y-auto border rounded-md p-2 space-y-2">
|
|
||||||
for i, lwi := range props.ListsWithItems {
|
|
||||||
{{ toggleID := "toggle-list-" + lwi.List.ID }}
|
|
||||||
{{ itemsID := "items-" + lwi.List.ID }}
|
|
||||||
<div class="space-y-1">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
@checkbox.Checkbox(checkbox.Props{
|
|
||||||
ID: "select-all-" + lwi.List.ID,
|
|
||||||
Attributes: templ.Attributes{
|
|
||||||
"_": "on change repeat for cb in <input[name='item_ids']/> in #" + itemsID + " set cb.checked to my.checked end",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id={ toggleID }
|
|
||||||
class="flex-1 flex items-center gap-1 text-sm font-medium cursor-pointer select-none"
|
|
||||||
_={ "on click toggle .hidden on #" + itemsID + " then toggle .rotate-90 on <svg/> in me" }
|
|
||||||
>
|
|
||||||
@icon.ChevronRight(icon.Props{Size: 14})
|
|
||||||
{ lwi.List.Name }
|
|
||||||
<span class="text-muted-foreground">
|
|
||||||
({ strconv.Itoa(len(lwi.Items)) })
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id={ itemsID } class="hidden pl-6 space-y-1">
|
|
||||||
for _, item := range lwi.Items {
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
@checkbox.Checkbox(checkbox.Props{
|
|
||||||
ID: "item-cb-" + item.ID,
|
|
||||||
Name: "item_ids",
|
|
||||||
Value: item.ID,
|
|
||||||
})
|
|
||||||
<label for={ "item-cb-" + item.ID } class="text-sm cursor-pointer select-none">
|
|
||||||
{ item.Name }
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
if i < len(props.ListsWithItems) - 1 {
|
|
||||||
<hr class="border-border"/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
// Post-action radio group
|
|
||||||
<div class="mt-2 space-y-1">
|
|
||||||
<p class="text-sm text-muted-foreground">After linking items:</p>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
@radio.Radio(radio.Props{
|
|
||||||
ID: "item-action-check",
|
|
||||||
Name: "item_action",
|
|
||||||
Value: "check",
|
|
||||||
Checked: true,
|
|
||||||
})
|
|
||||||
@label.Label(label.Props{For: "item-action-check"}) {
|
|
||||||
Mark as checked
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
@radio.Radio(radio.Props{
|
|
||||||
ID: "item-action-delete",
|
|
||||||
Name: "item_action",
|
|
||||||
Value: "delete",
|
|
||||||
})
|
|
||||||
@label.Label(label.Props{For: "item-action-delete"}) {
|
|
||||||
Delete from list
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
@button.Button(button.Props{Type: button.TypeSubmit}) {
|
@button.Button(button.Props{Type: button.TypeSubmit}) {
|
||||||
Save
|
Save
|
||||||
|
|
@ -332,6 +252,95 @@ templ EditExpenseForm(spaceID string, exp *model.ExpenseWithTags) {
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ ItemSelectorSection(listsWithItems []model.ListWithUncheckedItems, oob bool) {
|
||||||
|
<div
|
||||||
|
id="item-selector-section"
|
||||||
|
if oob {
|
||||||
|
hx-swap-oob="true"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
@label.Label(label.Props{}) {
|
||||||
|
Link Shopping List Items
|
||||||
|
}
|
||||||
|
if len(listsWithItems) == 0 {
|
||||||
|
<p class="text-sm text-muted-foreground">No unchecked items available.</p>
|
||||||
|
} else {
|
||||||
|
<div class="max-h-48 overflow-y-auto border rounded-md p-2 space-y-2">
|
||||||
|
for i, lwi := range listsWithItems {
|
||||||
|
{{ toggleID := "toggle-list-" + lwi.List.ID }}
|
||||||
|
{{ itemsID := "items-" + lwi.List.ID }}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
@checkbox.Checkbox(checkbox.Props{
|
||||||
|
ID: "select-all-" + lwi.List.ID,
|
||||||
|
Attributes: templ.Attributes{
|
||||||
|
"_": "on change repeat for cb in <input[name='item_ids']/> in #" + itemsID + " set cb.checked to my.checked end",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id={ toggleID }
|
||||||
|
class="flex-1 flex items-center gap-1 text-sm font-medium cursor-pointer select-none"
|
||||||
|
_={ "on click toggle .hidden on #" + itemsID + " then toggle .rotate-90 on <svg/> in me" }
|
||||||
|
>
|
||||||
|
@icon.ChevronRight(icon.Props{Size: 14})
|
||||||
|
{ lwi.List.Name }
|
||||||
|
<span class="text-muted-foreground">
|
||||||
|
({ strconv.Itoa(len(lwi.Items)) })
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id={ itemsID } class="hidden pl-6 space-y-1">
|
||||||
|
for _, item := range lwi.Items {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
@checkbox.Checkbox(checkbox.Props{
|
||||||
|
ID: "item-cb-" + item.ID,
|
||||||
|
Name: "item_ids",
|
||||||
|
Value: item.ID,
|
||||||
|
})
|
||||||
|
<label for={ "item-cb-" + item.ID } class="text-sm cursor-pointer select-none">
|
||||||
|
{ item.Name }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
if i < len(listsWithItems) - 1 {
|
||||||
|
<hr class="border-border"/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
// Post-action radio group
|
||||||
|
<div class="mt-2 space-y-1">
|
||||||
|
<p class="text-sm text-muted-foreground">After linking items:</p>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
@radio.Radio(radio.Props{
|
||||||
|
ID: "item-action-check",
|
||||||
|
Name: "item_action",
|
||||||
|
Value: "check",
|
||||||
|
Checked: true,
|
||||||
|
})
|
||||||
|
@label.Label(label.Props{For: "item-action-check"}) {
|
||||||
|
Mark as checked
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
@radio.Radio(radio.Props{
|
||||||
|
ID: "item-action-delete",
|
||||||
|
Name: "item_action",
|
||||||
|
Value: "delete",
|
||||||
|
})
|
||||||
|
@label.Label(label.Props{For: "item-action-delete"}) {
|
||||||
|
Delete from list
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
templ BalanceCard(spaceID string, balance int, oob bool) {
|
templ BalanceCard(spaceID string, balance int, oob bool) {
|
||||||
<div
|
<div
|
||||||
id="balance-card"
|
id="balance-card"
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,18 @@ package pages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/expense"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/expense"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
|
||||||
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/pagination"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
||||||
)
|
)
|
||||||
|
|
||||||
templ SpaceExpensesPage(space *model.Space, expenses []*model.ExpenseWithTags, balance int, tags []*model.Tag, listsWithItems []model.ListWithUncheckedItems) {
|
templ SpaceExpensesPage(space *model.Space, expenses []*model.ExpenseWithTags, balance int, tags []*model.Tag, listsWithItems []model.ListWithUncheckedItems, currentPage, totalPages int) {
|
||||||
@layouts.Space("Expenses", space) {
|
@layouts.Space("Expenses", space) {
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
|
|
@ -44,13 +46,15 @@ templ SpaceExpensesPage(space *model.Space, expenses []*model.ExpenseWithTags, b
|
||||||
@expense.BalanceCard(space.ID, balance, false)
|
@expense.BalanceCard(space.ID, balance, false)
|
||||||
// List of expenses
|
// List of expenses
|
||||||
<div class="border rounded-lg">
|
<div class="border rounded-lg">
|
||||||
@ExpensesListContent(space.ID, expenses)
|
<div id="expenses-list-wrapper">
|
||||||
|
@ExpensesListContent(space.ID, expenses, currentPage, totalPages)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ExpensesListContent(spaceID string, expenses []*model.ExpenseWithTags) {
|
templ ExpensesListContent(spaceID string, expenses []*model.ExpenseWithTags, currentPage, totalPages int) {
|
||||||
<h2 class="text-lg font-semibold p-4">History</h2>
|
<h2 class="text-lg font-semibold p-4">History</h2>
|
||||||
<div id="expenses-list" class="divide-y">
|
<div id="expenses-list" class="divide-y">
|
||||||
if len(expenses) == 0 {
|
if len(expenses) == 0 {
|
||||||
|
|
@ -60,6 +64,48 @@ templ ExpensesListContent(spaceID string, expenses []*model.ExpenseWithTags) {
|
||||||
@ExpenseListItem(spaceID, exp)
|
@ExpenseListItem(spaceID, exp)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
if totalPages > 1 {
|
||||||
|
<div class="border-t p-2">
|
||||||
|
@pagination.Pagination(pagination.Props{Class: "justify-center"}) {
|
||||||
|
@pagination.Content() {
|
||||||
|
@pagination.Item() {
|
||||||
|
@pagination.Previous(pagination.PreviousProps{
|
||||||
|
Disabled: currentPage <= 1,
|
||||||
|
Attributes: templ.Attributes{
|
||||||
|
"hx-get": fmt.Sprintf("/app/spaces/%s/components/expenses?page=%d", spaceID, currentPage-1),
|
||||||
|
"hx-target": "#expenses-list-wrapper",
|
||||||
|
"hx-swap": "innerHTML",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, pg := range pagination.CreatePagination(currentPage, totalPages, 3).Pages {
|
||||||
|
@pagination.Item() {
|
||||||
|
@pagination.Link(pagination.LinkProps{
|
||||||
|
IsActive: pg == currentPage,
|
||||||
|
Attributes: templ.Attributes{
|
||||||
|
"hx-get": fmt.Sprintf("/app/spaces/%s/components/expenses?page=%d", spaceID, pg),
|
||||||
|
"hx-target": "#expenses-list-wrapper",
|
||||||
|
"hx-swap": "innerHTML",
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
{ strconv.Itoa(pg) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@pagination.Item() {
|
||||||
|
@pagination.Next(pagination.NextProps{
|
||||||
|
Disabled: currentPage >= totalPages,
|
||||||
|
Attributes: templ.Attributes{
|
||||||
|
"hx-get": fmt.Sprintf("/app/spaces/%s/components/expenses?page=%d", spaceID, currentPage+1),
|
||||||
|
"hx-target": "#expenses-list-wrapper",
|
||||||
|
"hx-swap": "innerHTML",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ExpenseListItem(spaceID string, exp *model.ExpenseWithTags) {
|
templ ExpenseListItem(spaceID string, exp *model.ExpenseWithTags) {
|
||||||
|
|
@ -145,9 +191,9 @@ templ ExpenseListItem(spaceID string, exp *model.ExpenseWithTags) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ExpenseCreatedResponse(spaceID string, newExpense *model.ExpenseWithTags, balance int) {
|
templ ExpenseCreatedResponse(spaceID string, expenses []*model.ExpenseWithTags, balance int, currentPage, totalPages int) {
|
||||||
@ExpenseListItem(spaceID, newExpense)
|
@ExpensesListContent(spaceID, expenses, currentPage, totalPages)
|
||||||
@expense.BalanceCard(newExpense.SpaceID, balance, true)
|
@expense.BalanceCard(spaceID, balance, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ExpenseUpdatedResponse(spaceID string, exp *model.ExpenseWithTags, balance int) {
|
templ ExpenseUpdatedResponse(spaceID string, exp *model.ExpenseWithTags, balance int) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue