Fix amount for available balance #50
135
Taskfile.yml
135
Taskfile.yml
@ -5,30 +5,31 @@ vars:
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
default:
|
default:
|
||||||
|
desc: Build budgeteer in production mode
|
||||||
|
deps: [frontend, go-mod, go-sqlc]
|
||||||
cmds:
|
cmds:
|
||||||
- task: build-prod
|
- task: backend
|
||||||
|
|
||||||
sqlc:
|
run:
|
||||||
desc: sqlc code generation
|
desc: Start budgeteer
|
||||||
sources:
|
deps: [backend, go-mod, go-sqlc]
|
||||||
- ./sqlc.yaml
|
|
||||||
- ./postgres/schema/*
|
|
||||||
- ./postgres/queries/*
|
|
||||||
generates:
|
|
||||||
- ./postgres/*.sql.go
|
|
||||||
cmds:
|
cmds:
|
||||||
- sqlc generate
|
- ./build/budgeteer{{exeExt}}
|
||||||
|
|
||||||
gomod:
|
dev:
|
||||||
desc: Go modules
|
desc: Build budgeteer in dev mode (without frontend)
|
||||||
sources:
|
deps: [go-mod, go-sqlc]
|
||||||
- ./go.mod
|
|
||||||
- ./go.sum
|
|
||||||
method: checksum
|
|
||||||
cmds:
|
cmds:
|
||||||
- go mod download
|
- task: backend
|
||||||
|
|
||||||
build:
|
ci:
|
||||||
|
desc: Run CI build
|
||||||
|
deps: [default, static]
|
||||||
|
|
||||||
|
static:
|
||||||
|
deps: [go-lint, go-vet, go-fmt, js-tsc, js-lint, cover]
|
||||||
|
|
||||||
|
backend:
|
||||||
desc: Build budgeteer
|
desc: Build budgeteer
|
||||||
sources:
|
sources:
|
||||||
- ./go.mod
|
- ./go.mod
|
||||||
@ -43,29 +44,37 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- go build -o ./build/budgeteer{{exeExt}} ./cmd/budgeteer
|
- go build -o ./build/budgeteer{{exeExt}} ./cmd/budgeteer
|
||||||
|
|
||||||
build-dev:
|
go-vet:
|
||||||
desc: Build budgeteer in dev mode
|
|
||||||
deps: [gomod, sqlc]
|
|
||||||
cmds:
|
cmds:
|
||||||
- go vet
|
- go vet
|
||||||
- go fmt
|
|
||||||
- golangci-lint run
|
|
||||||
- task: build
|
|
||||||
|
|
||||||
build-prod:
|
go-fmt:
|
||||||
desc: Build budgeteer in prod mode
|
|
||||||
deps: [gomod, sqlc, frontend]
|
|
||||||
cmds:
|
cmds:
|
||||||
- go vet
|
|
||||||
- go fmt
|
- go fmt
|
||||||
- golangci-lint run
|
|
||||||
- task: build
|
|
||||||
|
|
||||||
ci:
|
go-lint:
|
||||||
desc: Run CI build
|
|
||||||
cmds:
|
cmds:
|
||||||
- task: build-prod
|
- golangci-lint run
|
||||||
- task: cover
|
|
||||||
|
go-sqlc:
|
||||||
|
desc: sqlc code generation
|
||||||
|
sources:
|
||||||
|
- ./sqlc.yaml
|
||||||
|
- ./postgres/schema/*
|
||||||
|
- ./postgres/queries/*
|
||||||
|
generates:
|
||||||
|
- ./postgres/*.sql.go
|
||||||
|
cmds:
|
||||||
|
- sqlc generate
|
||||||
|
|
||||||
|
go-mod:
|
||||||
|
desc: Go modules
|
||||||
|
sources:
|
||||||
|
- ./go.mod
|
||||||
|
- ./go.sum
|
||||||
|
method: checksum
|
||||||
|
cmds:
|
||||||
|
- go mod download
|
||||||
|
|
||||||
cover:
|
cover:
|
||||||
desc: Run test and analyze coverage
|
desc: Run test and analyze coverage
|
||||||
@ -75,26 +84,46 @@ tasks:
|
|||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
desc: Build vue frontend
|
desc: Build vue frontend
|
||||||
dir: web
|
deps: [js-build]
|
||||||
sources:
|
sources:
|
||||||
- web/src/**/*
|
- web/src/**/*
|
||||||
generates:
|
generates:
|
||||||
- web/dist/**/*
|
- web/dist/**/*
|
||||||
|
|
||||||
|
frontend-dev:
|
||||||
|
desc: Run dev-server for frontend
|
||||||
|
dir: web
|
||||||
|
cmds:
|
||||||
|
- yarn run dev
|
||||||
|
|
||||||
|
js-build:
|
||||||
|
dir: web
|
||||||
|
deps: [js-mod]
|
||||||
|
cmds:
|
||||||
|
- yarn build
|
||||||
|
|
||||||
|
js-mod:
|
||||||
|
run: once
|
||||||
|
sources:
|
||||||
|
- web/src/package.json
|
||||||
|
- web/src/yarn.lock
|
||||||
|
generates:
|
||||||
|
- web/node_modules/**/*
|
||||||
|
dir: web
|
||||||
cmds:
|
cmds:
|
||||||
- yarn
|
- yarn
|
||||||
- yarn build
|
|
||||||
- yarn run vue-tsc --noEmit
|
|
||||||
- yarn run eslint "./src/**"
|
|
||||||
|
|
||||||
docker:
|
js-tsc:
|
||||||
desc: Build budgeeter:latest
|
dir: web
|
||||||
deps: [build-prod]
|
deps: [js-mod]
|
||||||
sources:
|
|
||||||
- ./build/budgeteer{{exeExt}}
|
|
||||||
- ./build/Dockerfile
|
|
||||||
cmds:
|
cmds:
|
||||||
- docker build -t {{.IMAGE_NAME}}:latest ./build
|
- yarn run vue-tsc --noEmit
|
||||||
- docker push {{.IMAGE_NAME}}:latest
|
|
||||||
|
js-lint:
|
||||||
|
dir: web
|
||||||
|
deps: [js-mod]
|
||||||
|
cmds:
|
||||||
|
- yarn run eslint "./src/**"
|
||||||
|
|
||||||
dev-docker:
|
dev-docker:
|
||||||
desc: Build budgeeter:dev
|
desc: Build budgeeter:dev
|
||||||
@ -106,14 +135,8 @@ tasks:
|
|||||||
- docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile.dev
|
- docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile.dev
|
||||||
- docker push {{.IMAGE_NAME}}:dev
|
- docker push {{.IMAGE_NAME}}:dev
|
||||||
|
|
||||||
run:
|
run-dev:
|
||||||
desc: Start budgeteer
|
desc: Run dev environment in docker
|
||||||
deps: [build-dev]
|
deps: [dev-docker]
|
||||||
cmds:
|
cmds:
|
||||||
- ./build/budgeteer{{exeExt}}
|
- docker-compose -f docker/docker-compose.dev.yml -p budgeteer up -d
|
||||||
|
|
||||||
rundocker:
|
|
||||||
desc: Start docker-compose
|
|
||||||
deps: [docker]
|
|
||||||
cmds:
|
|
||||||
- docker-compose up -d
|
|
@ -17,22 +17,22 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
cfg, err := config.LoadConfig()
|
cfg, err := config.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Could not load config: %v", err)
|
log.Fatalf("load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
queries, err := postgres.Connect("pgx", cfg.DatabaseConnection)
|
queries, err := postgres.Connect("pgx", cfg.DatabaseConnection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed connecting to DB: %v", err)
|
log.Fatalf("connect to database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
static, err := fs.Sub(web.Static, "dist")
|
static, err := fs.Sub(web.Static, "dist")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("couldn't open static files")
|
panic("open static files")
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret)
|
tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("couldn't create token verifier: %w", err))
|
panic(fmt.Errorf("create token verifier: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := &server.Handler{
|
handler := &server.Handler{
|
||||||
|
@ -6,10 +6,10 @@ RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
|||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
RUN apk --no-cache add go nodejs yarn bash curl git git-perl
|
RUN apk --no-cache add go nodejs yarn bash curl git git-perl
|
||||||
RUN yarn global add @vue/cli
|
|
||||||
ENV PATH="/root/.yarn/bin/:${PATH}"
|
ENV PATH="/root/.yarn/bin/:${PATH}"
|
||||||
WORKDIR /src/web
|
WORKDIR /src/web
|
||||||
ADD web/package.json web/yarn.lock /src/web/
|
ADD web/package.json web/yarn.lock /src/web/
|
||||||
RUN yarn
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
VOLUME /go
|
||||||
|
VOLUME /.cache
|
||||||
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/
|
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/
|
@ -6,11 +6,10 @@ services:
|
|||||||
command: task -w run
|
command: task -w run
|
||||||
ports:
|
ports:
|
||||||
- 1323:1323
|
- 1323:1323
|
||||||
user: '1000'
|
|
||||||
volumes:
|
volumes:
|
||||||
- ~/budgeteer:/src
|
- ~/budgeteer:/src
|
||||||
- ~/.go:/go
|
- go-cache:/go
|
||||||
- ~/.cache:/.cache
|
- yarn-cache:/.cache
|
||||||
environment:
|
environment:
|
||||||
BUDGETEER_DB: postgres://budgeteer:budgeteer@db:5432/budgeteer
|
BUDGETEER_DB: postgres://budgeteer:budgeteer@db:5432/budgeteer
|
||||||
BUDGETEER_SESSION_SECRET: random string for JWT authorization
|
BUDGETEER_SESSION_SECRET: random string for JWT authorization
|
||||||
@ -19,13 +18,11 @@ services:
|
|||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: hub.javil.eu/budgeteer:dev
|
image: hub.javil.eu/budgeteer:dev
|
||||||
command: bash -c "cd web; yarn run dev"
|
command: task frontend-dev
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
user: '1000'
|
|
||||||
volumes:
|
volumes:
|
||||||
- ~/budgeteer:/src
|
- ~/budgeteer:/src
|
||||||
- ~/.cache:/.cache
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|
||||||
@ -49,3 +46,5 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db:
|
db:
|
||||||
|
go-cache:
|
||||||
|
yarn-cache:
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
// source: accounts.sql
|
// source: accounts.sql
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
// source: assignments.sql
|
// source: assignments.sql
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
// source: budgets.sql
|
// source: budgets.sql
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
@ -59,5 +59,6 @@ func (s *Database) NewBudget(context context.Context, name string, userID uuid.U
|
|||||||
return nil, fmt.Errorf("commit: %w", err)
|
return nil, fmt.Errorf("commit: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
budget.IncomeCategoryID = cat.ID
|
||||||
return &budget, nil
|
return &budget, nil
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
// source: categories.sql
|
// source: categories.sql
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
// source: cumultative-balances.sql
|
// source: cumultative-balances.sql
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
@ -13,21 +15,19 @@ import (
|
|||||||
|
|
||||||
const getCumultativeBalances = `-- name: GetCumultativeBalances :many
|
const getCumultativeBalances = `-- name: GetCumultativeBalances :many
|
||||||
SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id),
|
SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id),
|
||||||
COALESCE(ass.amount, 0)::decimal(12,2) as assignments, SUM(ass.amount) OVER (PARTITION BY ass.category_id ORDER BY ass.date)::decimal(12,2) as assignments_cum,
|
COALESCE(ass.amount, 0)::decimal(12,2) as assignments,
|
||||||
COALESCE(tra.amount, 0)::decimal(12,2) as transactions, SUM(tra.amount) OVER (PARTITION BY tra.category_id ORDER BY tra.date)::decimal(12,2) as transactions_cum
|
COALESCE(tra.amount, 0)::decimal(12,2) as transactions
|
||||||
FROM assignments_by_month as ass
|
FROM assignments_by_month as ass
|
||||||
FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id
|
FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id
|
||||||
WHERE (ass.budget_id IS NULL OR ass.budget_id = $1) AND (tra.budget_id IS NULL OR tra.budget_id = $1)
|
WHERE (ass.budget_id IS NULL OR ass.budget_id = $1) AND (tra.budget_id IS NULL OR tra.budget_id = $1)
|
||||||
ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id)
|
ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.amount, tra.amount)
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetCumultativeBalancesRow struct {
|
type GetCumultativeBalancesRow struct {
|
||||||
Date time.Time
|
Date time.Time
|
||||||
CategoryID uuid.UUID
|
CategoryID uuid.UUID
|
||||||
Assignments numeric.Numeric
|
Assignments numeric.Numeric
|
||||||
AssignmentsCum numeric.Numeric
|
|
||||||
Transactions numeric.Numeric
|
Transactions numeric.Numeric
|
||||||
TransactionsCum numeric.Numeric
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetCumultativeBalances(ctx context.Context, budgetID uuid.UUID) ([]GetCumultativeBalancesRow, error) {
|
func (q *Queries) GetCumultativeBalances(ctx context.Context, budgetID uuid.UUID) ([]GetCumultativeBalancesRow, error) {
|
||||||
@ -43,9 +43,7 @@ func (q *Queries) GetCumultativeBalances(ctx context.Context, budgetID uuid.UUID
|
|||||||
&i.Date,
|
&i.Date,
|
||||||
&i.CategoryID,
|
&i.CategoryID,
|
||||||
&i.Assignments,
|
&i.Assignments,
|
||||||
&i.AssignmentsCum,
|
|
||||||
&i.Transactions,
|
&i.Transactions,
|
||||||
&i.TransactionsCum,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
|
||||||
|
@ -25,6 +25,13 @@ func FromInt64WithExp(value int64, exp int32) Numeric {
|
|||||||
return Numeric{Numeric: pgtype.Numeric{Int: big.NewInt(value), Exp: exp, Status: pgtype.Present}}
|
return Numeric{Numeric: pgtype.Numeric{Int: big.NewInt(value), Exp: exp, Status: pgtype.Present}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Numeric) SetZero() {
|
||||||
|
n.Exp = 0
|
||||||
|
n.Int = big.NewInt(0)
|
||||||
|
n.Status = pgtype.Present
|
||||||
|
n.NaN = false
|
||||||
|
}
|
||||||
|
|
||||||
func (n Numeric) GetFloat64() float64 {
|
func (n Numeric) GetFloat64() float64 {
|
||||||
if n.Status != pgtype.Present {
|
if n.Status != pgtype.Present {
|
||||||
return 0
|
return 0
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
// source: payees.sql
|
// source: payees.sql
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
-- name: GetCumultativeBalances :many
|
-- name: GetCumultativeBalances :many
|
||||||
SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id),
|
SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id),
|
||||||
COALESCE(ass.amount, 0)::decimal(12,2) as assignments, SUM(ass.amount) OVER (PARTITION BY ass.category_id ORDER BY ass.date)::decimal(12,2) as assignments_cum,
|
COALESCE(ass.amount, 0)::decimal(12,2) as assignments,
|
||||||
COALESCE(tra.amount, 0)::decimal(12,2) as transactions, SUM(tra.amount) OVER (PARTITION BY tra.category_id ORDER BY tra.date)::decimal(12,2) as transactions_cum
|
COALESCE(tra.amount, 0)::decimal(12,2) as transactions
|
||||||
FROM assignments_by_month as ass
|
FROM assignments_by_month as ass
|
||||||
FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id
|
FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id
|
||||||
WHERE (ass.budget_id IS NULL OR ass.budget_id = @budget_id) AND (tra.budget_id IS NULL OR tra.budget_id = @budget_id)
|
WHERE (ass.budget_id IS NULL OR ass.budget_id = @budget_id) AND (tra.budget_id IS NULL OR tra.budget_id = @budget_id)
|
||||||
ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id);
|
ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.amount, tra.amount);
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
// source: transactions.sql
|
// source: transactions.sql
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
// source: user_budgets.sql
|
// source: user_budgets.sql
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
// source: users.sql
|
// source: users.sql
|
||||||
|
|
||||||
package postgres
|
package postgres
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
|
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
|
||||||
@ -12,20 +11,9 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getFirstOfMonth(year, month int, location *time.Location) time.Time {
|
|
||||||
return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, location)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFirstOfMonthTime(date time.Time) time.Time {
|
|
||||||
var monthM time.Month
|
|
||||||
year, monthM, _ := date.Date()
|
|
||||||
month := int(monthM)
|
|
||||||
return getFirstOfMonth(year, month, date.Location())
|
|
||||||
}
|
|
||||||
|
|
||||||
type CategoryWithBalance struct {
|
type CategoryWithBalance struct {
|
||||||
*postgres.GetCategoriesRow
|
*postgres.GetCategoriesRow
|
||||||
Available numeric.Numeric
|
AvailableLastMonth numeric.Numeric
|
||||||
Activity numeric.Numeric
|
Activity numeric.Numeric
|
||||||
Assigned numeric.Numeric
|
Assigned numeric.Numeric
|
||||||
}
|
}
|
||||||
@ -33,7 +21,7 @@ type CategoryWithBalance struct {
|
|||||||
func NewCategoryWithBalance(category *postgres.GetCategoriesRow) CategoryWithBalance {
|
func NewCategoryWithBalance(category *postgres.GetCategoriesRow) CategoryWithBalance {
|
||||||
return CategoryWithBalance{
|
return CategoryWithBalance{
|
||||||
GetCategoriesRow: category,
|
GetCategoriesRow: category,
|
||||||
Available: numeric.Zero(),
|
AvailableLastMonth: numeric.Zero(),
|
||||||
Activity: numeric.Zero(),
|
Activity: numeric.Zero(),
|
||||||
Assigned: numeric.Zero(),
|
Assigned: numeric.Zero(),
|
||||||
}
|
}
|
||||||
@ -53,13 +41,13 @@ func (h *Handler) budgetingForMonth(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
firstOfMonth, err := getDate(c)
|
month, err := getDate(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budget.ID.String())
|
c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budget.ID.String())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := h.prepareBudgeting(c.Request.Context(), budget, firstOfMonth)
|
data, err := h.getBudgetingViewForMonth(c.Request.Context(), budget, month)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@ -67,8 +55,7 @@ func (h *Handler) budgetingForMonth(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, data)
|
c.JSON(http.StatusOK, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) prepareBudgeting(ctx context.Context, budget postgres.Budget, firstOfMonth time.Time) (BudgetingForMonthResponse, error) {
|
func (h *Handler) getBudgetingViewForMonth(ctx context.Context, budget postgres.Budget, month Month) (BudgetingForMonthResponse, error) {
|
||||||
firstOfNextMonth := firstOfMonth.AddDate(0, 1, 0)
|
|
||||||
categories, err := h.Service.GetCategories(ctx, budget.ID)
|
categories, err := h.Service.GetCategories(ctx, budget.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return BudgetingForMonthResponse{}, fmt.Errorf("error loading categories: %w", err)
|
return BudgetingForMonthResponse{}, fmt.Errorf("error loading categories: %w", err)
|
||||||
@ -79,8 +66,8 @@ func (h *Handler) prepareBudgeting(ctx context.Context, budget postgres.Budget,
|
|||||||
return BudgetingForMonthResponse{}, fmt.Errorf("error loading balances: %w", err)
|
return BudgetingForMonthResponse{}, fmt.Errorf("error loading balances: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
categoriesWithBalance, moneyUsed := h.calculateBalances(firstOfNextMonth, firstOfMonth, categories, cumultativeBalances)
|
categoriesWithBalance, moneyUsed := h.calculateBalances(budget, month, categories, cumultativeBalances)
|
||||||
availableBalance := h.getAvailableBalance(budget, moneyUsed, cumultativeBalances, categoriesWithBalance, firstOfNextMonth)
|
availableBalance := h.getAvailableBalance(budget, month, moneyUsed, cumultativeBalances)
|
||||||
|
|
||||||
data := BudgetingForMonthResponse{categoriesWithBalance, availableBalance}
|
data := BudgetingForMonthResponse{categoriesWithBalance, availableBalance}
|
||||||
return data, nil
|
return data, nil
|
||||||
@ -91,9 +78,8 @@ type BudgetingForMonthResponse struct {
|
|||||||
AvailableBalance numeric.Numeric
|
AvailableBalance numeric.Numeric
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Handler) getAvailableBalance(budget postgres.Budget,
|
func (*Handler) getAvailableBalance(budget postgres.Budget, month Month,
|
||||||
moneyUsed numeric.Numeric, cumultativeBalances []postgres.GetCumultativeBalancesRow,
|
moneyUsed numeric.Numeric, cumultativeBalances []postgres.GetCumultativeBalancesRow,
|
||||||
categoriesWithBalance []CategoryWithBalance, firstOfNextMonth time.Time,
|
|
||||||
) numeric.Numeric {
|
) numeric.Numeric {
|
||||||
availableBalance := moneyUsed
|
availableBalance := moneyUsed
|
||||||
|
|
||||||
@ -102,22 +88,14 @@ func (*Handler) getAvailableBalance(budget postgres.Budget,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bal.Date.Before(firstOfNextMonth) {
|
if month.InFuture(bal.Date) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
availableBalance.AddI(bal.Transactions)
|
availableBalance.AddI(bal.Transactions)
|
||||||
availableBalance.AddI(bal.Assignments)
|
availableBalance.AddI(bal.Assignments) // should be zero, but who knows
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range categoriesWithBalance {
|
|
||||||
cat := &categoriesWithBalance[i]
|
|
||||||
if cat.ID != budget.IncomeCategoryID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cat.Available = availableBalance
|
|
||||||
}
|
|
||||||
return availableBalance
|
return availableBalance
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +133,7 @@ func (h *Handler) getBudget(c *gin.Context, budgetUUID uuid.UUID) {
|
|||||||
c.JSON(http.StatusOK, data)
|
c.JSON(http.StatusOK, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) calculateBalances(firstOfNextMonth time.Time, firstOfMonth time.Time,
|
func (h *Handler) calculateBalances(budget postgres.Budget, month Month,
|
||||||
categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow,
|
categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow,
|
||||||
) ([]CategoryWithBalance, numeric.Numeric) {
|
) ([]CategoryWithBalance, numeric.Numeric) {
|
||||||
categoriesWithBalance := []CategoryWithBalance{}
|
categoriesWithBalance := []CategoryWithBalance{}
|
||||||
@ -163,6 +141,10 @@ func (h *Handler) calculateBalances(firstOfNextMonth time.Time, firstOfMonth tim
|
|||||||
moneyUsed := numeric.Zero()
|
moneyUsed := numeric.Zero()
|
||||||
for i := range categories {
|
for i := range categories {
|
||||||
cat := &categories[i]
|
cat := &categories[i]
|
||||||
|
if cat.ID == budget.IncomeCategoryID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
categoryWithBalance := NewCategoryWithBalance(cat)
|
categoryWithBalance := NewCategoryWithBalance(cat)
|
||||||
for _, bal := range cumultativeBalances {
|
for _, bal := range cumultativeBalances {
|
||||||
if bal.CategoryID != cat.ID {
|
if bal.CategoryID != cat.ID {
|
||||||
@ -170,21 +152,22 @@ func (h *Handler) calculateBalances(firstOfNextMonth time.Time, firstOfMonth tim
|
|||||||
}
|
}
|
||||||
|
|
||||||
// skip everything in the future
|
// skip everything in the future
|
||||||
if !bal.Date.Before(firstOfNextMonth) {
|
if month.InFuture(bal.Date) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
moneyUsed.SubI(bal.Assignments)
|
moneyUsed.SubI(bal.Assignments)
|
||||||
categoryWithBalance.Available.AddI(bal.Assignments)
|
if month.InPresent(bal.Date) {
|
||||||
categoryWithBalance.Available.AddI(bal.Transactions)
|
|
||||||
if !categoryWithBalance.Available.IsPositive() && bal.Date.Before(firstOfMonth) {
|
|
||||||
moneyUsed.AddI(categoryWithBalance.Available)
|
|
||||||
categoryWithBalance.Available = numeric.Zero()
|
|
||||||
}
|
|
||||||
|
|
||||||
if bal.Date.Year() == firstOfMonth.Year() && bal.Date.Month() == firstOfMonth.Month() {
|
|
||||||
categoryWithBalance.Activity = bal.Transactions
|
categoryWithBalance.Activity = bal.Transactions
|
||||||
categoryWithBalance.Assigned = bal.Assignments
|
categoryWithBalance.Assigned = bal.Assignments
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryWithBalance.AvailableLastMonth.AddI(bal.Assignments)
|
||||||
|
categoryWithBalance.AvailableLastMonth.AddI(bal.Transactions)
|
||||||
|
if !categoryWithBalance.AvailableLastMonth.IsPositive() {
|
||||||
|
moneyUsed.AddI(categoryWithBalance.AvailableLastMonth)
|
||||||
|
categoryWithBalance.AvailableLastMonth = numeric.Zero()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SetCategoryAssignmentRequest struct {
|
type SetCategoryAssignmentRequest struct {
|
||||||
Assigned string
|
Assigned float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) setCategoryAssignment(c *gin.Context) {
|
func (h *Handler) setCategoryAssignment(c *gin.Context) {
|
||||||
@ -44,7 +44,7 @@ func (h *Handler) setCategoryAssignment(c *gin.Context) {
|
|||||||
|
|
||||||
updateArgs := postgres.UpdateAssignmentParams{
|
updateArgs := postgres.UpdateAssignmentParams{
|
||||||
CategoryID: categoryUUID,
|
CategoryID: categoryUUID,
|
||||||
Date: date,
|
Date: date.FirstOfMonth(),
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
}
|
}
|
||||||
err = h.Service.UpdateAssignment(c.Request.Context(), updateArgs)
|
err = h.Service.UpdateAssignment(c.Request.Context(), updateArgs)
|
||||||
|
@ -181,9 +181,8 @@ func AssertCategoriesAndAvailableEqual(ctx context.Context, t *testing.T, loc *t
|
|||||||
}
|
}
|
||||||
year, _ := strconv.Atoi(parts[0])
|
year, _ := strconv.Atoi(parts[0])
|
||||||
month, _ := strconv.Atoi(parts[1])
|
month, _ := strconv.Atoi(parts[1])
|
||||||
first := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, loc)
|
|
||||||
testCaseFile := filepath.Join(resultDir, file.Name())
|
testCaseFile := filepath.Join(resultDir, file.Name())
|
||||||
handler.CheckAvailableBalance(ctx, t, testCaseFile, budget, first)
|
handler.CheckAvailableBalance(ctx, t, testCaseFile, budget, Month{year, month})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -199,12 +198,12 @@ type CategoryTestData struct {
|
|||||||
Assigned float64
|
Assigned float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCaseFile string, budget *postgres.Budget, first time.Time) {
|
func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCaseFile string, budget *postgres.Budget, month Month) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
t.Run(first.Format("2006-01"), func(t *testing.T) {
|
t.Run(month.String(), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
data, err := h.prepareBudgeting(ctx, *budget, first)
|
data, err := h.getBudgetingViewForMonth(ctx, *budget, month)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("prepare budgeting: %s", err)
|
t.Errorf("prepare budgeting: %s", err)
|
||||||
return
|
return
|
||||||
@ -232,9 +231,11 @@ func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCa
|
|||||||
name := category.Group + " : " + category.Name
|
name := category.Group + " : " + category.Name
|
||||||
|
|
||||||
if name == categoryName {
|
if name == categoryName {
|
||||||
assertEqual(t, categoryTestData.Available, category.Available.GetFloat64(), "available for "+categoryName)
|
|
||||||
assertEqual(t, categoryTestData.Activity, category.Activity.GetFloat64(), "activity for "+categoryName)
|
assertEqual(t, categoryTestData.Activity, category.Activity.GetFloat64(), "activity for "+categoryName)
|
||||||
assertEqual(t, categoryTestData.Assigned, category.Assigned.GetFloat64(), "assigned for "+categoryName)
|
assertEqual(t, categoryTestData.Assigned, category.Assigned.GetFloat64(), "assigned for "+categoryName)
|
||||||
|
available := category.AvailableLastMonth
|
||||||
|
available.AddI(category.Activity).AddI(category.Assigned)
|
||||||
|
assertEqual(t, categoryTestData.Available, available.GetFloat64(), "available for "+categoryName)
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -246,6 +247,15 @@ func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCa
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AssertEqualBool(t *testing.T, expected, actual bool, message string) {
|
||||||
|
t.Helper()
|
||||||
|
if expected == actual {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Errorf("%s: expected %v, got %v", message, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
func assertEqual(t *testing.T, expected, actual float64, message string) {
|
func assertEqual(t *testing.T, expected, actual float64, message string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if expected == actual {
|
if expected == actual {
|
||||||
|
55
server/month.go
Normal file
55
server/month.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Month struct {
|
||||||
|
Year int
|
||||||
|
Month int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromTime(date time.Time) Month {
|
||||||
|
return Month{date.Year(), int(date.Month())}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Month) String() string {
|
||||||
|
return fmt.Sprintf("%d-%d", m.Year, m.Month)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Month) FirstOfMonth() time.Time {
|
||||||
|
return time.Date(m.Year, time.Month(m.Month), 1, 0, 0, 0, 0, time.Now().Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Month) InFuture(date time.Time) bool {
|
||||||
|
if m.Year < date.Year() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Year > date.Year() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Month(m.Month) < date.Month()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Month) InPast(date time.Time) bool {
|
||||||
|
if m.Year > date.Year() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Year < date.Year() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Month(m.Month) > date.Month()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Month) InPresent(date time.Time) bool {
|
||||||
|
if date.Year() != m.Year {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.Month() == time.Month(m.Month)
|
||||||
|
}
|
46
server/month_test.go
Normal file
46
server/month_test.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package server_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestCaseCompare struct {
|
||||||
|
Value server.Month
|
||||||
|
Date time.Time
|
||||||
|
InPast bool
|
||||||
|
InPresent bool
|
||||||
|
InFuture bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComparisons(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
loc := time.Now().Location()
|
||||||
|
tests := []TestCaseCompare{
|
||||||
|
{server.Month{2022, 2}, time.Date(2022, 3, 1, 0, 0, 0, 0, loc), false, false, true},
|
||||||
|
{server.Month{2022, 3}, time.Date(2022, 3, 1, 0, 0, 0, 0, loc), false, true, false},
|
||||||
|
{server.Month{2022, 4}, time.Date(2022, 3, 1, 0, 0, 0, 0, loc), true, false, false},
|
||||||
|
{server.Month{2022, 2}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true},
|
||||||
|
{server.Month{2022, 3}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, true, false},
|
||||||
|
{server.Month{2022, 4}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false},
|
||||||
|
{server.Month{2021, 2}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true},
|
||||||
|
{server.Month{2021, 3}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true},
|
||||||
|
{server.Month{2021, 4}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true},
|
||||||
|
{server.Month{2023, 2}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false},
|
||||||
|
{server.Month{2023, 3}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false},
|
||||||
|
{server.Month{2023, 4}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false},
|
||||||
|
{server.Month{2021, 11}, time.Date(2021, 12, 1, 0, 0, 0, 0, loc), false, false, true},
|
||||||
|
}
|
||||||
|
for i := range tests { //nolint:paralleltest
|
||||||
|
test := tests[i]
|
||||||
|
t.Run(test.Date.Format("2006-01-02")+" is in of "+test.Value.String(), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
server.AssertEqualBool(t, test.InPast, test.Value.InPast(test.Date), "in past")
|
||||||
|
server.AssertEqualBool(t, test.InPresent, test.Value.InPresent(test.Date), "in present")
|
||||||
|
server.AssertEqualBool(t, test.InFuture, test.Value.InFuture(test.Date), "in future")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getDate(c *gin.Context) (time.Time, error) {
|
func getDate(c *gin.Context) (Month, error) {
|
||||||
var year, month int
|
var year, month int
|
||||||
yearString := c.Param("year")
|
yearString := c.Param("year")
|
||||||
monthString := c.Param("month")
|
monthString := c.Param("month")
|
||||||
@ -18,13 +18,20 @@ func getDate(c *gin.Context) (time.Time, error) {
|
|||||||
|
|
||||||
year, err := strconv.Atoi(yearString)
|
year, err := strconv.Atoi(yearString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, fmt.Errorf("parse year: %w", err)
|
return Month{}, fmt.Errorf("parse year: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
month, err = strconv.Atoi(monthString)
|
month, err = strconv.Atoi(monthString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, fmt.Errorf("parse month: %w", err)
|
return Month{}, fmt.Errorf("parse month: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getFirstOfMonth(year, month, time.Now().Location()), nil
|
return Month{year, month}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFirstOfMonthTime(date time.Time) Month {
|
||||||
|
var monthM time.Month
|
||||||
|
year, monthM, _ := date.Date()
|
||||||
|
month := int(monthM)
|
||||||
|
return Month{year, month}
|
||||||
}
|
}
|
||||||
|
2
testdata
2
testdata
@ -1 +1 @@
|
|||||||
Subproject commit 6ca3adcee2713e8205133bec6c24b45aa8d730d9
|
Subproject commit 8de369b17a81f2e6ed079374ab35f868f259f9c1
|
@ -7,6 +7,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
// override/add rules settings here, such as:
|
// override/add rules settings here, such as:
|
||||||
|
'vue/max-attributes-per-line': 'off'
|
||||||
// 'vue/no-unused-vars': 'error'
|
// 'vue/no-unused-vars': 'error'
|
||||||
},
|
},
|
||||||
parser: "vue-eslint-parser",
|
parser: "vue-eslint-parser",
|
||||||
|
0
web/dist/generate-directory-for-ci
vendored
Normal file
0
web/dist/generate-directory-for-ci
vendored
Normal file
@ -22,9 +22,6 @@
|
|||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
"@typescript-eslint/parser": "^5.13.0",
|
"@typescript-eslint/parser": "^5.13.0",
|
||||||
"@vitejs/plugin-vue": "^2.0.0",
|
"@vitejs/plugin-vue": "^2.0.0",
|
||||||
"@vue/cli-plugin-babel": "5.0.0-beta.7",
|
|
||||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
|
||||||
"@vue/cli-service": "5.0.0-beta.7",
|
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
"eslint-plugin-vue": "^8.5.0",
|
"eslint-plugin-vue": "^8.5.0",
|
||||||
"prettier": "2.5.1",
|
"prettier": "2.5.1",
|
||||||
|
@ -1,17 +1,27 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue?: number | string
|
modelValue?: number | string,
|
||||||
|
type: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
(e: "update:modelValue", value: number | string): void
|
(e: "update:modelValue", value: number | string): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
function valueChanged(e: Event) {
|
||||||
|
const target = <HTMLInputElement>e.target;
|
||||||
|
switch (props.type) {
|
||||||
|
case "number":
|
||||||
|
emits('update:modelValue', target.valueAsNumber);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("STR-INPUT", props.type)
|
||||||
|
emits('update:modelValue', target.value)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<input
|
<input :value="modelValue" :type="type" class="dark:bg-slate-900" @input="valueChanged">
|
||||||
:value="modelValue"
|
|
||||||
class="dark:bg-slate-900"
|
|
||||||
@input="emits('update:modelValue', ($event.target as HTMLInputElement)?.value)"
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
|
@ -6,7 +6,6 @@ import Currency from "./Currency.vue";
|
|||||||
import TransactionEditRow from "./TransactionEditRow.vue";
|
import TransactionEditRow from "./TransactionEditRow.vue";
|
||||||
import { formatDate } from "../date";
|
import { formatDate } from "../date";
|
||||||
import { useAccountStore } from "../stores/budget-account";
|
import { useAccountStore } from "../stores/budget-account";
|
||||||
import Input from "./Input.vue";
|
|
||||||
import Checkbox from "./Checkbox.vue";
|
import Checkbox from "./Checkbox.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -72,14 +72,35 @@ function assignedChanged(e : Event, category : Category){
|
|||||||
POST("/budget/"+CurrentBudgetID.value+"/category/" + category.ID + "/" + selected.value.Year + "/" + (selected.value.Month+1),
|
POST("/budget/"+CurrentBudgetID.value+"/category/" + category.ID + "/" + selected.value.Year + "/" + (selected.value.Month+1),
|
||||||
JSON.stringify({Assigned: category.Assigned}));
|
JSON.stringify({Assigned: category.Assigned}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const budgeted = computed(() => accountStore.GetBudgeted(selected.value.Year, selected.value.Month))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1>
|
<h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1>
|
||||||
|
<span>
|
||||||
|
Available last month:
|
||||||
|
<Currency
|
||||||
|
:value="accountStore.GetIncomeAvailable(previous.Year, previous.Month)"
|
||||||
|
/>
|
||||||
|
</span><br>
|
||||||
<span>Available balance:
|
<span>Available balance:
|
||||||
<Currency
|
<Currency
|
||||||
:value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)"
|
:value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)"
|
||||||
/></span>
|
/>
|
||||||
|
</span><br>
|
||||||
|
<span>Budgeted this month:
|
||||||
|
<Currency :value="budgeted.Assigned" /> - <Currency :value="-budgeted.Deassigned" /> = <Currency :value="budgeted.Assigned+budgeted.Deassigned" />
|
||||||
|
</span><br>
|
||||||
|
<span>Income:
|
||||||
|
<Currency
|
||||||
|
:value="budgeted.Income"
|
||||||
|
/> <Currency
|
||||||
|
:value="budgeted.Spent"
|
||||||
|
/> = <Currency
|
||||||
|
:value="budgeted.Income + budgeted.Spent"
|
||||||
|
/>
|
||||||
|
</span><br>
|
||||||
<div>
|
<div>
|
||||||
<router-link
|
<router-link
|
||||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
|
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
|
||||||
|
@ -9,6 +9,8 @@ import Button from "../components/SimpleButton.vue";
|
|||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import Input from "../components/Input.vue";
|
import Input from "../components/Input.vue";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const transactionsFile = ref<File | undefined>(undefined);
|
const transactionsFile = ref<File | undefined>(undefined);
|
||||||
const assignmentsFile = ref<File | undefined>(undefined);
|
const assignmentsFile = ref<File | undefined>(undefined);
|
||||||
|
|
||||||
@ -39,7 +41,7 @@ function deleteBudget() {
|
|||||||
|
|
||||||
const budgetStore = useSessionStore();
|
const budgetStore = useSessionStore();
|
||||||
budgetStore.Budgets.delete(CurrentBudgetID.value);
|
budgetStore.Budgets.delete(CurrentBudgetID.value);
|
||||||
useRouter().push("/")
|
router.push("/dashboard")
|
||||||
};
|
};
|
||||||
function clearBudget() {
|
function clearBudget() {
|
||||||
POST("/budget/" + CurrentBudgetID.value + "/settings/clear", null)
|
POST("/budget/" + CurrentBudgetID.value + "/settings/clear", null)
|
||||||
|
@ -9,6 +9,7 @@ interface State {
|
|||||||
CurrentAccountID: string | null;
|
CurrentAccountID: string | null;
|
||||||
Categories: Map<string, Category>;
|
Categories: Map<string, Category>;
|
||||||
Months: Map<number, Map<number, Map<string, Category>>>;
|
Months: Map<number, Map<number, Map<string, Category>>>;
|
||||||
|
Available: Map<number, Map<number, number>>;
|
||||||
Assignments: [];
|
Assignments: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,11 +39,19 @@ export interface Category {
|
|||||||
Activity: number;
|
Activity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BudgetedAmounts {
|
||||||
|
Assigned: number,
|
||||||
|
Deassigned: number,
|
||||||
|
Spent: number,
|
||||||
|
Income: number,
|
||||||
|
}
|
||||||
|
|
||||||
export const useAccountStore = defineStore("budget/account", {
|
export const useAccountStore = defineStore("budget/account", {
|
||||||
state: (): State => ({
|
state: (): State => ({
|
||||||
Accounts: new Map<string, Account>(),
|
Accounts: new Map<string, Account>(),
|
||||||
CurrentAccountID: null,
|
CurrentAccountID: null,
|
||||||
Months: new Map<number, Map<number, Map<string, Category>>>(),
|
Months: new Map<number, Map<number, Map<string, Category>>>(),
|
||||||
|
Available: new Map<number, Map<number, number>>(),
|
||||||
Categories: new Map<string, Category>(),
|
Categories: new Map<string, Category>(),
|
||||||
Assignments: [],
|
Assignments: [],
|
||||||
}),
|
}),
|
||||||
@ -59,7 +68,7 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
return (category: Category): number => {
|
return (category: Category): number => {
|
||||||
return (
|
return (
|
||||||
category.AvailableLastMonth +
|
category.AvailableLastMonth +
|
||||||
Number(category.Assigned) +
|
category.Assigned +
|
||||||
category.Activity
|
category.Activity
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -68,17 +77,40 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
const budget = useBudgetsStore();
|
const budget = useBudgetsStore();
|
||||||
return budget.CurrentBudget?.IncomeCategoryID;
|
return budget.CurrentBudget?.IncomeCategoryID;
|
||||||
},
|
},
|
||||||
GetIncomeAvailable(state) {
|
GetBudgeted(state) {
|
||||||
return (year: number, month: number) => {
|
return (year: number, month: number) : BudgetedAmounts => {
|
||||||
const IncomeCategoryID = this.GetIncomeCategoryID;
|
const IncomeCategoryID = this.GetIncomeCategoryID;
|
||||||
if (IncomeCategoryID == null) return 0;
|
if (IncomeCategoryID == null) return {Spent: 0, Income: 0, Assigned: 0, Deassigned: 0};
|
||||||
|
|
||||||
const categories = this.AllCategoriesForMonth(year, month);
|
const categories = this.AllCategoriesForMonth(year, month);
|
||||||
const category = categories.filter(
|
|
||||||
(x) => x.ID == IncomeCategoryID
|
let assigned = 0, deassigned = 0;
|
||||||
)[0];
|
let spent = 0, income = 0;
|
||||||
if (category == null) return 0;
|
for (const category of categories) {
|
||||||
return category.AvailableLastMonth;
|
if (category.ID == IncomeCategoryID)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(category.Activity > 0)
|
||||||
|
income += category.Activity;
|
||||||
|
else
|
||||||
|
spent += category.Activity;
|
||||||
|
if(category.Assigned > 0)
|
||||||
|
assigned += category.Assigned;
|
||||||
|
else
|
||||||
|
deassigned += category.Assigned;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
Assigned: assigned,
|
||||||
|
Deassigned: deassigned,
|
||||||
|
Spent: spent,
|
||||||
|
Income: income
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
GetIncomeAvailable(state) {
|
||||||
|
return (year: number, month: number) => {
|
||||||
|
const yearMapAv = this.Available.get(year);
|
||||||
|
return yearMapAv?.get(month);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
CategoryGroupsForMonth(state) {
|
CategoryGroupsForMonth(state) {
|
||||||
@ -87,7 +119,7 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
const categoryGroups = [];
|
const categoryGroups = [];
|
||||||
let prev = undefined;
|
let prev = undefined;
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
if (category.ID == this.GetIncomeCategoryID) continue;
|
//if (category.ID == this.GetIncomeCategoryID) continue;
|
||||||
|
|
||||||
if (prev == undefined || category.Group != prev.Name) {
|
if (prev == undefined || category.Group != prev.Name) {
|
||||||
prev = {
|
prev = {
|
||||||
@ -184,7 +216,7 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
response.Categories.length <= 0
|
response.Categories.length <= 0
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
this.addCategoriesForMonth(year, month, response.Categories);
|
this.addCategoriesForMonth(year, month, response.Categories, response.AvailableBalance);
|
||||||
},
|
},
|
||||||
async EditAccount(
|
async EditAccount(
|
||||||
accountid: string,
|
accountid: string,
|
||||||
@ -210,7 +242,8 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
addCategoriesForMonth(
|
addCategoriesForMonth(
|
||||||
year: number,
|
year: number,
|
||||||
month: number,
|
month: number,
|
||||||
categories: Category[]
|
categories: Category[],
|
||||||
|
available: number
|
||||||
): void {
|
): void {
|
||||||
this.$patch((state) => {
|
this.$patch((state) => {
|
||||||
const yearMap =
|
const yearMap =
|
||||||
@ -224,6 +257,12 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
|
|
||||||
yearMap.set(month, monthMap);
|
yearMap.set(month, monthMap);
|
||||||
state.Months.set(year, yearMap);
|
state.Months.set(year, yearMap);
|
||||||
|
|
||||||
|
const yearMapAv =
|
||||||
|
state.Available.get(year) ||
|
||||||
|
new Map<number, number>();
|
||||||
|
yearMapAv.set(month, available);
|
||||||
|
state.Available.set(year, yearMapAv);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
logout() {
|
logout() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user