feat(transaction): #66 implement transactions
This commit is contained in:
136
db/transaction.go
Normal file
136
db/transaction.go
Normal file
@@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
138
handler/transaction.go
Normal file
138
handler/transaction.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"spend-sparrow/handler/middleware"
|
||||
"spend-sparrow/service"
|
||||
t "spend-sparrow/template/transaction"
|
||||
"spend-sparrow/types"
|
||||
"spend-sparrow/utils"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
type Transaction interface {
|
||||
Handle(router *http.ServeMux)
|
||||
}
|
||||
|
||||
type TransactionImpl struct {
|
||||
s service.Transaction
|
||||
a service.Auth
|
||||
r *Render
|
||||
}
|
||||
|
||||
func NewTransaction(s service.Transaction, a service.Auth, r *Render) Transaction {
|
||||
return TransactionImpl{
|
||||
s: s,
|
||||
a: a,
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (h TransactionImpl) Handle(r *http.ServeMux) {
|
||||
r.Handle("GET /transaction", h.handleTransactionPage())
|
||||
r.Handle("GET /transaction/{id}", h.handleTransactionItemComp())
|
||||
r.Handle("POST /transaction/{id}", h.handleUpdateTransaction())
|
||||
r.Handle("DELETE /transaction/{id}", h.handleDeleteTransaction())
|
||||
}
|
||||
|
||||
func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := middleware.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
transactions, err := h.s.GetAll(user)
|
||||
if err != nil {
|
||||
handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
comp := t.Transaction(transactions)
|
||||
h.r.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
|
||||
func (h TransactionImpl) handleTransactionItemComp() 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")
|
||||
if id == "new" {
|
||||
comp := t.EditTransaction(nil)
|
||||
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.EditTransaction(transaction)
|
||||
} else {
|
||||
comp = t.TransactionItem(transaction)
|
||||
}
|
||||
h.r.Render(r, w, comp)
|
||||
}
|
||||
}
|
||||
|
||||
func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := middleware.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
transaction *types.Transaction
|
||||
err error
|
||||
)
|
||||
id := r.PathValue("id")
|
||||
name := r.FormValue("name")
|
||||
if id == "new" {
|
||||
transaction, err = h.s.Add(user, name)
|
||||
if err != nil {
|
||||
handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
transaction, err = h.s.Update(user, id, name)
|
||||
if err != nil {
|
||||
handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
comp := t.TransactionItem(transaction)
|
||||
h.r.Render(r, w, comp)
|
||||
}
|
||||
}
|
||||
|
||||
func (h TransactionImpl) handleDeleteTransaction() 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")
|
||||
|
||||
err := h.s.Delete(user, id)
|
||||
if err != nil {
|
||||
handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
270
service/transaction.go
Normal file
270
service/transaction.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"spend-sparrow/db"
|
||||
"spend-sparrow/log"
|
||||
"spend-sparrow/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
transactionMetric = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "spendsparrow_transaction_total",
|
||||
Help: "The total of transaction operations",
|
||||
},
|
||||
[]string{"operation"},
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
func NewTransaction(db db.Transaction, account Account, treasureChest TreasureChest, random Random, clock Clock, settings *types.Settings) Transaction {
|
||||
return TransactionImpl{
|
||||
db: db,
|
||||
account: account,
|
||||
treasureChest: treasureChest,
|
||||
clock: clock,
|
||||
random: random,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
func (s TransactionImpl) Add(user *types.User, accountId, treasureChestId, internal, value, timestamp, note string) (*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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.db.Insert(user.Id, transaction)
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (s TransactionImpl) Update(user *types.User, id, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) {
|
||||
transactionMetric.WithLabelValues("update").Inc()
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(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)
|
||||
if err != nil {
|
||||
if err == db.ErrNotFound {
|
||||
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
|
||||
}
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
transaction, err = s.validateTransaction(transaction, user.Id, accountId, treasureChestId, internal, value, timestamp, note)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.db.Update(user.Id, transaction)
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
return transaction, nil
|
||||
}
|
||||
|
||||
func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, error) {
|
||||
transactionMetric.WithLabelValues("get").Inc()
|
||||
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
log.Error("transaction get: %v", err)
|
||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
transaction, err := s.db.Get(user.Id, uuid)
|
||||
if err != nil {
|
||||
if err == db.ErrNotFound {
|
||||
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
|
||||
}
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
return transaction, nil
|
||||
}
|
||||
|
||||
func (s TransactionImpl) GetAll(user *types.User) ([]*types.Transaction, error) {
|
||||
transactionMetric.WithLabelValues("get_all").Inc()
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
transactions, err := s.db.GetAll(user.Id)
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
func (s TransactionImpl) Delete(user *types.User, id string) error {
|
||||
transactionMetric.WithLabelValues("delete").Inc()
|
||||
if user == nil {
|
||||
return ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
log.Error("transaction delete: %v", err)
|
||||
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
transaction, err := s.db.Get(user.Id, uuid)
|
||||
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 nil
|
||||
}
|
||||
|
||||
func (s TransactionImpl) validateTransaction(transaction *types.Transaction, userId uuid.UUID, accountId, treasureChestId, internal, value, timestamp, note string) (*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
|
||||
)
|
||||
|
||||
if transaction == nil {
|
||||
id, err = s.random.UUID()
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
createdAt = s.clock.Now()
|
||||
createdBy = userId
|
||||
} else {
|
||||
id = transaction.Id
|
||||
createdAt = transaction.CreatedAt
|
||||
createdBy = transaction.CreatedBy
|
||||
time := s.clock.Now()
|
||||
updatedAt = &time
|
||||
updatedBy = userId
|
||||
}
|
||||
|
||||
if accountId != "" {
|
||||
accountUuid, err = uuid.Parse(accountId)
|
||||
if err != nil {
|
||||
log.Error("transaction validate: %v", err)
|
||||
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
if treasureChestId != "" {
|
||||
treasureChestUuid, err = uuid.Parse(treasureChestId)
|
||||
if err != nil {
|
||||
log.Error("transaction validate: %v", err)
|
||||
return nil, fmt.Errorf("could not parse treasureChestId: %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)
|
||||
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)
|
||||
if err != nil {
|
||||
log.Error("transaction validate: %v", err)
|
||||
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
err = validateString(note)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := types.Transaction{
|
||||
Id: id,
|
||||
UserId: userId,
|
||||
|
||||
AccountId: accountUuid,
|
||||
TreasureChestId: &treasureChestUuid,
|
||||
Internal: internalBool,
|
||||
Value: valueInt,
|
||||
Timestamp: timestampTime,
|
||||
Note: note,
|
||||
|
||||
CreatedAt: createdAt,
|
||||
CreatedBy: createdBy,
|
||||
UpdatedAt: updatedAt,
|
||||
UpdatedBy: &updatedBy,
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
1
template/transaction/default.go
Normal file
1
template/transaction/default.go
Normal file
@@ -0,0 +1 @@
|
||||
package transaction
|
||||
114
template/transaction/transaction.templ
Normal file
114
template/transaction/transaction.templ
Normal file
@@ -0,0 +1,114 @@
|
||||
package transaction
|
||||
|
||||
import "fmt"
|
||||
import "spend-sparrow/template/svg"
|
||||
import "spend-sparrow/types"
|
||||
|
||||
templ Transaction(transactions []*types.Transaction) {
|
||||
<div class="max-w-6xl mt-10 mx-auto">
|
||||
<button
|
||||
hx-get="/transaction/new"
|
||||
hx-target="#transaction-items"
|
||||
hx-swap="afterbegin"
|
||||
class="ml-auto button button-primary px-2 flex-1 flex items-center gap-2 justify-center"
|
||||
>
|
||||
@svg.Plus()
|
||||
<p class="">New Transaction</p>
|
||||
</button>
|
||||
<div id="transaction-items" class="my-6 flex flex-col items-center">
|
||||
for _, transaction := range transactions {
|
||||
@TransactionItem(transaction)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ EditTransaction(transaction *types.Transaction) {
|
||||
{{
|
||||
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
|
||||
}
|
||||
}}
|
||||
<div id="transaction" class="border-1 border-gray-300 w-full my-4 p-4 bg-gray-50 rounded-lg">
|
||||
<form
|
||||
hx-post={ "/transaction/" + id }
|
||||
hx-target="closest #transaction"
|
||||
hx-swap="outerHTML"
|
||||
class="text-xl flex justify-end gap-4 items-center"
|
||||
>
|
||||
<input
|
||||
autofocus
|
||||
name="name"
|
||||
type="text"
|
||||
value={ name }
|
||||
placeholder="Transaction Name"
|
||||
class="mr-auto bg-white input"
|
||||
/>
|
||||
<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="closest #transaction"
|
||||
hx-swap="outerHTML"
|
||||
class="button button-neglect px-1 flex items-center gap-2"
|
||||
>
|
||||
@svg.Cancel()
|
||||
<span>
|
||||
Cancel
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ TransactionItem(transaction *types.Transaction) {
|
||||
<div id="transaction" class="border-1 border-gray-300 w-full my-4 p-4 bg-gray-50 rounded-lg">
|
||||
<div class="text-xl flex justify-end gap-4">
|
||||
<p class="mr-auto">{ transaction.Name }</p>
|
||||
<p class="mr-20 text-green-700">{ displayBalance(transaction.CurrentBalance) }</p>
|
||||
<button
|
||||
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
|
||||
hx-target="closest #transaction"
|
||||
hx-swap="outerHTML"
|
||||
class="button button-neglect px-1 flex items-center gap-2"
|
||||
>
|
||||
@svg.Edit()
|
||||
<span>
|
||||
Edit
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-delete={ "/transaction/" + transaction.Id.String() }
|
||||
hx-target="closest #transaction"
|
||||
hx-swap="outerHTML"
|
||||
class="button button-neglect px-1 flex items-center gap-2"
|
||||
>
|
||||
@svg.Delete()
|
||||
<span>
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
func displayBalance(balance int64) string {
|
||||
|
||||
euros := float64(balance) / 100
|
||||
return fmt.Sprintf("%.2f €", euros)
|
||||
}
|
||||
Reference in New Issue
Block a user