wip: recurring transactions
Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 5m15s

This commit is contained in:
2025-05-24 23:56:42 +02:00
parent e625ef21b4
commit 49a886cc52
6 changed files with 120 additions and 87 deletions

View File

@@ -7,8 +7,6 @@ import (
t "spend-sparrow/template/transaction_recurring"
"spend-sparrow/types"
"spend-sparrow/utils"
"github.com/a-h/templ"
)
type TransactionRecurring interface {
@@ -28,7 +26,7 @@ func NewTransactionRecurring(s service.TransactionRecurring, r *Render) Transact
}
func (h TransactionRecurringImpl) Handle(r *http.ServeMux) {
r.Handle("GET /transaction-recurring/{id}", h.handleTransactionRecurringItemComp())
r.Handle("GET /transaction-recurring", h.handleTransactionRecurringItemComp())
r.Handle("POST /transaction-recurring/{id}", h.handleUpdateTransactionRecurring())
r.Handle("DELETE /transaction-recurring/{id}", h.handleDeleteTransactionRecurring())
}
@@ -41,29 +39,10 @@ func (h TransactionRecurringImpl) handleTransactionRecurringItemComp() http.Hand
return
}
id := r.PathValue("id")
id := r.URL.Query().Get("id")
accountId := r.URL.Query().Get("account-id")
treasureChestId := r.URL.Query().Get("treasure-chest-id")
if id == "new" {
comp := t.EditTransactionRecurring(nil, accountId, treasureChestId)
h.r.Render(r, w, comp)
return
}
transaction, err := h.s.Get(user, id)
if err != nil {
handleError(w, r, err)
return
}
var comp templ.Component
if r.URL.Query().Get("edit") == "true" {
comp = t.EditTransactionRecurring(transaction, accountId, treasureChestId)
} else {
comp = t.TransactionRecurringItem(transaction)
}
h.r.Render(r, w, comp)
h.renderItems(w, r, user, id, accountId, treasureChestId)
}
}
@@ -75,10 +54,6 @@ func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.Handle
return
}
var (
transaction *types.TransactionRecurring
err error
)
input := types.TransactionRecurringInput{
Id: r.PathValue("id"),
IntervalMonths: r.FormValue("interval-months"),
@@ -91,21 +66,20 @@ func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.Handle
}
if input.Id == "new" {
transaction, err = h.s.Add(user, input)
_, err := h.s.Add(user, input)
if err != nil {
handleError(w, r, err)
return
}
} else {
transaction, err = h.s.Update(user, input)
_, err := h.s.Update(user, input)
if err != nil {
handleError(w, r, err)
return
}
}
comp := t.TransactionRecurringItem(transaction)
h.r.Render(r, w, comp)
h.renderItems(w, r, user, "", input.AccountId, input.TreasureChestId)
}
}
@@ -118,11 +92,40 @@ func (h TransactionRecurringImpl) handleDeleteTransactionRecurring() http.Handle
}
id := r.PathValue("id")
accountId := r.URL.Query().Get("account-id")
treasureChestId := r.URL.Query().Get("treasure-chest-id")
err := h.s.Delete(user, id)
if err != nil {
handleError(w, r, err)
return
}
h.renderItems(w, r, user, "", accountId, treasureChestId)
}
}
func (h TransactionRecurringImpl) renderItems(w http.ResponseWriter, r *http.Request, user *types.User, id, accountId, treasureChestId string) {
var transactionsRecurring []*types.TransactionRecurring
var err error
if accountId == "" && treasureChestId == "" {
utils.TriggerToastWithStatus(w, r, "error", "Please select an account or treasure chest", http.StatusBadRequest)
}
if accountId != "" {
transactionsRecurring, err = h.s.GetAllByAccount(user, accountId)
if err != nil {
handleError(w, r, err)
return
}
} else {
transactionsRecurring, err = h.s.GetAllByTreasureChest(user, treasureChestId)
if err != nil {
handleError(w, r, err)
return
}
}
comp := t.TransactionRecurringItems(transactionsRecurring, id, accountId, treasureChestId)
h.r.Render(r, w, comp)
}

