wip: recurring transactions
Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 5m15s
Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 5m15s
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user