From 1ffe3b39616c604a0b87239b1ab397c49df13a3e Mon Sep 17 00:00:00 2001 From: Tim Wundenberg Date: Wed, 14 May 2025 23:34:52 +0200 Subject: [PATCH] feat(transaction): #66 implement transactions --- db/error.go | 28 ++++ db/transaction.go | 136 ----------------- handler/transaction.go | 65 ++++++-- input.css | 10 ++ main.go | 3 + migration/002_account.up.sql | 4 +- migration/003_treasure_chest.up.sql | 2 +- migration/004_transaction.up.sql | 17 +++ package.json | 2 +- service/transaction.go | 159 +++++++++++--------- service/treasure_chest.go | 36 ++++- template/transaction/transaction.templ | 121 +++++++++++---- template/treasurechest/treasure_chest.templ | 36 +---- types/transaction.go | 25 +-- 14 files changed, 342 insertions(+), 302 deletions(-) delete mode 100644 db/transaction.go create mode 100644 migration/004_transaction.up.sql diff --git a/db/error.go b/db/error.go index d427c11..f83ba68 100644 --- a/db/error.go +++ b/db/error.go @@ -1,10 +1,38 @@ package db import ( + "database/sql" "errors" + "spend-sparrow/log" + "spend-sparrow/types" ) var ( ErrNotFound = errors.New("the value does not exist") ErrAlreadyExists = errors.New("row already exists") ) + +func TransformAndLogDbError(module string, r sql.Result, err error) error { + if err != nil { + if err == sql.ErrNoRows { + return ErrNotFound + } + log.Error("%v: %v", module, err) + return types.ErrInternal + } + + if r != nil { + rows, err := r.RowsAffected() + if err != nil { + log.Error("%v: %v", module, err) + return types.ErrInternal + } + + if rows == 0 { + log.Info("%v: not found", module) + return ErrNotFound + } + } + + return nil +} diff --git a/db/transaction.go b/db/transaction.go deleted file mode 100644 index e3c4472..0000000 --- a/db/transaction.go +++ /dev/null @@ -1,136 +0,0 @@ -package db - -import ( - "database/sql" - "spend-sparrow/log" - "spend-sparrow/types" - - "github.com/google/uuid" - "github.com/jmoiron/sqlx" -) - -// While it may be duplicated to check for userId in the database access, it serves as a security layer -type Transaction interface { - Insert(userId uuid.UUID, transaction *types.Transaction) error - Update(userId uuid.UUID, transaction *types.Transaction) error - GetAll(userId uuid.UUID) ([]*types.Transaction, error) - Get(userId uuid.UUID, id uuid.UUID) (*types.Transaction, error) - Delete(userId uuid.UUID, id uuid.UUID) error -} - -type TransactionSqlite struct { - db *sqlx.DB -} - -func NewTransactionSqlite(db *sqlx.DB) *TransactionSqlite { - return &TransactionSqlite{db: db} -} - -func (db TransactionSqlite) Insert(userId uuid.UUID, transaction *types.Transaction) error { - - _, err := db.db.Exec(` - INSERT INTO transaction (id, user_id, account_id, treasure_chest_id, internal, value, timestamp, note, created_at, created_by) - VALUES (?,?,?,?,?,?,?,?,?,?)`, transaction.Id, userId, transaction.AccountId, transaction.TreasureChestId, transaction.Internal, transaction.Value, transaction.Timestamp, transaction.Note, transaction.CreatedAt, transaction.CreatedBy) - if err != nil { - log.Error("transaction Insert: %v", err) - return types.ErrInternal - } - - return nil -} - -func (db TransactionSqlite) Update(userId uuid.UUID, transaction *types.Transaction) error { - - r, err := db.db.Exec(` - UPDATE transaction - SET - account_id = ?, - treasure_chest_id = ?, - internal = ?, - value = ?, - timestamp = ?, - note = ?, - updated_at = ?, - updated_by = ? - WHERE id = ? - AND user_id = ?`, transaction.AccountId, transaction.TreasureChestId, transaction.Internal, transaction.Value, transaction.Timestamp, transaction.UpdatedBy, transaction.Id, userId) - if err != nil { - log.Error("transaction Update: %v", err) - return types.ErrInternal - } - rows, err := r.RowsAffected() - if err != nil { - log.Error("transaction Update: %v", err) - return types.ErrInternal - } - - if rows == 0 { - log.Info("transaction Update: not found") - return ErrNotFound - } - - return nil -} - -func (db TransactionSqlite) GetAll(userId uuid.UUID) ([]*types.Transaction, error) { - - transactions := make([]*types.Transaction, 0) - err := db.db.Select(&transactions, ` - SELECT - id, user_id, - account_id, treasure_chest_id, internal, value, timestamp, note, - created_at, created_by, updated_at, updated_by - FROM transaction - WHERE user_id = ? - ORDER BY name`, userId) - if err != nil { - log.Error("transaction GetAll: %v", err) - return nil, types.ErrInternal - } - - return transactions, nil -} - -func (db TransactionSqlite) Get(userId uuid.UUID, id uuid.UUID) (*types.Transaction, error) { - - transaction := &types.Transaction{} - err := db.db.Get(transaction, ` - SELECT - id, user_id, - account_id, treasure_chest_id, internal, value, timestamp, note, - created_at, created_by, updated_at, updated_by - FROM transaction - WHERE user_id = ? - AND id = ?`, userId, id) - if err != nil { - if err == sql.ErrNoRows { - return nil, ErrNotFound - } - log.Error("transaction Get: %v", err) - return nil, types.ErrInternal - } - - return transaction, nil -} - -func (db TransactionSqlite) Delete(userId uuid.UUID, id uuid.UUID) error { - - res, err := db.db.Exec("DELETE FROM transaction WHERE id = ? and user_id = ?", id, userId) - if err != nil { - log.Error("transaction Delete: %v", err) - return types.ErrInternal - } - - rows, err := res.RowsAffected() - if err != nil { - log.Error("transaction Delete: %v", err) - return types.ErrInternal - } - - if rows == 0 { - log.Info("transaction Delete: not found") - return ErrNotFound - } - - return nil -} diff --git a/handler/transaction.go b/handler/transaction.go index c1c8ec6..f575352 100644 --- a/handler/transaction.go +++ b/handler/transaction.go @@ -16,16 +16,20 @@ type Transaction interface { } type TransactionImpl struct { - s service.Transaction - a service.Auth - r *Render + s service.Transaction + account service.Account + treasureChest service.TreasureChest + a service.Auth + r *Render } -func NewTransaction(s service.Transaction, a service.Auth, r *Render) Transaction { +func NewTransaction(s service.Transaction, account service.Account, treasureChest service.TreasureChest, a service.Auth, r *Render) Transaction { return TransactionImpl{ - s: s, - a: a, - r: r, + s: s, + account: account, + treasureChest: treasureChest, + a: a, + r: r, } } @@ -50,7 +54,19 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc { return } - comp := t.Transaction(transactions) + accounts, err := h.account.GetAll(user) + if err != nil { + handleError(w, r, err) + return + } + + treasureChests, err := h.treasureChest.GetAll(user) + if err != nil { + handleError(w, r, err) + return + } + + comp := t.Transaction(transactions, accounts, treasureChests) h.r.RenderLayout(r, w, comp, user) } } @@ -63,9 +79,21 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc { return } + accounts, err := h.account.GetAll(user) + if err != nil { + handleError(w, r, err) + return + } + + treasureChests, err := h.treasureChest.GetAll(user) + if err != nil { + handleError(w, r, err) + return + } + id := r.PathValue("id") if id == "new" { - comp := t.EditTransaction(nil) + comp := t.EditTransaction(nil, accounts, treasureChests) h.r.Render(r, w, comp) return } @@ -78,7 +106,7 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc { var comp templ.Component if r.URL.Query().Get("edit") == "true" { - comp = t.EditTransaction(transaction) + comp = t.EditTransaction(transaction, accounts, treasureChests) } else { comp = t.TransactionItem(transaction) } @@ -98,16 +126,23 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc { transaction *types.Transaction err error ) - id := r.PathValue("id") - name := r.FormValue("name") - if id == "new" { - transaction, err = h.s.Add(user, name) + input := types.TransactionInput{ + Id: r.PathValue("id"), + AccountId: r.FormValue("account_id"), + TreasureChestId: r.FormValue("treasure_chest_id"), + Value: r.FormValue("value"), + Timestamp: r.FormValue("timestamp"), + Note: r.FormValue("note"), + } + + if input.Id == "new" { + transaction, err = h.s.Add(user, input) if err != nil { handleError(w, r, err) return } } else { - transaction, err = h.s.Update(user, id, name) + transaction, err = h.s.Update(user, input) if err != nil { handleError(w, r, err) return diff --git a/input.css b/input.css index 2812f81..5799520 100644 --- a/input.css +++ b/input.css @@ -24,6 +24,16 @@ input:focus { --font-shippori: "Shippori Mincho", sans-serif; } +select, +::picker(select) { + appearance: none; +} +option { + appearance: none; +} +::picker(select) { + border: none; +} /* Button */ .button { diff --git a/main.go b/main.go index e938e00..69f4c2a 100644 --- a/main.go +++ b/main.go @@ -117,17 +117,20 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler { authService := service.NewAuth(authDb, randomService, clockService, mailService, serverSettings) accountService := service.NewAccount(accountDb, randomService, clockService, serverSettings) treasureChestService := service.NewTreasureChest(treasureChestDb, randomService, clockService, serverSettings) + transactionService := service.NewTransaction(d, randomService, clockService, serverSettings) render := handler.NewRender() indexHandler := handler.NewIndex(authService, render) authHandler := handler.NewAuth(authService, render) accountHandler := handler.NewAccount(accountService, authService, render) treasureChestHandler := handler.NewTreasureChest(treasureChestService, authService, render) + transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, authService, render) indexHandler.Handle(router) accountHandler.Handle(router) treasureChestHandler.Handle(router) authHandler.Handle(router) + transactionHandler.Handle(router) // Serve static files (CSS, JS and images) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) diff --git a/migration/002_account.up.sql b/migration/002_account.up.sql index 11a4ec5..e18122f 100644 --- a/migration/002_account.up.sql +++ b/migration/002_account.up.sql @@ -5,9 +5,9 @@ CREATE TABLE account ( name TEXT NOT NULL, - current_balance int64 NOT NULL, + current_balance INTEGER NOT NULL, last_transaction DATETIME, - oink_balance int64 NOT NULL, + oink_balance INTEGER NOT NULL, created_at DATETIME NOT NULL, created_by TEXT NOT NULL, diff --git a/migration/003_treasure_chest.up.sql b/migration/003_treasure_chest.up.sql index b836d9a..dfc0a84 100644 --- a/migration/003_treasure_chest.up.sql +++ b/migration/003_treasure_chest.up.sql @@ -6,7 +6,7 @@ CREATE TABLE treasure_chest ( name TEXT NOT NULL, - current_balance int64 NOT NULL, + current_balance INTEGER NOT NULL, created_at DATETIME NOT NULL, created_by TEXT NOT NULL, diff --git a/migration/004_transaction.up.sql b/migration/004_transaction.up.sql new file mode 100644 index 0000000..8cdb993 --- /dev/null +++ b/migration/004_transaction.up.sql @@ -0,0 +1,17 @@ + +CREATE TABLE "transaction" ( + id TEXT NOT NULL UNIQUE PRIMARY KEY, + user_id TEXT NOT NULL, + + timestamp DATETIME NOT NULL, + note TEXT NOT NULL, + + account_id TEXT, + treasure_chest_id TEXT, + value INTEGER NOT NULL, + + created_at DATETIME NOT NULL, + created_by TEXT NOT NULL, + updated_at DATETIME, + updated_by TEXT +) WITHOUT ROWID; diff --git a/package.json b/package.json index 93e1a2c..8bb497e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "build": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --minify", - "watch": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch" + "watch": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch --minify" }, "keywords": [], "author": "", diff --git a/service/transaction.go b/service/transaction.go index acab508..e209f03 100644 --- a/service/transaction.go +++ b/service/transaction.go @@ -10,6 +10,7 @@ import ( "spend-sparrow/types" "github.com/google/uuid" + "github.com/jmoiron/sqlx" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -25,87 +26,93 @@ var ( ) type Transaction interface { - Add(user *types.User, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) - Update(user *types.User, id, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) + Add(user *types.User, transaction types.TransactionInput) (*types.Transaction, error) + Update(user *types.User, transaction types.TransactionInput) (*types.Transaction, error) Get(user *types.User, id string) (*types.Transaction, error) GetAll(user *types.User) ([]*types.Transaction, error) Delete(user *types.User, id string) error - CanDeleteTreasureChest(user *types.User, treasureChestId uuid.UUID) (bool, error) - CanDeleteAccount(user *types.User, accountId uuid.UUID) (bool, error) } type TransactionImpl struct { - db db.Transaction - account Account - treasureChest TreasureChest - clock Clock - random Random - settings *types.Settings + db *sqlx.DB + clock Clock + random Random + settings *types.Settings } -func NewTransaction(db db.Transaction, account Account, treasureChest TreasureChest, random Random, clock Clock, settings *types.Settings) Transaction { +func NewTransaction(db *sqlx.DB, random Random, clock Clock, settings *types.Settings) Transaction { return TransactionImpl{ - db: db, - account: account, - treasureChest: treasureChest, - clock: clock, - random: random, - settings: settings, + db: db, + clock: clock, + random: random, + settings: settings, } } -func (s TransactionImpl) Add(user *types.User, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) { +func (s TransactionImpl) Add(user *types.User, transactionInput types.TransactionInput) (*types.Transaction, error) { transactionMetric.WithLabelValues("add").Inc() if user == nil { return nil, ErrUnauthorized } - transaction, err := s.validateTransaction(nil, user.Id, accountId, treasureChestId, internal, value, timestamp, note) + transaction, err := s.validateAndEnrichTransaction(nil, user.Id, transactionInput) if err != nil { return nil, err } - err = s.db.Insert(user.Id, transaction) + r, err := s.db.NamedExec(` + INSERT INTO "transaction" (id, user_id, account_id, treasure_chest_id, value, timestamp, note, created_at, created_by) + VALUES (:id, :user_id, :account_id, :treasure_chest_id, :value, :timestamp, :note, :created_at, :created_by)`, transaction) + err = db.TransformAndLogDbError("transaction Insert", r, err) if err != nil { - return nil, types.ErrInternal + return nil, err } - savedTransaction, err := s.db.Get(user.Id, transaction.Id) - if err != nil { - log.Error("transaction %v not found after insert: %v", transaction.Id, err) - return nil, types.ErrInternal - } - return savedTransaction, nil + return transaction, nil } -func (s TransactionImpl) Update(user *types.User, id, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) { +func (s TransactionImpl) Update(user *types.User, input types.TransactionInput) (*types.Transaction, error) { transactionMetric.WithLabelValues("update").Inc() if user == nil { return nil, ErrUnauthorized } - uuid, err := uuid.Parse(id) + uuid, err := uuid.Parse(input.Id) if err != nil { log.Error("transaction update: %v", err) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) } - transaction, err := s.db.Get(user.Id, uuid) + var transaction *types.Transaction + err = s.db.Select(transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid) + err = db.TransformAndLogDbError("transaction Get", nil, err) if err != nil { if err == db.ErrNotFound { - return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest) + return nil, fmt.Errorf("transaction %v not found: %w", input.Id, ErrBadRequest) } return nil, types.ErrInternal } - transaction, err = s.validateTransaction(transaction, user.Id, accountId, treasureChestId, internal, value, timestamp, note) + transaction, err = s.validateAndEnrichTransaction(transaction, user.Id, input) if err != nil { return nil, err } - err = s.db.Update(user.Id, transaction) + r, err := s.db.NamedExec(` + UPDATE transaction + SET + account_id = :account_id, + treasure_chest_id = :treasure_chest_id, + value = :value, + timestamp = :timestamp, + note = :note, + updated_at = :updated_at, + updated_by = :updated_by + WHERE id = :id + AND user_id = :user_id`, transaction) + err = db.TransformAndLogDbError("transaction Update", r, err) if err != nil { - return nil, types.ErrInternal + return nil, err } return transaction, nil @@ -123,7 +130,9 @@ func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, e return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) } - transaction, err := s.db.Get(user.Id, uuid) + var transaction *types.Transaction + err = s.db.Select(transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid) + err = db.TransformAndLogDbError("transaction Get", nil, err) if err != nil { if err == db.ErrNotFound { return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest) @@ -139,10 +148,11 @@ func (s TransactionImpl) GetAll(user *types.User) ([]*types.Transaction, error) if user == nil { return nil, ErrUnauthorized } - - transactions, err := s.db.GetAll(user.Id) + transactions := make([]*types.Transaction, 0) + err := s.db.Select(&transactions, `SELECT * FROM "transaction" WHERE user_id = ? ORDER BY timestamp`, user.Id) + err = db.TransformAndLogDbError("transaction GetAll", nil, err) if err != nil { - return nil, types.ErrInternal + return nil, err } return transactions, nil @@ -159,39 +169,28 @@ func (s TransactionImpl) Delete(user *types.User, id string) error { return fmt.Errorf("could not parse Id: %w", ErrBadRequest) } - transaction, err := s.db.Get(user.Id, uuid) + r, err := s.db.Exec("DELETE FROM \"transaction\" WHERE id = ? AND user_id = ?", uuid, user.Id) + err = db.TransformAndLogDbError("transaction Delete", r, err) if err != nil { - if err == db.ErrNotFound { - return fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest) - } - return types.ErrInternal - } - - if transaction.UserId != user.Id { - return types.ErrUnauthorized - } - - err = s.db.Delete(user.Id, transaction.Id) - if err != nil { - return types.ErrInternal + return err } return nil } -func (s TransactionImpl) validateTransaction(transaction *types.Transaction, userId uuid.UUID, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) { +func (s TransactionImpl) validateAndEnrichTransaction(transaction *types.Transaction, userId uuid.UUID, input types.TransactionInput) (*types.Transaction, error) { var ( id uuid.UUID accountUuid uuid.UUID treasureChestUuid uuid.UUID - internalBool bool createdAt time.Time createdBy uuid.UUID updatedAt *time.Time updatedBy uuid.UUID - err error + err error + rowCount int ) if transaction == nil { @@ -210,61 +209,71 @@ func (s TransactionImpl) validateTransaction(transaction *types.Transaction, use updatedBy = userId } - if accountId != "" { - accountUuid, err = uuid.Parse(accountId) + if input.AccountId != "" { + accountUuid, err = uuid.Parse(input.AccountId) if err != nil { log.Error("transaction validate: %v", err) return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest) } + err = s.db.Select(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId) + err = db.TransformAndLogDbError("transaction validate", nil, err) + if err != nil { + return nil, err + } + if rowCount == 0 { + log.Error("transaction validate: %v", err) + return nil, fmt.Errorf("account not found: %w", ErrBadRequest) + } + } - if treasureChestId != "" { - treasureChestUuid, err = uuid.Parse(treasureChestId) + if input.TreasureChestId != "" { + treasureChestUuid, err = uuid.Parse(input.TreasureChestId) if err != nil { log.Error("transaction validate: %v", err) return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest) } + err = s.db.Select(&rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, accountUuid, userId) + err = db.TransformAndLogDbError("transaction validate", nil, err) + if err != nil { + return nil, err + } + if rowCount == 0 { + log.Error("transaction validate: %v", err) + return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest) + } } - internalBool, err = strconv.ParseBool(internal) - if err != nil { - log.Error("transaction validate: %v", err) - return nil, fmt.Errorf("could not parse internal: %w", ErrBadRequest) - } - - valueInt, err := strconv.ParseInt(value, 10, 64) + valueInt, err := strconv.ParseInt(input.Value, 10, 64) if err != nil { log.Error("transaction validate: %v", err) return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest) } - timestampTime, err := time.Parse(time.RFC3339, timestamp) + timestampTime, err := time.Parse(time.RFC3339, input.Timestamp) if err != nil { log.Error("transaction validate: %v", err) return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest) } - err = validateString(note) + err = validateString(input.Note) if err != nil { return nil, err } - result := types.Transaction{ + return &types.Transaction{ Id: id, UserId: userId, - AccountId: accountUuid, + AccountId: &accountUuid, TreasureChestId: &treasureChestUuid, - Internal: internalBool, Value: valueInt, Timestamp: timestampTime, - Note: note, + Note: input.Note, CreatedAt: createdAt, CreatedBy: createdBy, UpdatedAt: updatedAt, UpdatedBy: &updatedBy, - } - - return &result, nil + }, nil } diff --git a/service/treasure_chest.go b/service/treasure_chest.go index 527487f..522b5b3 100644 --- a/service/treasure_chest.go +++ b/service/treasure_chest.go @@ -195,7 +195,7 @@ func (s TreasureChestImpl) GetAll(user *types.User) ([]*types.TreasureChest, err return nil, types.ErrInternal } - return treasureChests, nil + return sortTree(treasureChests), nil } func (s TreasureChestImpl) Delete(user *types.User, idStr string) error { @@ -237,3 +237,37 @@ func (s TreasureChestImpl) Delete(user *types.User, idStr string) error { return nil } + +func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest { + + var ( + roots []*types.TreasureChest + result []*types.TreasureChest + ) + children := make(map[uuid.UUID][]*types.TreasureChest) + + for _, node := range nodes { + if node.ParentId == uuid.Nil { + roots = append(roots, node) + } else { + children[node.ParentId] = append(children[node.ParentId], node) + } + } + + for _, root := range roots { + result = append(result, root) + result = append(result, children[root.Id]...) + } + + return result +} + +func compareStrings(a, b string) int { + if a < b { + return -1 + } + if a > b { + return 1 + } + return 0 +} diff --git a/template/transaction/transaction.templ b/template/transaction/transaction.templ index d2380e1..5f5478f 100644 --- a/template/transaction/transaction.templ +++ b/template/transaction/transaction.templ @@ -1,10 +1,12 @@ package transaction import "fmt" +import "time" import "spend-sparrow/template/svg" import "spend-sparrow/types" +import "github.com/google/uuid" -templ Transaction(transactions []*types.Transaction) { +templ Transaction(transactions []*types.Transaction, accounts []*types.Account, treasureChests []*types.TreasureChest) {
+ @EditTransaction(nil, accounts, treasureChests) for _, transaction := range transactions { @TransactionItem(transaction) } @@ -23,22 +26,37 @@ templ Transaction(transactions []*types.Transaction) {
} -templ EditTransaction(transaction *types.Transaction) { +templ EditTransaction(transaction *types.Transaction, accounts []*types.Account, treasureChests []*types.TreasureChest) { {{ - var ( - name string - id string - cancelUrl string - ) - if transaction == nil { - name = "" - id = "new" - cancelUrl = "/empty" - } else { - name = transaction.Name - id = transaction.Id.String() - cancelUrl = "/transaction/" + id - } + var ( + timestamp time.Time + note string + // accountId string + // treasureChestId string + value int64 + + id string + cancelUrl string + ) + if transaction == nil { + timestamp = time.Now() + note = "" + // accountId = "" + // treasureChestId = "" + value = 0 + + id = "new" + cancelUrl = "/empty" + } else { + timestamp = transaction.Timestamp + note = transaction.Note + // accountId = transaction.AccountId.String() + // treasureChestId = transaction.TreasureChestId.String() + value = transaction.Value + + id = transaction.Id.String() + cancelUrl = "/transaction/" + id + } }}
- +
+ + + + + + + + + + + + + + +
- for _, treasureChest := range sortTree(treasureChests) { + for _, treasureChest := range treasureChests { @TreasureChestItem(treasureChest) }
@@ -148,37 +148,3 @@ func displayBalance(balance int64) string { euros := float64(balance) / 100 return fmt.Sprintf("%.2f €", euros) } - -func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest { - - var ( - roots []*types.TreasureChest - result []*types.TreasureChest - ) - children := make(map[uuid.UUID][]*types.TreasureChest) - - for _, node := range nodes { - if node.ParentId == uuid.Nil { - roots = append(roots, node) - } else { - children[node.ParentId] = append(children[node.ParentId], node) - } - } - - for _, root := range roots { - result = append(result, root) - result = append(result, children[root.Id]...) - } - - return result -} - -func compareStrings(a, b string) int { - if a < b { - return -1 - } - if a > b { - return 1 - } - return 0 -} diff --git a/types/transaction.go b/types/transaction.go index 1f22c0f..645ea9e 100644 --- a/types/transaction.go +++ b/types/transaction.go @@ -17,21 +17,26 @@ type Transaction struct { Id uuid.UUID UserId uuid.UUID `db:"user_id"` - AccountId uuid.UUID `db:"account_id"` - // nil indicates that the transaction is not yet associated with a piggy bank - TreasureChestId *uuid.UUID `db:"treasure_chest_id"` - - // The internal transaction is amove between e.g. an account and a piggy bank to execute a savings plan - Internal bool - - // The value of the transacion. Negative for outgoing and positive for incoming - Value int64 Timestamp time.Time + Note string - Note string + // account id is only nil, if the transaction is a deposit to a treasure chest + AccountId *uuid.UUID `db:"account_id"` + TreasureChestId *uuid.UUID `db:"treasure_chest_id"` + // The value of the transacion. Negative for outgoing and positive for incoming transactions. + Value int64 CreatedAt time.Time `db:"created_at"` CreatedBy uuid.UUID `db:"created_by"` UpdatedAt *time.Time `db:"updated_at"` UpdatedBy *uuid.UUID `db:"updated_by"` } + +type TransactionInput struct { + Id string + AccountId string + TreasureChestId string + Value string + Timestamp string + Note string +}