feat(dashboard): #163 first summary
Some checks failed
Build Docker Image / Build-Docker-Image (push) Has been cancelled
Some checks failed
Build Docker Image / Build-Docker-Image (push) Has been cancelled
This commit is contained in:
@@ -116,9 +116,10 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
|
|||||||
treasureChestService := service.NewTreasureChest(d, randomService, clockService)
|
treasureChestService := service.NewTreasureChest(d, randomService, clockService)
|
||||||
transactionService := service.NewTransaction(d, randomService, clockService)
|
transactionService := service.NewTransaction(d, randomService, clockService)
|
||||||
transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService)
|
transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService)
|
||||||
|
dashboardService := service.NewDashboard(d)
|
||||||
|
|
||||||
render := handler.NewRender()
|
render := handler.NewRender()
|
||||||
indexHandler := handler.NewIndex(render)
|
indexHandler := handler.NewIndex(render, dashboardService)
|
||||||
authHandler := handler.NewAuth(authService, render)
|
authHandler := handler.NewAuth(authService, render)
|
||||||
accountHandler := handler.NewAccount(accountService, render)
|
accountHandler := handler.NewAccount(accountService, render)
|
||||||
treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render)
|
treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render)
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/handler/middleware"
|
"spend-sparrow/internal/handler/middleware"
|
||||||
|
"spend-sparrow/internal/service"
|
||||||
"spend-sparrow/internal/template"
|
"spend-sparrow/internal/template"
|
||||||
|
"spend-sparrow/internal/template/dashboard"
|
||||||
|
"spend-sparrow/internal/types"
|
||||||
|
"spend-sparrow/internal/utils"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
)
|
)
|
||||||
@@ -13,12 +20,14 @@ type Index interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IndexImpl struct {
|
type IndexImpl struct {
|
||||||
render *Render
|
r *Render
|
||||||
|
d *service.Dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndex(render *Render) Index {
|
func NewIndex(r *Render, d *service.Dashboard) Index {
|
||||||
return IndexImpl{
|
return IndexImpl{
|
||||||
render: render,
|
r: r,
|
||||||
|
d: d,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +42,8 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
|
|||||||
|
|
||||||
user := middleware.GetUser(r)
|
user := middleware.GetUser(r)
|
||||||
|
|
||||||
|
htmx := utils.IsHtmx(r)
|
||||||
|
|
||||||
var comp templ.Component
|
var comp templ.Component
|
||||||
|
|
||||||
var status int
|
var status int
|
||||||
@@ -41,14 +52,48 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
|
|||||||
status = http.StatusNotFound
|
status = http.StatusNotFound
|
||||||
} else {
|
} else {
|
||||||
if user != nil {
|
if user != nil {
|
||||||
comp = template.Dashboard()
|
var err error
|
||||||
|
comp, err = handler.dashboard(user, htmx, r)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get dashboard summary", "err", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
comp = template.Index()
|
comp = template.Index()
|
||||||
}
|
}
|
||||||
status = http.StatusOK
|
status = http.StatusOK
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.render.RenderLayoutWithStatus(r, w, comp, user, status)
|
if htmx {
|
||||||
|
handler.r.RenderWithStatus(r, w, comp, status)
|
||||||
|
} else {
|
||||||
|
handler.r.RenderLayoutWithStatus(r, w, comp, user, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler IndexImpl) dashboard(user *types.User, htmx bool, r *http.Request) (templ.Component, error) {
|
||||||
|
|
||||||
|
var month time.Time
|
||||||
|
var err error
|
||||||
|
monthStr := r.URL.Query().Get("month")
|
||||||
|
if monthStr != "" {
|
||||||
|
month, err = time.Parse("2006-01-02", monthStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse timestamp: %w", service.ErrBadRequest)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
month = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := handler.d.Summary(r.Context(), user, month)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if htmx {
|
||||||
|
return dashboard.DashboardData(summary), nil
|
||||||
|
} else {
|
||||||
|
return dashboard.Dashboard(summary), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import (
|
|||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Transaction interface {
|
type Transaction interface {
|
||||||
@@ -56,9 +54,6 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSpan := trace.SpanFromContext(r.Context())
|
|
||||||
currentSpan.SetAttributes(attribute.String("", "test"))
|
|
||||||
|
|
||||||
filter := types.TransactionItemsFilter{
|
filter := types.TransactionItemsFilter{
|
||||||
AccountId: r.URL.Query().Get("account-id"),
|
AccountId: r.URL.Query().Get("account-id"),
|
||||||
TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
|
TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
|
||||||
|
|||||||
88
internal/service/dashboard.go
Normal file
88
internal/service/dashboard.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"spend-sparrow/internal/db"
|
||||||
|
"spend-sparrow/internal/types"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dashboard struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDashboard(db *sqlx.DB) *Dashboard {
|
||||||
|
return &Dashboard{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Dashboard) Summary(ctx context.Context, user *types.User, month time.Time) (*types.DashboardMonthlySummary, error) {
|
||||||
|
if user == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary types.DashboardMonthlySummary
|
||||||
|
|
||||||
|
var value *int64
|
||||||
|
err := s.db.GetContext(ctx, &value, `
|
||||||
|
SELECT SUM(value)
|
||||||
|
FROM "transaction"
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND treasure_chest_id IS NOT NULL
|
||||||
|
AND account_id IS NULL
|
||||||
|
AND error IS NULL
|
||||||
|
AND date(timestamp, 'start of month') = date($2, 'start of month')`,
|
||||||
|
user.Id, month)
|
||||||
|
err = db.TransformAndLogDbError("dashboard", nil, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if value != nil {
|
||||||
|
summary.Savings = *value
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.db.GetContext(ctx, &value, `
|
||||||
|
SELECT SUM(value)
|
||||||
|
FROM "transaction"
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND account_id IS NOT NULL
|
||||||
|
AND treasure_chest_id IS NULL
|
||||||
|
AND error IS NULL
|
||||||
|
AND date(timestamp, 'start of month') = date($2, 'start of month')`,
|
||||||
|
user.Id, month)
|
||||||
|
err = db.TransformAndLogDbError("dashboard", nil, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if value != nil {
|
||||||
|
summary.Income = *value
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.db.GetContext(ctx, &value, `
|
||||||
|
SELECT SUM(value)
|
||||||
|
FROM "transaction"
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND account_id IS NOT NULL
|
||||||
|
AND treasure_chest_id IS NOT NULL
|
||||||
|
AND error IS NULL
|
||||||
|
AND date(timestamp, 'start of month') = date($2, 'start of month')`,
|
||||||
|
user.Id, month)
|
||||||
|
err = db.TransformAndLogDbError("dashboard", nil, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if value != nil {
|
||||||
|
summary.Expenses = *value
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.Total = summary.Income + summary.Expenses
|
||||||
|
summary.Month = month
|
||||||
|
|
||||||
|
slog.Info("Dashboard summary", "summary", summary)
|
||||||
|
|
||||||
|
return &summary, nil
|
||||||
|
}
|
||||||
@@ -512,27 +512,20 @@ func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *s
|
|||||||
return &transaction, nil
|
return &transaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) updateErrors(transaction *types.Transaction) {
|
func (s TransactionImpl) updateErrors(t *types.Transaction) {
|
||||||
errorStr := ""
|
errorStr := ""
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case transaction.Value < 0:
|
case (t.AccountId != nil && t.TreasureChestId != nil && t.Value > 0) ||
|
||||||
if transaction.TreasureChestId == nil {
|
(t.AccountId == nil && t.TreasureChestId == nil):
|
||||||
errorStr = "no treasure chest specified"
|
|
||||||
}
|
|
||||||
case transaction.Value > 0:
|
|
||||||
if transaction.AccountId == nil && transaction.TreasureChestId == nil {
|
|
||||||
errorStr = "either an account or a treasure chest needs to be specified"
|
errorStr = "either an account or a treasure chest needs to be specified"
|
||||||
} else if transaction.AccountId != nil && transaction.TreasureChestId != nil {
|
case t.Value == 0:
|
||||||
errorStr = "positive amounts can only be applied to either an account or a treasure chest"
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
errorStr = "\"value\" needs to be specified"
|
errorStr = "\"value\" needs to be specified"
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorStr == "" {
|
if errorStr == "" {
|
||||||
transaction.Error = nil
|
t.Error = nil
|
||||||
} else {
|
} else {
|
||||||
transaction.Error = &errorStr
|
t.Error = &errorStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package account
|
package account
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
import "spend-sparrow/internal/template/svg"
|
import "spend-sparrow/internal/template/svg"
|
||||||
import "spend-sparrow/internal/types"
|
import "spend-sparrow/internal/types"
|
||||||
|
|
||||||
@@ -81,9 +80,9 @@ templ AccountItem(account *types.Account) {
|
|||||||
<div class="text-xl flex justify-end gap-4">
|
<div class="text-xl flex justify-end gap-4">
|
||||||
<p class="mr-auto">{ account.Name }</p>
|
<p class="mr-auto">{ account.Name }</p>
|
||||||
if account.CurrentBalance < 0 {
|
if account.CurrentBalance < 0 {
|
||||||
<p class="mr-20 text-red-700">{ displayBalance(account.CurrentBalance) }</p>
|
<p class="mr-20 text-red-700">{ types.FormatEuros(account.CurrentBalance) }</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="mr-20 text-green-700">{ displayBalance(account.CurrentBalance) }</p>
|
<p class="mr-20 text-green-700">{ types.FormatEuros(account.CurrentBalance) }</p>
|
||||||
}
|
}
|
||||||
<a
|
<a
|
||||||
href={ templ.URL("/transaction?account-id=" + account.Id.String()) }
|
href={ templ.URL("/transaction?account-id=" + account.Id.String()) }
|
||||||
@@ -121,9 +120,3 @@ templ AccountItem(account *types.Account) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayBalance(balance int64) string {
|
|
||||||
|
|
||||||
euros := float64(balance) / 100
|
|
||||||
return fmt.Sprintf("%.2f €", euros)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package template
|
|
||||||
|
|
||||||
templ Dashboard() {
|
|
||||||
<div>
|
|
||||||
<h1 class="text-8xl">
|
|
||||||
Dashboard
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
44
internal/template/dashboard/dashboard.templ
Normal file
44
internal/template/dashboard/dashboard.templ
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package dashboard
|
||||||
|
|
||||||
|
import "spend-sparrow/internal/types"
|
||||||
|
|
||||||
|
templ Dashboard(summary *types.DashboardMonthlySummary) {
|
||||||
|
<div class="mt-10">
|
||||||
|
<form hx-get="/" hx-target="#dashboard" hx-trigger="change" hx-push-url="true">
|
||||||
|
<label for="month">Select Month:</label>
|
||||||
|
<input
|
||||||
|
name="month"
|
||||||
|
type="date"
|
||||||
|
class="input"
|
||||||
|
value={ summary.Month.String() }
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div id="dashboard">
|
||||||
|
@DashboardData(summary)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ DashboardData(summary *types.DashboardMonthlySummary) {
|
||||||
|
<div class="mt-10">
|
||||||
|
<section class="grid grid-cols-[auto_auto_auto_auto_1fr] gap-4">
|
||||||
|
<span>Savings</span>
|
||||||
|
<span>Income</span>
|
||||||
|
<span>Expenses</span>
|
||||||
|
<span>Total</span>
|
||||||
|
<span></span>
|
||||||
|
<span>{ types.FormatEuros(summary.Savings) }</span>
|
||||||
|
@balance(summary.Income)
|
||||||
|
@balance(summary.Expenses)
|
||||||
|
@balance(summary.Total)
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ balance(balance int64) {
|
||||||
|
if balance < 0 {
|
||||||
|
<span class="text-red-700">{ types.FormatEuros(balance) }</span>
|
||||||
|
} else {
|
||||||
|
<span class="text-green-700">{ types.FormatEuros(balance) }</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
2
internal/template/dashboard/default.go
Normal file
2
internal/template/dashboard/default.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
package dashboard
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ templ EditTransaction(transaction *types.Transaction, accounts []*types.Account,
|
|||||||
if transaction.TreasureChestId != nil {
|
if transaction.TreasureChestId != nil {
|
||||||
treasureChestId = transaction.TreasureChestId.String()
|
treasureChestId = transaction.TreasureChestId.String()
|
||||||
}
|
}
|
||||||
value = displayBalance(transaction.Value)
|
value = formatFloat(transaction.Value)
|
||||||
|
|
||||||
id = transaction.Id.String()
|
id = transaction.Id.String()
|
||||||
cancelUrl = "/transaction/" + id
|
cancelUrl = "/transaction/" + id
|
||||||
@@ -250,9 +250,9 @@ templ TransactionItem(transaction *types.Transaction, accounts, treasureChests m
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
if transaction.Value < 0 {
|
if transaction.Value < 0 {
|
||||||
<p class="mr-8 min-w-22 text-right text-red-700">{ displayBalance(transaction.Value)+" €" }</p>
|
<p class="mr-8 min-w-22 text-right text-red-700">{ types.FormatEuros(transaction.Value) }</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="mr-8 w-22 text-right text-green-700">{ displayBalance(transaction.Value)+" €" }</p>
|
<p class="mr-8 w-22 text-right text-green-700">{ types.FormatEuros(transaction.Value) }</p>
|
||||||
}
|
}
|
||||||
<button
|
<button
|
||||||
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
|
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
|
||||||
@@ -280,11 +280,8 @@ templ TransactionItem(transaction *types.Transaction, accounts, treasureChests m
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayBalance(balance int64) string {
|
func formatFloat(balance int64) string {
|
||||||
|
|
||||||
euros := float64(balance) / 100
|
euros := float64(balance) / 100
|
||||||
return fmt.Sprintf("%.2f", euros)
|
return fmt.Sprintf("%.2f", euros)
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateReferences() {
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ templ TransactionRecurringItem(transactionRecurring *types.TransactionRecurring,
|
|||||||
Every <span class="text-xl">{ transactionRecurring.IntervalMonths }</span> month(s)
|
Every <span class="text-xl">{ transactionRecurring.IntervalMonths }</span> month(s)
|
||||||
</p>
|
</p>
|
||||||
if transactionRecurring.Value < 0 {
|
if transactionRecurring.Value < 0 {
|
||||||
<p class="text-right text-red-700">{ displayBalance(transactionRecurring.Value)+" €" }</p>
|
<p class="text-right text-red-700">{ types.FormatEuros(transactionRecurring.Value) }</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="text-right text-green-700">{ displayBalance(transactionRecurring.Value)+" €" }</p>
|
<p class="text-right text-green-700">{ types.FormatEuros(transactionRecurring.Value) }</p>
|
||||||
}
|
}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -104,7 +104,7 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
|
|||||||
}
|
}
|
||||||
party = transactionRecurring.Party
|
party = transactionRecurring.Party
|
||||||
description = transactionRecurring.Description
|
description = transactionRecurring.Description
|
||||||
value = displayBalance(transactionRecurring.Value)
|
value = formatFloat(transactionRecurring.Value)
|
||||||
|
|
||||||
id = transactionRecurring.Id.String()
|
id = transactionRecurring.Id.String()
|
||||||
}
|
}
|
||||||
@@ -201,11 +201,8 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayBalance(balance int64) string {
|
func formatFloat(balance int64) string {
|
||||||
|
|
||||||
euros := float64(balance) / 100
|
euros := float64(balance) / 100
|
||||||
return fmt.Sprintf("%.2f", euros)
|
return fmt.Sprintf("%.2f", euros)
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateReferences() {
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package treasurechest
|
package treasurechest
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
import "spend-sparrow/internal/template/svg"
|
import "spend-sparrow/internal/template/svg"
|
||||||
import "spend-sparrow/internal/types"
|
import "spend-sparrow/internal/types"
|
||||||
import "github.com/google/uuid"
|
import "github.com/google/uuid"
|
||||||
@@ -130,14 +129,14 @@ templ TreasureChestItem(treasureChest *types.TreasureChest, monthlySums map[uuid
|
|||||||
<p class="mr-auto">{ treasureChest.Name }</p>
|
<p class="mr-auto">{ treasureChest.Name }</p>
|
||||||
<p class="mr-20 text-gray-600">
|
<p class="mr-20 text-gray-600">
|
||||||
if treasureChest.ParentId != nil {
|
if treasureChest.ParentId != nil {
|
||||||
+ { displayBalance(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm"> per month</span>
|
+ { types.FormatEuros(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm"> per month</span>
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
if treasureChest.ParentId != nil {
|
if treasureChest.ParentId != nil {
|
||||||
if treasureChest.CurrentBalance < 0 {
|
if treasureChest.CurrentBalance < 0 {
|
||||||
<p class="mr-20 min-w-20 text-right text-red-700">{ displayBalance(treasureChest.CurrentBalance) }</p>
|
<p class="mr-20 min-w-20 text-right text-red-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="mr-20 min-w-20 text-right text-green-700">{ displayBalance(treasureChest.CurrentBalance) }</p>
|
<p class="mr-20 min-w-20 text-right text-green-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<a
|
<a
|
||||||
@@ -187,9 +186,3 @@ func filterNoChildNoSelf(nodes []*types.TreasureChest, selfId string) []*types.T
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayBalance(balance int64) string {
|
|
||||||
|
|
||||||
euros := float64(balance) / 100
|
|
||||||
return fmt.Sprintf("%.2f €", euros)
|
|
||||||
}
|
|
||||||
|
|||||||
26
internal/types/dashboard.go
Normal file
26
internal/types/dashboard.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Account | TreasureChest | Value | Description
|
||||||
|
// --------|---------------|-------|----------------
|
||||||
|
// Y | Y | + | Invalid
|
||||||
|
// Y | Y | - | Expense
|
||||||
|
// Y | N | + | Deposit
|
||||||
|
// Y | N | - | Withdrawal (for moving between accounts)
|
||||||
|
// N | Y | + | Saving
|
||||||
|
// N | Y | - | Withdrawal (for moving between treasure chests)
|
||||||
|
// N | N | + | Invalid
|
||||||
|
// N | N | - | Invalid
|
||||||
|
|
||||||
|
type DashboardMonthlySummary struct {
|
||||||
|
Month time.Time
|
||||||
|
// Sum of all Transactions with TreasureChests and no Accounts
|
||||||
|
Savings int64
|
||||||
|
// Sum of all Transactions with Accounts and no TreasureChests
|
||||||
|
Income int64
|
||||||
|
// Sum of all Transactions with Accounts and TreasureChests
|
||||||
|
Expenses int64
|
||||||
|
// Income - Expenses
|
||||||
|
Total int64
|
||||||
|
}
|
||||||
37
internal/types/format.go
Normal file
37
internal/types/format.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FormatEuros(balance int64) string {
|
||||||
|
|
||||||
|
prefix := ""
|
||||||
|
if balance < 0 {
|
||||||
|
prefix = "- "
|
||||||
|
balance = -balance
|
||||||
|
}
|
||||||
|
|
||||||
|
n := float64(balance) / 100
|
||||||
|
s := fmt.Sprintf("%.2f", n) // "1234567.89"
|
||||||
|
|
||||||
|
parts := strings.Split(s, ".")
|
||||||
|
intPart := parts[0]
|
||||||
|
fracPart := parts[1]
|
||||||
|
|
||||||
|
var result strings.Builder
|
||||||
|
numberOfSeperators := len(intPart) % 3
|
||||||
|
if numberOfSeperators == 0 {
|
||||||
|
result.WriteString(intPart)
|
||||||
|
} else {
|
||||||
|
for i := range intPart {
|
||||||
|
if i > 0 && (i-numberOfSeperators)%3 == 0 {
|
||||||
|
result.WriteString(",")
|
||||||
|
}
|
||||||
|
result.WriteByte(intPart[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix + result.String() + "." + fracPart + " €"
|
||||||
|
}
|
||||||
@@ -13,6 +13,19 @@ import (
|
|||||||
//
|
//
|
||||||
// If it becomes necessary to precalculate snapshots for performance reasons, this can be done in the future.
|
// If it becomes necessary to precalculate snapshots for performance reasons, this can be done in the future.
|
||||||
// But the transaction should always be the source of truth.
|
// But the transaction should always be the source of truth.
|
||||||
|
//
|
||||||
|
// There are the following constallations and their explanation:
|
||||||
|
//
|
||||||
|
// Account | TreasureChest | Value | Description
|
||||||
|
// --------|---------------|-------|----------------
|
||||||
|
// Y | Y | + | Invalid
|
||||||
|
// Y | Y | - | Expense
|
||||||
|
// Y | N | + | Deposit
|
||||||
|
// Y | N | - | Withdrawal (for moving between accounts)
|
||||||
|
// N | Y | + | Saving
|
||||||
|
// N | Y | - | Withdrawal (for moving between treasure chests)
|
||||||
|
// N | N | + | Invalid
|
||||||
|
// N | N | - | Invalid
|
||||||
type Transaction struct {
|
type Transaction struct {
|
||||||
Id uuid.UUID `db:"id"`
|
Id uuid.UUID `db:"id"`
|
||||||
UserId uuid.UUID `db:"user_id"`
|
UserId uuid.UUID `db:"user_id"`
|
||||||
|
|||||||
Reference in New Issue
Block a user