feat(dashboard): #82 add chart for sum of account and savings
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,6 +31,7 @@ secrets/
|
|||||||
node_modules/
|
node_modules/
|
||||||
static/css/tailwind.css
|
static/css/tailwind.css
|
||||||
static/js/htmx.min.js
|
static/js/htmx.min.js
|
||||||
|
static/js/echarts.min.js
|
||||||
tmp/
|
tmp/
|
||||||
|
|
||||||
mocks/*
|
mocks/*
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
dashboardService := service.NewDashboard(d)
|
dashboardService := service.NewDashboard(d)
|
||||||
|
|
||||||
render := handler.NewRender()
|
render := handler.NewRender()
|
||||||
indexHandler := handler.NewIndex(render, dashboardService, clockService)
|
indexHandler := handler.NewIndex(render, clockService)
|
||||||
|
dashboardHandler := handler.NewDashboard(render, dashboardService, clockService)
|
||||||
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)
|
||||||
@@ -130,6 +131,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
go dailyTaskTimer(ctx, transactionRecurringService, authService)
|
go dailyTaskTimer(ctx, transactionRecurringService, authService)
|
||||||
|
|
||||||
indexHandler.Handle(router)
|
indexHandler.Handle(router)
|
||||||
|
dashboardHandler.Handle(router)
|
||||||
accountHandler.Handle(router)
|
accountHandler.Handle(router)
|
||||||
treasureChestHandler.Handle(router)
|
treasureChestHandler.Handle(router)
|
||||||
authHandler.Handle(router)
|
authHandler.Handle(router)
|
||||||
|
|||||||
145
internal/handler/dashboard.go
Normal file
145
internal/handler/dashboard.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"spend-sparrow/internal/handler/middleware"
|
||||||
|
"spend-sparrow/internal/service"
|
||||||
|
"spend-sparrow/internal/template/dashboard"
|
||||||
|
"spend-sparrow/internal/types"
|
||||||
|
"spend-sparrow/internal/utils"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dashboard interface {
|
||||||
|
Handle(router *http.ServeMux)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardImpl struct {
|
||||||
|
r *Render
|
||||||
|
d *service.Dashboard
|
||||||
|
c service.Clock
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDashboard(r *Render, d *service.Dashboard, c service.Clock) Dashboard {
|
||||||
|
return DashboardImpl{
|
||||||
|
r: r,
|
||||||
|
d: d,
|
||||||
|
c: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler DashboardImpl) Handle(router *http.ServeMux) {
|
||||||
|
router.Handle("GET /dashboard", handler.handleDashboard())
|
||||||
|
router.Handle("GET /dashboard/dataset", handler.handleDashboardDataset())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler DashboardImpl) handleDashboard() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
updateSpan(r)
|
||||||
|
|
||||||
|
user := middleware.GetUser(r)
|
||||||
|
if user == nil {
|
||||||
|
utils.DoRedirect(w, r, "/auth/signin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var comp templ.Component
|
||||||
|
htmx := utils.IsHtmx(r)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
comp, err = handler.dashboard(user, r)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(r.Context(), "Failed to get dashboard summary", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if htmx {
|
||||||
|
handler.r.RenderWithStatus(r, w, comp, http.StatusOK)
|
||||||
|
} else {
|
||||||
|
handler.r.RenderLayoutWithStatus(r, w, comp, user, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler DashboardImpl) handleDashboardDataset() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
updateSpan(r)
|
||||||
|
|
||||||
|
user := middleware.GetUser(r)
|
||||||
|
|
||||||
|
series, err := handler.d.MainChart(r.Context(), user)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
account := ""
|
||||||
|
savings := ""
|
||||||
|
|
||||||
|
for _, entry := range series {
|
||||||
|
account += fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100)
|
||||||
|
savings += fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Savings)/100)
|
||||||
|
}
|
||||||
|
account = account[:len(account)-1]
|
||||||
|
savings = savings[:len(savings)-1]
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(w, `
|
||||||
|
{
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": "axis",
|
||||||
|
"formatter": "<updated by client>"
|
||||||
|
},
|
||||||
|
"xAxis": {
|
||||||
|
"type": "time"
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"axisLabel": {
|
||||||
|
"formatter": "{value} €"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"data": [%s],
|
||||||
|
"type": "line",
|
||||||
|
"name": "Account Value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": [%s],
|
||||||
|
"type": "line",
|
||||||
|
"name": "Savings"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`, account, savings)
|
||||||
|
if err != nil {
|
||||||
|
slog.InfoContext(r.Context(), "could not write response", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler DashboardImpl) dashboard(user *types.User, 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 = handler.c.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := handler.d.Summary(r.Context(), user, month)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashboard.Dashboard(summary), nil
|
||||||
|
}
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
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/service"
|
||||||
"spend-sparrow/internal/template"
|
"spend-sparrow/internal/template"
|
||||||
"spend-sparrow/internal/template/dashboard"
|
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"spend-sparrow/internal/utils"
|
"spend-sparrow/internal/utils"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
)
|
)
|
||||||
@@ -21,14 +16,12 @@ type Index interface {
|
|||||||
|
|
||||||
type IndexImpl struct {
|
type IndexImpl struct {
|
||||||
r *Render
|
r *Render
|
||||||
d *service.Dashboard
|
|
||||||
c service.Clock
|
c service.Clock
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndex(r *Render, d *service.Dashboard, c service.Clock) Index {
|
func NewIndex(r *Render, c service.Clock) Index {
|
||||||
return IndexImpl{
|
return IndexImpl{
|
||||||
r: r,
|
r: r,
|
||||||
d: d,
|
|
||||||
c: c,
|
c: c,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,11 +47,8 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
|
|||||||
status = http.StatusNotFound
|
status = http.StatusNotFound
|
||||||
} else {
|
} else {
|
||||||
if user != nil {
|
if user != nil {
|
||||||
var err error
|
utils.DoRedirect(w, r, "/dashboard")
|
||||||
comp, err = handler.dashboard(user, htmx, r)
|
return
|
||||||
if err != nil {
|
|
||||||
slog.ErrorContext(r.Context(), "Failed to get dashboard summary", "err", err)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
comp = template.Index()
|
comp = template.Index()
|
||||||
}
|
}
|
||||||
@@ -73,35 +63,8 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = handler.c.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler IndexImpl) handleEmpty() http.HandlerFunc {
|
func (handler IndexImpl) handleEmpty() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
updateSpan(r)
|
// do nothing
|
||||||
|
|
||||||
// Return nothing
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,3 +109,60 @@ func (s Dashboard) Summary(ctx context.Context, user *types.User, month time.Tim
|
|||||||
|
|
||||||
return &summary, nil
|
return &summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Dashboard) MainChart(
|
||||||
|
ctx context.Context,
|
||||||
|
user *types.User,
|
||||||
|
) ([]types.DashboardMainChartEntry, error) {
|
||||||
|
if user == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions := make([]types.Transaction, 0)
|
||||||
|
err := s.db.SelectContext(ctx, &transactions, `
|
||||||
|
SELECT *
|
||||||
|
FROM "transaction"
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY timestamp`, user.Id)
|
||||||
|
err = db.TransformAndLogDbError(ctx, "dashboard Chart", nil, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeEntries := make([]types.DashboardMainChartEntry, 0)
|
||||||
|
var lastEntry *types.DashboardMainChartEntry
|
||||||
|
for _, t := range transactions {
|
||||||
|
if t.Error != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newDay := t.Timestamp.Truncate(24 * time.Hour)
|
||||||
|
if lastEntry == nil {
|
||||||
|
lastEntry = &types.DashboardMainChartEntry{
|
||||||
|
Day: newDay,
|
||||||
|
Value: 0,
|
||||||
|
Savings: 0,
|
||||||
|
}
|
||||||
|
} else if lastEntry.Day != newDay {
|
||||||
|
timeEntries = append(timeEntries, *lastEntry)
|
||||||
|
lastEntry = &types.DashboardMainChartEntry{
|
||||||
|
Day: newDay,
|
||||||
|
Value: lastEntry.Value,
|
||||||
|
Savings: lastEntry.Savings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.AccountId != nil {
|
||||||
|
lastEntry.Value += t.Value
|
||||||
|
}
|
||||||
|
if t.TreasureChestId != nil {
|
||||||
|
lastEntry.Savings += t.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastEntry != nil {
|
||||||
|
timeEntries = append(timeEntries, *lastEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeEntries, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,51 +3,7 @@ package dashboard
|
|||||||
import "spend-sparrow/internal/types"
|
import "spend-sparrow/internal/types"
|
||||||
|
|
||||||
templ Dashboard(summary *types.DashboardMonthlySummary) {
|
templ Dashboard(summary *types.DashboardMonthlySummary) {
|
||||||
<div class="mt-10">
|
<div class="mt-10 h-full">
|
||||||
<form hx-get="/" hx-target="#dashboard" hx-trigger="change" hx-push-url="true">
|
<div id="graph" class="h-96"></div>
|
||||||
<label for="month">Select Month:</label>
|
|
||||||
<input
|
|
||||||
name="month"
|
|
||||||
type="date"
|
|
||||||
class="input datetime"
|
|
||||||
value={ summary.Month.String() }
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<div id="dashboard">
|
|
||||||
@DashboardData(summary)
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
<section class="grid grid-cols-[auto_auto_auto_1fr] gap-4">
|
|
||||||
<span>Total Savings</span>
|
|
||||||
<span>Total Account Balance</span>
|
|
||||||
<span>Net</span>
|
|
||||||
<span></span>
|
|
||||||
<span>{ types.FormatEuros(summary.SumOfSavings) }</span>
|
|
||||||
<span>{ types.FormatEuros(summary.SumOfAccounts) }</span>
|
|
||||||
@balance(summary.SumOfAccounts - summary.SumOfSavings)
|
|
||||||
</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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path str
|
|||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/toast.js"></script>
|
<script src="/static/js/toast.js"></script>
|
||||||
<script src="/static/js/time.js"></script>
|
<script src="/static/js/time.js"></script>
|
||||||
|
<script src="/static/js/echarts.min.js"></script>
|
||||||
|
<script src="/static/js/dashboard.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="h-screen flex flex-col" hx-headers='{"Csrf-Token": "CSRF_TOKEN"}'>
|
<body class="h-screen flex flex-col" hx-headers='{"Csrf-Token": "CSRF_TOKEN"}'>
|
||||||
// Header
|
// Header
|
||||||
@@ -36,7 +38,7 @@ templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path str
|
|||||||
<span class="text-4xl font-bold font-pirata">SpendSparrow</span>
|
<span class="text-4xl font-bold font-pirata">SpendSparrow</span>
|
||||||
</a>
|
</a>
|
||||||
if loggedIn {
|
if loggedIn {
|
||||||
<a class={ layoutLinkClass(path == "/") } href="/">Dashboard</a>
|
<a class={ layoutLinkClass(path == "/dashboard") } href="/dashboard">Dashboard</a>
|
||||||
<a class={ layoutLinkClass(path == "/transaction") } href="/transaction">Transaction</a>
|
<a class={ layoutLinkClass(path == "/transaction") } href="/transaction">Transaction</a>
|
||||||
<a class={ layoutLinkClass(path == "/treasurechest") } href="/treasurechest">Treasure Chest</a>
|
<a class={ layoutLinkClass(path == "/treasurechest") } href="/treasurechest">Treasure Chest</a>
|
||||||
<a class={ layoutLinkClass(path == "/account") } href="/account">Account</a>
|
<a class={ layoutLinkClass(path == "/account") } href="/account">Account</a>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package template
|
package template
|
||||||
|
|
||||||
templ Index() {
|
templ Index() {
|
||||||
<!-- <div class="h-full flex flex-col items-center justify-center"> -->
|
|
||||||
<div class="h-full flex flex-col items-center justify-center">
|
<div class="h-full flex flex-col items-center justify-center">
|
||||||
<h1 class="flex gap-2 w-full justify-center">
|
<h1 class="flex gap-2 w-full justify-center">
|
||||||
<img class="w-24" src="/static/favicon.svg" alt="SpendSparrow logo"/>
|
<img class="w-24" src="/static/favicon.svg" alt="SpendSparrow logo"/>
|
||||||
|
|||||||
@@ -16,3 +16,9 @@ type DashboardMonthlySummary struct {
|
|||||||
SumOfSavings int64
|
SumOfSavings int64
|
||||||
SumOfAccounts int64
|
SumOfAccounts int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DashboardMainChartEntry struct {
|
||||||
|
Day time.Time
|
||||||
|
Value int64
|
||||||
|
Savings int64
|
||||||
|
}
|
||||||
|
|||||||
28
package-lock.json
generated
28
package-lock.json
generated
@@ -8,6 +8,9 @@
|
|||||||
"name": "spend-sparrow",
|
"name": "spend-sparrow",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"echarts": "^5.6.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/cli": "4.1.10",
|
"@tailwindcss/cli": "4.1.10",
|
||||||
"htmx.org": "2.0.4",
|
"htmx.org": "2.0.4",
|
||||||
@@ -790,6 +793,16 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "5.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.1",
|
"version": "5.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||||
@@ -1281,6 +1294,12 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
@@ -1290,6 +1309,15 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "5.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
|
||||||
|
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "spend-sparrow",
|
"name": "spend-sparrow",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Your (almost) independent tech stack to host on a VPC.",
|
"description": "Personal finance tracking done right",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"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",
|
"build": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && cp -f node_modules/echarts/dist/echarts.min.js static/js/echarts.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 && cp -f node_modules/echarts/dist/echarts.min.js static/js/echarts.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/cli": "4.1.10",
|
||||||
"htmx.org": "2.0.4",
|
"htmx.org": "2.0.4",
|
||||||
"tailwindcss": "4.1.10",
|
"tailwindcss": "4.1.10",
|
||||||
"@tailwindcss/cli": "4.1.10"
|
"echarts": "5.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
static/js/dashboard.js
Normal file
38
static/js/dashboard.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Initialize the echarts instance based on the prepared dom
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const element = document.getElementById('graph')
|
||||||
|
if (element === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var myChart = echarts.init(element);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/dashboard/dataset");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Response status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = await response.json();
|
||||||
|
option.tooltip.formatter = function (params) {
|
||||||
|
return new Date(params[0].data[0]).toLocaleString([], { day: 'numeric', month: 'short', year: 'numeric' }) +
|
||||||
|
'<br />' +
|
||||||
|
'Sum of Accounts: <span class="font-bold">' + params[0].data[1] + '</span> € <br />' +
|
||||||
|
'Sum of Savings: <span class="font-bold">' + params[1].data[1] + '</span> €'
|
||||||
|
};
|
||||||
|
|
||||||
|
const chart = myChart.setOption(option);
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
myChart.resize();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("initialized charts");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the chart using the configuration items and data just specified.
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
45
static/js/echarts.min.js
vendored
Normal file
45
static/js/echarts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
45
static/js/echarts.simple.min.js
vendored
Normal file
45
static/js/echarts.simple.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -4,7 +4,6 @@ htmx.on("htmx:afterSwap", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
console.log("DOMContentLoaded");
|
|
||||||
updateTime(document);
|
updateTime(document);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user