Compare commits
28 Commits
fc50313907
...
38e21786a7
Author | SHA1 | Date | |
---|---|---|---|
38e21786a7 | |||
971c3d3be5 | |||
946f14c1cc | |||
9ce0da0182 | |||
4c93e4635d | |||
f3a50c790b | |||
0c5f68ed80 | |||
7fdd8bd935 | |||
d4287f8aac | |||
6712af10d9 | |||
70edb382e1 | |||
390a042441 | |||
e8028dae34 | |||
fc249adc9e | |||
c186a14644 | |||
8a27303670 | |||
44e9bb6ec0 | |||
42d431ba8b | |||
3727061065 | |||
c7a8adb3ab | |||
29f534bf10 | |||
15381c84f6 | |||
a0cabbf4f7 | |||
f26ee8f472 | |||
4fb3c2a335 | |||
8899ff5772 | |||
347a0c9e50 | |||
66b8e1f69f |
10
.earthignore
10
.earthignore
@ -1,10 +0,0 @@
|
|||||||
build/
|
|
||||||
.git/
|
|
||||||
docker-compose.yml
|
|
||||||
README.md
|
|
||||||
Earthfile
|
|
||||||
config.example.json
|
|
||||||
.gitignore
|
|
||||||
.vscode/
|
|
||||||
budgeteer
|
|
||||||
budgeteer.exe
|
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/node_modules": true,
|
"**/node_modules": true,
|
||||||
"**/vendor": true
|
"**/vendor": true,
|
||||||
|
"**/*.sql.go": true,
|
||||||
|
".task/": true,
|
||||||
|
"build/": true,
|
||||||
|
"web/dist/": true
|
||||||
},
|
},
|
||||||
"gopls": {
|
"gopls": {
|
||||||
"formatting.gofumpt": true,
|
"formatting.gofumpt": true,
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
pipeline:
|
|
||||||
build:
|
|
||||||
name: Taskfile.dev
|
|
||||||
image: hub.javil.eu/budgeteer:dev
|
|
||||||
pull: true
|
|
||||||
commands:
|
|
||||||
- task ci
|
|
||||||
|
|
||||||
docker:
|
|
||||||
image: plugins/docker
|
|
||||||
secrets: [ docker_username, docker_password ]
|
|
||||||
settings:
|
|
||||||
registry: hub.javil.eu
|
|
||||||
repo: hub.javil.eu/budgeteer
|
|
||||||
context: build
|
|
||||||
dockerfile: build/Dockerfile
|
|
||||||
tags:
|
|
||||||
- latest
|
|
||||||
when:
|
|
||||||
event: [push, tag, deployment]
|
|
||||||
|
|
||||||
image_pull_secrets:
|
|
||||||
- hub.javil.eu
|
|
21
Earthfile
21
Earthfile
@ -1,21 +0,0 @@
|
|||||||
FROM golang:1.17
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
build:
|
|
||||||
COPY go.mod go.sum .
|
|
||||||
RUN go mod download
|
|
||||||
COPY . .
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build go build -o build/budgeteer ./cmd/budgeteer
|
|
||||||
SAVE ARTIFACT build/budgeteer /budgeteer AS LOCAL build/budgeteer
|
|
||||||
|
|
||||||
docker:
|
|
||||||
WORKDIR /app
|
|
||||||
COPY +build/budgeteer .
|
|
||||||
ENTRYPOINT ["/app/budgeteer"]
|
|
||||||
SAVE IMAGE hub.javil.eu/budgeteer:latest
|
|
||||||
|
|
||||||
run:
|
|
||||||
LOCALLY
|
|
||||||
WITH DOCKER --load=+docker
|
|
||||||
RUN docker-compose up -d
|
|
||||||
END
|
|
@ -96,7 +96,7 @@ tasks:
|
|||||||
- ./docker/build.sh
|
- ./docker/build.sh
|
||||||
- ./web/package.json
|
- ./web/package.json
|
||||||
cmds:
|
cmds:
|
||||||
- docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile
|
- docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile.dev
|
||||||
- docker push {{.IMAGE_NAME}}:dev
|
- docker push {{.IMAGE_NAME}}:dev
|
||||||
|
|
||||||
run:
|
run:
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
FROM scratch
|
|
||||||
COPY ./budgeteer /app/budgeteer
|
|
||||||
ENTRYPOINT ["/app/budgeteer"]
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"DatabaseHost": "localhost",
|
|
||||||
"DatabaseUser": "user",
|
|
||||||
"DatabasePassword": "thisismypassword",
|
|
||||||
"DatabaseName": "budgeteer"
|
|
||||||
}
|
|
@ -1,17 +1,3 @@
|
|||||||
FROM alpine as godeps
|
FROM scratch
|
||||||
RUN apk --no-cache add go
|
COPY ./budgeteer /app/budgeteer
|
||||||
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
|
ENTRYPOINT ["/app/budgeteer"]
|
||||||
RUN go install github.com/go-task/task/v3/cmd/task@latest
|
|
||||||
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
|
||||||
|
|
||||||
FROM alpine
|
|
||||||
RUN apk --no-cache add go nodejs yarn bash curl git git-perl tmux
|
|
||||||
ADD docker/build.sh /
|
|
||||||
RUN yarn global add @vue/cli
|
|
||||||
ENV PATH="/root/.yarn/bin/:${PATH}"
|
|
||||||
WORKDIR /src/web
|
|
||||||
ADD web/package.json web/yarn.lock /src/web/
|
|
||||||
RUN yarn
|
|
||||||
WORKDIR /src
|
|
||||||
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/
|
|
||||||
CMD /build.sh
|
|
||||||
|
17
docker/Dockerfile.dev
Normal file
17
docker/Dockerfile.dev
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
FROM alpine as godeps
|
||||||
|
RUN apk --no-cache add go
|
||||||
|
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
|
||||||
|
RUN go install github.com/go-task/task/v3/cmd/task@latest
|
||||||
|
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
|
FROM alpine
|
||||||
|
RUN apk --no-cache add go nodejs yarn bash curl git git-perl tmux
|
||||||
|
ADD docker/dev.sh /
|
||||||
|
RUN yarn global add @vue/cli
|
||||||
|
ENV PATH="/root/.yarn/bin/:${PATH}"
|
||||||
|
WORKDIR /src/web
|
||||||
|
ADD web/package.json web/yarn.lock /src/web/
|
||||||
|
RUN yarn
|
||||||
|
WORKDIR /src
|
||||||
|
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/
|
||||||
|
CMD /dev.sh
|
2
go.mod
2
go.mod
@ -11,7 +11,7 @@ require (
|
|||||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
|
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/DATA-DOG/go-txdb v0.1.5 // indirect
|
require github.com/DATA-DOG/go-txdb v0.1.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
1
go.sum
1
go.sum
@ -74,6 +74,7 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
|||||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
@ -12,10 +12,11 @@ import (
|
|||||||
|
|
||||||
// TokenVerifier verifies Tokens.
|
// TokenVerifier verifies Tokens.
|
||||||
type TokenVerifier struct {
|
type TokenVerifier struct {
|
||||||
secret string
|
Expiration time.Duration
|
||||||
|
secret string
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrEmptySecret = fmt.Errorf("secret is required")
|
const DefaultExpiration = time.Hour * time.Duration(72)
|
||||||
|
|
||||||
func NewTokenVerifier(secret string) (*TokenVerifier, error) {
|
func NewTokenVerifier(secret string) (*TokenVerifier, error) {
|
||||||
if secret == "" {
|
if secret == "" {
|
||||||
@ -23,20 +24,16 @@ func NewTokenVerifier(secret string) (*TokenVerifier, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &TokenVerifier{
|
return &TokenVerifier{
|
||||||
secret: secret,
|
Expiration: DefaultExpiration,
|
||||||
|
secret: secret,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token contains everything to authenticate a user.
|
var (
|
||||||
type Token struct {
|
ErrUnexpectedSigningMethod = fmt.Errorf("unexpected signing method")
|
||||||
username string
|
ErrInvalidToken = fmt.Errorf("token is invalid")
|
||||||
name string
|
ErrTokenExpired = fmt.Errorf("token has expired")
|
||||||
expiry float64
|
ErrEmptySecret = fmt.Errorf("secret is required")
|
||||||
id uuid.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
expiration = 72
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateToken creates a new token from username and name.
|
// CreateToken creates a new token from username and name.
|
||||||
@ -48,7 +45,7 @@ func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
|
|||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
"usr": user.Email,
|
"usr": user.Email,
|
||||||
"name": user.Name,
|
"name": user.Name,
|
||||||
"exp": time.Now().Add(time.Hour * expiration).Unix(),
|
"exp": time.Now().Add(tv.Expiration).Unix(),
|
||||||
"id": user.ID,
|
"id": user.ID,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -61,13 +58,7 @@ func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// VerifyToken verifies a given string-token.
|
||||||
ErrUnexpectedSigningMethod = fmt.Errorf("unexpected signing method")
|
|
||||||
ErrInvalidToken = fmt.Errorf("token is invalid")
|
|
||||||
ErrTokenExpired = fmt.Errorf("token has expired")
|
|
||||||
)
|
|
||||||
|
|
||||||
// VerifyToken verifys a given string-token.
|
|
||||||
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn
|
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn
|
||||||
if tv.secret == "" {
|
if tv.secret == "" {
|
||||||
return nil, ErrEmptySecret
|
return nil, ErrEmptySecret
|
||||||
@ -96,36 +87,3 @@ func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error
|
|||||||
}
|
}
|
||||||
return tkn, nil
|
return tkn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyToken(token *jwt.Token) (jwt.MapClaims, error) {
|
|
||||||
if !token.Valid {
|
|
||||||
return nil, ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
|
|
||||||
return nil, ErrTokenExpired
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) GetName() string {
|
|
||||||
return t.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) GetUsername() string {
|
|
||||||
return t.username
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) GetExpiry() float64 {
|
|
||||||
return t.expiry
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) GetID() uuid.UUID {
|
|
||||||
return t.id
|
|
||||||
}
|
|
49
jwt/token.go
Normal file
49
jwt/token.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Token contains everything to authenticate a user.
|
||||||
|
type Token struct {
|
||||||
|
username string
|
||||||
|
name string
|
||||||
|
expiry float64
|
||||||
|
id uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyToken(token *jwt.Token) (jwt.MapClaims, error) {
|
||||||
|
if !token.Valid {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
|
||||||
|
return nil, ErrTokenExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) GetName() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) GetUsername() string {
|
||||||
|
return t.username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) GetExpiry() float64 {
|
||||||
|
return t.expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) GetID() uuid.UUID {
|
||||||
|
return t.id
|
||||||
|
}
|
@ -15,7 +15,7 @@ const createAccount = `-- name: CreateAccount :one
|
|||||||
INSERT INTO accounts
|
INSERT INTO accounts
|
||||||
(name, budget_id)
|
(name, budget_id)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2)
|
||||||
RETURNING id, budget_id, name, on_budget
|
RETURNING id, budget_id, name, on_budget, is_open
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateAccountParams struct {
|
type CreateAccountParams struct {
|
||||||
@ -31,12 +31,13 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A
|
|||||||
&i.BudgetID,
|
&i.BudgetID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.OnBudget,
|
&i.OnBudget,
|
||||||
|
&i.IsOpen,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAccount = `-- name: GetAccount :one
|
const getAccount = `-- name: GetAccount :one
|
||||||
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget FROM accounts
|
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open FROM accounts
|
||||||
WHERE accounts.id = $1
|
WHERE accounts.id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -48,13 +49,15 @@ func (q *Queries) GetAccount(ctx context.Context, id uuid.UUID) (Account, error)
|
|||||||
&i.BudgetID,
|
&i.BudgetID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.OnBudget,
|
&i.OnBudget,
|
||||||
|
&i.IsOpen,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAccounts = `-- name: GetAccounts :many
|
const getAccounts = `-- name: GetAccounts :many
|
||||||
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget FROM accounts
|
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open FROM accounts
|
||||||
WHERE accounts.budget_id = $1
|
WHERE accounts.budget_id = $1
|
||||||
|
AND accounts.is_open = TRUE
|
||||||
ORDER BY accounts.name
|
ORDER BY accounts.name
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -72,6 +75,7 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
|
|||||||
&i.BudgetID,
|
&i.BudgetID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.OnBudget,
|
&i.OnBudget,
|
||||||
|
&i.IsOpen,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -87,13 +91,14 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many
|
const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many
|
||||||
SELECT accounts.id, accounts.name, accounts.on_budget,
|
SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open,
|
||||||
(SELECT MAX(transactions.date) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.status = 'Reconciled')::date as last_reconciled,
|
(SELECT MAX(transactions.date) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.status = 'Reconciled')::date as last_reconciled,
|
||||||
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance,
|
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance,
|
||||||
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance,
|
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance,
|
||||||
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance
|
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE accounts.budget_id = $1
|
WHERE accounts.budget_id = $1
|
||||||
|
AND accounts.is_open = TRUE
|
||||||
ORDER BY accounts.name
|
ORDER BY accounts.name
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -101,6 +106,7 @@ type GetAccountsWithBalanceRow struct {
|
|||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
OnBudget bool
|
OnBudget bool
|
||||||
|
IsOpen bool
|
||||||
LastReconciled time.Time
|
LastReconciled time.Time
|
||||||
WorkingBalance numeric.Numeric
|
WorkingBalance numeric.Numeric
|
||||||
ClearedBalance numeric.Numeric
|
ClearedBalance numeric.Numeric
|
||||||
@ -120,6 +126,7 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.OnBudget,
|
&i.OnBudget,
|
||||||
|
&i.IsOpen,
|
||||||
&i.LastReconciled,
|
&i.LastReconciled,
|
||||||
&i.WorkingBalance,
|
&i.WorkingBalance,
|
||||||
&i.ClearedBalance,
|
&i.ClearedBalance,
|
||||||
@ -141,6 +148,7 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
|
|||||||
const searchAccounts = `-- name: SearchAccounts :many
|
const searchAccounts = `-- name: SearchAccounts :many
|
||||||
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
|
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
|
||||||
WHERE accounts.budget_id = $1
|
WHERE accounts.budget_id = $1
|
||||||
|
AND accounts.is_open = TRUE
|
||||||
AND accounts.name LIKE $2
|
AND accounts.name LIKE $2
|
||||||
ORDER BY accounts.name
|
ORDER BY accounts.name
|
||||||
`
|
`
|
||||||
@ -188,25 +196,33 @@ func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams)
|
|||||||
const updateAccount = `-- name: UpdateAccount :one
|
const updateAccount = `-- name: UpdateAccount :one
|
||||||
UPDATE accounts
|
UPDATE accounts
|
||||||
SET name = $1,
|
SET name = $1,
|
||||||
on_budget = $2
|
on_budget = $2,
|
||||||
WHERE accounts.id = $3
|
is_open = $3
|
||||||
RETURNING id, budget_id, name, on_budget
|
WHERE accounts.id = $4
|
||||||
|
RETURNING id, budget_id, name, on_budget, is_open
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateAccountParams struct {
|
type UpdateAccountParams struct {
|
||||||
Name string
|
Name string
|
||||||
OnBudget bool
|
OnBudget bool
|
||||||
|
IsOpen bool
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) {
|
func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) {
|
||||||
row := q.db.QueryRowContext(ctx, updateAccount, arg.Name, arg.OnBudget, arg.ID)
|
row := q.db.QueryRowContext(ctx, updateAccount,
|
||||||
|
arg.Name,
|
||||||
|
arg.OnBudget,
|
||||||
|
arg.IsOpen,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
var i Account
|
var i Account
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.BudgetID,
|
&i.BudgetID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.OnBudget,
|
&i.OnBudget,
|
||||||
|
&i.IsOpen,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ type Account struct {
|
|||||||
BudgetID uuid.UUID
|
BudgetID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
OnBudget bool
|
OnBudget bool
|
||||||
|
IsOpen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Assignment struct {
|
type Assignment struct {
|
||||||
|
@ -11,27 +11,31 @@ WHERE accounts.id = $1;
|
|||||||
-- name: GetAccounts :many
|
-- name: GetAccounts :many
|
||||||
SELECT accounts.* FROM accounts
|
SELECT accounts.* FROM accounts
|
||||||
WHERE accounts.budget_id = $1
|
WHERE accounts.budget_id = $1
|
||||||
|
AND accounts.is_open = TRUE
|
||||||
ORDER BY accounts.name;
|
ORDER BY accounts.name;
|
||||||
|
|
||||||
-- name: GetAccountsWithBalance :many
|
-- name: GetAccountsWithBalance :many
|
||||||
SELECT accounts.id, accounts.name, accounts.on_budget,
|
SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open,
|
||||||
(SELECT MAX(transactions.date) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.status = 'Reconciled')::date as last_reconciled,
|
(SELECT MAX(transactions.date) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.status = 'Reconciled')::date as last_reconciled,
|
||||||
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance,
|
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance,
|
||||||
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance,
|
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance,
|
||||||
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance
|
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE accounts.budget_id = $1
|
WHERE accounts.budget_id = $1
|
||||||
|
AND accounts.is_open = TRUE
|
||||||
ORDER BY accounts.name;
|
ORDER BY accounts.name;
|
||||||
|
|
||||||
-- name: SearchAccounts :many
|
-- name: SearchAccounts :many
|
||||||
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
|
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
|
||||||
WHERE accounts.budget_id = @budget_id
|
WHERE accounts.budget_id = @budget_id
|
||||||
|
AND accounts.is_open = TRUE
|
||||||
AND accounts.name LIKE @search
|
AND accounts.name LIKE @search
|
||||||
ORDER BY accounts.name;
|
ORDER BY accounts.name;
|
||||||
|
|
||||||
-- name: UpdateAccount :one
|
-- name: UpdateAccount :one
|
||||||
UPDATE accounts
|
UPDATE accounts
|
||||||
SET name = $1,
|
SET name = $1,
|
||||||
on_budget = $2
|
on_budget = $2,
|
||||||
WHERE accounts.id = $3
|
is_open = $3
|
||||||
|
WHERE accounts.id = $4
|
||||||
RETURNING *;
|
RETURNING *;
|
5
postgres/schema/0016_closed-accounts.sql
Normal file
5
postgres/schema/0016_closed-accounts.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE accounts ADD COLUMN is_open BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE accounts DROP COLUMN is_open;
|
@ -39,6 +39,7 @@ type TransactionsResponse struct {
|
|||||||
type EditAccountRequest struct {
|
type EditAccountRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
OnBudget bool `json:"onBudget"`
|
OnBudget bool `json:"onBudget"`
|
||||||
|
IsOpen bool `json:"isOpen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) editAccount(c *gin.Context) {
|
func (h *Handler) editAccount(c *gin.Context) {
|
||||||
@ -59,6 +60,7 @@ func (h *Handler) editAccount(c *gin.Context) {
|
|||||||
updateParams := postgres.UpdateAccountParams{
|
updateParams := postgres.UpdateAccountParams{
|
||||||
Name: request.Name,
|
Name: request.Name,
|
||||||
OnBudget: request.OnBudget,
|
OnBudget: request.OnBudget,
|
||||||
|
IsOpen: request.IsOpen,
|
||||||
ID: accountUUID,
|
ID: accountUUID,
|
||||||
}
|
}
|
||||||
account, err := h.Service.UpdateAccount(c.Request.Context(), updateParams)
|
account, err := h.Service.UpdateAccount(c.Request.Context(), updateParams)
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite App</title>
|
<title>Vite App</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200">
|
<body class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200 box-border w-full">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -27,39 +27,34 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="box-border w-full">
|
<div class="flex flex-col md:flex-row flex-1">
|
||||||
<div class="flex bg-gray-400 dark:bg-gray-600 p-4 m-2 rounded-lg">
|
<div
|
||||||
<span class="flex-1 font-bold text-5xl -my-3 hidden md:inline" @click="toggleMenuSize">≡</span>
|
:class="[Menu.Expand ? 'md:w-72' : 'md:w-36', Menu.Show ? '' : 'hidden']"
|
||||||
<span class="flex-1 font-bold text-5xl -my-3 md:hidden" @click="toggleMenu">≡</span>
|
class="md:block flex-shrink-0 w-full bg-gray-500 border-r-4 border-black"
|
||||||
|
>
|
||||||
<span class="flex-1">{{ CurrentBudgetName }}</span>
|
<router-view name="sidebar"></router-view>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-row justify-end -mx-4">
|
|
||||||
<router-link class="mx-4" v-if="LoggedIn" to="/dashboard">Dashboard</router-link>
|
|
||||||
<router-link class="mx-4" v-if="!LoggedIn" to="/login">Login</router-link>
|
|
||||||
<a class="mx-4" v-if="LoggedIn" @click="logout">Logout</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row flex-1">
|
<div class="flex-1">
|
||||||
<div
|
<div class="flex bg-gray-400 dark:bg-gray-600 p-4 fixed md:static top-0 left-0 w-full h-14">
|
||||||
:class="[Menu.Expand ? 'md:w-72' : 'md:w-36', Menu.Show ? '' : 'hidden']"
|
<span
|
||||||
class="md:block flex-shrink-0 w-full"
|
class="flex-1 font-bold text-5xl -my-3 hidden md:inline"
|
||||||
>
|
@click="toggleMenuSize"
|
||||||
<router-view name="sidebar"></router-view>
|
>≡</span>
|
||||||
|
<span class="flex-1 font-bold text-5xl -my-3 md:hidden" @click="toggleMenu">≡</span>
|
||||||
|
|
||||||
|
<span class="flex-1">{{ CurrentBudgetName }}</span>
|
||||||
|
|
||||||
|
<div class="flex flex-1 flex-row justify-end -mx-4">
|
||||||
|
<router-link class="mx-4" v-if="LoggedIn" to="/dashboard">Dashboard</router-link>
|
||||||
|
<router-link class="mx-4" v-if="!LoggedIn" to="/login">Login</router-link>
|
||||||
|
<a class="mx-4" v-if="LoggedIn" @click="logout">Logout</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 p-6">
|
<div class="p-3 pl-6">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
#app {
|
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -7,7 +7,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'submit'): void,
|
(e: 'submit', event : {cancel:boolean}): boolean,
|
||||||
(e: 'open'): void,
|
(e: 'open'): void,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@ -20,8 +20,12 @@ function openDialog() {
|
|||||||
visible.value = true;
|
visible.value = true;
|
||||||
};
|
};
|
||||||
function submitDialog() {
|
function submitDialog() {
|
||||||
|
const e = {cancel: false};
|
||||||
|
emit("submit", e);
|
||||||
|
if(e.cancel)
|
||||||
|
return;
|
||||||
|
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
emit("submit");
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -38,9 +42,9 @@ function submitDialog() {
|
|||||||
v-if="visible"
|
v-if="visible"
|
||||||
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"
|
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"
|
||||||
>
|
>
|
||||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
<div class="relative top-20 mx-auto p-5 w-96 shadow-lg rounded-md bg-white dark:bg-black">
|
||||||
<div class="mt-3 text-center">
|
<div class="mt-3 text-center">
|
||||||
<h3 class="mt-3 text-lg leading-6 font-medium text-gray-900">{{ buttonText }}</h3>
|
<h3 class="mt-3 text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{{ buttonText }}</h3>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<div class="grid grid-cols-2 gap-6">
|
<div class="grid grid-cols-2 gap-6">
|
||||||
<button
|
<button
|
||||||
|
@ -3,29 +3,50 @@ import { computed, ref } from 'vue';
|
|||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
import { useAccountStore } from '../stores/budget-account';
|
import { useAccountStore } from '../stores/budget-account';
|
||||||
import Input from '../components/Input.vue';
|
import Input from '../components/Input.vue';
|
||||||
|
import Checkbox from '../components/Checkbox.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useBudgetsStore } from '../stores/budget';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const CurrentAccount = computed(() => accountStore.CurrentAccount);
|
const CurrentAccount = computed(() => accountStore.CurrentAccount);
|
||||||
|
|
||||||
const accountName = ref("");
|
const accountName = ref("");
|
||||||
const accountOnBudget = ref(true);
|
const accountOnBudget = ref(true);
|
||||||
|
const accountOpen = ref(true);
|
||||||
|
const error = ref("");
|
||||||
|
|
||||||
function editAccount(e : any) {
|
function editAccount(e : {cancel:boolean}) : boolean {
|
||||||
accountStore.EditAccount(CurrentAccount.value?.ID ?? "", accountName.value, accountOnBudget.value);
|
if(CurrentAccount.value?.ClearedBalance != 0 && !accountOpen.value){
|
||||||
|
e.cancel = true;
|
||||||
|
error.value = "Cannot close account with balance";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.value = "";
|
||||||
|
accountStore.EditAccount(CurrentAccount.value?.ID ?? "", accountName.value, accountOnBudget.value, accountOpen.value);
|
||||||
|
|
||||||
|
// account closed, move to Budget
|
||||||
|
if(!accountOpen.value){
|
||||||
|
const currentBudgetID = useBudgetsStore().CurrentBudgetID;
|
||||||
|
router.replace('/budget/'+currentBudgetID+'/budgeting');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditAccount(e : any) {
|
function openEditAccount(e : any) {
|
||||||
accountName.value = CurrentAccount.value?.Name ?? "";
|
accountName.value = CurrentAccount.value?.Name ?? "";
|
||||||
accountOnBudget.value = CurrentAccount.value?.OnBudget ?? true;
|
accountOnBudget.value = CurrentAccount.value?.OnBudget ?? true;
|
||||||
|
accountOpen.value = CurrentAccount.value?.IsOpen ?? true;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal button-text="Edit Account" @open="openEditAccount" @submit="editAccount">
|
<Modal button-text="Edit Account" @open="openEditAccount" @submit="editAccount">
|
||||||
<template v-slot:placeholder>✎</template>
|
<template v-slot:placeholder><span class="ml-2">✎</span></template>
|
||||||
<div class="mt-2 px-7 py-3">
|
<div class="mt-2 px-7 py-3">
|
||||||
<Input
|
<Input
|
||||||
class="border-2"
|
class="border-2 dark:border-gray-700"
|
||||||
type="text"
|
type="text"
|
||||||
v-model="accountName"
|
v-model="accountName"
|
||||||
placeholder="Account name"
|
placeholder="Account name"
|
||||||
@ -33,13 +54,23 @@ function openEditAccount(e : any) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 px-7 py-3">
|
<div class="mt-2 px-7 py-3">
|
||||||
<Input
|
<Checkbox
|
||||||
class="border-2"
|
class="border-2"
|
||||||
type="checkbox"
|
|
||||||
v-model="accountOnBudget"
|
v-model="accountOnBudget"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label>On Budget</label>
|
<label>On Budget</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-2 px-7 py-3">
|
||||||
|
<Checkbox
|
||||||
|
class="border-2"
|
||||||
|
v-model="accountOpen"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label>Open</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="error != ''" class="dark:text-red-300 text-red-700">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
@ -8,4 +8,10 @@ h1 {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
@ -43,10 +43,12 @@ function createReconcilationTransaction() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-[1fr_auto]">
|
<div class="grid grid-cols-[1fr_auto]">
|
||||||
<h1 class="inline">
|
<div>
|
||||||
{{ accounts.CurrentAccount?.Name }}
|
<h1 class="inline">
|
||||||
|
{{ accounts.CurrentAccount?.Name }}
|
||||||
|
</h1>
|
||||||
<EditAccount />
|
<EditAccount />
|
||||||
</h1>
|
</div>
|
||||||
|
|
||||||
<div class="text-right flex flex-wrap flex-col md:flex-row justify-end gap-2 max-w-sm">
|
<div class="text-right flex flex-wrap flex-col md:flex-row justify-end gap-2 max-w-sm">
|
||||||
<span class="rounded-lg p-1 whitespace-nowrap flex-1">
|
<span class="rounded-lg p-1 whitespace-nowrap flex-1">
|
||||||
|
@ -30,17 +30,17 @@ function getAccountName(account : Account) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col mt-14 md:mt-0">
|
||||||
<span class="m-1 p-1 px-3 text-xl">
|
<span class="m-2 p-1 px-3 h-10 overflow-hidden" :class="[ExpandMenu ? 'text-2xl' : 'text-md']">
|
||||||
<router-link to="/dashboard">⌂</router-link>
|
<router-link to="/dashboard" style="font-size:150%">⌂</router-link>
|
||||||
{{CurrentBudgetName}}
|
{{CurrentBudgetName}}
|
||||||
</span>
|
</span>
|
||||||
<span class="bg-orange-200 dark:bg-orange-700 rounded-lg m-1 p-1 px-3 flex flex-col">
|
<span class="bg-gray-100 dark:bg-gray-700 p-2 px-3 flex flex-col">
|
||||||
<router-link :to="'/budget/'+CurrentBudgetID+'/budgeting'">Budget</router-link><br />
|
<router-link :to="'/budget/'+CurrentBudgetID+'/budgeting'">Budget</router-link><br />
|
||||||
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
|
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
|
||||||
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
|
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
|
||||||
</span>
|
</span>
|
||||||
<li class="bg-orange-200 dark:bg-orange-700 rounded-lg m-1 p-1 px-3">
|
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
|
||||||
<div class="flex flex-row justify-between font-bold">
|
<div class="flex flex-row justify-between font-bold">
|
||||||
<span>On-Budget Accounts</span>
|
<span>On-Budget Accounts</span>
|
||||||
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OnBudgetAccountsBalance" />
|
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OnBudgetAccountsBalance" />
|
||||||
@ -50,7 +50,7 @@ function getAccountName(account : Account) {
|
|||||||
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.ClearedBalance" />
|
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.ClearedBalance" />
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="bg-red-200 dark:bg-red-800 rounded-lg m-1 p-1 px-3">
|
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
|
||||||
<div class="flex flex-row justify-between font-bold">
|
<div class="flex flex-row justify-between font-bold">
|
||||||
<span>Off-Budget Accounts</span>
|
<span>Off-Budget Accounts</span>
|
||||||
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OffBudgetAccountsBalance" />
|
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OffBudgetAccountsBalance" />
|
||||||
@ -60,20 +60,20 @@ function getAccountName(account : Account) {
|
|||||||
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.ClearedBalance" />
|
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.ClearedBalance" />
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="bg-red-200 dark:bg-red-800 rounded-lg m-1 p-1 px-3">
|
<!--
|
||||||
|
<li class="bg-slate-100 dark:bg-slate-800 my-2 p-2 px-3">
|
||||||
<div class="flex flex-row justify-between font-bold">
|
<div class="flex flex-row justify-between font-bold">
|
||||||
<span>Closed Accounts</span>
|
<span>Closed Accounts</span>
|
||||||
</div>
|
</div>
|
||||||
|
+ Add Account
|
||||||
</li>
|
</li>
|
||||||
|
-->
|
||||||
<!--<li>
|
<!--<li>
|
||||||
<router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link>
|
<router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link>
|
||||||
</li>-->
|
</li>-->
|
||||||
<li class="bg-red-200 dark:bg-red-800 rounded-lg m-1 p-1 px-3">
|
<li class="bg-red-100 dark:bg-slate-600 my-2 p-2 px-3">
|
||||||
+ Add Account
|
|
||||||
</li>
|
|
||||||
<li class="bg-red-200 dark:bg-red-800 rounded-lg m-1 p-1 px-3">
|
|
||||||
<router-link :to="'/budget/'+CurrentBudgetID+'/settings'">Budget-Settings</router-link>
|
<router-link :to="'/budget/'+CurrentBudgetID+'/settings'">Budget-Settings</router-link>
|
||||||
</li>
|
</li>
|
||||||
<!--<li><router-link to="/admin">Admin</router-link></li>-->
|
<!--<li><router-link to="/admin">Admin</router-link></li>-->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -16,6 +16,7 @@ export interface Account {
|
|||||||
ID: string
|
ID: string
|
||||||
Name: string
|
Name: string
|
||||||
OnBudget: boolean
|
OnBudget: boolean
|
||||||
|
IsOpen: boolean
|
||||||
ClearedBalance: number
|
ClearedBalance: number
|
||||||
WorkingBalance: number
|
WorkingBalance: number
|
||||||
ReconciledBalance: number
|
ReconciledBalance: number
|
||||||
@ -123,10 +124,14 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
return;
|
return;
|
||||||
this.addCategoriesForMonth(year, month, response.Categories);
|
this.addCategoriesForMonth(year, month, response.Categories);
|
||||||
},
|
},
|
||||||
async EditAccount(accountid: string, name: string, onBudget: boolean) {
|
async EditAccount(accountid: string, name: string, onBudget: boolean, isOpen: boolean) {
|
||||||
const result = await POST("/account/" + accountid, JSON.stringify({ name: name, onBudget: onBudget }));
|
const result = await POST("/account/" + accountid, JSON.stringify({ name: name, onBudget: onBudget, isOpen: isOpen }));
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
useBudgetsStore().MergeBudgetingData(response);
|
useBudgetsStore().MergeBudgetingData(response);
|
||||||
|
|
||||||
|
if(!isOpen) {
|
||||||
|
this.Accounts.delete(accountid);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
|
addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
|
||||||
this.$patch((state) => {
|
this.$patch((state) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user