feat: move treasure_chest to seperate module
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m18s
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m18s
This commit is contained in:
196
internal/treasure_chest/handler.go
Normal file
196
internal/treasure_chest/handler.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package treasure_chest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"spend-sparrow/internal/core"
|
||||
"spend-sparrow/internal/service"
|
||||
tr "spend-sparrow/internal/template/transaction_recurring"
|
||||
"spend-sparrow/internal/treasure_chest_types"
|
||||
"spend-sparrow/internal/types"
|
||||
"spend-sparrow/internal/utils"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
Handle(router *http.ServeMux)
|
||||
}
|
||||
|
||||
type HandlerImpl struct {
|
||||
s Service
|
||||
transactionRecurring service.TransactionRecurring
|
||||
r *core.Render
|
||||
}
|
||||
|
||||
func NewHandler(s Service, transactionRecurring service.TransactionRecurring, r *core.Render) Handler {
|
||||
return HandlerImpl{
|
||||
s: s,
|
||||
transactionRecurring: transactionRecurring,
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) Handle(r *http.ServeMux) {
|
||||
r.Handle("GET /treasurechest", h.handleHandlerPage())
|
||||
r.Handle("GET /treasurechest/{id}", h.handleHandlerItemComp())
|
||||
r.Handle("POST /treasurechest/{id}", h.handleUpdateHandler())
|
||||
r.Handle("DELETE /treasurechest/{id}", h.handleDeleteHandler())
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleHandlerPage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
treasureChests, err := h.s.GetAll(r.Context(), user)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
transactionsRecurring, err := h.transactionRecurring.GetAll(r.Context(), user)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
monthlySums := h.calculateMonthlySums(treasureChests, transactionsRecurring)
|
||||
|
||||
comp := TreasureChestComp(treasureChests, monthlySums)
|
||||
h.r.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleHandlerItemComp() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
treasureChests, err := h.s.GetAll(r.Context(), user)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.PathValue("id")
|
||||
if id == "new" {
|
||||
comp := EditTreasureChest(nil, treasureChests, nil)
|
||||
h.r.Render(r, w, comp)
|
||||
return
|
||||
}
|
||||
|
||||
treasureChest, err := h.s.Get(r.Context(), user, id)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(r.Context(), user, treasureChest.Id.String())
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
transactionsRec := tr.TransactionRecurringItems(transactionsRecurring, "", "", treasureChest.Id.String())
|
||||
|
||||
var comp templ.Component
|
||||
if r.URL.Query().Get("edit") == "true" {
|
||||
comp = EditTreasureChest(treasureChest, treasureChests, transactionsRec)
|
||||
} else {
|
||||
monthlySums := h.calculateMonthlySums(treasureChests, transactionsRecurring)
|
||||
comp = TreasureChestItem(treasureChest, monthlySums)
|
||||
}
|
||||
h.r.Render(r, w, comp)
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleUpdateHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
treasureChest *treasure_chest_types.TreasureChest
|
||||
err error
|
||||
)
|
||||
id := r.PathValue("id")
|
||||
parentId := r.FormValue("parent-id")
|
||||
name := r.FormValue("name")
|
||||
if id == "new" {
|
||||
treasureChest, err = h.s.Add(r.Context(), user, parentId, name)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
treasureChest, err = h.s.Update(r.Context(), user, id, parentId, name)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(r.Context(), user, treasureChest.Id.String())
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
treasureChests := make([]*treasure_chest_types.TreasureChest, 1)
|
||||
treasureChests[0] = treasureChest
|
||||
monthlySums := h.calculateMonthlySums(treasureChests, transactionsRecurring)
|
||||
comp := TreasureChestItem(treasureChest, monthlySums)
|
||||
h.r.Render(r, w, comp)
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleDeleteHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
id := r.PathValue("id")
|
||||
|
||||
err := h.s.Delete(r.Context(), user, id)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) calculateMonthlySums(
|
||||
treasureChests []*treasure_chest_types.TreasureChest,
|
||||
transactionsRecurring []*types.TransactionRecurring,
|
||||
) map[uuid.UUID]int64 {
|
||||
monthlySums := make(map[uuid.UUID]int64)
|
||||
for _, tc := range treasureChests {
|
||||
monthlySums[tc.Id] = 0
|
||||
}
|
||||
for _, t := range transactionsRecurring {
|
||||
if t.TreasureChestId != nil && t.Value > 0 && t.IntervalMonths > 0 {
|
||||
monthlySums[*t.TreasureChestId] += t.Value / t.IntervalMonths
|
||||
}
|
||||
}
|
||||
return monthlySums
|
||||
}
|
||||
322
internal/treasure_chest/service.go
Normal file
322
internal/treasure_chest/service.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package treasure_chest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"spend-sparrow/internal/auth_types"
|
||||
"spend-sparrow/internal/core"
|
||||
"spend-sparrow/internal/treasure_chest_types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Add(ctx context.Context, user *auth_types.User, parentId, name string) (*treasure_chest_types.TreasureChest, error)
|
||||
Update(ctx context.Context, user *auth_types.User, id, parentId, name string) (*treasure_chest_types.TreasureChest, error)
|
||||
Get(ctx context.Context, user *auth_types.User, id string) (*treasure_chest_types.TreasureChest, error)
|
||||
GetAll(ctx context.Context, user *auth_types.User) ([]*treasure_chest_types.TreasureChest, error)
|
||||
Delete(ctx context.Context, user *auth_types.User, id string) error
|
||||
}
|
||||
|
||||
type ServiceImpl struct {
|
||||
db *sqlx.DB
|
||||
clock core.Clock
|
||||
random core.Random
|
||||
}
|
||||
|
||||
func NewService(db *sqlx.DB, random core.Random, clock core.Clock) Service {
|
||||
return ServiceImpl{
|
||||
db: db,
|
||||
clock: clock,
|
||||
random: random,
|
||||
}
|
||||
}
|
||||
|
||||
func (s ServiceImpl) Add(ctx context.Context, user *auth_types.User, parentId, name string) (*treasure_chest_types.TreasureChest, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
newId, err := s.random.UUID(ctx)
|
||||
if err != nil {
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
err = core.ValidateString(name, "name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var parentUuid *uuid.UUID
|
||||
if parentId != "" {
|
||||
parent, err := s.Get(ctx, user, parentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent.ParentId != nil {
|
||||
return nil, fmt.Errorf("only a depth of 1 allowed: %w", core.ErrBadRequest)
|
||||
}
|
||||
parentUuid = &parent.Id
|
||||
}
|
||||
|
||||
treasureChest := &treasure_chest_types.TreasureChest{
|
||||
Id: newId,
|
||||
ParentId: parentUuid,
|
||||
UserId: user.Id,
|
||||
|
||||
Name: name,
|
||||
|
||||
CurrentBalance: 0,
|
||||
|
||||
CreatedAt: s.clock.Now(),
|
||||
CreatedBy: user.Id,
|
||||
UpdatedAt: nil,
|
||||
UpdatedBy: nil,
|
||||
}
|
||||
|
||||
r, err := s.db.NamedExecContext(ctx, `
|
||||
INSERT INTO treasure_chest (id, parent_id, user_id, name, current_balance, created_at, created_by)
|
||||
VALUES (:id, :parent_id, :user_id, :name, :current_balance, :created_at, :created_by)`, treasureChest)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Insert", r, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return treasureChest, nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) Update(ctx context.Context, user *auth_types.User, idStr, parentId, name string) (*treasure_chest_types.TreasureChest, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
err := core.ValidateString(name, "name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "treasureChest update", "err", err)
|
||||
return nil, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
treasureChest := &treasure_chest_types.TreasureChest{}
|
||||
err = tx.GetContext(ctx, treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, id)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrNotFound) {
|
||||
return nil, fmt.Errorf("treasureChest %v not found: %w", idStr, err)
|
||||
}
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
var parentUuid *uuid.UUID
|
||||
if parentId != "" {
|
||||
parent, err := s.Get(ctx, user, parentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var childCount int
|
||||
err = tx.GetContext(ctx, &childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent.ParentId != nil || childCount > 0 {
|
||||
return nil, fmt.Errorf("only one level allowed: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
parentUuid = &parent.Id
|
||||
}
|
||||
|
||||
timestamp := s.clock.Now()
|
||||
treasureChest.Name = name
|
||||
treasureChest.ParentId = parentUuid
|
||||
treasureChest.UpdatedAt = ×tamp
|
||||
treasureChest.UpdatedBy = &user.Id
|
||||
|
||||
r, err := tx.NamedExecContext(ctx, `
|
||||
UPDATE treasure_chest
|
||||
SET
|
||||
parent_id = :parent_id,
|
||||
name = :name,
|
||||
current_balance = :current_balance,
|
||||
updated_at = :updated_at,
|
||||
updated_by = :updated_by
|
||||
WHERE id = :id
|
||||
AND user_id = :user_id`, treasureChest)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Update", r, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return treasureChest, nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) Get(ctx context.Context, user *auth_types.User, id string) (*treasure_chest_types.TreasureChest, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "treasureChest get", "err", err)
|
||||
return nil, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
var treasureChest treasure_chest_types.TreasureChest
|
||||
err = s.db.GetContext(ctx, &treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Get", nil, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrNotFound) {
|
||||
return nil, fmt.Errorf("treasureChest %v not found: %w", id, err)
|
||||
}
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
return &treasureChest, nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) GetAll(ctx context.Context, user *auth_types.User) ([]*treasure_chest_types.TreasureChest, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
treasureChests := make([]*treasure_chest_types.TreasureChest, 0)
|
||||
err := s.db.SelectContext(ctx, &treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest GetAll", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return SortTreasureChests(treasureChests), nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) Delete(ctx context.Context, user *auth_types.User, idStr string) error {
|
||||
if user == nil {
|
||||
return core.ErrUnauthorized
|
||||
}
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "treasureChest delete", "err", err)
|
||||
return fmt.Errorf("could not parse Id: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
childCount := 0
|
||||
err = tx.GetContext(ctx, &childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if childCount > 0 {
|
||||
return fmt.Errorf("treasure chest has children: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
transactionsCount := 0
|
||||
err = tx.GetContext(ctx, &transactionsCount,
|
||||
`SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND treasure_chest_id = ?`,
|
||||
user.Id, id)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if transactionsCount > 0 {
|
||||
return fmt.Errorf("treasure chest has transactions: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
recurringCount := 0
|
||||
err = tx.GetContext(ctx, &recurringCount, `
|
||||
SELECT COUNT(*) FROM transaction_recurring WHERE user_id = ? AND treasure_chest_id = ?`,
|
||||
user.Id, id)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if recurringCount > 0 {
|
||||
return fmt.Errorf("cannot delete treasure chest with existing recurring transactions: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
r, err := tx.ExecContext(ctx, `DELETE FROM treasure_chest WHERE id = ? AND user_id = ?`, id, user.Id)
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Delete", r, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = core.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SortTreasureChests(nodes []*treasure_chest_types.TreasureChest) []*treasure_chest_types.TreasureChest {
|
||||
var (
|
||||
roots []*treasure_chest_types.TreasureChest
|
||||
)
|
||||
children := make(map[uuid.UUID][]*treasure_chest_types.TreasureChest)
|
||||
result := make([]*treasure_chest_types.TreasureChest, 0)
|
||||
|
||||
for _, node := range nodes {
|
||||
if node.ParentId == nil {
|
||||
roots = append(roots, node)
|
||||
} else {
|
||||
children[*node.ParentId] = append(children[*node.ParentId], node)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(roots, func(a, b *treasure_chest_types.TreasureChest) int {
|
||||
return compareStrings(a.Name, b.Name)
|
||||
})
|
||||
|
||||
for _, root := range roots {
|
||||
result = append(result, root)
|
||||
|
||||
childList := children[root.Id]
|
||||
|
||||
slices.SortFunc(childList, func(a, b *treasure_chest_types.TreasureChest) int {
|
||||
return compareStrings(a.Name, b.Name)
|
||||
})
|
||||
result = append(result, childList...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func compareStrings(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
if a < b {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
193
internal/treasure_chest/treasure_chest.templ
Normal file
193
internal/treasure_chest/treasure_chest.templ
Normal file
@@ -0,0 +1,193 @@
|
||||
package treasure_chest
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"spend-sparrow/internal/template/svg"
|
||||
"spend-sparrow/internal/treasure_chest_types"
|
||||
"spend-sparrow/internal/types"
|
||||
)
|
||||
|
||||
templ TreasureChestComp(treasureChests []*treasure_chest_types.TreasureChest, monthlySums map[uuid.UUID]int64) {
|
||||
<div class="max-w-6xl mt-10 mx-auto">
|
||||
<button
|
||||
hx-get="/treasurechest/new"
|
||||
hx-target="#treasurechest-items"
|
||||
hx-swap="afterbegin"
|
||||
class="ml-auto text-center button button-primary px-2 flex items-center gap-2"
|
||||
>
|
||||
@svg.Plus()
|
||||
New Treasure Chest
|
||||
</button>
|
||||
<div id="treasurechest-items" class="my-6 flex flex-col">
|
||||
for _, treasureChest := range treasureChests {
|
||||
@TreasureChestItem(treasureChest, monthlySums)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ EditTreasureChest(treasureChest *treasure_chest_types.TreasureChest, parents []*treasure_chest_types.TreasureChest, transactionsRecurring templ.Component) {
|
||||
{{
|
||||
var (
|
||||
id string
|
||||
name string
|
||||
parentId uuid.UUID
|
||||
cancelUrl string
|
||||
)
|
||||
|
||||
indentation := " mt-10"
|
||||
if treasureChest == nil {
|
||||
id = "new"
|
||||
name = ""
|
||||
parentId = uuid.Nil
|
||||
cancelUrl = "/empty"
|
||||
} else {
|
||||
id = treasureChest.Id.String()
|
||||
name = treasureChest.Name
|
||||
if treasureChest.ParentId != nil {
|
||||
parentId = *treasureChest.ParentId
|
||||
indentation = " mt-2 ml-14"
|
||||
}
|
||||
cancelUrl = "/treasurechest/" + id
|
||||
}
|
||||
}}
|
||||
<div id={ "treasurechest-" + id } class={ "border-1 border-gray-300 p-4 bg-gray-50 rounded-lg" + indentation }>
|
||||
<form
|
||||
hx-post={ "/treasurechest/" + id }
|
||||
hx-target={ "#treasurechest-" + id }
|
||||
hx-swap="outerHTML"
|
||||
class="text-xl flex justify-end gap-4 items-center"
|
||||
>
|
||||
<div class="grow grid grid-cols-[auto_1fr] items-center gap-4">
|
||||
<label for="name" class="text-sm text-gray-500">Name</label>
|
||||
<input
|
||||
autofocus
|
||||
name="name"
|
||||
type="text"
|
||||
value={ name }
|
||||
placeholder="Treasure Chest Name"
|
||||
class="bg-white input max-w-96"
|
||||
/>
|
||||
<label for="parent-id" class="text-sm text-gray-500">Parent</label>
|
||||
<select name="parent-id" class="mr-auto bg-white input">
|
||||
<option value="" class="text-gray-500">-</option>
|
||||
for _, parent := range filterNoChildNoSelf(parents, id) {
|
||||
<option
|
||||
selected?={ parentId == parent.Id }
|
||||
value={ parent.Id.String() }
|
||||
>{ parent.Name }</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<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={ "#treasurechest-" + id }
|
||||
hx-swap="outerHTML"
|
||||
class="button button-neglect px-1 flex items-center gap-2"
|
||||
>
|
||||
<span class="h-4 w-4">
|
||||
@svg.Cancel()
|
||||
</span>
|
||||
<span>
|
||||
Cancel
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
if id != "new" {
|
||||
<div class="m-10 border-b-gray-400 border-b-1"></div>
|
||||
<div class="flex">
|
||||
<h3 class="text-sm text-gray-500">Monthly Transactions</h3>
|
||||
<button
|
||||
hx-get={ "/transaction-recurring?id=new&treasure-chest-id=" + id }
|
||||
hx-target="next #transaction-recurring"
|
||||
hx-swap="outerHTML"
|
||||
class="button button-primary ml-auto px-2 flex items-center gap-2"
|
||||
>
|
||||
@svg.Plus()
|
||||
<p>New Monthly Transaction</p>
|
||||
</button>
|
||||
</div>
|
||||
@transactionsRecurring
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ TreasureChestItem(treasureChest *treasure_chest_types.TreasureChest, monthlySums map[uuid.UUID]int64) {
|
||||
{{
|
||||
var indentation string
|
||||
viewTransactions := ""
|
||||
if treasureChest.ParentId != nil {
|
||||
indentation = " mt-2 ml-14"
|
||||
} else {
|
||||
indentation = " mt-10"
|
||||
viewTransactions = "hidden"
|
||||
}
|
||||
}}
|
||||
<div id={ "treasurechest-" + treasureChest.Id.String() } class={ "border-1 border-gray-300 p-4 bg-gray-50 rounded-lg" + indentation }>
|
||||
<div class="text-xl flex justify-end items-center gap-4">
|
||||
<p class="mr-auto">{ treasureChest.Name }</p>
|
||||
<p class="mr-20 text-gray-600">
|
||||
if treasureChest.ParentId != nil {
|
||||
+ { types.FormatEuros(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm"> per month</span>
|
||||
}
|
||||
</p>
|
||||
if treasureChest.ParentId != nil {
|
||||
if treasureChest.CurrentBalance < 0 {
|
||||
<p class="mr-20 min-w-20 text-right text-red-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
|
||||
} else {
|
||||
<p class="mr-20 min-w-20 text-right text-green-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
|
||||
}
|
||||
}
|
||||
<a
|
||||
href={ templ.URL("/transaction?treasure-chest-id=" + treasureChest.Id.String()) }
|
||||
class={ "button button-neglect px-1 flex items-center gap-2 " + viewTransactions }
|
||||
title="View transactions"
|
||||
>
|
||||
@svg.Eye()
|
||||
<span>
|
||||
View
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
hx-get={ "/treasurechest/" + treasureChest.Id.String() + "?edit=true" }
|
||||
hx-target={ "#treasurechest-" + treasureChest.Id.String() }
|
||||
hx-swap="outerHTML"
|
||||
class="button button-neglect px-1 flex items-center gap-2"
|
||||
>
|
||||
@svg.Edit()
|
||||
<span>
|
||||
Edit
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-delete={ "/treasurechest/" + treasureChest.Id.String() }
|
||||
hx-target={ "#treasurechest-" + treasureChest.Id.String() }
|
||||
hx-swap="outerHTML"
|
||||
class="button button-neglect px-1 flex items-center gap-2"
|
||||
>
|
||||
@svg.Delete()
|
||||
<span>
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
func filterNoChildNoSelf(nodes []*treasure_chest_types.TreasureChest, selfId string) []*treasure_chest_types.TreasureChest {
|
||||
var result []*treasure_chest_types.TreasureChest
|
||||
|
||||
for _, node := range nodes {
|
||||
if node.ParentId == nil && node.Id.String() != selfId {
|
||||
result = append(result, node)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user