View File

@@ -84,7 +84,7 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
}
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(user, treasureChest.Id.String())
transactionsRec := tr.TransactionRecurringItems(transactionsRecurring, "", "", "")
transactionsRec := tr.TransactionRecurringItems(transactionsRecurring, "", "", treasureChest.Id.String())
var comp templ.Component
if r.URL.Query().Get("edit") == "true" {

View File

@@ -56,7 +56,7 @@ input:focus {
transition: all 150ms linear;
@apply px-3 py-2 text-lg;
}
.input:has(input:focus) {
.input:has(input:focus), .input:focus {
border-color: var(--color-gray-400);
box-shadow: 0 0 0 2px var(--color-gray-200);
}

View File

@@ -6,7 +6,7 @@ import (
)
var (
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9ÄÖÜäöüß,& -]+$`)
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9ÄÖÜäöüß,&'" -]+$`)
)
func validateString(value string, fieldName string) error {

View File

@@ -6,23 +6,34 @@ import "spend-sparrow/types"
templ TransactionRecurringItems(transactionsRecurring []*types.TransactionRecurring, editId, accountId, treasureChestId string) {
<!-- Don't use table, because embedded forms are only valid for cells -->
<div class="grid gap-4 mt-10 grid-cols-[auto_auto_auto_auto_max-content] items-center text-xl">
<div id="transaction-recurring" class="max-w-full grid gap-4 mt-10 grid-cols-[auto_auto_auto_auto_auto_max-content] items-center text-xl">
<span class="text-sm text-gray-500">Active</span>
<span class="text-sm text-gray-500">Party</span>
<span class="text-sm text-gray-500">Description</span>
<span class="text-sm text-gray-500">Interval</span>
<span class="text-sm text-right text-gray-500">Value</span>
<span></span>
if editId == "new" {
@EditTransactionRecurring(nil, accountId, treasureChestId)
}
for _, transaction := range transactionsRecurring {
if transaction.Id.String() == editId {
@EditTransactionRecurring(transaction, accountId, treasureChestId)
} else {
@TransactionRecurringItem(transaction)
@TransactionRecurringItem(transaction, accountId, treasureChestId)
}
}
</div>
}
templ TransactionRecurringItem(transactionRecurring *types.TransactionRecurring) {
templ TransactionRecurringItem(transactionRecurring *types.TransactionRecurring, accountId, treasureChestId string) {
<p class="text-gray-600">
if transactionRecurring.Active {
Yes
} else {
No
}
</p>
<p class="text-gray-600">
if transactionRecurring.Party != "" {
{ transactionRecurring.Party }
@@ -47,8 +58,8 @@ templ TransactionRecurringItem(transactionRecurring *types.TransactionRecurring)
}
<div class="flex gap-2">
<button
hx-get={ "/transaction-recurring/" + transactionRecurring.Id.String() + "?edit=true" }
hx-target={ "#transaction-recurring" + transactionRecurring.Id.String() }
hx-get={ "/transaction-recurring?id=" + transactionRecurring.Id.String() + "&account-id=" + accountId + "&treasure-chest-id=" + treasureChestId + "&edit=true" }
hx-target="closest #transaction-recurring"
hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2"
>
@@ -58,8 +69,8 @@ templ TransactionRecurringItem(transactionRecurring *types.TransactionRecurring)
</span>
</button>
<button
hx-delete={ "/transaction-recurring/" + transactionRecurring.Id.String() }
hx-target={ "#transaction-recurring" + transactionRecurring.Id.String() }
hx-delete={ "/transaction-recurring/" + transactionRecurring.Id.String() + "?account-id=" + accountId + "&treasure-chest-id=" + treasureChestId }
hx-target="closest #transaction-recurring"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete this transaction?"
class="button button-neglect px-1 flex items-center gap-2"
@@ -75,8 +86,7 @@ templ TransactionRecurringItem(transactionRecurring *types.TransactionRecurring)
templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring, accountId, treasureChestId string) {
{{
var (
id string
cancelUrl string
id string
)
party := ""
description := ""
@@ -85,7 +95,6 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
active := true
if transactionRecurring == nil {
id = "new"
cancelUrl = "/empty"
} else {
intervalMonths = fmt.Sprintf("%d", transactionRecurring.IntervalMonths)
active = transactionRecurring.Active
@@ -94,78 +103,99 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
value = displayBalance(transactionRecurring.Value)
id = transactionRecurring.Id.String()
cancelUrl = "/transaction-recurring/" + id
}
}}
<form
id="transaction-recurring-form"
hx-post={ "/transaction-recurring/" + id }
hx-target="closest #transaction"
hx-target="closest #transaction-recurring"
hx-swap="outerHTML"
class="text-xl flex justify-end gap-4 items-center"
class="hidden"
></form>
<div class="col-span-2">
<input
name="active"
id="active"
type="checkbox"
checked?={ active }
class="bg-white input"
/>
<label for="active" class="select-none text-sm text-gray-800">Active</label>
</div>
<label for="interval-months" class="text-sm text-gray-500">Interval Months</label>
<input
name="interval-months"
type="number"
value={ intervalMonths }
name="active"
form="transaction-recurring-form"
id="active"
type="checkbox"
checked?={ active }
class="bg-white input"
/>
<label for="party" class="text-sm text-gray-500">Party</label>
<input
autofocus
form="transaction-recurring-form"
name="party"
type="text"
value={ party }
size="5"
class="bg-white input"
/>
<label for="description" class="text-sm text-gray-500">Description</label>
<input
name="description"
form="transaction-recurring-form"
type="text"
value={ description }
size="10"
class="bg-white input"
/>
<input
name="interval-months"
form="transaction-recurring-form"
type="number"
value={ intervalMonths }
size="1"
class="bg-white input"
/>
<label for="value" class="text-sm text-gray-500">Value (€)</label>
<input
name="value"
form="transaction-recurring-form"
step="0.01"
type="number"
size="1"
value={ value }
class="bg-white input"
/>
if accountId != "" {
<input type="text" name="account-id" class="hidden text-sm text-gray-500" value={ accountId }/>
<input
form="transaction-recurring-form"
type="text"
name="account-id"
class="hidden text-sm text-gray-500"
value={ accountId }
/>
}
if treasureChestId != "" {
<input type="text" name="treasure-chest-id" class="hidden text-sm text-gray-500" value={ treasureChestId }/>
<input
form="transaction-recurring-form"
type="text"
name="treasure-chest-id"
class="hidden text-sm text-gray-500"
value={ treasureChestId }
/>
}
<button type="submit" class="button button-neglect px-1 flex items-center gap-2">
@svg.Save()
<span>
Save
</span>
</button>
<button
hx-get={ cancelUrl }
hx-target={ "#transaction-recurring" + transactionRecurring.Id.String() }
hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2"
>
@svg.Cancel()
<span>
Cancel
</span>
</button>
<div class="flex gap-2">
<button
form="transaction-recurring-form"
type="submit"
class="button button-neglect px-1 flex items-center gap-2"
>
@svg.Save()
<span>
Save
</span>
</button>
<button
form="transaction-recurring-form"
hx-get={ "/transaction-recurring?account-id=" + accountId + "&treasure-chest-id=" + treasureChestId }
hx-target="closest #transaction-recurring"
hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2"
>
@svg.Cancel()
<span>
Cancel
</span>
</button>
</div>
}
func displayBalance(balance int64) string {

View File

@@ -100,9 +100,9 @@ templ EditTreasureChest(treasureChest *types.TreasureChest, parents []*types.Tre
<div class="flex">
<h3 class="text-sm text-gray-500">Monthly Transactions</h3>
<button
hx-get={ "/transaction-recurring/new?treasure-chest-id=" + id }
hx-target="#transaction-recurring-items"
hx-swap="afterbegin"
hx-get={ "/transaction-recurring?id=new&treasure-chest-id=" + id }
hx-target="#transaction-recurring"
hx-swap="outerHTML"
class="button button-primary ml-auto px-2 flex items-center gap-2"
>
@svg.Plus()