feat(transaction): #243 add pagination to transactions
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m34s

This commit was merged in pull request #249.
This commit is contained in:
2025-08-08 23:44:29 +02:00
parent 867c0ca1cd
commit 0517e7ec89
7 changed files with 106 additions and 11 deletions

View File

@@ -15,7 +15,6 @@ import (
type migrationLogger struct{} type migrationLogger struct{}
func (l migrationLogger) Printf(format string, v ...any) { func (l migrationLogger) Printf(format string, v ...any) {
//nolint:noctx
slog.Info(format, v...) slog.Info(format, v...)
} }
func (l migrationLogger) Verbose() bool { func (l migrationLogger) Verbose() bool {

View File

@@ -58,6 +58,7 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
AccountId: r.URL.Query().Get("account-id"), AccountId: r.URL.Query().Get("account-id"),
TreasureChestId: r.URL.Query().Get("treasure-chest-id"), TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
Error: r.URL.Query().Get("error"), Error: r.URL.Query().Get("error"),
Page: r.URL.Query().Get("page"),
} }
transactions, err := h.s.GetAll(r.Context(), user, filter) transactions, err := h.s.GetAll(r.Context(), user, filter)

View File

@@ -7,12 +7,15 @@ import (
"log/slog" "log/slog"
"spend-sparrow/internal/db" "spend-sparrow/internal/db"
"spend-sparrow/internal/types" "spend-sparrow/internal/types"
"strconv"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
const page_size = 25
type Transaction interface { type Transaction interface {
Add(ctx context.Context, tx *sqlx.Tx, user *types.User, transaction types.Transaction) (*types.Transaction, error) Add(ctx context.Context, tx *sqlx.Tx, user *types.User, transaction types.Transaction) (*types.Transaction, error)
Update(ctx context.Context, user *types.User, transaction types.Transaction) (*types.Transaction, error) Update(ctx context.Context, user *types.User, transaction types.Transaction) (*types.Transaction, error)
@@ -231,22 +234,41 @@ func (s TransactionImpl) GetAll(ctx context.Context, user *types.User, filter ty
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
var (
page int64
offset int64
err error
)
if filter.Page != "" {
page, err = strconv.ParseInt(filter.Page, 10, 64)
if err != nil {
offset = 0
} else {
offset = page - 1
offset *= page_size
}
}
transactions := make([]*types.Transaction, 0) transactions := make([]*types.Transaction, 0)
err := s.db.SelectContext(ctx, &transactions, ` err = s.db.SelectContext(ctx, &transactions, `
SELECT * SELECT *
FROM "transaction" FROM "transaction"
WHERE user_id = ? WHERE user_id = ?
AND (? = '' OR account_id = ?) AND ($1 = '' OR account_id = $1)
AND (? = '' OR treasure_chest_id = ?) AND ($2 = '' OR treasure_chest_id = $2)
AND (? = '' AND ($3 = ''
OR (? = "true" AND error IS NOT NULL) OR ($3 = "true" AND error IS NOT NULL)
OR (? = "false" AND error IS NULL) OR ($3 = "false" AND error IS NULL)
) )
ORDER BY timestamp DESC, created_at DESC`, ORDER BY timestamp DESC, created_at DESC
LIMIT $4 OFFSET $5
`,
user.Id, user.Id,
filter.AccountId, filter.AccountId, filter.AccountId,
filter.TreasureChestId, filter.TreasureChestId, filter.TreasureChestId,
filter.Error, filter.Error, filter.Error) filter.Error,
page_size,
offset)
err = db.TransformAndLogDbError(ctx, "transaction GetAll", nil, err) err = db.TransformAndLogDbError(ctx, "transaction GetAll", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -31,6 +31,7 @@ templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path str
<script src="/static/js/htmx.min.js"></script> <script src="/static/js/htmx.min.js"></script>
<script src="/static/js/toast.js"></script> <script src="/static/js/toast.js"></script>
<script src="/static/js/layout.js"></script> <script src="/static/js/layout.js"></script>
<script src="/static/js/transaction.js"></script>
<script src="/static/js/time.js"></script> <script src="/static/js/time.js"></script>
<script src="/static/js/echarts.min.js"></script> <script src="/static/js/echarts.min.js"></script>
<script src="/static/js/dashboard.js" defer></script> <script src="/static/js/dashboard.js" defer></script>

View File

@@ -10,6 +10,7 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
<div class="max-w-6xl mt-10 mx-auto"> <div class="max-w-6xl mt-10 mx-auto">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<form <form
id="transactionFilterForm"
hx-get="/transaction" hx-get="/transaction"
hx-target="#transaction-items" hx-target="#transaction-items"
hx-push-url="true" hx-push-url="true"
@@ -52,6 +53,7 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
selected?={ filter.Error == "false" } selected?={ filter.Error == "false" }
>Has no Errors</option> >Has no Errors</option>
</select> </select>
<input id="page" name="page" type="hidden" value={ filter.Page }/>
</form> </form>
<button <button
hx-get="/transaction/new" hx-get="/transaction/new"
@@ -63,7 +65,25 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
<p>New Transaction</p> <p>New Transaction</p>
</button> </button>
</div> </div>
<div class="flex justify-end items-center gap-5 mt-5">
<button id="pagePrev1" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
&lt;
</button>
<span class="text-gray-400 text-sm">Page: <span class="text-gray-800 text-xl" id="page1">{ getPageNumber(filter.Page) }</span></span>
<button id="pageNext1" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
&gt;
</button>
</div>
@items @items
<div class="flex justify-end items-center gap-5 mt-5">
<button id="pagePrev2" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
&lt;
</button>
<span class="text-gray-400 text-sm">Page: <span class="text-gray-800 text-xl" id="page2">{ getPageNumber(filter.Page) }</span></span>
<button id="pageNext2" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
&gt;
</button>
</div>
</div> </div>
} }
@@ -287,3 +307,11 @@ func formatFloat(balance int64) string {
euros := float64(balance) / 100 euros := float64(balance) / 100
return fmt.Sprintf("%.2f", euros) return fmt.Sprintf("%.2f", euros)
} }
func getPageNumber(page string) string {
if page == "" {
return "1"
} else {
return page
}
}

View File

@@ -51,4 +51,5 @@ type TransactionItemsFilter struct {
AccountId string AccountId string
TreasureChestId string TreasureChestId string
Error string Error string
Page string
} }

43
static/js/transaction.js Normal file
View File

@@ -0,0 +1,43 @@
document.addEventListener("DOMContentLoaded", () => {
if (!page || !page1 || !pagePrev1 || !pageNext1 || !page2 || !pagePrev2 || !pageNext2 || !transactionFilterForm) {
return;
}
const scrollToTop = function() {
window.scrollTo(0, 0);
};
const incPage = function() {
const currPage = Number(page.value);
var nextPage = currPage
if (currPage > 1) {
nextPage -= 1;
page.value = nextPage;
transactionFilterForm.dispatchEvent(new Event('change'));
}
page1.textContent = nextPage;
page2.textContent = nextPage;
scrollToTop();
};
const decPage = function() {
const currPage = Number(page.value);
var nextPage = currPage + 1;
page.value = nextPage;
transactionFilterForm.dispatchEvent(new Event('change'));
page1.textContent = nextPage;
page2.textContent = nextPage;
scrollToTop();
};
pagePrev1.addEventListener("click", incPage);
pagePrev2.addEventListener("click", incPage);
pageNext1.addEventListener("click", decPage);
pageNext2.addEventListener("click", decPage);
console.log("initialized pagination");
})