diff --git a/handler/account.go b/handler/account.go index 5b2216e..fedcc2e 100644 --- a/handler/account.go +++ b/handler/account.go @@ -17,14 +17,12 @@ type Account interface { type AccountImpl struct { s service.Account - a service.Auth r *Render } -func NewAccount(s service.Account, a service.Auth, r *Render) Account { +func NewAccount(s service.Account, r *Render) Account { return AccountImpl{ s: s, - a: a, r: r, } } diff --git a/handler/root_and_404.go b/handler/root_and_404.go index f4f66d3..2638244 100644 --- a/handler/root_and_404.go +++ b/handler/root_and_404.go @@ -3,7 +3,6 @@ package handler import ( "net/http" "spend-sparrow/handler/middleware" - "spend-sparrow/service" "spend-sparrow/template" "github.com/a-h/templ" @@ -14,14 +13,12 @@ type Index interface { } type IndexImpl struct { - service service.Auth - render *Render + render *Render } -func NewIndex(service service.Auth, render *Render) Index { +func NewIndex(render *Render) Index { return IndexImpl{ - service: service, - render: render, + render: render, } } diff --git a/handler/transaction.go b/handler/transaction.go index f6d79c6..4fdf94e 100644 --- a/handler/transaction.go +++ b/handler/transaction.go @@ -20,16 +20,14 @@ type TransactionImpl struct { s service.Transaction account service.Account treasureChest service.TreasureChest - a service.Auth r *Render } -func NewTransaction(s service.Transaction, account service.Account, treasureChest service.TreasureChest, a service.Auth, r *Render) Transaction { +func NewTransaction(s service.Transaction, account service.Account, treasureChest service.TreasureChest, r *Render) Transaction { return TransactionImpl{ s: s, account: account, treasureChest: treasureChest, - a: a, r: r, } } diff --git a/handler/transaction_recurring.go b/handler/transaction_recurring.go new file mode 100644 index 0000000..437e31c --- /dev/null +++ b/handler/transaction_recurring.go @@ -0,0 +1,131 @@ +package handler + +import ( + "net/http" + "spend-sparrow/handler/middleware" + "spend-sparrow/service" + t "spend-sparrow/template/transaction_recurring" + "spend-sparrow/types" + "spend-sparrow/utils" +) + +type TransactionRecurring interface { + Handle(router *http.ServeMux) +} + +type TransactionRecurringImpl struct { + s service.TransactionRecurring + r *Render +} + +func NewTransactionRecurring(s service.TransactionRecurring, r *Render) TransactionRecurring { + return TransactionRecurringImpl{ + s: s, + r: r, + } +} + +func (h TransactionRecurringImpl) Handle(r *http.ServeMux) { + r.Handle("GET /transaction-recurring", h.handleTransactionRecurringItemComp()) + r.Handle("POST /transaction-recurring/{id}", h.handleUpdateTransactionRecurring()) + r.Handle("DELETE /transaction-recurring/{id}", h.handleDeleteTransactionRecurring()) +} + +func (h TransactionRecurringImpl) handleTransactionRecurringItemComp() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := middleware.GetUser(r) + if user == nil { + utils.DoRedirect(w, r, "/auth/signin") + return + } + + id := r.URL.Query().Get("id") + accountId := r.URL.Query().Get("account-id") + treasureChestId := r.URL.Query().Get("treasure-chest-id") + h.renderItems(w, r, user, id, accountId, treasureChestId) + } +} + +func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := middleware.GetUser(r) + if user == nil { + utils.DoRedirect(w, r, "/auth/signin") + return + } + + input := types.TransactionRecurringInput{ + Id: r.PathValue("id"), + IntervalMonths: r.FormValue("interval-months"), + Active: r.FormValue("active"), + Party: r.FormValue("party"), + Description: r.FormValue("description"), + AccountId: r.FormValue("account-id"), + TreasureChestId: r.FormValue("treasure-chest-id"), + Value: r.FormValue("value"), + } + + if input.Id == "new" { + _, err := h.s.Add(user, input) + if err != nil { + handleError(w, r, err) + return + } + } else { + _, err := h.s.Update(user, input) + if err != nil { + handleError(w, r, err) + return + } + } + + h.renderItems(w, r, user, "", input.AccountId, input.TreasureChestId) + } +} + +func (h TransactionRecurringImpl) handleDeleteTransactionRecurring() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := middleware.GetUser(r) + if user == nil { + utils.DoRedirect(w, r, "/auth/signin") + return + } + + 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) +} diff --git a/handler/treasure_chest.go b/handler/treasure_chest.go index 38e1e4d..7af68b3 100644 --- a/handler/treasure_chest.go +++ b/handler/treasure_chest.go @@ -4,6 +4,7 @@ import ( "net/http" "spend-sparrow/handler/middleware" "spend-sparrow/service" + tr "spend-sparrow/template/transaction_recurring" t "spend-sparrow/template/treasurechest" "spend-sparrow/types" "spend-sparrow/utils" @@ -16,16 +17,16 @@ type TreasureChest interface { } type TreasureChestImpl struct { - s service.TreasureChest - a service.Auth - r *Render + s service.TreasureChest + transactionRecurring service.TransactionRecurring + r *Render } -func NewTreasureChest(s service.TreasureChest, a service.Auth, r *Render) TreasureChest { +func NewTreasureChest(s service.TreasureChest, transactionRecurring service.TransactionRecurring, r *Render) TreasureChest { return TreasureChestImpl{ - s: s, - a: a, - r: r, + s: s, + transactionRecurring: transactionRecurring, + r: r, } } @@ -71,7 +72,7 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc { id := r.PathValue("id") if id == "new" { - comp := t.EditTreasureChest(nil, treasureChests) + comp := t.EditTreasureChest(nil, treasureChests, nil) h.r.Render(r, w, comp) return } @@ -82,9 +83,16 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc { return } + transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(user, treasureChest.Id.String()) + if err != nil { + handleError(w, r, err) + return + } + transactionsRec := tr.TransactionRecurringItems(transactionsRecurring, "", "", treasureChest.Id.String()) + var comp templ.Component if r.URL.Query().Get("edit") == "true" { - comp = t.EditTreasureChest(treasureChest, treasureChests) + comp = t.EditTreasureChest(treasureChest, treasureChests, transactionsRec) } else { comp = t.TreasureChestItem(treasureChest) } diff --git a/input.css b/input.css index 6c4abce..669d824 100644 --- a/input.css +++ b/input.css @@ -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); } diff --git a/main.go b/main.go index 657ded7..062fbab 100644 --- a/main.go +++ b/main.go @@ -116,19 +116,22 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler { accountService := service.NewAccount(d, randomService, clockService, serverSettings) treasureChestService := service.NewTreasureChest(d, randomService, clockService, serverSettings) transactionService := service.NewTransaction(d, randomService, clockService, serverSettings) + transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, serverSettings) render := handler.NewRender() - indexHandler := handler.NewIndex(authService, render) + indexHandler := handler.NewIndex(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) + accountHandler := handler.NewAccount(accountService, render) + treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render) + transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, render) + transactionRecurringHandler := handler.NewTransactionRecurring(transactionRecurringService, render) indexHandler.Handle(router) accountHandler.Handle(router) treasureChestHandler.Handle(router) authHandler.Handle(router) transactionHandler.Handle(router) + transactionRecurringHandler.Handle(router) // Serve static files (CSS, JS and images) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) diff --git a/main_test.go b/main_test.go index 10bdfc9..832cf1d 100644 --- a/main_test.go +++ b/main_test.go @@ -1803,7 +1803,6 @@ func TestIntegrationAccount(t *testing.T) { ":": 400, "*": 400, "|": 400, - "\"": 400, "Account": 200, } diff --git a/migration/008_recurring_transaction.up.sql b/migration/008_recurring_transaction.up.sql new file mode 100644 index 0000000..b766b5c --- /dev/null +++ b/migration/008_recurring_transaction.up.sql @@ -0,0 +1,21 @@ + +CREATE TABLE "transaction_recurring" ( + id TEXT NOT NULL UNIQUE PRIMARY KEY, + user_id TEXT NOT NULL, + + interval_months INTEGER NOT NULL, + last_execution DATETIME, + active INTEGER NOT NULL, + + party TEXT, + description TEXT, + + 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/service/default.go b/service/default.go index 130f672..56ccd3d 100644 --- a/service/default.go +++ b/service/default.go @@ -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 { diff --git a/service/transaction.go b/service/transaction.go index 6cab62b..d2c2978 100644 --- a/service/transaction.go +++ b/service/transaction.go @@ -390,7 +390,7 @@ func (s TransactionImpl) RecalculateBalances(user *types.User) error { return err } - updateErrors(transaction) + s.updateErrors(transaction) r, err = tx.Exec(` UPDATE "transaction" SET error = ? @@ -551,12 +551,12 @@ func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransactio UpdatedBy: &updatedBy, } - updateErrors(&transaction) + s.updateErrors(&transaction) return &transaction, nil } -func updateErrors(transaction *types.Transaction) { +func (s TransactionImpl) updateErrors(transaction *types.Transaction) { error := "" if transaction.Value < 0 { diff --git a/service/transaction_recurring.go b/service/transaction_recurring.go new file mode 100644 index 0000000..eee9939 --- /dev/null +++ b/service/transaction_recurring.go @@ -0,0 +1,468 @@ +package service + +import ( + "fmt" + "strconv" + "time" + + "spend-sparrow/db" + "spend-sparrow/log" + "spend-sparrow/types" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + transactionRecurringMetric = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "spendsparrow_transactionRecurring_recurring_total", + Help: "The total of transactionRecurring recurring operations", + }, + []string{"operation"}, + ) +) + +type TransactionRecurring interface { + Add(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error) + Update(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error) + Get(user *types.User, id string) (*types.TransactionRecurring, error) + GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) + GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error) + Delete(user *types.User, id string) error +} + +type TransactionRecurringImpl struct { + db *sqlx.DB + clock Clock + random Random + settings *types.Settings +} + +func NewTransactionRecurring(db *sqlx.DB, random Random, clock Clock, settings *types.Settings) TransactionRecurring { + return TransactionRecurringImpl{ + db: db, + clock: clock, + random: random, + settings: settings, + } +} + +func (s TransactionRecurringImpl) Add(user *types.User, transactionRecurringInput types.TransactionRecurringInput) (*types.TransactionRecurring, error) { + transactionRecurringMetric.WithLabelValues("add").Inc() + + if user == nil { + return nil, ErrUnauthorized + } + + tx, err := s.db.Beginx() + err = db.TransformAndLogDbError("transactionRecurring Add", nil, err) + if err != nil { + return nil, err + } + defer func() { + _ = tx.Rollback() + }() + + transactionRecurring, err := s.validateAndEnrichTransactionRecurring(tx, nil, user.Id, transactionRecurringInput) + if err != nil { + return nil, err + } + + r, err := tx.NamedExec(` + INSERT INTO "transaction_recurring" (id, user_id, interval_months, active, party, description, account_id, treasure_chest_id, value, created_at, created_by) + VALUES (:id, :user_id, :interval_months, :active, :party, :description, :account_id, :treasure_chest_id, :value, :created_at, :created_by)`, transactionRecurring) + err = db.TransformAndLogDbError("transactionRecurring Insert", r, err) + if err != nil { + return nil, err + } + + err = tx.Commit() + err = db.TransformAndLogDbError("transactionRecurring Add", nil, err) + if err != nil { + return nil, err + } + + return transactionRecurring, nil +} + +func (s TransactionRecurringImpl) Update(user *types.User, input types.TransactionRecurringInput) (*types.TransactionRecurring, error) { + transactionRecurringMetric.WithLabelValues("update").Inc() + if user == nil { + return nil, ErrUnauthorized + } + uuid, err := uuid.Parse(input.Id) + if err != nil { + log.Error("transactionRecurring update: %v", err) + return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) + } + + tx, err := s.db.Beginx() + err = db.TransformAndLogDbError("transactionRecurring Update", nil, err) + if err != nil { + return nil, err + } + defer func() { + _ = tx.Rollback() + }() + + transactionRecurring := &types.TransactionRecurring{} + err = tx.Get(transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid) + err = db.TransformAndLogDbError("transactionRecurring Update", nil, err) + if err != nil { + if err == db.ErrNotFound { + return nil, fmt.Errorf("transactionRecurring %v not found: %w", input.Id, ErrBadRequest) + } + return nil, types.ErrInternal + } + + transactionRecurring, err = s.validateAndEnrichTransactionRecurring(tx, transactionRecurring, user.Id, input) + if err != nil { + return nil, err + } + + r, err := tx.NamedExec(` + UPDATE transaction_recurring + SET + interval_months = :interval_months, + active = :active, + party = :party, + description = :description, + account_id = :account_id, + treasure_chest_id = :treasure_chest_id, + value = :value, + updated_at = :updated_at, + updated_by = :updated_by + WHERE id = :id + AND user_id = :user_id`, transactionRecurring) + err = db.TransformAndLogDbError("transactionRecurring Update", r, err) + if err != nil { + return nil, err + } + + err = tx.Commit() + err = db.TransformAndLogDbError("transactionRecurring Update", nil, err) + if err != nil { + return nil, err + } + + return transactionRecurring, nil +} + +func (s TransactionRecurringImpl) Get(user *types.User, id string) (*types.TransactionRecurring, error) { + transactionRecurringMetric.WithLabelValues("get").Inc() + + if user == nil { + return nil, ErrUnauthorized + } + uuid, err := uuid.Parse(id) + if err != nil { + log.Error("transactionRecurring get: %v", err) + return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) + } + + var transactionRecurring types.TransactionRecurring + err = s.db.Get(&transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid) + err = db.TransformAndLogDbError("transactionRecurring Get", nil, err) + if err != nil { + if err == db.ErrNotFound { + return nil, fmt.Errorf("transactionRecurring %v not found: %w", id, ErrBadRequest) + } + return nil, types.ErrInternal + } + + return &transactionRecurring, nil +} + +func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) { + transactionRecurringMetric.WithLabelValues("get_all_by_account").Inc() + if user == nil { + return nil, ErrUnauthorized + } + + accountUuid, err := uuid.Parse(accountId) + if err != nil { + log.Error("transactionRecurring GetAllByAccount: %v", err) + return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest) + } + + tx, err := s.db.Beginx() + err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err) + if err != nil { + return nil, err + } + defer func() { + _ = tx.Rollback() + }() + + var rowCount int + err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, user.Id) + err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err) + if err != nil { + if err == db.ErrNotFound { + return nil, fmt.Errorf("account %v not found: %w", accountId, ErrBadRequest) + } + return nil, types.ErrInternal + } + + transactionRecurrings := make([]*types.TransactionRecurring, 0) + err = tx.Select(&transactionRecurrings, ` + SELECT * + FROM transaction_recurring + WHERE user_id = ? + AND account_id = ? + ORDER BY created_at DESC`, + user.Id, accountUuid) + err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err) + if err != nil { + return nil, err + } + + err = tx.Commit() + err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err) + if err != nil { + return nil, err + } + + return transactionRecurrings, nil +} + +func (s TransactionRecurringImpl) GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error) { + transactionRecurringMetric.WithLabelValues("get_all_by_treasurechest").Inc() + if user == nil { + return nil, ErrUnauthorized + } + + treasureChestUuid, err := uuid.Parse(treasureChestId) + if err != nil { + log.Error("transactionRecurring GetAllByTreasureChest: %v", err) + return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest) + } + + tx, err := s.db.Beginx() + err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err) + if err != nil { + return nil, err + } + defer func() { + _ = tx.Rollback() + }() + + var rowCount int + err = tx.Get(&rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestId, user.Id) + err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err) + if err != nil { + if err == db.ErrNotFound { + return nil, fmt.Errorf("treasurechest %v not found: %w", treasureChestId, ErrBadRequest) + } + return nil, types.ErrInternal + } + + transactionRecurrings := make([]*types.TransactionRecurring, 0) + err = tx.Select(&transactionRecurrings, ` + SELECT * + FROM transaction_recurring + WHERE user_id = ? + AND treasure_chest_id = ? + ORDER BY created_at DESC`, + user.Id, treasureChestUuid) + err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err) + if err != nil { + return nil, err + } + + err = tx.Commit() + err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err) + if err != nil { + return nil, err + } + + return transactionRecurrings, nil +} + +func (s TransactionRecurringImpl) Delete(user *types.User, id string) error { + transactionRecurringMetric.WithLabelValues("delete").Inc() + if user == nil { + return ErrUnauthorized + } + uuid, err := uuid.Parse(id) + if err != nil { + log.Error("transactionRecurring delete: %v", err) + return fmt.Errorf("could not parse Id: %w", ErrBadRequest) + } + + tx, err := s.db.Beginx() + err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err) + if err != nil { + return nil + } + defer func() { + _ = tx.Rollback() + }() + + var transactionRecurring types.TransactionRecurring + err = tx.Get(&transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid) + err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err) + if err != nil { + return err + } + + r, err := tx.Exec("DELETE FROM transaction_recurring WHERE id = ? AND user_id = ?", uuid, user.Id) + err = db.TransformAndLogDbError("transactionRecurring Delete", r, err) + if err != nil { + return err + } + + err = tx.Commit() + err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err) + if err != nil { + return err + } + + return nil +} + +func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring( + tx *sqlx.Tx, + oldTransactionRecurring *types.TransactionRecurring, + userId uuid.UUID, + input types.TransactionRecurringInput) (*types.TransactionRecurring, error) { + + var ( + id uuid.UUID + accountUuid *uuid.UUID + treasureChestUuid *uuid.UUID + createdAt time.Time + createdBy uuid.UUID + updatedAt *time.Time + updatedBy uuid.UUID + intervalMonths int64 + + err error + rowCount int + ) + + if oldTransactionRecurring == nil { + id, err = s.random.UUID() + if err != nil { + return nil, types.ErrInternal + } + createdAt = s.clock.Now() + createdBy = userId + } else { + id = oldTransactionRecurring.Id + createdAt = oldTransactionRecurring.CreatedAt + createdBy = oldTransactionRecurring.CreatedBy + time := s.clock.Now() + updatedAt = &time + updatedBy = userId + } + + hasAccount := false + hasTreasureChest := false + if input.AccountId != "" { + temp, err := uuid.Parse(input.AccountId) + if err != nil { + log.Error("transactionRecurring validate: %v", err) + return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest) + } + accountUuid = &temp + err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId) + err = db.TransformAndLogDbError("transactionRecurring validate", nil, err) + if err != nil { + return nil, err + } + if rowCount == 0 { + log.Error("transactionRecurring validate: %v", err) + return nil, fmt.Errorf("account not found: %w", ErrBadRequest) + } + + hasAccount = true + } + + if input.TreasureChestId != "" { + temp, err := uuid.Parse(input.TreasureChestId) + if err != nil { + log.Error("transactionRecurring validate: %v", err) + return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest) + } + treasureChestUuid = &temp + var treasureChest types.TreasureChest + err = tx.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, userId) + err = db.TransformAndLogDbError("transactionRecurring validate", nil, err) + if err != nil { + if err == db.ErrNotFound { + return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest) + } + return nil, err + } + if treasureChest.ParentId == nil { + return nil, fmt.Errorf("treasure chest is a group: %w", ErrBadRequest) + } + hasTreasureChest = true + } + + if !hasAccount && !hasTreasureChest { + log.Error("transactionRecurring validate: %v", err) + return nil, fmt.Errorf("either account or treasure chest is required: %w", ErrBadRequest) + } + if hasAccount && hasTreasureChest { + log.Error("transactionRecurring validate: %v", err) + return nil, fmt.Errorf("either account or treasure chest is required, not both: %w", ErrBadRequest) + } + + valueFloat, err := strconv.ParseFloat(input.Value, 64) + if err != nil { + log.Error("transactionRecurring validate: %v", err) + return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest) + } + valueInt := int64(valueFloat * 100) + + if input.Party != "" { + err = validateString(input.Party, "party") + if err != nil { + return nil, err + } + } + if input.Description != "" { + err = validateString(input.Description, "description") + if err != nil { + return nil, err + } + } + intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0) + if err != nil { + log.Error("transactionRecurring validate: %v", err) + return nil, fmt.Errorf("could not parse intervalMonths: %w", ErrBadRequest) + } + if intervalMonths < 1 { + log.Error("transactionRecurring validate: %v", err) + return nil, fmt.Errorf("intervalMonths needs to be greater than 0: %w", ErrBadRequest) + } + active := input.Active == "on" + + transactionRecurring := types.TransactionRecurring{ + + Id: id, + UserId: userId, + + IntervalMonths: intervalMonths, + Active: active, + + Party: input.Party, + Description: input.Description, + + AccountId: accountUuid, + TreasureChestId: treasureChestUuid, + Value: valueInt, + + CreatedAt: createdAt, + CreatedBy: createdBy, + UpdatedAt: updatedAt, + UpdatedBy: &updatedBy, + } + + return &transactionRecurring, nil +} diff --git a/static/js/time.js b/static/js/time.js index 3694bd8..3023997 100644 --- a/static/js/time.js +++ b/static/js/time.js @@ -1,6 +1,6 @@ -htmx.on("htmx:afterSwap", (e) => { - updateTime(e.target); +htmx.on("htmx:afterSwap", () => { + updateTime(); }); document.addEventListener("DOMContentLoaded", () => { @@ -8,7 +8,7 @@ document.addEventListener("DOMContentLoaded", () => { updateTime(document); }) -function updateTime(e) { +function updateTime() { document.querySelectorAll(".datetime").forEach((el) => { if (el.textContent !== "") { el.textContent = el.textContent.includes("UTC") ? new Date(el.textContent).toLocaleString([], { day: 'numeric', month: 'short', year: 'numeric' }) : el.textContent; diff --git a/template/transaction_recurring/default.go b/template/transaction_recurring/default.go new file mode 100644 index 0000000..5b5b034 --- /dev/null +++ b/template/transaction_recurring/default.go @@ -0,0 +1 @@ +package transaction_recurring diff --git a/template/transaction_recurring/transaction_recurring.templ b/template/transaction_recurring/transaction_recurring.templ new file mode 100644 index 0000000..3310eb7 --- /dev/null +++ b/template/transaction_recurring/transaction_recurring.templ @@ -0,0 +1,208 @@ +package transaction_recurring + +import "fmt" +import "spend-sparrow/template/svg" +import "spend-sparrow/types" + +templ TransactionRecurringItems(transactionsRecurring []*types.TransactionRecurring, editId, accountId, treasureChestId string) { + +
+ if transactionRecurring.Active { + ✅ + } else { + ❌ + } +
++ if transactionRecurring.Party != "" { + { transactionRecurring.Party } + } else { + - + } +
++ if transactionRecurring.Description != "" { + { transactionRecurring.Description } + } else { + - + } +
++ Every { transactionRecurring.IntervalMonths } month(s) +
+ if transactionRecurring.Value < 0 { +{ displayBalance(transactionRecurring.Value)+" €" }
+ } else { +{ displayBalance(transactionRecurring.Value)+" €" }
+ } +New Treasure Chest
+ New Treasure Chest