76 Commits
0.5.0 ... 0.5.3

Author SHA1 Message Date
0038db90ac Merge pull request 'Fix docker build' (#38) from docker-build into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #38
2022-03-02 23:55:29 +01:00
9759a7cc1e Fix docker build
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 22:55:21 +00:00
81c31a3d5f Merge pull request 'Cleanup' (#37) from cleanup into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #37
2022-03-02 23:53:15 +01:00
38e21786a7 Fix app bar for small screens
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-03-02 22:52:12 +00:00
971c3d3be5 Improve sidebar and show next to appbar instead of below 2022-03-02 22:52:12 +00:00
946f14c1cc Fix const 2022-03-02 22:51:20 +00:00
9ce0da0182 Move CSS to _index.css 2022-03-02 22:51:20 +00:00
4c93e4635d Extract const for default expiration 2022-03-02 22:51:20 +00:00
f3a50c790b Hide .task, build and dist folders 2022-03-02 22:51:20 +00:00
0c5f68ed80 Hide files generated by sqlc 2022-03-02 22:51:20 +00:00
7fdd8bd935 Split jwt into two files 2022-03-02 22:51:20 +00:00
d4287f8aac Remove example config 2022-03-02 22:51:20 +00:00
6712af10d9 Move production Dockerfile to docker directory 2022-03-02 22:51:20 +00:00
70edb382e1 Move compose files to docker directory 2022-03-02 22:51:20 +00:00
390a042441 Rename development Dockerfile and build script 2022-03-02 22:51:20 +00:00
e8028dae34 Remove woodpecker config 2022-03-02 22:51:20 +00:00
fc249adc9e Remove Earthfile 2022-03-02 22:51:20 +00:00
c186a14644 go mod tidy 2022-03-02 22:51:20 +00:00
8a27303670 Remove earthignore 2022-03-02 22:51:20 +00:00
44e9bb6ec0 Merge pull request 'Implement closing of accounts' (#36) from closed-accounts into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #36
2022-03-02 22:54:02 +01:00
42d431ba8b Fix modal layout for dark theme
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 21:52:35 +00:00
3727061065 Prevent closing of accounts with balance 2022-03-02 21:52:16 +00:00
c7a8adb3ab Remove closed account from sidebar
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 21:22:15 +00:00
29f534bf10 Hide closed accounts menuentry 2022-03-02 21:22:06 +00:00
15381c84f6 Navigate to budgeting from closed account 2022-03-02 21:21:56 +00:00
a0cabbf4f7 Pass is_open to API from EditAccount 2022-03-02 21:16:06 +00:00
f26ee8f472 Add ability to change is_open from API 2022-03-02 21:15:56 +00:00
4fb3c2a335 Show is_open in EditAccount 2022-03-02 21:14:12 +00:00
8899ff5772 Return and filter by is_open 2022-03-02 21:14:00 +00:00
347a0c9e50 Add column is_open to accounts 2022-03-02 21:13:42 +00:00
66b8e1f69f Fix edit-accounts' font being off 2022-03-02 21:07:53 +00:00
5d1b49c896 Merge pull request 'Implement custom checkbox' (#35) from checkbox into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #35
2022-03-02 21:34:27 +01:00
42dc51fe9a Improve layout of Reconcilation
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-03-02 20:33:01 +00:00
1ca95f8768 Remove old logging calls
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 20:06:33 +00:00
a73f7c2934 Bind to value instead of using v-model 2022-03-02 20:06:16 +00:00
489aa88c4b Add custom checkbox component 2022-03-02 20:06:06 +00:00
7b5b16c1b2 Merge pull request 'Design fixes for dark mode' (#34) from design-fixes into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #34
2022-03-02 20:54:26 +01:00
29cfeb6fa6 Use Input component for all inputs except file-input 2022-03-02 20:54:26 +01:00
42baafd273 Invert if 2022-03-02 20:54:26 +01:00
1e79f193be Extract Input component and add some dark color tags 2022-03-02 20:54:26 +01:00
f0ec7fb30d Merge pull request 'Show symbol for accounts that haven't been recently reconciled' (#33) from recently-reconciled into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #33
2022-03-02 20:53:41 +01:00
034f4f2a90 Show * in sidebar for unreconciled accounts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 19:25:07 +00:00
51ece59866 Calculate last reconciled date on backend 2022-03-02 19:24:30 +00:00
bbfda6f402 Improve layout of balances header
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-01 20:59:06 +00:00
2b3afbf448 Merge pull request 'Implement simple dark mode' (#32) from dark-mode into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #32
2022-03-01 21:48:05 +01:00
a53c3d23a4 Implement simple dark mode
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-01 20:47:55 +00:00
c899c21256 Merge pull request 'Fix budget data being one month off' (#31) from hotfix-budgeting-off-by-one into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #31
2022-03-01 21:38:23 +01:00
cccc948048 Fix budget data being one month off
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-01 20:38:12 +00:00
e8a0670a83 Merge pull request 'Improve mobile usability' (#30) from mobile-usability into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #30
2022-03-01 21:33:07 +01:00
9638676b8f Add new transaction button on small screens
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-01 20:32:05 +00:00
08bda8d14f Make buttonText optional 2022-03-01 20:31:22 +00:00
d0ade0f2f1 Remove height from Card 2022-03-01 20:31:13 +00:00
466239ce11 Remove defaults from Button 2022-03-01 20:31:01 +00:00
d52e5c63d4 Hide Input on small screens and match columns 2022-03-01 20:12:27 +00:00
3dcb362372 Prevent wrap in balance-labels 2022-03-01 20:12:11 +00:00
67c9b53e91 Extract date to own row on small screens 2022-03-01 20:11:59 +00:00
7c08ddacb7 Remove widths from TransactionInputRow and use Button 2022-03-01 20:11:41 +00:00
ecbb85aeaa Decrease displayed information 2022-03-01 18:52:04 +00:00
3696bbde43 Check empty secret in other spots
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-01 18:37:07 +00:00
09a227d08d Wrap error for more details 2022-03-01 18:34:22 +00:00
dae6185857 Prevent startup on empty secret
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-01 18:33:46 +00:00
6dd8a3791f Use grid instead of table 2022-03-01 08:36:03 +00:00
18149eef8b Hide Leftover on small screens 2022-03-01 08:33:01 +00:00
f0f97a2e77 Merge pull request 'Fix budgeting not being openable' (#29) from hotfix-budget into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #29
2022-03-01 09:11:33 +01:00
71b2f8a9a3 Fix budgeting not being openable
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-28 20:14:27 +00:00
8f666a0f26 Merge pull request 'Improve Transactions handling' (#28) from transactions into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #28
2022-02-28 14:28:38 +01:00
2b9c76960e Run vue-tsc on CI build
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-28 13:27:02 +00:00
7fd057f9f6 Remove vuetify plugin 2022-02-28 13:25:14 +00:00
a9be9367a9 Use transactionid for TransactionRow 2022-02-28 13:23:30 +00:00
bc75757ac7 Complete migration two transactions store 2022-02-28 13:13:21 +00:00
a3e12df2e2 Extract transactions store 2022-02-28 13:09:15 +00:00
024c5e0a1c Refactor transactions store 2022-02-28 13:03:53 +00:00
27372199f7 Improve logger 2022-02-28 09:34:57 +00:00
b3b878854e Extract GetAccount 2022-02-28 09:28:53 +00:00
bbb12a788d Improve layout of Account header 2022-02-28 07:58:51 +00:00
7dfbef60a4 Fix date input getting stuck on invalid input 2022-02-27 22:10:47 +00:00
52 changed files with 1181 additions and 531 deletions

View File

@ -32,7 +32,7 @@ steps:
from_secret: docker_password
repo: hub.javil.eu/budgeteer
context: build
dockerfile: build/Dockerfile
dockerfile: docker/Dockerfile
tags:
- latest
when:
@ -51,7 +51,7 @@ steps:
from_secret: docker_password
repo: hub.javil.eu/budgeteer
context: build
dockerfile: build/Dockerfile
dockerfile: docker/Dockerfile
auto_tag: true
when:
event:

View File

@ -1,10 +0,0 @@
build/
.git/
docker-compose.yml
README.md
Earthfile
config.example.json
.gitignore
.vscode/
budgeteer
budgeteer.exe

View File

@ -1,7 +1,11 @@
{
"files.exclude": {
"**/node_modules": true,
"**/vendor": true
"**/vendor": true,
"**/*.sql.go": true,
".task/": true,
"build/": true,
"web/dist/": true
},
"gopls": {
"formatting.gofumpt": true,

View File

@ -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

View File

@ -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

View File

@ -77,6 +77,7 @@ tasks:
cmds:
- yarn
- yarn build
- yarn run vue-tsc --noEmit
docker:
desc: Build budgeeter:latest
@ -95,7 +96,7 @@ tasks:
- ./docker/build.sh
- ./web/package.json
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
run:

View File

@ -1,3 +0,0 @@
FROM scratch
COPY ./budgeteer /app/budgeteer
ENTRYPOINT ["/app/budgeteer"]

View File

@ -1,6 +1,7 @@
package main
import (
"fmt"
"io/fs"
"log"
"net/http"
@ -29,11 +30,14 @@ func main() {
panic("couldn't open static files")
}
tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret)
if err != nil {
panic(fmt.Errorf("couldn't create token verifier: %w", err))
}
handler := &server.Handler{
Service: queries,
TokenVerifier: &jwt.TokenVerifier{
Secret: cfg.SessionSecret,
},
Service: queries,
TokenVerifier: tokenVerifier,
CredentialsVerifier: &bcrypt.Verifier{},
StaticFS: http.FS(static),
}

View File

@ -1,6 +0,0 @@
{
"DatabaseHost": "localhost",
"DatabaseUser": "user",
"DatabasePassword": "thisismypassword",
"DatabaseName": "budgeteer"
}

View File

@ -1,17 +1,3 @@
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/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
FROM scratch
COPY ./budgeteer /app/budgeteer
ENTRYPOINT ["/app/budgeteer"]

17
docker/Dockerfile.dev Normal file
View 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
View File

@ -11,7 +11,7 @@ require (
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 (
github.com/gin-contrib/sse v0.1.0 // indirect

1
go.sum
View File

@ -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/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.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=

View File

@ -12,32 +12,45 @@ import (
// TokenVerifier verifies Tokens.
type TokenVerifier struct {
Secret string
Expiration time.Duration
secret string
}
// Token contains everything to authenticate a user.
type Token struct {
username string
name string
expiry float64
id uuid.UUID
const DefaultExpiration = time.Hour * time.Duration(72)
func NewTokenVerifier(secret string) (*TokenVerifier, error) {
if secret == "" {
return nil, ErrEmptySecret
}
return &TokenVerifier{
Expiration: DefaultExpiration,
secret: secret,
}, nil
}
const (
expiration = 72
var (
ErrUnexpectedSigningMethod = fmt.Errorf("unexpected signing method")
ErrInvalidToken = fmt.Errorf("token is invalid")
ErrTokenExpired = fmt.Errorf("token has expired")
ErrEmptySecret = fmt.Errorf("secret is required")
)
// CreateToken creates a new token from username and name.
func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
if tv.secret == "" {
return "", ErrEmptySecret
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"usr": user.Email,
"name": user.Name,
"exp": time.Now().Add(time.Hour * expiration).Unix(),
"exp": time.Now().Add(tv.Expiration).Unix(),
"id": user.ID,
})
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte(tv.Secret))
t, err := token.SignedString([]byte(tv.secret))
if err != nil {
return "", fmt.Errorf("create token: %w", err)
}
@ -45,19 +58,17 @@ func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
return t, nil
}
var (
ErrUnexpectedSigningMethod = fmt.Errorf("unexpected signing method")
ErrInvalidToken = fmt.Errorf("token is invalid")
ErrTokenExpired = fmt.Errorf("token has expired")
)
// VerifyToken verifys a given string-token.
// VerifyToken verifies a given string-token.
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn
if tv.secret == "" {
return nil, ErrEmptySecret
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod)
}
return []byte(tv.Secret), nil
return []byte(tv.secret), nil
})
if err != nil {
return nil, fmt.Errorf("parse jwt: %w", err)
@ -76,36 +87,3 @@ func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error
}
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
View 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
}

View File

@ -5,6 +5,7 @@ package postgres
import (
"context"
"time"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid"
@ -14,7 +15,7 @@ const createAccount = `-- name: CreateAccount :one
INSERT INTO accounts
(name, budget_id)
VALUES ($1, $2)
RETURNING id, budget_id, name, on_budget
RETURNING id, budget_id, name, on_budget, is_open
`
type CreateAccountParams struct {
@ -30,12 +31,13 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A
&i.BudgetID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
)
return i, err
}
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
`
@ -47,13 +49,15 @@ func (q *Queries) GetAccount(ctx context.Context, id uuid.UUID) (Account, error)
&i.BudgetID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
)
return i, err
}
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
AND accounts.is_open = TRUE
ORDER BY accounts.name
`
@ -71,6 +75,7 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
&i.BudgetID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
); err != nil {
return nil, err
}
@ -86,12 +91,14 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
}
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 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 = 'Reconciled')::decimal(12,2) as reconciled_balance
FROM accounts
WHERE accounts.budget_id = $1
AND accounts.is_open = TRUE
ORDER BY accounts.name
`
@ -99,6 +106,8 @@ type GetAccountsWithBalanceRow struct {
ID uuid.UUID
Name string
OnBudget bool
IsOpen bool
LastReconciled time.Time
WorkingBalance numeric.Numeric
ClearedBalance numeric.Numeric
ReconciledBalance numeric.Numeric
@ -117,6 +126,8 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
&i.ID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
&i.LastReconciled,
&i.WorkingBalance,
&i.ClearedBalance,
&i.ReconciledBalance,
@ -137,6 +148,7 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
const searchAccounts = `-- name: SearchAccounts :many
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
WHERE accounts.budget_id = $1
AND accounts.is_open = TRUE
AND accounts.name LIKE $2
ORDER BY accounts.name
`
@ -184,25 +196,33 @@ func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams)
const updateAccount = `-- name: UpdateAccount :one
UPDATE accounts
SET name = $1,
on_budget = $2
WHERE accounts.id = $3
RETURNING id, budget_id, name, on_budget
on_budget = $2,
is_open = $3
WHERE accounts.id = $4
RETURNING id, budget_id, name, on_budget, is_open
`
type UpdateAccountParams struct {
Name string
OnBudget bool
IsOpen bool
ID uuid.UUID
}
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
err := row.Scan(
&i.ID,
&i.BudgetID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
)
return i, err
}

View File

@ -36,6 +36,7 @@ type Account struct {
BudgetID uuid.UUID
Name string
OnBudget bool
IsOpen bool
}
type Assignment struct {

View File

@ -11,26 +11,31 @@ WHERE accounts.id = $1;
-- name: GetAccounts :many
SELECT accounts.* FROM accounts
WHERE accounts.budget_id = $1
AND accounts.is_open = TRUE
ORDER BY accounts.name;
-- 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 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 = 'Reconciled')::decimal(12,2) as reconciled_balance
FROM accounts
WHERE accounts.budget_id = $1
AND accounts.is_open = TRUE
ORDER BY accounts.name;
-- name: SearchAccounts :many
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
WHERE accounts.budget_id = @budget_id
AND accounts.is_open = TRUE
AND accounts.name LIKE @search
ORDER BY accounts.name;
-- name: UpdateAccount :one
UPDATE accounts
SET name = $1,
on_budget = $2
WHERE accounts.id = $3
on_budget = $2,
is_open = $3
WHERE accounts.id = $4
RETURNING *;

View 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;

View File

@ -39,6 +39,7 @@ type TransactionsResponse struct {
type EditAccountRequest struct {
Name string `json:"name"`
OnBudget bool `json:"onBudget"`
IsOpen bool `json:"isOpen"`
}
func (h *Handler) editAccount(c *gin.Context) {
@ -59,6 +60,7 @@ func (h *Handler) editAccount(c *gin.Context) {
updateParams := postgres.UpdateAccountParams{
Name: request.Name,
OnBudget: request.OnBudget,
IsOpen: request.IsOpen,
ID: accountUUID,
}
account, err := h.Service.UpdateAccount(c.Request.Context(), updateParams)

View File

@ -28,11 +28,10 @@ func TestRegisterUser(t *testing.T) { //nolint:funlen
return
}
tokenVerifier, _ := jwt.NewTokenVerifier("this_is_my_demo_secret_for_unit_tests")
h := Handler{
Service: database,
TokenVerifier: &jwt.TokenVerifier{
Secret: "this_is_my_demo_secret_for_unit_tests",
},
Service: database,
TokenVerifier: tokenVerifier,
CredentialsVerifier: &bcrypt.Verifier{},
}

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<body class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200 box-border w-full">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>

View File

@ -26,7 +26,8 @@
"@vue/cli-service": "5.0.0-beta.7",
"sass": "^1.38.0",
"sass-loader": "^10.0.0",
"typescript": "^4.5.5",
"vite": "^2.7.2",
"vue-cli-plugin-vuetify": "~2.4.5"
"vue-tsc": "^0.32.0"
}
}

View File

@ -27,39 +27,34 @@ export default defineComponent({
</script>
<template>
<div class="box-border w-full">
<div class="flex bg-gray-400 p-4 m-2 rounded-lg">
<span class="flex-1 font-bold text-5xl -my-3 hidden md:inline" @click="toggleMenuSize"></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 class="flex flex-col md:flex-row flex-1">
<div
:class="[Menu.Expand ? 'md:w-72' : 'md:w-36', Menu.Show ? '' : 'hidden']"
class="md:block flex-shrink-0 w-full bg-gray-500 border-r-4 border-black"
>
<router-view name="sidebar"></router-view>
</div>
<div class="flex flex-col md:flex-row flex-1">
<div
:class="[Menu.Expand ? 'md:w-72' : 'md:w-36', Menu.Show ? '' : 'hidden']"
class="md:block flex-shrink-0 w-full"
>
<router-view name="sidebar"></router-view>
<div class="flex-1">
<div class="flex bg-gray-400 dark:bg-gray-600 p-4 fixed md:static top-0 left-0 w-full h-14">
<span
class="flex-1 font-bold text-5xl -my-3 hidden md:inline"
@click="toggleMenuSize"
></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 class="flex-1 p-6">
<div class="p-3 pl-6">
<router-view></router-view>
</div>
</div>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
</template>

View File

@ -2,6 +2,7 @@
import { ref, watch } from "vue"
import { GET } from "../api";
import { useBudgetsStore } from "../stores/budget";
import Input from "./Input.vue";
export interface Suggestion {
ID: string
@ -43,22 +44,25 @@ function load(text: String) {
});
};
function keypress(e: KeyboardEvent) {
if (e.key == "Enter") {
const selected = Suggestions.value[0];
selectElement(selected);
const el = (<HTMLInputElement>e.target);
const inputElements = Array.from(el.ownerDocument.querySelectorAll('input:not([disabled]):not([readonly])'));
const currentIndex = inputElements.indexOf(el);
const nextElement = inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0];
(<HTMLInputElement>nextElement).focus();
}
if (e.key != "Enter")
return;
const selected = Suggestions.value[0];
selectElement(selected);
const el = (<HTMLInputElement>e.target);
const inputElements = Array.from(el.ownerDocument.querySelectorAll('input:not([disabled]):not([readonly])'));
const currentIndex = inputElements.indexOf(el);
const nextElement = inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0];
(<HTMLInputElement>nextElement).focus();
};
function selectElement(element: Suggestion) {
emit('update:id', element.ID);
emit('update:text', element.Name);
emit('update:type', element.Type);
Suggestions.value = [];
};
function select(e: MouseEvent) {
const target = (<HTMLInputElement>e.target);
const valueAttribute = target.attributes.getNamedItem("value");
@ -68,6 +72,7 @@ function select(e: MouseEvent) {
const selected = Suggestions.value.filter(x => x.ID == selectedID)[0];
selectElement(selected);
};
function clear() {
emit('update:id', null);
emit('update:text', SearchQuery.value);
@ -77,14 +82,15 @@ function clear() {
<template>
<div>
<input
<Input
type="text"
class="border-b-2 border-black"
@keypress="keypress"
v-if="id == undefined"
v-model="SearchQuery"
/>
<span @click="clear" v-if="id != undefined" class="bg-gray-300">{{ text }}</span>
<div v-if="Suggestions.length > 0" class="absolute bg-gray-400 w-64 p-2">
<span @click="clear" v-if="id != undefined" class="bg-gray-300 dark:bg-gray-700">{{ text }}</span>
<div v-if="Suggestions.length > 0" class="absolute bg-gray-400 dark:bg-gray-600 w-64 p-2">
<span
v-for="suggestion in Suggestions"
class="block"

View File

@ -3,7 +3,7 @@
<template>
<button
class="px-4 py-2 text-base font-medium rounded-md shadow-sm focus:outline-none focus:ring-2"
class="px-4 rounded-md shadow-sm focus:outline-none focus:ring-2"
>
<slot></slot>
</button>

View File

@ -2,7 +2,7 @@
</script>
<template>
<div class="flex flex-row items-center bg-gray-300 h-32 rounded-lg">
<div class="flex flex-row items-center bg-gray-300 dark:bg-gray-700 rounded-lg">
<slot></slot>
</div>
</template>

View File

@ -0,0 +1,11 @@
<script lang="ts" setup>
const props = defineProps(["modelValue"]);
</script>
<template>
<input
type="checkbox"
:checked="modelValue"
@change="$emit('update:modelValue', ($event.target as HTMLInputElement)?.checked)"
class="dark:bg-slate-900">
</template>

View File

@ -1,8 +1,11 @@
<script lang="ts" setup>
import Input from './Input.vue';
const props = defineProps(["modelValue"]);
const emit = defineEmits(['update:modelValue']);
function dateToYYYYMMDD(d: Date) : string {
if(d == null)
return "";
// alternative implementations in https://stackoverflow.com/q/23593052/1850609
//return new Date(d.getTime() - (d.getTimezoneOffset() * 60 * 1000)).toISOString().split('T')[0];
return d.toISOString().split('T')[0];
@ -23,7 +26,7 @@ function selectAll(event: FocusEvent) {
</script>
<template>
<input
<Input
type="date"
ref="input"
v-bind:value="dateToYYYYMMDD(modelValue)"

View File

@ -0,0 +1,10 @@
<script lang="ts" setup>
const props = defineProps(["modelValue"]);
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement)?.value)"
class="dark:bg-slate-900">
</template>

View File

@ -3,11 +3,11 @@ import Card from '../components/Card.vue';
import { ref } from "vue";
const props = defineProps<{
buttonText: string,
buttonText?: string,
}>();
const emit = defineEmits<{
(e: 'submit'): void,
(e: 'submit', event : {cancel:boolean}): boolean,
(e: 'open'): void,
}>();
@ -20,8 +20,12 @@ function openDialog() {
visible.value = true;
};
function submitDialog() {
const e = {cancel: false};
emit("submit", e);
if(e.cancel)
return;
visible.value = false;
emit("submit");
}
</script>
@ -38,9 +42,9 @@ function submitDialog() {
v-if="visible"
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">
<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>
<div class="grid grid-cols-2 gap-6">
<button

View File

@ -3,6 +3,9 @@ import { computed, ref } from "vue";
import Autocomplete from './Autocomplete.vue'
import { useAccountStore } from '../stores/budget-account'
import DateInput from "./DateInput.vue";
import { useTransactionsStore } from "../stores/transactions";
import Input from "./Input.vue";
import Button from "./Button.vue";
const props = defineProps<{
transactionid: string
@ -10,8 +13,8 @@ const props = defineProps<{
const emit = defineEmits(["save"]);
const accountStore = useAccountStore();
const TX = accountStore.Transactions.get(props.transactionid)!;
const transactionsStore = useTransactionsStore();
const TX = transactionsStore.Transactions.get(props.transactionid)!;
const payeeType = ref<string|undefined>(undefined);
const payload = computed(() => JSON.stringify({
@ -29,35 +32,35 @@ const payload = computed(() => JSON.stringify({
function saveTransaction(e: MouseEvent) {
e.preventDefault();
accountStore.editTransaction(TX.ID, payload.value);
transactionsStore.editTransaction(TX.ID, payload.value);
emit('save');
}
</script>
<template>
<tr>
<td style="width: 90px;" class="text-sm">
<td class="text-sm">
<DateInput class="border-b-2 border-black" v-model="TX.Date" />
</td>
<td style="max-width: 150px;">
<td>
<Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
</td>
<td style="max-width: 200px;">
<td>
<Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
</td>
<td>
<input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
<Input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
</td>
<td style="width: 80px;" class="text-right">
<input
<td class="text-right">
<Input
class="text-right block w-full border-b-2 border-black"
type="currency"
v-model="TX.Amount"
/>
</td>
<td style="width: 20px;">
<input type="submit" @click="saveTransaction" value="Save" />
<td>
<Button class="bg-blue-500" @click="saveTransaction">Save</Button>
</td>
<td style="width: 20px;"></td>
<td></td>
</tr>
</template>

View File

@ -1,8 +1,10 @@
<script lang="ts" setup>
import { computed, ref } from "vue";
import Autocomplete, { Suggestion } from '../components/Autocomplete.vue'
import { Transaction, useAccountStore } from '../stores/budget-account'
import Autocomplete from '../components/Autocomplete.vue'
import { Transaction, useTransactionsStore } from "../stores/transactions";
import DateInput from "./DateInput.vue";
import Button from "./Button.vue";
import Input from "./Input.vue";
const props = defineProps<{
budgetid: string
@ -22,6 +24,7 @@ const TX = ref<Transaction>({
ID: "",
Status: "Uncleared",
TransferAccount: "",
Reconciled: false
});
const payeeType = ref<string|undefined>(undefined);
@ -41,37 +44,40 @@ const payload = computed(() => JSON.stringify({
state: "Uncleared"
}));
const accountStore = useAccountStore();
const transactionsStore = useTransactionsStore();
function saveTransaction(e: MouseEvent) {
e.preventDefault();
accountStore.saveTransaction(payload.value);
transactionsStore.saveTransaction(payload.value);
}
</script>
<template>
<tr>
<td style="width: 90px;" class="text-sm">
<label class="md:hidden">Date</label>
<td class="text-sm">
<DateInput class="border-b-2 border-black" v-model="TX.Date" />
</td>
<td style="max-width: 150px;">
<label class="md:hidden">Payee</label>
<td>
<Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
</td>
<td style="max-width: 200px;">
<label class="md:hidden">Category</label>
<td>
<Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
</td>
<td>
<input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
<td class="col-span-2">
<Input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
</td>
<td style="width: 80px;" class="text-right">
<input
<label class="md:hidden">Amount</label>
<td class="text-right">
<Input
class="text-right block w-full border-b-2 border-black"
type="currency"
v-model="TX.Amount"
/>
</td>
<td style="width: 20px;">
<input type="submit" @click="saveTransaction" value="Save" />
<td class="hidden md:table-cell">
<Button class="bg-blue-500" @click="saveTransaction">Save</Button>
</td>
<td style="width: 20px;"></td>
</tr>
</template>

View File

@ -1,50 +1,80 @@
<script lang="ts" setup>
import { computed, ref } from "vue";
import { useBudgetsStore } from "../stores/budget";
import { Transaction, useAccountStore } from "../stores/budget-account";
import { useTransactionsStore } from "../stores/transactions";
import Currency from "./Currency.vue";
import TransactionEditRow from "./TransactionEditRow.vue";
import { formatDate } from "../date";
import { useAccountStore } from "../stores/budget-account";
import Input from "./Input.vue";
import Checkbox from "./Checkbox.vue";
const props = defineProps<{
transaction: Transaction,
transactionid: string,
index: number,
}>();
const edit = ref(false);
const CurrentBudgetID = computed(() => useBudgetsStore().CurrentBudgetID);
const Reconciling = computed(() => useAccountStore().Reconciling);
const Reconciling = computed(() => useTransactionsStore().Reconciling);
const transactionsStore = useTransactionsStore();
const TX = transactionsStore.Transactions.get(props.transactionid)!;
function dateChanged() {
const currentAccount = useAccountStore().CurrentAccount;
if (currentAccount == null)
return true;
const transactionIndex = currentAccount.Transactions.indexOf(props.transactionid);
if(transactionIndex<=0)
return true;
const previousTransactionId = currentAccount.Transactions[transactionIndex-1];
const previousTransaction = transactionsStore.Transactions.get(previousTransactionId);
return TX.Date.getTime() != previousTransaction?.Date.getTime();
}
function getStatusSymbol() {
if(TX.Status == "Reconciled")
return "✔";
if(TX.Status == "Uncleared")
return "*";
return "✘";
}
</script>
<template>
<tr v-if="dateChanged()" class="table-row md:hidden">
<td class="bg-gray-200 dark:bg-gray-800 rounded-lg p-2" colspan="5">{{ formatDate(TX.Date) }}</td>
</tr>
<tr
v-if="!edit"
class="{{new Date(transaction.Date) > new Date() ? 'future' : ''}}"
:class="[index % 6 < 3 ? 'bg-gray-300' : 'bg-gray-100']"
class="{{new Date(TX.Date) > new Date() ? 'future' : ''}}"
:class="[index % 6 < 3 ? 'md:bg-gray-300 dark:md:bg-gray-700' : 'md:bg-gray-100 dark:md:bg-gray-900']"
>
<!--:class="[index % 6 < 3 ? index % 6 === 1 ? 'bg-gray-400' : 'bg-gray-300' : index % 6 !== 4 ? 'bg-gray-100' : '']">-->
<td>{{ formatDate(transaction.Date) }}</td>
<td>{{ transaction.TransferAccount ? "Transfer : " + transaction.TransferAccount : transaction.Payee }}</td>
<td>{{ transaction.CategoryGroup ? transaction.CategoryGroup + " : " + transaction.Category : "" }}</td>
<td class="hidden md:block">{{ formatDate(TX.Date) }}</td>
<td class="pl-2 md:pl-0">{{ TX.TransferAccount ? "Transfer : " + TX.TransferAccount : TX.Payee }}</td>
<td>{{ TX.CategoryGroup ? TX.CategoryGroup + " : " + TX.Category : "" }}</td>
<td>
<a
:href="'/budget/' + CurrentBudgetID + '/transaction/' + transaction.ID"
>{{ transaction.Memo }}</a>
:href="'/budget/' + CurrentBudgetID + '/transaction/' + TX.ID"
>{{ TX.Memo }}</a>
</td>
<td>
<Currency class="block" :value="transaction.Amount" />
<Currency class="block" :value="TX.Amount" />
</td>
<td>{{ transaction.Status == "Reconciled" ? "✔" : (transaction.Status == "Uncleared" ? "" : "*") }}</td>
<td class="text-right">
{{ transaction.GroupID ? "☀" : "" }}
{{ TX.GroupID ? "☀" : "" }}
{{ getStatusSymbol() }}
<a @click="edit = true;"></a>
</td>
<td v-if="Reconciling && transaction.Status != 'Reconciled'">
<input type="checkbox" v-model="transaction.Reconciled" />
<Checkbox v-if="Reconciling && TX.Status != 'Reconciled'" v-model="TX.Reconciled" />
</td>
</tr>
<TransactionEditRow v-if="edit" :transactionid="transaction.ID" @save="edit = false" />
<TransactionEditRow v-if="edit" :transactionid="TX.ID" @save="edit = false" />
</template>
<style>

View File

@ -2,29 +2,51 @@
import { computed, ref } from 'vue';
import Modal from '../components/Modal.vue';
import { useAccountStore } from '../stores/budget-account';
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 CurrentAccount = computed(() => accountStore.CurrentAccount);
const accountName = ref("");
const accountOnBudget = ref(true);
const accountOpen = ref(true);
const error = ref("");
function editAccount(e : any) {
accountStore.EditAccount(CurrentAccount.value?.ID ?? "", accountName.value, accountOnBudget.value);
function editAccount(e : {cancel:boolean}) : boolean {
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) {
accountName.value = CurrentAccount.value?.Name ?? "";
accountOnBudget.value = CurrentAccount.value?.OnBudget ?? true;
accountOpen.value = CurrentAccount.value?.IsOpen ?? true;
}
</script>
<template>
<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">
<input
class="border-2"
<Input
class="border-2 dark:border-gray-700"
type="text"
v-model="accountName"
placeholder="Account name"
@ -32,13 +54,23 @@ function openEditAccount(e : any) {
/>
</div>
<div class="mt-2 px-7 py-3">
<input
<Checkbox
class="border-2"
type="checkbox"
v-model="accountOnBudget"
required
/>
<label>On Budget</label>
</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>
</template>

View File

@ -2,6 +2,7 @@
import Modal from '../components/Modal.vue';
import { ref } from "vue";
import { useBudgetsStore } from '../stores/budget';
import Input from '../components/Input.vue';
const budgetName = ref("");
function saveBudget() {
@ -12,7 +13,7 @@ function saveBudget() {
<template>
<Modal button-text="New Budget" @submit="saveBudget">
<div class="mt-2 px-7 py-3">
<input class="border-2" type="text" v-model="budgetName" placeholder="Budget name" required />
<Input class="border-2" type="text" v-model="budgetName" placeholder="Budget name" required />
</div>
</Modal>
</template>

View File

@ -8,4 +8,10 @@ h1 {
a {
text-decoration: underline;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -11,7 +11,10 @@ const app = createApp(App)
app.use(router)
const pinia = createPinia()
pinia.use(PiniaLogger())
pinia.use(PiniaLogger({
expanded: false,
showDuration: true
}))
app.use(pinia)
app.mount('#app')

View File

@ -6,6 +6,9 @@ import TransactionInputRow from "../components/TransactionInputRow.vue";
import { useAccountStore } from "../stores/budget-account";
import EditAccount from "../dialogs/EditAccount.vue";
import Button from "../components/Button.vue";
import { useTransactionsStore } from "../stores/transactions";
import Modal from "../components/Modal.vue";
import Input from "../components/Input.vue";
defineProps<{
budgetid: string
@ -13,89 +16,120 @@ defineProps<{
}>()
const accounts = useAccountStore();
const transactions = useTransactionsStore();
const TargetReconcilingBalance = ref(0);
function setReconciled(event: Event) {
const target = event.target as HTMLInputElement;
accounts.SetReconciledForAllTransactions(target.checked);
transactions.SetReconciledForAllTransactions(target.checked);
}
function cancelReconcilation() {
accounts.SetReconciledForAllTransactions(false);
accounts.Reconciling = false;
transactions.SetReconciledForAllTransactions(false);
transactions.Reconciling = false;
}
function submitReconcilation() {
accounts.SubmitReconcilation(0);
accounts.Reconciling = false;
transactions.SubmitReconcilation(0);
transactions.Reconciling = false;
}
function createReconcilationTransaction() {
const diff = TargetReconcilingBalance.value - accounts.ReconcilingBalance ;
accounts.SubmitReconcilation(diff);
accounts.Reconciling = false;
const diff = TargetReconcilingBalance.value - transactions.ReconcilingBalance;
transactions.SubmitReconcilation(diff);
transactions.Reconciling = false;
}
</script>
<template>
<h1 class="inline">{{ accounts.CurrentAccount?.Name }}</h1>
<EditAccount />
<br />
<div class="grid grid-cols-[1fr_auto]">
<div>
<h1 class="inline">
{{ accounts.CurrentAccount?.Name }}
</h1>
<EditAccount />
</div>
<span>
Current Balance:
<Currency :value="accounts.CurrentAccount?.WorkingBalance" />
</span>
<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">
Working:
<Currency :value="accounts.CurrentAccount?.WorkingBalance" />
</span>
<span>
Cleared Balance:
<Currency :value="accounts.CurrentAccount?.ClearedBalance" />
</span>
<span class="rounded-lg p-1 whitespace-nowrap flex-1">
Cleared:
<Currency :value="accounts.CurrentAccount?.ClearedBalance" />
</span>
<span v-if="accounts.Reconciling" class="border-2 block bg-gray-200 rounded-lg p-2">
Is <Currency :value="accounts.ReconcilingBalance" /> your current balance?
<Button
class="bg-blue-500 mx-3"
@click="submitReconcilation">Yes!</Button>
<br />
<span
class="rounded-lg bg-blue-500 p-1 whitespace-nowrap flex-1"
v-if="!transactions.Reconciling"
@click="transactions.Reconciling = true"
>
Reconciled:
<Currency :value="accounts.CurrentAccount?.ReconciledBalance" />
</span>
<span v-if="transactions.Reconciling" class="contents">
<Button @click="submitReconcilation"
class="bg-blue-500 p-1 whitespace-nowrap flex-1">
My current balance is&nbsp;
<Currency :value="transactions.ReconcilingBalance" />
</Button>
<Button @click="createReconcilationTransaction"
class="bg-orange-500 p-1 whitespace-nowrap flex-1">
No, it's:
<Input
class="text-right w-20 bg-transparent dark:bg-transparent border-b-2"
type="number"
v-model="TargetReconcilingBalance"
/>
(Difference
<Currency
:value="transactions.ReconcilingBalance - TargetReconcilingBalance"
/>)
</Button>
<Button class="bg-red-500 p-1 flex-1" @click="cancelReconcilation">Cancel</Button>
</span>
</div>
</div>
No, it's: <input class="text-right" type="number" v-model="TargetReconcilingBalance" />
Difference: <Currency :value="accounts.ReconcilingBalance - TargetReconcilingBalance" />
<Button
class="bg-orange-500 mx-3"
v-if="Math.abs(accounts.ReconcilingBalance - TargetReconcilingBalance) > 0.01"
@click="createReconcilationTransaction"
>Create reconciling Transaction</Button>
<Button
class="bg-red-500 mx-3"
@click="cancelReconcilation"
>Cancel</Button>
</span>
<span v-if="!accounts.Reconciling">
Reconciled Balance:
<Currency :value="accounts.CurrentAccount?.ReconciledBalance" />
<Button class="bg-blue-500" @click="accounts.Reconciling = true" v-if="!accounts.Reconciling">Reconcile</Button>
</span>
<table>
<tr class="font-bold">
<td style="width: 90px;">Date</td>
<td class="hidden md:block" style="width: 90px;">Date</td>
<td style="max-width: 150px;">Payee</td>
<td style="max-width: 200px;">Category</td>
<td>Memo</td>
<td class="text-right">Amount</td>
<td style="width: 20px;"></td>
<td style="width: 40px;"></td>
<td style="width: 20px;" v-if="accounts.Reconciling">
<input type="checkbox" @input="setReconciled" />
<td style="width: 80px;">
<Input v-if="transactions.Reconciling" type="checkbox" @input="setReconciled" />
</td>
</tr>
<TransactionInputRow :budgetid="budgetid" :accountid="accountid" />
<TransactionInputRow
class="hidden md:table-row"
:budgetid="budgetid"
:accountid="accountid"
/>
<TransactionRow
v-for="(transaction, index) in accounts.TransactionsList" :key="transaction.ID"
:transaction="transaction"
v-for="(transaction, index) in transactions.TransactionsList"
:key="transaction.ID"
:transactionid="transaction.ID"
:index="index"
/>
</table>
<div class="md:hidden">
<Modal>
<template v-slot:placeholder>
<Button class="fixed right-4 bottom-4 font-bold text-lg bg-blue-500 py-2">+</Button>
</template>
<TransactionInputRow
class="grid grid-cols-2"
:budgetid="budgetid"
:accountid="accountid"
/>
</Modal>
</div>
</template>
<style>

View File

@ -2,7 +2,7 @@
import { computed } from "vue";
import Currency from "../components/Currency.vue"
import { useBudgetsStore } from "../stores/budget"
import { useAccountStore } from "../stores/budget-account"
import { Account, useAccountStore } from "../stores/budget-account"
import { useSettingsStore } from "../stores/settings"
const ExpandMenu = computed(() => useSettingsStore().Menu.Expand);
@ -16,51 +16,64 @@ const OnBudgetAccounts = computed(() => accountStore.OnBudgetAccounts);
const OffBudgetAccounts = computed(() => accountStore.OffBudgetAccounts);
const OnBudgetAccountsBalance = computed(() => accountStore.OnBudgetAccountsBalance);
const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBalance);
function isRecentlyReconciled(account : Account) {
const now = new Date().getTime();
const recently = 7 * 24 * 60 * 60 * 1000;
return new Date(now - recently).getTime() < account.LastReconciled.getTime();
}
function getAccountName(account : Account) {
const reconciledMarker = isRecentlyReconciled(account) ? "" : " *";
return account.Name + reconciledMarker;
}
</script>
<template>
<div class="flex flex-col">
<span class="m-1 p-1 px-3 text-xl">
<router-link to="/dashboard"></router-link>
<div class="flex flex-col mt-14 md:mt-0">
<span class="m-2 p-1 px-3 h-10 overflow-hidden" :class="[ExpandMenu ? 'text-2xl' : 'text-md']">
<router-link to="/dashboard" style="font-size:150%"></router-link>
{{CurrentBudgetName}}
</span>
<span class="bg-orange-200 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+'/reports'">Reports</router-link>-->
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
</span>
<li class="bg-orange-200 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">
<span>On-Budget Accounts</span>
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OnBudgetAccountsBalance" />
</div>
<div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between">
<router-link :to="'/budget/'+CurrentBudgetID+'/account/'+account.ID">{{account.Name}}</router-link>
<router-link :to="'/budget/'+CurrentBudgetID+'/account/'+account.ID">{{getAccountName(account)}}</router-link>
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.ClearedBalance" />
</div>
</li>
<li class="bg-red-200 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">
<span>Off-Budget Accounts</span>
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OffBudgetAccountsBalance" />
</div>
<div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between">
<router-link :to="'/budget/'+CurrentBudgetID+'/account/'+account.ID">{{account.Name}}</router-link>
<router-link :to="'/budget/'+CurrentBudgetID+'/account/'+account.ID">{{getAccountName(account)}}</router-link>
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.ClearedBalance" />
</div>
</li>
<li class="bg-red-200 rounded-lg m-1 p-1 px-3">
Closed Accounts
<!--
<li class="bg-slate-100 dark:bg-slate-800 my-2 p-2 px-3">
<div class="flex flex-row justify-between font-bold">
<span>Closed Accounts</span>
</div>
+ Add Account
</li>
-->
<!--<li>
<router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link>
</li>-->
<li class="bg-red-200 rounded-lg m-1 p-1 px-3">
+ Add Account
</li>
<li class="bg-red-200 rounded-lg m-1 p-1 px-3">
<li class="bg-red-100 dark:bg-slate-600 my-2 p-2 px-3">
<router-link :to="'/budget/'+CurrentBudgetID+'/settings'">Budget-Settings</router-link>
</li>
<!--<li><router-link to="/admin">Admin</router-link></li>-->
</div>
</template>
</template>

View File

@ -17,7 +17,7 @@ const CurrentBudgetID = computed(() => budgetsStore.CurrentBudgetID);
const accountStore = useAccountStore();
const categoriesForMonth = accountStore.CategoriesForMonthAndGroup;
function GetCategories(group : string) {
function GetCategories(group: string) {
return [...categoriesForMonth(selected.value.Year, selected.value.Month, group)];
};
@ -28,20 +28,20 @@ const GroupsForMonth = computed(() => {
const previous = computed(() => ({
Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(),
Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(),
Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(),
Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(),
}));
const current = computed(() => ({
Year: new Date().getFullYear(),
Month: new Date().getMonth(),
Year: new Date().getFullYear(),
Month: new Date().getMonth(),
}));
const selected = computed(() => ({
Year: Number(props.year) ?? current.value.Year,
Month: Number(props.month ?? current.value.Month)
Year: Number(props.year) ?? current.value.Year,
Month: Number(props.month ?? current.value.Month)
}));
const next = computed(() => ({
Year: new Date(selected.value.Year, Number(props.month) + 1, 1).getFullYear(),
Month: new Date(selected.value.Year, Number(props.month) + 1, 1).getMonth(),
Year: new Date(selected.value.Year, Number(props.month) + 1, 1).getFullYear(),
Month: new Date(selected.value.Year, Number(props.month) + 1, 1).getMonth(),
}));
watchEffect(() => {
@ -56,12 +56,11 @@ onMounted(() => {
const expandedGroups = ref<Map<string, boolean>>(new Map<string, boolean>())
function toggleGroup(group : {Name : string, Expand: boolean}) {
console.log(expandedGroups.value);
function toggleGroup(group: { Name: string, Expand: boolean }) {
expandedGroups.value.set(group.Name, !(expandedGroups.value.get(group.Name) ?? group.Expand))
}
function getGroupState(group : {Name : string, Expand: boolean}) : boolean {
function getGroupState(group: { Name: string, Expand: boolean }): boolean {
return expandedGroups.value.get(group.Name) ?? group.Expand;
}
</script>
@ -71,43 +70,32 @@ function getGroupState(group : {Name : string, Expand: boolean}) : boolean {
<div>
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
>Previous Month</router-link>-
>&lt;&lt;</router-link>&nbsp;
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
>Current Month</router-link>-
>Current Month</router-link>&nbsp;
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
>Next Month</router-link>
>&gt;&gt;</router-link>
</div>
<div class="container col-lg-12 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5" id="content">
<span class="hidden sm:block"></span>
<span class="hidden lg:block text-right">Leftover</span>
<span class="hidden sm:block text-right">Assigned</span>
<span class="hidden sm:block text-right">Activity</span>
<span class="hidden sm:block text-right">Available</span>
<template v-for="group in GroupsForMonth">
<a
class="text-lg font-bold col-span-2 sm:col-span-4 lg:col-span-5"
@click="toggleGroup(group)"
>{{ (getGroupState(group) ? "" : "+") + " " + group.Name }}</a>
<template v-for="category in GetCategories(group.Name)" v-if="getGroupState(group)">
<span class="whitespace-nowrap overflow-hidden">{{ category.Name }}</span>
<Currency :value="category.AvailableLastMonth" class="hidden lg:block" />
<Currency :value="category.Assigned" class="hidden sm:block" />
<Currency :value="category.Activity" class="hidden sm:block" />
<Currency :value="category.Available" />
</template>
</template>
</div>
<table class="container col-lg-12" id="content">
<tr>
<th>Category</th>
<th></th>
<th></th>
<th>Leftover</th>
<th>Assigned</th>
<th>Activity</th>
<th>Available</th>
</tr>
<tbody v-for="group in GroupsForMonth">
<a class="text-lg font-bold" @click="toggleGroup(group)">{{ (getGroupState(group) ? "" : "+") + " " + group.Name }}</a>
<tr v-for="category in GetCategories(group.Name)" v-if="getGroupState(group)">
<td>{{ category.Name }}</td>
<td></td>
<td></td>
<td class="text-right">
<Currency :value="category.AvailableLastMonth" />
</td>
<td class="text-right">
<Currency :value="category.Assigned" />
</td>
<td class="text-right">
<Currency :value="category.Activity" />
</td>
<td class="text-right">
<Currency :value="category.Available" />
</td>
</tr>
</tbody>
</table>
</template>

View File

@ -2,6 +2,7 @@
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { useSessionStore } from "../stores/session";
import Input from "../components/Input.vue";
const error = ref("");
const login = ref({ user: "", password: "" });
@ -28,10 +29,10 @@ function formSubmit(e: MouseEvent) {
<template>
<div>
<input type="text" v-model="login.user"
<Input type="text" v-model="login.user"
placeholder="Username"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
<input type="password" v-model="login.password"
<Input type="password" v-model="login.password"
placeholder="Password"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
</div>

View File

@ -2,6 +2,7 @@
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { useSessionStore } from "../stores/session";
import Input from "../components/Input.vue";
const error = ref("");
const login = ref({ email: "", password: "", name: "" });
@ -28,13 +29,13 @@ function formSubmit(e: MouseEvent) {
<template>
<div>
<input type="text" v-model="login.name"
<Input type="text" v-model="login.name"
placeholder="Name"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
<input type="text" v-model="login.email"
<Input type="text" v-model="login.email"
placeholder="Email"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
<input type="password" v-model="login.password"
<Input type="password" v-model="login.password"
placeholder="Password"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
</div>

View File

@ -7,6 +7,7 @@ import { useSessionStore } from "../stores/session";
import Card from "../components/Card.vue";
import Button from "../components/Button.vue";
import { saveAs } from 'file-saver';
import Input from "../components/Input.vue";
const transactionsFile = ref<File | undefined>(undefined);
const assignmentsFile = ref<File | undefined>(undefined);
@ -82,42 +83,40 @@ function ynabExport() {
<h2 class="text-lg font-bold">Clear Budget</h2>
<p>This removes transactions and assignments to start from scratch. Accounts and categories are kept. Not undoable!</p>
<Button class="bg-red-500" @click="clearBudget">Clear budget</Button>
<Button class="bg-red-500 py-2" @click="clearBudget">Clear budget</Button>
</Card>
<Card class="flex-col p-3">
<h2 class="text-lg font-bold">Delete Budget</h2>
<p>This deletes the whole bugdet including all transactions, assignments, accounts and categories. Not undoable!</p>
<Button class="bg-red-500" @click="deleteBudget">Delete budget</button>
<Button class="bg-red-500 py-2" @click="deleteBudget">Delete budget</button>
</Card>
<Card class="flex-col p-3">
<h2 class="text-lg font-bold">Fix all historic negative category-balances</h2>
<p>This restores YNABs functionality, that would substract any overspent categories' balances from next months inflows.</p>
<Button class="bg-orange-500" @click="cleanNegative">Fix negative</button>
<Button class="bg-orange-500 py-2" @click="cleanNegative">Fix negative</button>
</Card>
<Card class="flex-col p-3">
<h2 class="text-lg font-bold">Import YNAB Budget</h2>
<div class="flex flex-row">
<div>
<label for="transactions_file">
Transaktionen:
<input type="file" @change="gotTransactions" accept="text/*" />
</label>
<br />
<label for="assignments_file">
Budget:
<input type="file" @change="gotAssignments" accept="text/*" />
</label>
</div>
<Button class="bg-blue-500" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
<div>
<label for="transactions_file">
Transaktionen:
<input type="file" @change="gotTransactions" accept="text/*" />
</label>
<br />
<label for="assignments_file">
Budget:
<input type="file" @change="gotAssignments" accept="text/*" />
</label>
</div>
<Button class="bg-blue-500 py-2" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
</Card>
<Card class="flex-col p-3">
<h2 class="text-lg font-bold">Export as YNAB TSV</h2>
<div class="flex flex-row">
<Button class="bg-blue-500" @click="ynabExport">Export</Button>
<Button class="bg-blue-500 py-2" @click="ynabExport">Export</Button>
</div>
</Card>
</div>

View File

@ -11,9 +11,8 @@ const formatTime = (date = new Date()) => {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const milliseconds = date.getMilliseconds().toString();
return `${hours}:${minutes}:${seconds}:${milliseconds}`;
return `${hours}:${minutes}:${seconds}`;
};
export interface PiniaLoggerOptions {
@ -52,7 +51,7 @@ export const PiniaLogger = (config = defaultOptions) => (ctx: PiniaPluginContext
const duration = endTime - startTime + 'ms';
const nextState = cloneDeep(ctx.store.$state);
const storeName = action.store.$id;
const title = `action 🍍 ${options.showStoreName ? `[${storeName}] ` : ''}${action.name} ${isError ? `failed after ${duration} ` : ''}@ ${formatTime()}`;
const title = `${formatTime()} action 🍍 ${options.showStoreName ? `[${storeName}] ` : ''}${action.name} ${isError ? `failed after ` : ''}in ${duration}`;
console[options.expanded ? 'group' : 'groupCollapsed'](`%c${title}`, `font-weight: bold; ${isError ? 'color: #ed4981;' : ''}`);
console.log('%cprev state', 'font-weight: bold; color: grey;', prevState);

View File

@ -2,41 +2,26 @@ import { defineStore } from "pinia"
import { GET, POST } from "../api";
import { useBudgetsStore } from "./budget";
import { useSessionStore } from "./session";
import { useTransactionsStore } from "./transactions";
interface State {
Accounts: Map<string, Account>
CurrentAccountID: string | null
Categories: Map<string, Category>
Months: Map<number, Map<number, Map<string, Category>>>
Transactions: Map<string, Transaction>
Assignments: []
Reconciling: boolean
}
export interface Transaction {
ID: string
Date: Date
TransferAccount: string
CategoryGroup: string
Category: string
CategoryID: string | undefined
Memo: string
Status: string
GroupID: string
Payee: string
PayeeID: string | undefined
Amount: number
Reconciled: boolean
}
export interface Account {
ID: string
Name: string
OnBudget: boolean
IsOpen: boolean
ClearedBalance: number
WorkingBalance: number
ReconciledBalance: number
Transactions: string[]
LastReconciled: Date
}
export interface Category {
@ -55,9 +40,7 @@ export const useAccountStore = defineStore("budget/account", {
CurrentAccountID: null,
Months: new Map<number, Map<number, Map<string, Category>>>(),
Categories: new Map<string, Category>(),
Transactions: new Map<string, Transaction>(),
Assignments: [],
Reconciling: false,
}),
getters: {
AccountsList(state) {
@ -90,19 +73,16 @@ export const useAccountStore = defineStore("budget/account", {
return categories.filter(x => x.Group == group);
}
},
GetAccount(state) {
return (accountid: string) => {
return this.Accounts.get(accountid);
}
},
CurrentAccount(state): Account | undefined {
if (state.CurrentAccountID == null)
return undefined;
return state.Accounts.get(state.CurrentAccountID);
},
ReconcilingBalance(state): number {
let reconciledBalance = this.CurrentAccount!.ReconciledBalance;
for (const transaction of this.TransactionsList) {
if (transaction.Reconciled)
reconciledBalance += transaction.Amount;
}
return reconciledBalance;
return this.GetAccount(state.CurrentAccountID);
},
OnBudgetAccounts(state) {
return [...state.Accounts.values()].filter(x => x.OnBudget);
@ -116,49 +96,42 @@ export const useAccountStore = defineStore("budget/account", {
OffBudgetAccountsBalance(state): number {
return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.ClearedBalance), 0);
},
TransactionsList(state): Transaction[] {
return this.CurrentAccount!.Transactions.map(x => {
return this.Transactions.get(x)!
});
},
},
actions: {
async SetCurrentAccount(budgetid: string, accountid: string) {
if (budgetid == null)
return
return;
this.CurrentAccountID = accountid;
const account = this.CurrentAccount;
if (account == undefined)
return
if (accountid == null)
return;
const account = this.GetAccount(accountid)!;
useSessionStore().setTitle(account.Name);
await this.FetchAccount(account);
},
AddTransaction(account: Account, transaction: any) {
transaction.Date = new Date(transaction.Date);
this.Transactions.set(transaction.ID, transaction);
},
async FetchAccount(account: Account) {
const result = await GET("/account/" + account.ID + "/transactions");
const response = await result.json();
account.Transactions = [];
for (const transaction of response.Transactions) {
this.AddTransaction(account, transaction);
account.Transactions.push(transaction.ID);
}
const transactionsStore = useTransactionsStore()
const transactions = transactionsStore.AddTransactions(response.Transactions);
account.Transactions = transactions;
},
async FetchMonthBudget(budgetid: string, year: number, month: number) {
const result = await GET("/budget/" + budgetid + "/" + year + "/" + month);
const result = await GET("/budget/" + budgetid + "/" + year + "/" + (month+1));
const response = await result.json();
if (response.Categories == undefined || response.Categories.length <= 0)
return;
this.addCategoriesForMonth(year, month, response.Categories);
},
async EditAccount(accountid: string, name: string, onBudget: boolean) {
const result = await POST("/account/" + accountid, JSON.stringify({ name: name, onBudget: onBudget }));
async EditAccount(accountid: string, name: string, onBudget: boolean, isOpen: boolean) {
const result = await POST("/account/" + accountid, JSON.stringify({ name: name, onBudget: onBudget, isOpen: isOpen }));
const response = await result.json();
useBudgetsStore().MergeBudgetingData(response);
if(!isOpen) {
this.Accounts.delete(accountid);
}
},
addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
this.$patch((state) => {
@ -172,48 +145,9 @@ export const useAccountStore = defineStore("budget/account", {
state.Months.set(year, yearMap);
});
},
SetReconciledForAllTransactions(value: boolean) {
for (const transaction of this.TransactionsList) {
if (transaction.Status == "Reconciled")
continue;
transaction.Reconciled = value;
}
},
async SubmitReconcilation(reconciliationTransactionAmount: number) {
const account = this.CurrentAccount!;
const reconciledTransactions = this.TransactionsList.filter(x => x.Reconciled);
for (const transaction of reconciledTransactions) {
account.ReconciledBalance += transaction.Amount;
transaction.Status = "Reconciled";
transaction.Reconciled = false;
}
const result = await POST("/account/" + this.CurrentAccountID + "/reconcile", JSON.stringify({
transactionIDs: reconciledTransactions.map(x => x.ID),
reconciliationTransactionAmount: reconciliationTransactionAmount.toString(),
}));
const response = await result.json();
const recTrans = response.ReconciliationTransaction;
if (recTrans) {
this.AddTransaction(account, recTrans);
account.Transactions.unshift(recTrans.ID);
}
console.log("Reconcile: " + response.message);
},
logout() {
this.$reset()
},
async saveTransaction(payload: string) {
const result = await POST("/transaction/new", payload);
const response = await result.json();
this.AddTransaction(this.CurrentAccount!, response);
this.CurrentAccount?.Transactions.unshift(response.ID);
},
async editTransaction(transactionid: string, payload: string) {
const result = await POST("/transaction/" + transactionid, payload);
const response = await result.json();
this.AddTransaction(this.CurrentAccount!, response);
}
}
})

View File

@ -53,12 +53,16 @@ export const useBudgetsStore = defineStore('budget', {
const response = await result.json();
this.MergeBudgetingData(response);
},
MergeBudgetingData(response : any) {
MergeBudgetingData(response: any) {
const accounts = useAccountStore();
for (const account of response.Accounts || []) {
useAccountStore().Accounts.set(account.ID, account);
const existingAccount = accounts.Accounts.get(account.ID);
account.Transactions = existingAccount?.Transactions ?? [];
account.LastReconciled = new Date(account.LastReconciled);
accounts.Accounts.set(account.ID, account);
}
for (const category of response.Categories || []) {
useAccountStore().Categories.set(category.ID, category);
accounts.Categories.set(category.ID, category);
}
},
}

View File

@ -0,0 +1,105 @@
import { defineStore } from "pinia"
import { POST } from "../api";
import { useAccountStore } from "./budget-account";
interface State {
Transactions: Map<string, Transaction>
Reconciling: boolean
}
export interface Transaction {
ID: string
Date: Date
TransferAccount: string
CategoryGroup: string
Category: string
CategoryID: string | undefined
Memo: string
Status: string
GroupID: string
Payee: string
PayeeID: string | undefined
Amount: number
Reconciled: boolean
}
export const useTransactionsStore = defineStore("budget/transactions", {
state: (): State => ({
Transactions: new Map<string, Transaction>(),
Reconciling: false,
}),
getters: {
ReconcilingBalance(state): number {
const accountsStore = useAccountStore()
let reconciledBalance = accountsStore.CurrentAccount!.ReconciledBalance;
for (const transaction of this.TransactionsList) {
if (transaction.Reconciled)
reconciledBalance += transaction.Amount;
}
return reconciledBalance;
},
TransactionsList(state): Transaction[] {
const accountsStore = useAccountStore()
return accountsStore.CurrentAccount!.Transactions.map(x => {
return this.Transactions.get(x)!
});
},
},
actions: {
AddTransactions(transactions: Array<Transaction>) {
const transactionIds = [] as Array<string>;
this.$patch(() => {
for (const transaction of transactions) {
transaction.Date = new Date(transaction.Date);
this.Transactions.set(transaction.ID, transaction);
transactionIds.push(transaction.ID);
}
});
return transactionIds;
},
SetReconciledForAllTransactions(value: boolean) {
for (const transaction of this.TransactionsList) {
if (transaction.Status == "Reconciled")
continue;
transaction.Reconciled = value;
}
},
async SubmitReconcilation(reconciliationTransactionAmount: number) {
const accountsStore = useAccountStore()
const account = accountsStore.CurrentAccount!;
const reconciledTransactions = this.TransactionsList.filter(x => x.Reconciled);
for (const transaction of reconciledTransactions) {
account.ReconciledBalance += transaction.Amount;
transaction.Status = "Reconciled";
transaction.Reconciled = false;
}
const result = await POST("/account/" + accountsStore.CurrentAccountID + "/reconcile", JSON.stringify({
transactionIDs: reconciledTransactions.map(x => x.ID),
reconciliationTransactionAmount: reconciliationTransactionAmount.toString(),
}));
const response = await result.json();
const recTrans = response.ReconciliationTransaction;
if (recTrans) {
this.AddTransactions([recTrans]);
account.Transactions.unshift(recTrans.ID);
}
},
logout() {
this.$reset()
},
async saveTransaction(payload: string) {
const accountsStore = useAccountStore()
const result = await POST("/transaction/new", payload);
const response = await result.json() as Transaction;
this.AddTransactions([response]);
accountsStore.CurrentAccount?.Transactions.unshift(response.ID);
},
async editTransaction(transactionid: string, payload: string) {
const result = await POST("/transaction/" + transactionid, payload);
const response = await result.json() as Transaction;
this.AddTransactions([response]);
}
}
})

View File

@ -273,6 +273,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c"
integrity sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==
"@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
version "7.17.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.3.tgz#b07702b982990bf6fdc1da5049a23fece4c5c3d0"
integrity sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050"
@ -925,7 +930,7 @@
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.4.4":
"@babel/types@^7.0.0", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.4.4", "@babel/types@^7.6.1", "@babel/types@^7.9.6":
version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b"
integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==
@ -933,6 +938,25 @@
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
"@emmetio/abbreviation@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@emmetio/abbreviation/-/abbreviation-2.2.3.tgz#2b3c0383c1a4652f677d5b56fb3f1616fe16ef10"
integrity sha512-87pltuCPt99aL+y9xS6GPZ+Wmmyhll2WXH73gG/xpGcQ84DRnptBsI2r0BeIQ0EB/SQTOe2ANPqFqj3Rj5FOGA==
dependencies:
"@emmetio/scanner" "^1.0.0"
"@emmetio/css-abbreviation@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@emmetio/css-abbreviation/-/css-abbreviation-2.1.4.tgz#90362e8a1122ce3b76f6c3157907d30182f53f54"
integrity sha512-qk9L60Y+uRtM5CPbB0y+QNl/1XKE09mSO+AhhSauIfr2YOx/ta3NJw2d8RtCFxgzHeRqFRr8jgyzThbu+MZ4Uw==
dependencies:
"@emmetio/scanner" "^1.0.0"
"@emmetio/scanner@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@emmetio/scanner/-/scanner-1.0.0.tgz#065b2af6233fe7474d44823e3deb89724af42b5f"
integrity sha512-8HqW8EVqjnCmWXVpqAOZf+EGESdkR27odcMMMGefgKXtar00SoYNSryGv//TELI4T3QFsECo78p+0lmalk/CFA==
"@hapi/address@2.x.x":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@ -1262,6 +1286,75 @@
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.1.0.tgz#ddf5e0059f84f2ff649afc25ce5a59211e670542"
integrity sha512-AZ78WxvFMYd8JmM/GBV6a6SGGTU0GgN/0/4T+FnMMsLzFEzTeAUwuraapy50ifHZsC+G5SvWs86bvaCPTneFlA==
"@volar/code-gen@0.32.0":
version "0.32.0"
resolved "https://registry.yarnpkg.com/@volar/code-gen/-/code-gen-0.32.0.tgz#05bcb66e21b72a9ed632524d320323b1d9d0e579"
integrity sha512-vxXKzZs9DMf/iBEAFJRwPVCk6CQFYZjul9iQ9GZCAjmy2lotSvv5jBQm5unzIAQQpKv4HH3jfA0YD0aT58S4eQ==
dependencies:
"@volar/shared" "0.32.0"
"@volar/source-map" "0.32.0"
"@volar/html2pug@0.32.0":
version "0.32.0"
resolved "https://registry.yarnpkg.com/@volar/html2pug/-/html2pug-0.32.0.tgz#aaa7026d8162a0a54df0eb19ecfc20acf56ae813"
integrity sha512-VPu7O7x74KbUSOofpOH4dxH4jUpKF+9VmsY9ehXftOcuknlBV8v7o0RlIYDrirjq5kUINGJwalKJF33tjR5kTA==
dependencies:
domelementtype "^2.2.0"
domhandler "^4.3.0"
htmlparser2 "^7.2.0"
pug "^3.0.2"
"@volar/shared@0.32.0":
version "0.32.0"
resolved "https://registry.yarnpkg.com/@volar/shared/-/shared-0.32.0.tgz#98b96ac23be2388c4817dcefd92285d99eebdb12"
integrity sha512-RzpoyRAJlEjqAi0rsrqHn5aRJ+xi58JrXa+NCNuJOuGLhUKbPyR9n8JUI+mF4h01opYl3C/s8qYmWQQBOpBUUg==
dependencies:
upath "^2.0.1"
vscode-html-languageservice "^4.2.1"
vscode-jsonrpc "^8.0.0-next.5"
vscode-uri "^3.0.3"
"@volar/source-map@0.32.0":
version "0.32.0"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-0.32.0.tgz#b93438d39aaca8b518d30c9a54e0362ea854803c"
integrity sha512-DRDRvgPZtF/2Me+NBpGQ/bdK0uro7qOneoU1Xhrjmx7dwFB2QNxwEF2BXndmo7BNIc9Rc7g1AYvMRw3y80IhnQ==
dependencies:
"@volar/shared" "0.32.0"
vscode-languageserver-textdocument "^1.0.3"
"@volar/transforms@0.32.0":
version "0.32.0"
resolved "https://registry.yarnpkg.com/@volar/transforms/-/transforms-0.32.0.tgz#a161a3f921f87a400d87cbb9a1e25d81648d1e4d"
integrity sha512-F1ppg60SmPEaJmUfTTP0ZtXFe2u0HURklhFGaKnZ608yIBHq4EGW/kzH8xGc8TjrdGjrWpKkr9D+SHLpq5tirQ==
dependencies:
"@volar/shared" "0.32.0"
vscode-languageserver-types "^3.17.0-next.6"
"@volar/vue-code-gen@0.32.0":
version "0.32.0"
resolved "https://registry.yarnpkg.com/@volar/vue-code-gen/-/vue-code-gen-0.32.0.tgz#aba86b056d70e8a2076c75b53c081a9b3fdb178c"
integrity sha512-NxSYTvCEIDRj6kym/HSa4XIqA473emyVaWApFmg7mpd7ZoadyfhHPd7UuYB90uwMBj0oNQ53+BnvDhCgUMj+Tw==
dependencies:
"@volar/code-gen" "0.32.0"
"@volar/shared" "0.32.0"
"@volar/source-map" "0.32.0"
"@vue/compiler-core" "^3.2.27"
"@vue/compiler-dom" "^3.2.27"
"@vue/shared" "^3.2.27"
upath "^2.0.1"
"@vscode/emmet-helper@^2.8.3":
version "2.8.4"
resolved "https://registry.yarnpkg.com/@vscode/emmet-helper/-/emmet-helper-2.8.4.tgz#ab937e3ce79b0873c604d1ad50a9eeb7abae2937"
integrity sha512-lUki5QLS47bz/U8IlG9VQ+1lfxMtxMZENmU5nu4Z71eOD5j9FK0SmYGL5NiVJg9WBWeAU0VxRADMY2Qpq7BfVg==
dependencies:
emmet "^2.3.0"
jsonc-parser "^2.3.0"
vscode-languageserver-textdocument "^1.0.1"
vscode-languageserver-types "^3.15.1"
vscode-nls "^5.0.0"
vscode-uri "^2.1.2"
"@vue/babel-helper-vue-jsx-merge-props@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81"
@ -1540,6 +1633,16 @@
estree-walker "^2.0.2"
source-map "^0.6.1"
"@vue/compiler-core@3.2.31", "@vue/compiler-core@^3.2.27":
version "3.2.31"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.31.tgz#d38f06c2cf845742403b523ab4596a3fda152e89"
integrity sha512-aKno00qoA4o+V/kR6i/pE+aP+esng5siNAVQ422TkBNM6qA4veXiZbSe8OTXHXquEi/f6Akc+nLfB4JGfe4/WQ==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/shared" "3.2.31"
estree-walker "^2.0.2"
source-map "^0.6.1"
"@vue/compiler-dom@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.29.tgz#ad0ead405bd2f2754161335aad9758aa12430715"
@ -1548,6 +1651,14 @@
"@vue/compiler-core" "3.2.29"
"@vue/shared" "3.2.29"
"@vue/compiler-dom@^3.2.27":
version "3.2.31"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz#b1b7dfad55c96c8cc2b919cd7eb5fd7e4ddbf00e"
integrity sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg==
dependencies:
"@vue/compiler-core" "3.2.31"
"@vue/shared" "3.2.31"
"@vue/compiler-sfc@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.29.tgz#f76d556cd5fca6a55a3ea84c88db1a2a53a36ead"
@ -1616,6 +1727,13 @@
dependencies:
"@vue/shared" "3.2.29"
"@vue/reactivity@^3.2.27":
version "3.2.31"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.31.tgz#fc90aa2cdf695418b79e534783aca90d63a46bbd"
integrity sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==
dependencies:
"@vue/shared" "3.2.31"
"@vue/runtime-core@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.29.tgz#fb8577b2fcf52e8d967bd91cdf49ab9fb91f9417"
@ -1646,6 +1764,11 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.29.tgz#07dac7051117236431d2f737d16932aa38bbb925"
integrity sha512-BjNpU8OK6Z0LVzGUppEk0CMYm/hKDnZfYdjSmPOs0N+TR1cLKJAkDwW8ASZUvaaSLEi6d3hVM7jnWnX+6yWnHw==
"@vue/shared@3.2.31", "@vue/shared@^3.2.27":
version "3.2.31"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.31.tgz#c90de7126d833dcd3a4c7534d534be2fb41faa4e"
integrity sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==
"@vue/vue-loader-v15@npm:vue-loader@^15.9.7":
version "15.9.8"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.8.tgz#4b0f602afaf66a996be1e534fb9609dc4ab10e61"
@ -1990,7 +2113,7 @@ acorn@^6.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
acorn@^7.0.0:
acorn@^7.0.0, acorn@^7.1.1:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
@ -2201,6 +2324,11 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
asap@~2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
asn1.js@^5.2.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
@ -2218,6 +2346,11 @@ asn1@~0.2.3:
dependencies:
safer-buffer "~2.1.0"
assert-never@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe"
integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
@ -2335,6 +2468,13 @@ babel-plugin-polyfill-regenerator@^0.3.0:
dependencies:
"@babel/helper-define-polyfill-provider" "^0.3.1"
babel-walk@3.0.0-canary-5:
version "3.0.0-canary-5"
resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11"
integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==
dependencies:
"@babel/types" "^7.9.6"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -2764,6 +2904,13 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
character-parser@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A=
dependencies:
is-regex "^1.0.3"
charcodes@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4"
@ -3037,6 +3184,14 @@ consolidate@^0.15.1:
dependencies:
bluebird "^3.1.1"
constantinople@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151"
integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==
dependencies:
"@babel/parser" "^7.6.0"
"@babel/types" "^7.6.1"
constants-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@ -3569,6 +3724,11 @@ dns-txt@^2.0.2:
dependencies:
buffer-indexof "^1.0.0"
doctypes@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=
dom-converter@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@ -3595,7 +3755,7 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.0:
domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626"
integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==
@ -3680,6 +3840,14 @@ elliptic@^6.5.3:
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
emmet@^2.3.0:
version "2.3.6"
resolved "https://registry.yarnpkg.com/emmet/-/emmet-2.3.6.tgz#1d93c1ac03164da9ddf74864c1f341ed6ff6c336"
integrity sha512-pLS4PBPDdxuUAmw7Me7+TcHbykTsBKN/S9XJbUOMFQrNv9MoshzyMFK/R57JBm94/6HSL4vHnDeEmxlC82NQ4A==
dependencies:
"@emmetio/abbreviation" "^2.2.3"
"@emmetio/css-abbreviation" "^2.1.4"
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@ -3724,6 +3892,11 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
entities@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
errno@^0.1.3, errno@~0.1.7:
version "0.1.8"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
@ -4436,7 +4609,7 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4:
glob@^7.1.1, glob@^7.1.3, glob@^7.1.4:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
@ -4686,6 +4859,16 @@ htmlparser2@^6.1.0:
domutils "^2.5.2"
entities "^2.0.0"
htmlparser2@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5"
integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.2.2"
domutils "^2.8.0"
entities "^3.0.1"
http-deceiver@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@ -4839,11 +5022,6 @@ inherits@2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
interpret@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
ip@^1.1.0:
version "1.1.5"
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
@ -4963,6 +5141,14 @@ is-docker@^2.0.0, is-docker@^2.1.1:
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
is-expression@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab"
integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==
dependencies:
acorn "^7.1.1"
object-assign "^4.1.1"
is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
@ -5050,7 +5236,12 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-regex@^1.0.4:
is-promise@^2.0.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
is-regex@^1.0.3, is-regex@^1.0.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
@ -5159,6 +5350,11 @@ js-queue@2.0.2:
dependencies:
easy-stack "^1.0.1"
js-stringify@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds=
js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
@ -5236,6 +5432,16 @@ json5@^2.1.2:
dependencies:
minimist "^1.2.5"
jsonc-parser@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342"
integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==
jsonc-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22"
integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@ -5255,6 +5461,14 @@ jsprim@^1.2.2:
json-schema "0.4.0"
verror "1.10.0"
jstransformer@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
integrity sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=
dependencies:
is-promise "^2.0.0"
promise "^7.0.1"
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@ -5901,14 +6115,6 @@ nth-check@^2.0.1:
dependencies:
boolbase "^1.0.0"
null-loader@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-4.0.1.tgz#8e63bd3a2dd3c64236a4679428632edd0a6dbc6a"
integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==
dependencies:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
@ -6648,6 +6854,13 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
promise@^7.0.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
dependencies:
asap "~2.0.3"
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@ -6683,6 +6896,109 @@ public-encrypt@^4.0.0:
randombytes "^2.0.1"
safe-buffer "^5.1.2"
pug-attrs@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41"
integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==
dependencies:
constantinople "^4.0.1"
js-stringify "^1.0.2"
pug-runtime "^3.0.0"
pug-code-gen@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.2.tgz#ad190f4943133bf186b60b80de483100e132e2ce"
integrity sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==
dependencies:
constantinople "^4.0.1"
doctypes "^1.1.0"
js-stringify "^1.0.2"
pug-attrs "^3.0.0"
pug-error "^2.0.0"
pug-runtime "^3.0.0"
void-elements "^3.1.0"
with "^7.0.0"
pug-error@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5"
integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==
pug-filters@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e"
integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==
dependencies:
constantinople "^4.0.1"
jstransformer "1.0.0"
pug-error "^2.0.0"
pug-walk "^2.0.0"
resolve "^1.15.1"
pug-lexer@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5"
integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==
dependencies:
character-parser "^2.2.0"
is-expression "^4.0.0"
pug-error "^2.0.0"
pug-linker@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708"
integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==
dependencies:
pug-error "^2.0.0"
pug-walk "^2.0.0"
pug-load@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662"
integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==
dependencies:
object-assign "^4.1.1"
pug-walk "^2.0.0"
pug-parser@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260"
integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==
dependencies:
pug-error "^2.0.0"
token-stream "1.0.0"
pug-runtime@^3.0.0, pug-runtime@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7"
integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==
pug-strip-comments@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e"
integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==
dependencies:
pug-error "^2.0.0"
pug-walk@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe"
integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==
pug@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.2.tgz#f35c7107343454e43bc27ae0ff76c731b78ea535"
integrity sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==
dependencies:
pug-code-gen "^3.0.2"
pug-filters "^4.0.0"
pug-lexer "^5.0.1"
pug-linker "^4.0.0"
pug-load "^3.0.0"
pug-parser "^6.0.0"
pug-runtime "^3.0.1"
pug-strip-comments "^2.0.0"
pump@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
@ -6840,13 +7156,6 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=
dependencies:
resolve "^1.1.6"
regenerate-unicode-properties@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56"
@ -6993,7 +7302,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.21.0, resolve@^1.3.2:
resolve@^1.10.0, resolve@^1.14.2, resolve@^1.15.1, resolve@^1.20.0, resolve@^1.21.0, resolve@^1.3.2:
version "1.22.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
@ -7191,7 +7500,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@ -7323,15 +7632,6 @@ shell-quote@^1.6.1:
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
shelljs@^0.8.3:
version "0.8.5"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
dependencies:
glob "^7.0.0"
interpret "^1.0.0"
rechoir "^0.6.2"
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
@ -7918,6 +8218,11 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
token-stream@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4"
integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=
totalist@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
@ -8023,6 +8328,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^4.5.5:
version "4.5.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
@ -8093,6 +8403,11 @@ upath@^1.1.1:
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
upath@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b"
integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@ -8196,14 +8511,133 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vue-cli-plugin-vuetify@~2.4.5:
version "2.4.5"
resolved "https://registry.yarnpkg.com/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.4.5.tgz#5dfae4d78c717c400530731f0b75c0350c3b6add"
integrity sha512-CnCVzG6iZAsMMqTkijZ0gRkPB6s4zHPWyFX1VpBBKVyxZESayQhUoMCZRZUOZNjvvZpq1LPgviq+8zcliSu73g==
void-elements@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
vscode-css-languageservice@^5.1.9:
version "5.1.13"
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.13.tgz#debc7c8368223b211a734cb7eb7789c586d3e2d9"
integrity sha512-FA0foqMzMmEoO0WJP+MjoD4dRERhKS+Ag+yBrtmWQDmw2OuZ1R/5FkvI/XdTkCpHmTD9VMczugpHRejQyTXCNQ==
dependencies:
null-loader "^4.0.1"
semver "^7.1.2"
shelljs "^0.8.3"
vscode-languageserver-textdocument "^1.0.1"
vscode-languageserver-types "^3.16.0"
vscode-nls "^5.0.0"
vscode-uri "^3.0.2"
vscode-html-languageservice@^4.2.1:
version "4.2.2"
resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-4.2.2.tgz#e580b8f22b1b8c1dc0d6aaeda5a861f8b4120e4e"
integrity sha512-4ICwlpplGbiNQq6D/LZr4qLbPZuMmnSQeX/57UAYP7jD1LOvKeru4lVI+f6d6Eyd7uS46nLJ5DUY4AAlq35C0g==
dependencies:
vscode-languageserver-textdocument "^1.0.3"
vscode-languageserver-types "^3.16.0"
vscode-nls "^5.0.0"
vscode-uri "^3.0.3"
vscode-json-languageservice@^4.1.10:
version "4.2.0"
resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.2.0.tgz#df0693b69ba2fbf0a6add896087b6f1c9c38f06a"
integrity sha512-XNawv0Vdy/sUK0S+hGf7cq/qsVAbIniGJr89TvZOqMCNJmpgKTy1e8PL1aWW0uy6BfWMG7vxa5lZb3ypuFtuGQ==
dependencies:
jsonc-parser "^3.0.0"
vscode-languageserver-textdocument "^1.0.3"
vscode-languageserver-types "^3.16.0"
vscode-nls "^5.0.0"
vscode-uri "^3.0.3"
vscode-jsonrpc@8.0.0-next.6, vscode-jsonrpc@^8.0.0-next.5:
version "8.0.0-next.6"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.6.tgz#981f7c065ecc7e7e8595f9da6d073ac592b34114"
integrity sha512-6Ld3RYjygn5Ih7CkAtcAwiDQC+rakj2O+PnASfNyYv3sLmm44eJpEKzuPUN30Iy2UB09AZg8T6LBKWTJTEJDVw==
vscode-languageserver-protocol@^3.17.0-next.12:
version "3.17.0-next.14"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.14.tgz#d3500bef2ad2889385cda4802acfe6549842164d"
integrity sha512-iangobY8dL6sFZkOx4OhRPJM9gN0I1caUsOVR+MnPozsqQUtwMXmbIcfaIf0Akp0pd3KhJDPf/tdwRX68QGeeA==
dependencies:
vscode-jsonrpc "8.0.0-next.6"
vscode-languageserver-types "3.17.0-next.7"
vscode-languageserver-textdocument@^1.0.1, vscode-languageserver-textdocument@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz#3cd56dd14cec1d09e86c4bb04b09a246cb3df157"
integrity sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ==
vscode-languageserver-types@3.17.0-next.7, vscode-languageserver-types@^3.17.0-next.6:
version "3.17.0-next.7"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.7.tgz#3e41ebb290c95bb38595f568a9963212626290cc"
integrity sha512-KH4zdG1qBXxoso61ChgpeoZYyHGJo8bV7Jv4I+fwQ1Ryy59JAxoZ9GAbhR5TeeafHctLcg6RFvY3m8Jqfu17cg==
vscode-languageserver-types@^3.15.1, vscode-languageserver-types@^3.16.0:
version "3.16.0"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247"
integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==
vscode-nls@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840"
integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA==
vscode-pug-languageservice@0.32.0:
version "0.32.0"
resolved "https://registry.yarnpkg.com/vscode-pug-languageservice/-/vscode-pug-languageservice-0.32.0.tgz#1aea3cad4736014001260b946c67d95d44693d7e"
integrity sha512-6ACeoDERB0PZNEj9ZwHVRQl084PKw48CYLq2nWSzgpZNwg+bxH/D5CLE7wyRWnF1s78tHCa8gpIKcWlTPL8jgA==
dependencies:
"@volar/code-gen" "0.32.0"
"@volar/shared" "0.32.0"
"@volar/source-map" "0.32.0"
"@volar/transforms" "0.32.0"
pug-lexer "^5.0.1"
pug-parser "^6.0.0"
vscode-languageserver-textdocument "^1.0.3"
vscode-languageserver-types "^3.17.0-next.6"
vscode-typescript-languageservice@0.32.0:
version "0.32.0"
resolved "https://registry.yarnpkg.com/vscode-typescript-languageservice/-/vscode-typescript-languageservice-0.32.0.tgz#6911c77ee966a9c4c5ddd7939267735627adce2c"
integrity sha512-RdFJKbQcN6FQ3Vpx3ggM7XJpTDmmMG3MTAJy+IHn9RpuoQLF8z8gKpTsLAJeiPKXi1WTJjHnl1PT+ndNA3ujig==
dependencies:
"@volar/shared" "0.32.0"
semver "^7.3.5"
upath "^2.0.1"
vscode-languageserver-protocol "^3.17.0-next.12"
vscode-languageserver-textdocument "^1.0.3"
vscode-nls "^5.0.0"
vscode-uri@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.2.tgz#c8d40de93eb57af31f3c715dd650e2ca2c096f1c"
integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==
vscode-uri@^3.0.2, vscode-uri@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84"
integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA==
vscode-vue-languageservice@0.32.0:
version "0.32.0"
resolved "https://registry.yarnpkg.com/vscode-vue-languageservice/-/vscode-vue-languageservice-0.32.0.tgz#926fb67b5b083181bde715ef7b160a1a0704c126"
integrity sha512-D9mOE6mCH7uZ9RpXVJBWXi32R/9bcpZmfhJwxbHzXlU8oBowY36qtbiZJoI/rcXTF0tB51MXmHeLsXhAi52HVA==
dependencies:
"@volar/code-gen" "0.32.0"
"@volar/html2pug" "0.32.0"
"@volar/shared" "0.32.0"
"@volar/source-map" "0.32.0"
"@volar/transforms" "0.32.0"
"@volar/vue-code-gen" "0.32.0"
"@vscode/emmet-helper" "^2.8.3"
"@vue/reactivity" "^3.2.27"
"@vue/shared" "^3.2.27"
upath "^2.0.1"
vscode-css-languageservice "^5.1.9"
vscode-html-languageservice "^4.2.1"
vscode-json-languageservice "^4.1.10"
vscode-languageserver-protocol "^3.17.0-next.12"
vscode-languageserver-textdocument "^1.0.3"
vscode-pug-languageservice "0.32.0"
vscode-typescript-languageservice "0.32.0"
vue-demi@*:
version "0.12.1"
@ -8244,6 +8678,14 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue-tsc@^0.32.0:
version "0.32.0"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-0.32.0.tgz#dc200cba0d601ad0c25d6e85e96957fdc94911b8"
integrity sha512-ILmlPwpDM+f6fZGQgRnu/wx1xrbmyy7ovYBUoFcyO1/Lz4rs+FaDtl8KP0loMWFljuFu39sHHdHMv2BlEIPLWA==
dependencies:
"@volar/shared" "0.32.0"
vscode-vue-languageservice "0.32.0"
vue@^3.2.25:
version "3.2.29"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.29.tgz#3571b65dbd796d3a6347e2fd45a8e6e11c13d56a"
@ -8501,6 +8943,16 @@ wildcard@^2.0.0:
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
with@^7.0.0:
version "7.0.2"
resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac"
integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==
dependencies:
"@babel/parser" "^7.9.6"
"@babel/types" "^7.9.6"
assert-never "^1.2.1"
babel-walk "3.0.0-canary-5"
worker-farm@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"