Compare commits
	
		
			93 Commits
		
	
	
		
			0.4.3
			...
			38e21786a7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 38e21786a7 | |||
| 971c3d3be5 | |||
| 946f14c1cc | |||
| 9ce0da0182 | |||
| 4c93e4635d | |||
| f3a50c790b | |||
| 0c5f68ed80 | |||
| 7fdd8bd935 | |||
| d4287f8aac | |||
| 6712af10d9 | |||
| 70edb382e1 | |||
| 390a042441 | |||
| e8028dae34 | |||
| fc249adc9e | |||
| c186a14644 | |||
| 8a27303670 | |||
| 44e9bb6ec0 | |||
| 42d431ba8b | |||
| 3727061065 | |||
| c7a8adb3ab | |||
| 29f534bf10 | |||
| 15381c84f6 | |||
| a0cabbf4f7 | |||
| f26ee8f472 | |||
| 4fb3c2a335 | |||
| 8899ff5772 | |||
| 347a0c9e50 | |||
| 66b8e1f69f | |||
| 5d1b49c896 | |||
| 42dc51fe9a | |||
| 1ca95f8768 | |||
| a73f7c2934 | |||
| 489aa88c4b | |||
| 7b5b16c1b2 | |||
| 29cfeb6fa6 | |||
| 42baafd273 | |||
| 1e79f193be | |||
| f0ec7fb30d | |||
| 034f4f2a90 | |||
| 51ece59866 | |||
| bbfda6f402 | |||
| 2b3afbf448 | |||
| a53c3d23a4 | |||
| c899c21256 | |||
| cccc948048 | |||
| e8a0670a83 | |||
| 9638676b8f | |||
| 08bda8d14f | |||
| d0ade0f2f1 | |||
| 466239ce11 | |||
| d52e5c63d4 | |||
| 3dcb362372 | |||
| 67c9b53e91 | |||
| 7c08ddacb7 | |||
| ecbb85aeaa | |||
| 3696bbde43 | |||
| 09a227d08d | |||
| dae6185857 | |||
| 6dd8a3791f | |||
| 18149eef8b | |||
| f0f97a2e77 | |||
| 71b2f8a9a3 | |||
| 8f666a0f26 | |||
| 2b9c76960e | |||
| 7fd057f9f6 | |||
| a9be9367a9 | |||
| bc75757ac7 | |||
| a3e12df2e2 | |||
| 024c5e0a1c | |||
| 27372199f7 | |||
| b3b878854e | |||
| bbb12a788d | |||
| 7dfbef60a4 | |||
| d8a96535dc | |||
| ce26e76e8f | |||
| 6b3ac199fc | |||
| 4c6d21c2b4 | |||
| faef975f1a | |||
| bd686e0c00 | |||
| 4844889e0b | |||
| 2775578713 | |||
| 27dd6e923c | |||
| 2cf6b815bf | |||
| 81aacf339e | |||
| 16bcf516f6 | |||
| 52503a4c92 | |||
| b4321395d9 | |||
| d7058a49b0 | |||
| eb9fc722aa | |||
| 511081298e | |||
| 422a74704b | |||
| c4995bcbaf | |||
| 79fd95e152 | 
							
								
								
									
										10
									
								
								.earthignore
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.earthignore
									
									
									
									
									
								
							@@ -1,10 +0,0 @@
 | 
				
			|||||||
build/
 | 
					 | 
				
			||||||
.git/
 | 
					 | 
				
			||||||
docker-compose.yml
 | 
					 | 
				
			||||||
README.md
 | 
					 | 
				
			||||||
Earthfile
 | 
					 | 
				
			||||||
config.example.json
 | 
					 | 
				
			||||||
.gitignore
 | 
					 | 
				
			||||||
.vscode/
 | 
					 | 
				
			||||||
budgeteer
 | 
					 | 
				
			||||||
budgeteer.exe
 | 
					 | 
				
			||||||
							
								
								
									
										6
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -1,7 +1,11 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
        "files.exclude": {
 | 
					        "files.exclude": {
 | 
				
			||||||
                "**/node_modules": true,
 | 
					                "**/node_modules": true,
 | 
				
			||||||
                "**/vendor": true
 | 
					                "**/vendor": true,
 | 
				
			||||||
 | 
					                "**/*.sql.go": true,
 | 
				
			||||||
 | 
					                ".task/": true,
 | 
				
			||||||
 | 
					                "build/": true,
 | 
				
			||||||
 | 
					                "web/dist/": true
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "gopls": {
 | 
					        "gopls": {
 | 
				
			||||||
                "formatting.gofumpt": true,
 | 
					                "formatting.gofumpt": true,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
pipeline:
 | 
					 | 
				
			||||||
  build:
 | 
					 | 
				
			||||||
    name: Taskfile.dev
 | 
					 | 
				
			||||||
    image: hub.javil.eu/budgeteer:dev
 | 
					 | 
				
			||||||
    pull: true
 | 
					 | 
				
			||||||
    commands:
 | 
					 | 
				
			||||||
      - task ci
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  docker:
 | 
					 | 
				
			||||||
    image: plugins/docker
 | 
					 | 
				
			||||||
    secrets: [ docker_username, docker_password ]
 | 
					 | 
				
			||||||
    settings:
 | 
					 | 
				
			||||||
      registry: hub.javil.eu
 | 
					 | 
				
			||||||
      repo: hub.javil.eu/budgeteer
 | 
					 | 
				
			||||||
      context: build
 | 
					 | 
				
			||||||
      dockerfile: build/Dockerfile
 | 
					 | 
				
			||||||
      tags: 
 | 
					 | 
				
			||||||
        - latest
 | 
					 | 
				
			||||||
    when:
 | 
					 | 
				
			||||||
      event: [push, tag, deployment]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
image_pull_secrets:
 | 
					 | 
				
			||||||
- hub.javil.eu 
 | 
					 | 
				
			||||||
							
								
								
									
										21
									
								
								Earthfile
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								Earthfile
									
									
									
									
									
								
							@@ -1,21 +0,0 @@
 | 
				
			|||||||
FROM golang:1.17
 | 
					 | 
				
			||||||
WORKDIR /src
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
build:
 | 
					 | 
				
			||||||
	COPY go.mod go.sum .
 | 
					 | 
				
			||||||
	RUN go mod download
 | 
					 | 
				
			||||||
	COPY . .
 | 
					 | 
				
			||||||
	RUN --mount=type=cache,target=/root/.cache/go-build go build -o build/budgeteer ./cmd/budgeteer
 | 
					 | 
				
			||||||
	SAVE ARTIFACT build/budgeteer /budgeteer AS LOCAL build/budgeteer
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
docker:
 | 
					 | 
				
			||||||
	WORKDIR /app
 | 
					 | 
				
			||||||
	COPY +build/budgeteer .
 | 
					 | 
				
			||||||
	ENTRYPOINT ["/app/budgeteer"]
 | 
					 | 
				
			||||||
	SAVE IMAGE hub.javil.eu/budgeteer:latest
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
run:
 | 
					 | 
				
			||||||
	LOCALLY
 | 
					 | 
				
			||||||
	WITH DOCKER --load=+docker
 | 
					 | 
				
			||||||
		RUN docker-compose up -d
 | 
					 | 
				
			||||||
	END
 | 
					 | 
				
			||||||
@@ -77,6 +77,7 @@ tasks:
 | 
				
			|||||||
    cmds:
 | 
					    cmds:
 | 
				
			||||||
      - yarn
 | 
					      - yarn
 | 
				
			||||||
      - yarn build
 | 
					      - yarn build
 | 
				
			||||||
 | 
					      - yarn run vue-tsc --noEmit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  docker:
 | 
					  docker:
 | 
				
			||||||
    desc: Build budgeeter:latest
 | 
					    desc: Build budgeeter:latest
 | 
				
			||||||
@@ -95,7 +96,7 @@ tasks:
 | 
				
			|||||||
      - ./docker/build.sh
 | 
					      - ./docker/build.sh
 | 
				
			||||||
      - ./web/package.json
 | 
					      - ./web/package.json
 | 
				
			||||||
    cmds:
 | 
					    cmds:
 | 
				
			||||||
      - docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile
 | 
					      - docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile.dev
 | 
				
			||||||
      - docker push {{.IMAGE_NAME}}:dev
 | 
					      - docker push {{.IMAGE_NAME}}:dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  run:
 | 
					  run:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +0,0 @@
 | 
				
			|||||||
FROM scratch
 | 
					 | 
				
			||||||
COPY ./budgeteer /app/budgeteer
 | 
					 | 
				
			||||||
ENTRYPOINT ["/app/budgeteer"]
 | 
					 | 
				
			||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
@@ -29,11 +30,14 @@ func main() {
 | 
				
			|||||||
		panic("couldn't open static files")
 | 
							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{
 | 
						handler := &server.Handler{
 | 
				
			||||||
		Service:             queries,
 | 
							Service:             queries,
 | 
				
			||||||
		TokenVerifier: &jwt.TokenVerifier{
 | 
							TokenVerifier:       tokenVerifier,
 | 
				
			||||||
			Secret: cfg.SessionSecret,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		CredentialsVerifier: &bcrypt.Verifier{},
 | 
							CredentialsVerifier: &bcrypt.Verifier{},
 | 
				
			||||||
		StaticFS:            http.FS(static),
 | 
							StaticFS:            http.FS(static),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
	"DatabaseHost": "localhost", 
 | 
					 | 
				
			||||||
	"DatabaseUser": "user", 
 | 
					 | 
				
			||||||
	"DatabasePassword": "thisismypassword", 
 | 
					 | 
				
			||||||
	"DatabaseName": "budgeteer"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,17 +1,3 @@
 | 
				
			|||||||
FROM alpine as godeps
 | 
					FROM scratch
 | 
				
			||||||
RUN apk --no-cache add go
 | 
					COPY ./budgeteer /app/budgeteer
 | 
				
			||||||
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
 | 
					ENTRYPOINT ["/app/budgeteer"]
 | 
				
			||||||
RUN go install github.com/go-task/task/v3/cmd/task@latest
 | 
					 | 
				
			||||||
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
FROM alpine
 | 
					 | 
				
			||||||
RUN apk --no-cache add go nodejs yarn bash curl git git-perl tmux
 | 
					 | 
				
			||||||
ADD docker/build.sh /
 | 
					 | 
				
			||||||
RUN yarn global add @vue/cli
 | 
					 | 
				
			||||||
ENV PATH="/root/.yarn/bin/:${PATH}"
 | 
					 | 
				
			||||||
WORKDIR /src/web
 | 
					 | 
				
			||||||
ADD web/package.json web/yarn.lock /src/web/
 | 
					 | 
				
			||||||
RUN yarn
 | 
					 | 
				
			||||||
WORKDIR /src
 | 
					 | 
				
			||||||
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/
 | 
					 | 
				
			||||||
CMD /build.sh
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								docker/Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docker/Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					FROM alpine as godeps
 | 
				
			||||||
 | 
					RUN apk --no-cache add go
 | 
				
			||||||
 | 
					RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
 | 
				
			||||||
 | 
					RUN go install github.com/go-task/task/v3/cmd/task@latest
 | 
				
			||||||
 | 
					RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FROM alpine
 | 
				
			||||||
 | 
					RUN apk --no-cache add go nodejs yarn bash curl git git-perl tmux
 | 
				
			||||||
 | 
					ADD docker/dev.sh /
 | 
				
			||||||
 | 
					RUN yarn global add @vue/cli
 | 
				
			||||||
 | 
					ENV PATH="/root/.yarn/bin/:${PATH}"
 | 
				
			||||||
 | 
					WORKDIR /src/web
 | 
				
			||||||
 | 
					ADD web/package.json web/yarn.lock /src/web/
 | 
				
			||||||
 | 
					RUN yarn
 | 
				
			||||||
 | 
					WORKDIR /src
 | 
				
			||||||
 | 
					COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/
 | 
				
			||||||
 | 
					CMD /dev.sh
 | 
				
			||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@@ -11,7 +11,7 @@ require (
 | 
				
			|||||||
	golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
 | 
						golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require github.com/DATA-DOG/go-txdb v0.1.5 // indirect
 | 
					require github.com/DATA-DOG/go-txdb v0.1.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	github.com/gin-contrib/sse v0.1.0 // indirect
 | 
						github.com/gin-contrib/sse v0.1.0 // indirect
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.sum
									
									
									
									
									
								
							@@ -74,6 +74,7 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
 | 
				
			|||||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
 | 
					github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
 | 
				
			||||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 | 
					github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 | 
				
			||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 | 
					github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 | 
				
			||||||
 | 
					github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 | 
				
			||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 | 
					github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 | 
				
			||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 | 
					github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 | 
				
			||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 | 
					github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,32 +12,45 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// TokenVerifier verifies Tokens.
 | 
					// TokenVerifier verifies Tokens.
 | 
				
			||||||
type TokenVerifier struct {
 | 
					type TokenVerifier struct {
 | 
				
			||||||
	Secret string
 | 
						Expiration time.Duration
 | 
				
			||||||
 | 
						secret     string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Token contains everything to authenticate a user.
 | 
					const DefaultExpiration = time.Hour * time.Duration(72)
 | 
				
			||||||
type Token struct {
 | 
					
 | 
				
			||||||
	username string
 | 
					func NewTokenVerifier(secret string) (*TokenVerifier, error) {
 | 
				
			||||||
	name     string
 | 
						if secret == "" {
 | 
				
			||||||
	expiry   float64
 | 
							return nil, ErrEmptySecret
 | 
				
			||||||
	id       uuid.UUID
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &TokenVerifier{
 | 
				
			||||||
 | 
							Expiration: DefaultExpiration,
 | 
				
			||||||
 | 
							secret:     secret,
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					var (
 | 
				
			||||||
	expiration = 72
 | 
						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.
 | 
					// CreateToken creates a new token from username and name.
 | 
				
			||||||
func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
 | 
					func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
 | 
				
			||||||
 | 
						if tv.secret == "" {
 | 
				
			||||||
 | 
							return "", ErrEmptySecret
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
						token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
				
			||||||
		"usr":  user.Email,
 | 
							"usr":  user.Email,
 | 
				
			||||||
		"name": user.Name,
 | 
							"name": user.Name,
 | 
				
			||||||
		"exp":  time.Now().Add(time.Hour * expiration).Unix(),
 | 
							"exp":  time.Now().Add(tv.Expiration).Unix(),
 | 
				
			||||||
		"id":   user.ID,
 | 
							"id":   user.ID,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Generate encoded token and send it as response.
 | 
						// 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 {
 | 
						if err != nil {
 | 
				
			||||||
		return "", fmt.Errorf("create token: %w", err)
 | 
							return "", fmt.Errorf("create token: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -45,19 +58,17 @@ func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
 | 
				
			|||||||
	return t, nil
 | 
						return t, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					// VerifyToken verifies a given string-token.
 | 
				
			||||||
	ErrUnexpectedSigningMethod = fmt.Errorf("unexpected signing method")
 | 
					 | 
				
			||||||
	ErrInvalidToken            = fmt.Errorf("token is invalid")
 | 
					 | 
				
			||||||
	ErrTokenExpired            = fmt.Errorf("token has expired")
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// VerifyToken verifys a given string-token.
 | 
					 | 
				
			||||||
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn
 | 
					func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn
 | 
				
			||||||
 | 
						if tv.secret == "" {
 | 
				
			||||||
 | 
							return nil, ErrEmptySecret
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
 | 
						token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
 | 
				
			||||||
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 | 
							if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 | 
				
			||||||
			return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod)
 | 
								return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return []byte(tv.Secret), nil
 | 
							return []byte(tv.secret), nil
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("parse jwt: %w", err)
 | 
							return nil, fmt.Errorf("parse jwt: %w", err)
 | 
				
			||||||
@@ -76,36 +87,3 @@ func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return tkn, nil
 | 
						return tkn, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func verifyToken(token *jwt.Token) (jwt.MapClaims, error) {
 | 
					 | 
				
			||||||
	if !token.Valid {
 | 
					 | 
				
			||||||
		return nil, ErrInvalidToken
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	claims, ok := token.Claims.(jwt.MapClaims)
 | 
					 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		return nil, ErrInvalidToken
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
 | 
					 | 
				
			||||||
		return nil, ErrTokenExpired
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return claims, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *Token) GetName() string {
 | 
					 | 
				
			||||||
	return t.name
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *Token) GetUsername() string {
 | 
					 | 
				
			||||||
	return t.username
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *Token) GetExpiry() float64 {
 | 
					 | 
				
			||||||
	return t.expiry
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *Token) GetID() uuid.UUID {
 | 
					 | 
				
			||||||
	return t.id
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										49
									
								
								jwt/token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								jwt/token.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					package jwt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/dgrijalva/jwt-go"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Token contains everything to authenticate a user.
 | 
				
			||||||
 | 
					type Token struct {
 | 
				
			||||||
 | 
						username string
 | 
				
			||||||
 | 
						name     string
 | 
				
			||||||
 | 
						expiry   float64
 | 
				
			||||||
 | 
						id       uuid.UUID
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func verifyToken(token *jwt.Token) (jwt.MapClaims, error) {
 | 
				
			||||||
 | 
						if !token.Valid {
 | 
				
			||||||
 | 
							return nil, ErrInvalidToken
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						claims, ok := token.Claims.(jwt.MapClaims)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, ErrInvalidToken
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
 | 
				
			||||||
 | 
							return nil, ErrTokenExpired
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return claims, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Token) GetName() string {
 | 
				
			||||||
 | 
						return t.name
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Token) GetUsername() string {
 | 
				
			||||||
 | 
						return t.username
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Token) GetExpiry() float64 {
 | 
				
			||||||
 | 
						return t.expiry
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Token) GetID() uuid.UUID {
 | 
				
			||||||
 | 
						return t.id
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,6 +5,7 @@ package postgres
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
 | 
						"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
 | 
				
			||||||
	"github.com/google/uuid"
 | 
						"github.com/google/uuid"
 | 
				
			||||||
@@ -14,7 +15,7 @@ const createAccount = `-- name: CreateAccount :one
 | 
				
			|||||||
INSERT INTO accounts
 | 
					INSERT INTO accounts
 | 
				
			||||||
(name, budget_id)
 | 
					(name, budget_id)
 | 
				
			||||||
VALUES ($1, $2)
 | 
					VALUES ($1, $2)
 | 
				
			||||||
RETURNING id, budget_id, name, on_budget
 | 
					RETURNING id, budget_id, name, on_budget, is_open
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type CreateAccountParams struct {
 | 
					type CreateAccountParams struct {
 | 
				
			||||||
@@ -30,12 +31,13 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A
 | 
				
			|||||||
		&i.BudgetID,
 | 
							&i.BudgetID,
 | 
				
			||||||
		&i.Name,
 | 
							&i.Name,
 | 
				
			||||||
		&i.OnBudget,
 | 
							&i.OnBudget,
 | 
				
			||||||
 | 
							&i.IsOpen,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getAccount = `-- name: GetAccount :one
 | 
					const getAccount = `-- name: GetAccount :one
 | 
				
			||||||
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget FROM accounts
 | 
					SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open FROM accounts
 | 
				
			||||||
WHERE accounts.id = $1
 | 
					WHERE accounts.id = $1
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -47,13 +49,15 @@ func (q *Queries) GetAccount(ctx context.Context, id uuid.UUID) (Account, error)
 | 
				
			|||||||
		&i.BudgetID,
 | 
							&i.BudgetID,
 | 
				
			||||||
		&i.Name,
 | 
							&i.Name,
 | 
				
			||||||
		&i.OnBudget,
 | 
							&i.OnBudget,
 | 
				
			||||||
 | 
							&i.IsOpen,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getAccounts = `-- name: GetAccounts :many
 | 
					const getAccounts = `-- name: GetAccounts :many
 | 
				
			||||||
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget FROM accounts
 | 
					SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open FROM accounts
 | 
				
			||||||
WHERE accounts.budget_id = $1
 | 
					WHERE accounts.budget_id = $1
 | 
				
			||||||
 | 
					AND accounts.is_open = TRUE
 | 
				
			||||||
ORDER BY accounts.name
 | 
					ORDER BY accounts.name
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,6 +75,7 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
 | 
				
			|||||||
			&i.BudgetID,
 | 
								&i.BudgetID,
 | 
				
			||||||
			&i.Name,
 | 
								&i.Name,
 | 
				
			||||||
			&i.OnBudget,
 | 
								&i.OnBudget,
 | 
				
			||||||
 | 
								&i.IsOpen,
 | 
				
			||||||
		); err != nil {
 | 
							); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -86,11 +91,14 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many
 | 
					const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many
 | 
				
			||||||
SELECT accounts.id, accounts.name, accounts.on_budget, SUM(transactions.amount)::decimal(12,2) as balance
 | 
					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
 | 
					FROM accounts
 | 
				
			||||||
LEFT JOIN transactions ON transactions.account_id = accounts.id AND transactions.date < NOW()
 | 
					 | 
				
			||||||
WHERE accounts.budget_id = $1
 | 
					WHERE accounts.budget_id = $1
 | 
				
			||||||
GROUP BY accounts.id, accounts.name
 | 
					AND accounts.is_open = TRUE
 | 
				
			||||||
ORDER BY accounts.name
 | 
					ORDER BY accounts.name
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -98,7 +106,11 @@ type GetAccountsWithBalanceRow struct {
 | 
				
			|||||||
	ID                uuid.UUID
 | 
						ID                uuid.UUID
 | 
				
			||||||
	Name              string
 | 
						Name              string
 | 
				
			||||||
	OnBudget          bool
 | 
						OnBudget          bool
 | 
				
			||||||
	Balance  numeric.Numeric
 | 
						IsOpen            bool
 | 
				
			||||||
 | 
						LastReconciled    time.Time
 | 
				
			||||||
 | 
						WorkingBalance    numeric.Numeric
 | 
				
			||||||
 | 
						ClearedBalance    numeric.Numeric
 | 
				
			||||||
 | 
						ReconciledBalance numeric.Numeric
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID) ([]GetAccountsWithBalanceRow, error) {
 | 
					func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID) ([]GetAccountsWithBalanceRow, error) {
 | 
				
			||||||
@@ -114,7 +126,11 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
 | 
				
			|||||||
			&i.ID,
 | 
								&i.ID,
 | 
				
			||||||
			&i.Name,
 | 
								&i.Name,
 | 
				
			||||||
			&i.OnBudget,
 | 
								&i.OnBudget,
 | 
				
			||||||
			&i.Balance,
 | 
								&i.IsOpen,
 | 
				
			||||||
 | 
								&i.LastReconciled,
 | 
				
			||||||
 | 
								&i.WorkingBalance,
 | 
				
			||||||
 | 
								&i.ClearedBalance,
 | 
				
			||||||
 | 
								&i.ReconciledBalance,
 | 
				
			||||||
		); err != nil {
 | 
							); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -132,6 +148,7 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
 | 
				
			|||||||
const searchAccounts = `-- name: SearchAccounts :many
 | 
					const searchAccounts = `-- name: SearchAccounts :many
 | 
				
			||||||
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
 | 
					SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
 | 
				
			||||||
WHERE accounts.budget_id = $1
 | 
					WHERE accounts.budget_id = $1
 | 
				
			||||||
 | 
					AND accounts.is_open = TRUE
 | 
				
			||||||
AND accounts.name LIKE $2
 | 
					AND accounts.name LIKE $2
 | 
				
			||||||
ORDER BY accounts.name
 | 
					ORDER BY accounts.name
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
@@ -179,25 +196,33 @@ func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams)
 | 
				
			|||||||
const updateAccount = `-- name: UpdateAccount :one
 | 
					const updateAccount = `-- name: UpdateAccount :one
 | 
				
			||||||
UPDATE accounts
 | 
					UPDATE accounts
 | 
				
			||||||
SET name = $1,
 | 
					SET name = $1,
 | 
				
			||||||
    on_budget = $2
 | 
					    on_budget = $2,
 | 
				
			||||||
WHERE accounts.id = $3
 | 
					    is_open = $3
 | 
				
			||||||
RETURNING id, budget_id, name, on_budget
 | 
					WHERE accounts.id = $4
 | 
				
			||||||
 | 
					RETURNING id, budget_id, name, on_budget, is_open
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UpdateAccountParams struct {
 | 
					type UpdateAccountParams struct {
 | 
				
			||||||
	Name     string
 | 
						Name     string
 | 
				
			||||||
	OnBudget bool
 | 
						OnBudget bool
 | 
				
			||||||
 | 
						IsOpen   bool
 | 
				
			||||||
	ID       uuid.UUID
 | 
						ID       uuid.UUID
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) {
 | 
					func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) {
 | 
				
			||||||
	row := q.db.QueryRowContext(ctx, updateAccount, arg.Name, arg.OnBudget, arg.ID)
 | 
						row := q.db.QueryRowContext(ctx, updateAccount,
 | 
				
			||||||
 | 
							arg.Name,
 | 
				
			||||||
 | 
							arg.OnBudget,
 | 
				
			||||||
 | 
							arg.IsOpen,
 | 
				
			||||||
 | 
							arg.ID,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	var i Account
 | 
						var i Account
 | 
				
			||||||
	err := row.Scan(
 | 
						err := row.Scan(
 | 
				
			||||||
		&i.ID,
 | 
							&i.ID,
 | 
				
			||||||
		&i.BudgetID,
 | 
							&i.BudgetID,
 | 
				
			||||||
		&i.Name,
 | 
							&i.Name,
 | 
				
			||||||
		&i.OnBudget,
 | 
							&i.OnBudget,
 | 
				
			||||||
 | 
							&i.IsOpen,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,6 +36,7 @@ type Account struct {
 | 
				
			|||||||
	BudgetID uuid.UUID
 | 
						BudgetID uuid.UUID
 | 
				
			||||||
	Name     string
 | 
						Name     string
 | 
				
			||||||
	OnBudget bool
 | 
						OnBudget bool
 | 
				
			||||||
 | 
						IsOpen   bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Assignment struct {
 | 
					type Assignment struct {
 | 
				
			||||||
@@ -72,6 +73,24 @@ type CategoryGroup struct {
 | 
				
			|||||||
	Name     string
 | 
						Name     string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DisplayTransaction struct {
 | 
				
			||||||
 | 
						ID              uuid.UUID
 | 
				
			||||||
 | 
						Date            time.Time
 | 
				
			||||||
 | 
						Memo            string
 | 
				
			||||||
 | 
						Amount          numeric.Numeric
 | 
				
			||||||
 | 
						GroupID         uuid.NullUUID
 | 
				
			||||||
 | 
						Status          TransactionStatus
 | 
				
			||||||
 | 
						Account         string
 | 
				
			||||||
 | 
						PayeeID         uuid.NullUUID
 | 
				
			||||||
 | 
						CategoryID      uuid.NullUUID
 | 
				
			||||||
 | 
						Payee           string
 | 
				
			||||||
 | 
						CategoryGroup   string
 | 
				
			||||||
 | 
						Category        string
 | 
				
			||||||
 | 
						TransferAccount string
 | 
				
			||||||
 | 
						BudgetID        uuid.UUID
 | 
				
			||||||
 | 
						AccountID       uuid.UUID
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Payee struct {
 | 
					type Payee struct {
 | 
				
			||||||
	ID       uuid.UUID
 | 
						ID       uuid.UUID
 | 
				
			||||||
	BudgetID uuid.UUID
 | 
						BudgetID uuid.UUID
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,25 +11,31 @@ WHERE accounts.id = $1;
 | 
				
			|||||||
-- name: GetAccounts :many
 | 
					-- name: GetAccounts :many
 | 
				
			||||||
SELECT accounts.* FROM accounts
 | 
					SELECT accounts.* FROM accounts
 | 
				
			||||||
WHERE accounts.budget_id = $1
 | 
					WHERE accounts.budget_id = $1
 | 
				
			||||||
 | 
					AND accounts.is_open = TRUE
 | 
				
			||||||
ORDER BY accounts.name;
 | 
					ORDER BY accounts.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: GetAccountsWithBalance :many
 | 
					-- name: GetAccountsWithBalance :many
 | 
				
			||||||
SELECT accounts.id, accounts.name, accounts.on_budget, SUM(transactions.amount)::decimal(12,2) as balance
 | 
					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
 | 
					FROM accounts
 | 
				
			||||||
LEFT JOIN transactions ON transactions.account_id = accounts.id AND transactions.date < NOW()
 | 
					 | 
				
			||||||
WHERE accounts.budget_id = $1
 | 
					WHERE accounts.budget_id = $1
 | 
				
			||||||
GROUP BY accounts.id, accounts.name
 | 
					AND accounts.is_open = TRUE
 | 
				
			||||||
ORDER BY accounts.name;
 | 
					ORDER BY accounts.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: SearchAccounts :many
 | 
					-- name: SearchAccounts :many
 | 
				
			||||||
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
 | 
					SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
 | 
				
			||||||
WHERE accounts.budget_id = @budget_id
 | 
					WHERE accounts.budget_id = @budget_id
 | 
				
			||||||
 | 
					AND accounts.is_open = TRUE
 | 
				
			||||||
AND accounts.name LIKE @search
 | 
					AND accounts.name LIKE @search
 | 
				
			||||||
ORDER BY accounts.name;
 | 
					ORDER BY accounts.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: UpdateAccount :one
 | 
					-- name: UpdateAccount :one
 | 
				
			||||||
UPDATE accounts
 | 
					UPDATE accounts
 | 
				
			||||||
SET name = $1,
 | 
					SET name = $1,
 | 
				
			||||||
    on_budget = $2
 | 
					    on_budget = $2,
 | 
				
			||||||
WHERE accounts.id = $3
 | 
					    is_open = $3
 | 
				
			||||||
 | 
					WHERE accounts.id = $4
 | 
				
			||||||
RETURNING *;
 | 
					RETURNING *;
 | 
				
			||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
-- name: GetTransaction :one
 | 
					-- name: GetTransaction :one
 | 
				
			||||||
SELECT * FROM transactions
 | 
					SELECT * FROM display_transactions
 | 
				
			||||||
WHERE id = $1;
 | 
					WHERE id = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: CreateTransaction :one
 | 
					-- name: CreateTransaction :one
 | 
				
			||||||
INSERT INTO transactions
 | 
					INSERT INTO transactions
 | 
				
			||||||
(date, memo, amount, account_id, payee_id, category_id, group_id, status)
 | 
					(date, memo, amount, account_id, payee_id, category_id, group_id, status)
 | 
				
			||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
 | 
					VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
 | 
				
			||||||
RETURNING *;
 | 
					RETURNING id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: UpdateTransaction :exec
 | 
					-- name: UpdateTransaction :exec
 | 
				
			||||||
UPDATE transactions
 | 
					UPDATE transactions
 | 
				
			||||||
@@ -17,53 +17,24 @@ SET date = $1,
 | 
				
			|||||||
    category_id = $5
 | 
					    category_id = $5
 | 
				
			||||||
WHERE id = $6;
 | 
					WHERE id = $6;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: SetTransactionReconciled :exec
 | 
				
			||||||
 | 
					UPDATE transactions
 | 
				
			||||||
 | 
					SET status = 'Reconciled'
 | 
				
			||||||
 | 
					WHERE id = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: DeleteTransaction :exec
 | 
					-- name: DeleteTransaction :exec
 | 
				
			||||||
DELETE FROM transactions
 | 
					DELETE FROM transactions
 | 
				
			||||||
WHERE id = $1;
 | 
					WHERE id = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: GetAllTransactionsForBudget :many
 | 
					-- name: GetAllTransactionsForBudget :many
 | 
				
			||||||
SELECT  transactions.id, transactions.date, transactions.memo, 
 | 
					SELECT  t.*
 | 
				
			||||||
        transactions.amount, transactions.group_id, transactions.status,
 | 
					FROM display_transactions AS t
 | 
				
			||||||
        accounts.name as account, transactions.payee_id, transactions.category_id,
 | 
					WHERE t.budget_id = $1;
 | 
				
			||||||
        COALESCE(payees.name, '') as payee, 
 | 
					 | 
				
			||||||
        COALESCE(category_groups.name, '') as category_group, 
 | 
					 | 
				
			||||||
        COALESCE(categories.name, '') as category,
 | 
					 | 
				
			||||||
        COALESCE((
 | 
					 | 
				
			||||||
            SELECT CONCAT(otherAccounts.name) 
 | 
					 | 
				
			||||||
            FROM transactions otherTransactions 
 | 
					 | 
				
			||||||
            LEFT JOIN accounts otherAccounts ON otherAccounts.id = otherTransactions.account_id
 | 
					 | 
				
			||||||
            WHERE otherTransactions.group_id = transactions.group_id 
 | 
					 | 
				
			||||||
            AND otherTransactions.id != transactions.id
 | 
					 | 
				
			||||||
        ), '')::text as transfer_account
 | 
					 | 
				
			||||||
FROM transactions 
 | 
					 | 
				
			||||||
INNER JOIN accounts ON accounts.id = transactions.account_id
 | 
					 | 
				
			||||||
LEFT JOIN payees ON payees.id = transactions.payee_id
 | 
					 | 
				
			||||||
LEFT JOIN categories ON categories.id = transactions.category_id
 | 
					 | 
				
			||||||
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
 | 
					 | 
				
			||||||
WHERE accounts.budget_id = $1
 | 
					 | 
				
			||||||
ORDER BY transactions.date DESC;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: GetTransactionsForAccount :many
 | 
					-- name: GetTransactionsForAccount :many
 | 
				
			||||||
SELECT  transactions.id, transactions.date, transactions.memo, 
 | 
					SELECT  t.*
 | 
				
			||||||
        transactions.amount, transactions.group_id, transactions.status,
 | 
					FROM display_transactions AS t
 | 
				
			||||||
        accounts.name as account, transactions.payee_id, transactions.category_id,
 | 
					WHERE t.account_id = $1
 | 
				
			||||||
        COALESCE(payees.name, '') as payee, 
 | 
					 | 
				
			||||||
        COALESCE(category_groups.name, '') as category_group, 
 | 
					 | 
				
			||||||
        COALESCE(categories.name, '') as category,
 | 
					 | 
				
			||||||
        COALESCE((
 | 
					 | 
				
			||||||
            SELECT CONCAT(otherAccounts.name) 
 | 
					 | 
				
			||||||
            FROM transactions otherTransactions 
 | 
					 | 
				
			||||||
            LEFT JOIN accounts otherAccounts ON otherAccounts.id = otherTransactions.account_id
 | 
					 | 
				
			||||||
            WHERE otherTransactions.group_id = transactions.group_id 
 | 
					 | 
				
			||||||
            AND otherTransactions.id != transactions.id
 | 
					 | 
				
			||||||
        ), '')::text as transfer_account
 | 
					 | 
				
			||||||
FROM transactions 
 | 
					 | 
				
			||||||
INNER JOIN accounts ON accounts.id = transactions.account_id
 | 
					 | 
				
			||||||
LEFT JOIN payees ON payees.id = transactions.payee_id
 | 
					 | 
				
			||||||
LEFT JOIN categories ON categories.id = transactions.category_id
 | 
					 | 
				
			||||||
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
 | 
					 | 
				
			||||||
WHERE transactions.account_id = $1
 | 
					 | 
				
			||||||
ORDER BY transactions.date DESC
 | 
					 | 
				
			||||||
LIMIT 200;
 | 
					LIMIT 200;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: DeleteAllTransactions :execrows
 | 
					-- name: DeleteAllTransactions :execrows
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										25
									
								
								postgres/schema/0015_transactions-view.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								postgres/schema/0015_transactions-view.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					-- +goose Up
 | 
				
			||||||
 | 
					CREATE VIEW display_transactions AS 
 | 
				
			||||||
 | 
					        SELECT  transactions.id, transactions.date, transactions.memo, 
 | 
				
			||||||
 | 
					                transactions.amount, transactions.group_id, transactions.status,
 | 
				
			||||||
 | 
					                accounts.name as account, transactions.payee_id, transactions.category_id,
 | 
				
			||||||
 | 
					                COALESCE(payees.name, '') as payee, 
 | 
				
			||||||
 | 
					                COALESCE(category_groups.name, '') as category_group, 
 | 
				
			||||||
 | 
					                COALESCE(categories.name, '') as category,
 | 
				
			||||||
 | 
					                COALESCE((
 | 
				
			||||||
 | 
					                SELECT CONCAT(otherAccounts.name) 
 | 
				
			||||||
 | 
					                FROM transactions otherTransactions 
 | 
				
			||||||
 | 
					                LEFT JOIN accounts otherAccounts ON otherAccounts.id = otherTransactions.account_id
 | 
				
			||||||
 | 
					                WHERE otherTransactions.group_id = transactions.group_id 
 | 
				
			||||||
 | 
					                AND otherTransactions.id != transactions.id
 | 
				
			||||||
 | 
					                ), '')::text as transfer_account,
 | 
				
			||||||
 | 
					                accounts.budget_id, transactions.account_id
 | 
				
			||||||
 | 
					        FROM transactions 
 | 
				
			||||||
 | 
					        INNER JOIN accounts ON accounts.id = transactions.account_id
 | 
				
			||||||
 | 
					        LEFT JOIN payees ON payees.id = transactions.payee_id
 | 
				
			||||||
 | 
					        LEFT JOIN categories ON categories.id = transactions.category_id
 | 
				
			||||||
 | 
					        LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
 | 
				
			||||||
 | 
					        ORDER BY transactions.date DESC;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- +goose Down
 | 
				
			||||||
 | 
					DROP VIEW display_transactions;
 | 
				
			||||||
							
								
								
									
										5
									
								
								postgres/schema/0016_closed-accounts.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								postgres/schema/0016_closed-accounts.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					-- +goose Up
 | 
				
			||||||
 | 
					ALTER TABLE accounts ADD COLUMN is_open BOOLEAN NOT NULL DEFAULT TRUE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- +goose Down
 | 
				
			||||||
 | 
					ALTER TABLE accounts DROP COLUMN is_open;
 | 
				
			||||||
@@ -15,7 +15,7 @@ const createTransaction = `-- name: CreateTransaction :one
 | 
				
			|||||||
INSERT INTO transactions
 | 
					INSERT INTO transactions
 | 
				
			||||||
(date, memo, amount, account_id, payee_id, category_id, group_id, status)
 | 
					(date, memo, amount, account_id, payee_id, category_id, group_id, status)
 | 
				
			||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
 | 
					VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
 | 
				
			||||||
RETURNING id, date, memo, amount, account_id, category_id, payee_id, group_id, status
 | 
					RETURNING id
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type CreateTransactionParams struct {
 | 
					type CreateTransactionParams struct {
 | 
				
			||||||
@@ -29,7 +29,7 @@ type CreateTransactionParams struct {
 | 
				
			|||||||
	Status     TransactionStatus
 | 
						Status     TransactionStatus
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (Transaction, error) {
 | 
					func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (uuid.UUID, error) {
 | 
				
			||||||
	row := q.db.QueryRowContext(ctx, createTransaction,
 | 
						row := q.db.QueryRowContext(ctx, createTransaction,
 | 
				
			||||||
		arg.Date,
 | 
							arg.Date,
 | 
				
			||||||
		arg.Memo,
 | 
							arg.Memo,
 | 
				
			||||||
@@ -40,19 +40,9 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa
 | 
				
			|||||||
		arg.GroupID,
 | 
							arg.GroupID,
 | 
				
			||||||
		arg.Status,
 | 
							arg.Status,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	var i Transaction
 | 
						var id uuid.UUID
 | 
				
			||||||
	err := row.Scan(
 | 
						err := row.Scan(&id)
 | 
				
			||||||
		&i.ID,
 | 
						return id, err
 | 
				
			||||||
		&i.Date,
 | 
					 | 
				
			||||||
		&i.Memo,
 | 
					 | 
				
			||||||
		&i.Amount,
 | 
					 | 
				
			||||||
		&i.AccountID,
 | 
					 | 
				
			||||||
		&i.CategoryID,
 | 
					 | 
				
			||||||
		&i.PayeeID,
 | 
					 | 
				
			||||||
		&i.GroupID,
 | 
					 | 
				
			||||||
		&i.Status,
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	return i, err
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const deleteAllTransactions = `-- name: DeleteAllTransactions :execrows
 | 
					const deleteAllTransactions = `-- name: DeleteAllTransactions :execrows
 | 
				
			||||||
@@ -81,53 +71,20 @@ func (q *Queries) DeleteTransaction(ctx context.Context, id uuid.UUID) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getAllTransactionsForBudget = `-- name: GetAllTransactionsForBudget :many
 | 
					const getAllTransactionsForBudget = `-- name: GetAllTransactionsForBudget :many
 | 
				
			||||||
SELECT  transactions.id, transactions.date, transactions.memo, 
 | 
					SELECT  t.id, t.date, t.memo, t.amount, t.group_id, t.status, t.account, t.payee_id, t.category_id, t.payee, t.category_group, t.category, t.transfer_account, t.budget_id, t.account_id
 | 
				
			||||||
        transactions.amount, transactions.group_id, transactions.status,
 | 
					FROM display_transactions AS t
 | 
				
			||||||
        accounts.name as account, transactions.payee_id, transactions.category_id,
 | 
					WHERE t.budget_id = $1
 | 
				
			||||||
        COALESCE(payees.name, '') as payee, 
 | 
					 | 
				
			||||||
        COALESCE(category_groups.name, '') as category_group, 
 | 
					 | 
				
			||||||
        COALESCE(categories.name, '') as category,
 | 
					 | 
				
			||||||
        COALESCE((
 | 
					 | 
				
			||||||
            SELECT CONCAT(otherAccounts.name) 
 | 
					 | 
				
			||||||
            FROM transactions otherTransactions 
 | 
					 | 
				
			||||||
            LEFT JOIN accounts otherAccounts ON otherAccounts.id = otherTransactions.account_id
 | 
					 | 
				
			||||||
            WHERE otherTransactions.group_id = transactions.group_id 
 | 
					 | 
				
			||||||
            AND otherTransactions.id != transactions.id
 | 
					 | 
				
			||||||
        ), '')::text as transfer_account
 | 
					 | 
				
			||||||
FROM transactions 
 | 
					 | 
				
			||||||
INNER JOIN accounts ON accounts.id = transactions.account_id
 | 
					 | 
				
			||||||
LEFT JOIN payees ON payees.id = transactions.payee_id
 | 
					 | 
				
			||||||
LEFT JOIN categories ON categories.id = transactions.category_id
 | 
					 | 
				
			||||||
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
 | 
					 | 
				
			||||||
WHERE accounts.budget_id = $1
 | 
					 | 
				
			||||||
ORDER BY transactions.date DESC
 | 
					 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type GetAllTransactionsForBudgetRow struct {
 | 
					func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid.UUID) ([]DisplayTransaction, error) {
 | 
				
			||||||
	ID              uuid.UUID
 | 
					 | 
				
			||||||
	Date            time.Time
 | 
					 | 
				
			||||||
	Memo            string
 | 
					 | 
				
			||||||
	Amount          numeric.Numeric
 | 
					 | 
				
			||||||
	GroupID         uuid.NullUUID
 | 
					 | 
				
			||||||
	Status          TransactionStatus
 | 
					 | 
				
			||||||
	Account         string
 | 
					 | 
				
			||||||
	PayeeID         uuid.NullUUID
 | 
					 | 
				
			||||||
	CategoryID      uuid.NullUUID
 | 
					 | 
				
			||||||
	Payee           string
 | 
					 | 
				
			||||||
	CategoryGroup   string
 | 
					 | 
				
			||||||
	Category        string
 | 
					 | 
				
			||||||
	TransferAccount string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid.UUID) ([]GetAllTransactionsForBudgetRow, error) {
 | 
					 | 
				
			||||||
	rows, err := q.db.QueryContext(ctx, getAllTransactionsForBudget, budgetID)
 | 
						rows, err := q.db.QueryContext(ctx, getAllTransactionsForBudget, budgetID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer rows.Close()
 | 
						defer rows.Close()
 | 
				
			||||||
	var items []GetAllTransactionsForBudgetRow
 | 
						var items []DisplayTransaction
 | 
				
			||||||
	for rows.Next() {
 | 
						for rows.Next() {
 | 
				
			||||||
		var i GetAllTransactionsForBudgetRow
 | 
							var i DisplayTransaction
 | 
				
			||||||
		if err := rows.Scan(
 | 
							if err := rows.Scan(
 | 
				
			||||||
			&i.ID,
 | 
								&i.ID,
 | 
				
			||||||
			&i.Date,
 | 
								&i.Date,
 | 
				
			||||||
@@ -142,6 +99,8 @@ func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid
 | 
				
			|||||||
			&i.CategoryGroup,
 | 
								&i.CategoryGroup,
 | 
				
			||||||
			&i.Category,
 | 
								&i.Category,
 | 
				
			||||||
			&i.TransferAccount,
 | 
								&i.TransferAccount,
 | 
				
			||||||
 | 
								&i.BudgetID,
 | 
				
			||||||
 | 
								&i.AccountID,
 | 
				
			||||||
		); err != nil {
 | 
							); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -157,23 +116,29 @@ func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getTransaction = `-- name: GetTransaction :one
 | 
					const getTransaction = `-- name: GetTransaction :one
 | 
				
			||||||
SELECT id, date, memo, amount, account_id, category_id, payee_id, group_id, status FROM transactions
 | 
					SELECT id, date, memo, amount, group_id, status, account, payee_id, category_id, payee, category_group, category, transfer_account, budget_id, account_id FROM display_transactions
 | 
				
			||||||
WHERE id = $1
 | 
					WHERE id = $1
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (q *Queries) GetTransaction(ctx context.Context, id uuid.UUID) (Transaction, error) {
 | 
					func (q *Queries) GetTransaction(ctx context.Context, id uuid.UUID) (DisplayTransaction, error) {
 | 
				
			||||||
	row := q.db.QueryRowContext(ctx, getTransaction, id)
 | 
						row := q.db.QueryRowContext(ctx, getTransaction, id)
 | 
				
			||||||
	var i Transaction
 | 
						var i DisplayTransaction
 | 
				
			||||||
	err := row.Scan(
 | 
						err := row.Scan(
 | 
				
			||||||
		&i.ID,
 | 
							&i.ID,
 | 
				
			||||||
		&i.Date,
 | 
							&i.Date,
 | 
				
			||||||
		&i.Memo,
 | 
							&i.Memo,
 | 
				
			||||||
		&i.Amount,
 | 
							&i.Amount,
 | 
				
			||||||
		&i.AccountID,
 | 
					 | 
				
			||||||
		&i.CategoryID,
 | 
					 | 
				
			||||||
		&i.PayeeID,
 | 
					 | 
				
			||||||
		&i.GroupID,
 | 
							&i.GroupID,
 | 
				
			||||||
		&i.Status,
 | 
							&i.Status,
 | 
				
			||||||
 | 
							&i.Account,
 | 
				
			||||||
 | 
							&i.PayeeID,
 | 
				
			||||||
 | 
							&i.CategoryID,
 | 
				
			||||||
 | 
							&i.Payee,
 | 
				
			||||||
 | 
							&i.CategoryGroup,
 | 
				
			||||||
 | 
							&i.Category,
 | 
				
			||||||
 | 
							&i.TransferAccount,
 | 
				
			||||||
 | 
							&i.BudgetID,
 | 
				
			||||||
 | 
							&i.AccountID,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -213,54 +178,21 @@ func (q *Queries) GetTransactionsByMonthAndCategory(ctx context.Context, budgetI
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many
 | 
					const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many
 | 
				
			||||||
SELECT  transactions.id, transactions.date, transactions.memo, 
 | 
					SELECT  t.id, t.date, t.memo, t.amount, t.group_id, t.status, t.account, t.payee_id, t.category_id, t.payee, t.category_group, t.category, t.transfer_account, t.budget_id, t.account_id
 | 
				
			||||||
        transactions.amount, transactions.group_id, transactions.status,
 | 
					FROM display_transactions AS t
 | 
				
			||||||
        accounts.name as account, transactions.payee_id, transactions.category_id,
 | 
					WHERE t.account_id = $1
 | 
				
			||||||
        COALESCE(payees.name, '') as payee, 
 | 
					 | 
				
			||||||
        COALESCE(category_groups.name, '') as category_group, 
 | 
					 | 
				
			||||||
        COALESCE(categories.name, '') as category,
 | 
					 | 
				
			||||||
        COALESCE((
 | 
					 | 
				
			||||||
            SELECT CONCAT(otherAccounts.name) 
 | 
					 | 
				
			||||||
            FROM transactions otherTransactions 
 | 
					 | 
				
			||||||
            LEFT JOIN accounts otherAccounts ON otherAccounts.id = otherTransactions.account_id
 | 
					 | 
				
			||||||
            WHERE otherTransactions.group_id = transactions.group_id 
 | 
					 | 
				
			||||||
            AND otherTransactions.id != transactions.id
 | 
					 | 
				
			||||||
        ), '')::text as transfer_account
 | 
					 | 
				
			||||||
FROM transactions 
 | 
					 | 
				
			||||||
INNER JOIN accounts ON accounts.id = transactions.account_id
 | 
					 | 
				
			||||||
LEFT JOIN payees ON payees.id = transactions.payee_id
 | 
					 | 
				
			||||||
LEFT JOIN categories ON categories.id = transactions.category_id
 | 
					 | 
				
			||||||
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
 | 
					 | 
				
			||||||
WHERE transactions.account_id = $1
 | 
					 | 
				
			||||||
ORDER BY transactions.date DESC
 | 
					 | 
				
			||||||
LIMIT 200
 | 
					LIMIT 200
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type GetTransactionsForAccountRow struct {
 | 
					func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.UUID) ([]DisplayTransaction, error) {
 | 
				
			||||||
	ID              uuid.UUID
 | 
					 | 
				
			||||||
	Date            time.Time
 | 
					 | 
				
			||||||
	Memo            string
 | 
					 | 
				
			||||||
	Amount          numeric.Numeric
 | 
					 | 
				
			||||||
	GroupID         uuid.NullUUID
 | 
					 | 
				
			||||||
	Status          TransactionStatus
 | 
					 | 
				
			||||||
	Account         string
 | 
					 | 
				
			||||||
	PayeeID         uuid.NullUUID
 | 
					 | 
				
			||||||
	CategoryID      uuid.NullUUID
 | 
					 | 
				
			||||||
	Payee           string
 | 
					 | 
				
			||||||
	CategoryGroup   string
 | 
					 | 
				
			||||||
	Category        string
 | 
					 | 
				
			||||||
	TransferAccount string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.UUID) ([]GetTransactionsForAccountRow, error) {
 | 
					 | 
				
			||||||
	rows, err := q.db.QueryContext(ctx, getTransactionsForAccount, accountID)
 | 
						rows, err := q.db.QueryContext(ctx, getTransactionsForAccount, accountID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer rows.Close()
 | 
						defer rows.Close()
 | 
				
			||||||
	var items []GetTransactionsForAccountRow
 | 
						var items []DisplayTransaction
 | 
				
			||||||
	for rows.Next() {
 | 
						for rows.Next() {
 | 
				
			||||||
		var i GetTransactionsForAccountRow
 | 
							var i DisplayTransaction
 | 
				
			||||||
		if err := rows.Scan(
 | 
							if err := rows.Scan(
 | 
				
			||||||
			&i.ID,
 | 
								&i.ID,
 | 
				
			||||||
			&i.Date,
 | 
								&i.Date,
 | 
				
			||||||
@@ -275,6 +207,8 @@ func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.
 | 
				
			|||||||
			&i.CategoryGroup,
 | 
								&i.CategoryGroup,
 | 
				
			||||||
			&i.Category,
 | 
								&i.Category,
 | 
				
			||||||
			&i.TransferAccount,
 | 
								&i.TransferAccount,
 | 
				
			||||||
 | 
								&i.BudgetID,
 | 
				
			||||||
 | 
								&i.AccountID,
 | 
				
			||||||
		); err != nil {
 | 
							); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -289,6 +223,17 @@ func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.
 | 
				
			|||||||
	return items, nil
 | 
						return items, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const setTransactionReconciled = `-- name: SetTransactionReconciled :exec
 | 
				
			||||||
 | 
					UPDATE transactions
 | 
				
			||||||
 | 
					SET status = 'Reconciled'
 | 
				
			||||||
 | 
					WHERE id = $1
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) SetTransactionReconciled(ctx context.Context, id uuid.UUID) error {
 | 
				
			||||||
 | 
						_, err := q.db.ExecContext(ctx, setTransactionReconciled, id)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updateTransaction = `-- name: UpdateTransaction :exec
 | 
					const updateTransaction = `-- name: UpdateTransaction :exec
 | 
				
			||||||
UPDATE transactions
 | 
					UPDATE transactions
 | 
				
			||||||
SET date = $1,
 | 
					SET date = $1,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -110,7 +110,7 @@ func (ynab *YNABExport) ExportTransactions(context context.Context, w io.Writer)
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetTransactionRow(transaction GetAllTransactionsForBudgetRow) []string {
 | 
					func GetTransactionRow(transaction DisplayTransaction) []string {
 | 
				
			||||||
	row := []string{
 | 
						row := []string{
 | 
				
			||||||
		transaction.Account,
 | 
							transaction.Account,
 | 
				
			||||||
		"", // Flag
 | 
							"", // Flag
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,12 +33,13 @@ func (h *Handler) transactionsForAccount(c *gin.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type TransactionsResponse struct {
 | 
					type TransactionsResponse struct {
 | 
				
			||||||
	Account      postgres.Account
 | 
						Account      postgres.Account
 | 
				
			||||||
	Transactions []postgres.GetTransactionsForAccountRow
 | 
						Transactions []postgres.DisplayTransaction
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type EditAccountRequest struct {
 | 
					type EditAccountRequest struct {
 | 
				
			||||||
	Name     string `json:"name"`
 | 
						Name     string `json:"name"`
 | 
				
			||||||
	OnBudget bool   `json:"onBudget"`
 | 
						OnBudget bool   `json:"onBudget"`
 | 
				
			||||||
 | 
						IsOpen   bool   `json:"isOpen"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (h *Handler) editAccount(c *gin.Context) {
 | 
					func (h *Handler) editAccount(c *gin.Context) {
 | 
				
			||||||
@@ -59,6 +60,7 @@ func (h *Handler) editAccount(c *gin.Context) {
 | 
				
			|||||||
	updateParams := postgres.UpdateAccountParams{
 | 
						updateParams := postgres.UpdateAccountParams{
 | 
				
			||||||
		Name:     request.Name,
 | 
							Name:     request.Name,
 | 
				
			||||||
		OnBudget: request.OnBudget,
 | 
							OnBudget: request.OnBudget,
 | 
				
			||||||
 | 
							IsOpen:   request.IsOpen,
 | 
				
			||||||
		ID:       accountUUID,
 | 
							ID:       accountUUID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	account, err := h.Service.UpdateAccount(c.Request.Context(), updateParams)
 | 
						account, err := h.Service.UpdateAccount(c.Request.Context(), updateParams)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,11 +28,10 @@ func TestRegisterUser(t *testing.T) { //nolint:funlen
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tokenVerifier, _ := jwt.NewTokenVerifier("this_is_my_demo_secret_for_unit_tests")
 | 
				
			||||||
	h := Handler{
 | 
						h := Handler{
 | 
				
			||||||
		Service:             database,
 | 
							Service:             database,
 | 
				
			||||||
		TokenVerifier: &jwt.TokenVerifier{
 | 
							TokenVerifier:       tokenVerifier,
 | 
				
			||||||
			Secret: "this_is_my_demo_secret_for_unit_tests",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		CredentialsVerifier: &bcrypt.Verifier{},
 | 
							CredentialsVerifier: &bcrypt.Verifier{},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,6 +36,10 @@ type ErrorResponse struct {
 | 
				
			|||||||
	Message string
 | 
						Message string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SuccessResponse struct {
 | 
				
			||||||
 | 
						Message string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadRoutes initializes all the routes.
 | 
					// LoadRoutes initializes all the routes.
 | 
				
			||||||
func (h *Handler) LoadRoutes(router *gin.Engine) {
 | 
					func (h *Handler) LoadRoutes(router *gin.Engine) {
 | 
				
			||||||
	router.Use(enableCachingForStaticFiles())
 | 
						router.Use(enableCachingForStaticFiles())
 | 
				
			||||||
@@ -52,6 +56,7 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
 | 
				
			|||||||
	authenticated.Use(h.verifyLoginWithForbidden)
 | 
						authenticated.Use(h.verifyLoginWithForbidden)
 | 
				
			||||||
	authenticated.GET("/dashboard", h.dashboard)
 | 
						authenticated.GET("/dashboard", h.dashboard)
 | 
				
			||||||
	authenticated.GET("/account/:accountid/transactions", h.transactionsForAccount)
 | 
						authenticated.GET("/account/:accountid/transactions", h.transactionsForAccount)
 | 
				
			||||||
 | 
						authenticated.POST("/account/:accountid/reconcile", h.reconcileTransactions)
 | 
				
			||||||
	authenticated.POST("/account/:accountid", h.editAccount)
 | 
						authenticated.POST("/account/:accountid", h.editAccount)
 | 
				
			||||||
	authenticated.GET("/admin/clear-database", h.clearDatabase)
 | 
						authenticated.GET("/admin/clear-database", h.clearDatabase)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										103
									
								
								server/reconcile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								server/reconcile.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
				
			|||||||
 | 
					package server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.javil.eu/jacob1123/budgeteer/postgres"
 | 
				
			||||||
 | 
						"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ReconcileTransactionsRequest struct {
 | 
				
			||||||
 | 
						TransactionIDs                 []uuid.UUID `json:"transactionIds"`
 | 
				
			||||||
 | 
						ReconcilationTransactionAmount string      `json:"reconciliationTransactionAmount"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ReconcileTransactionsResponse struct {
 | 
				
			||||||
 | 
						Message                   string
 | 
				
			||||||
 | 
						ReconciliationTransaction *postgres.DisplayTransaction
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *Handler) reconcileTransactions(c *gin.Context) {
 | 
				
			||||||
 | 
						accountID := c.Param("accountid")
 | 
				
			||||||
 | 
						accountUUID, err := uuid.Parse(accountID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithError(http.StatusBadRequest, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var request ReconcileTransactionsRequest
 | 
				
			||||||
 | 
						err = c.BindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithError(http.StatusBadRequest, fmt.Errorf("parse request: %w", err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var amount numeric.Numeric
 | 
				
			||||||
 | 
						err = amount.Set(request.ReconcilationTransactionAmount)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithError(http.StatusBadRequest, fmt.Errorf("parse request: %w", err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tx, err := h.Service.BeginTx(c.Request.Context(), &sql.TxOptions{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("begin tx: %w", err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						db := h.Service.WithTx(tx)
 | 
				
			||||||
 | 
						for _, transactionID := range request.TransactionIDs {
 | 
				
			||||||
 | 
							err := db.SetTransactionReconciled(c.Request.Context(), transactionID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("update transaction: %w", err))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						reconciliationTransaction, err := h.CreateReconcilationTransaction(amount, accountUUID, db, c)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("insert new transaction: %w", err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = tx.Commit()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("commit: %w", err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, ReconcileTransactionsResponse{
 | 
				
			||||||
 | 
							Message:                   fmt.Sprintf("Set status for %d transactions", len(request.TransactionIDs)),
 | 
				
			||||||
 | 
							ReconciliationTransaction: reconciliationTransaction,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (*Handler) CreateReconcilationTransaction(amount numeric.Numeric, accountUUID uuid.UUID, db *postgres.Queries, c *gin.Context) (*postgres.DisplayTransaction, error) {
 | 
				
			||||||
 | 
						if amount.IsZero() {
 | 
				
			||||||
 | 
							return nil, nil //nolint: nilnil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						createTransaction := postgres.CreateTransactionParams{
 | 
				
			||||||
 | 
							Date:      time.Now(),
 | 
				
			||||||
 | 
							Memo:      "Reconciliation Transaction",
 | 
				
			||||||
 | 
							Amount:    amount,
 | 
				
			||||||
 | 
							AccountID: accountUUID,
 | 
				
			||||||
 | 
							Status:    "Reconciled",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						transactionUUID, err := db.CreateTransaction(c.Request.Context(), createTransaction)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("insert new transaction: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						transaction, err := db.GetTransaction(c.Request.Context(), transactionUUID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("get created transaction: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &transaction, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -57,11 +57,13 @@ func (h *Handler) newTransaction(c *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if payload.Payee.Type == "account" {
 | 
						if payload.Payee.Type == "account" {
 | 
				
			||||||
		err := h.CreateTransferForOtherAccount(newTransaction, amount, payload, c)
 | 
							groupID, err := h.CreateTransferForOtherAccount(newTransaction, amount, payload, c)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			c.AbortWithError(http.StatusInternalServerError, err)
 | 
								c.AbortWithError(http.StatusInternalServerError, err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							newTransaction.GroupID = groupID
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		payeeID, err := GetPayeeID(c.Request.Context(), payload, h)
 | 
							payeeID, err := GetPayeeID(c.Request.Context(), payload, h)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@@ -70,11 +72,18 @@ func (h *Handler) newTransaction(c *gin.Context) {
 | 
				
			|||||||
		newTransaction.PayeeID = payeeID
 | 
							newTransaction.PayeeID = payeeID
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	transaction, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction)
 | 
						transactionUUID, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
 | 
							c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						transaction, err := h.Service.GetTransaction(c.Request.Context(), transactionUUID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("get transaction: %w", err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.JSON(http.StatusOK, transaction)
 | 
						c.JSON(http.StatusOK, transaction)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -100,10 +109,19 @@ func (h *Handler) UpdateTransaction(payload NewTransactionPayload, amount numeri
 | 
				
			|||||||
	err := h.Service.UpdateTransaction(c.Request.Context(), editTransaction)
 | 
						err := h.Service.UpdateTransaction(c.Request.Context(), editTransaction)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("edit transaction: %w", err))
 | 
							c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("edit transaction: %w", err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						transaction, err := h.Service.GetTransaction(c.Request.Context(), transactionUUID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("get transaction: %w", err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, transaction)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (h *Handler) CreateTransferForOtherAccount(newTransaction postgres.CreateTransactionParams, amount numeric.Numeric, payload NewTransactionPayload, c *gin.Context) error {
 | 
					func (h *Handler) CreateTransferForOtherAccount(newTransaction postgres.CreateTransactionParams, amount numeric.Numeric, payload NewTransactionPayload, c *gin.Context) (uuid.NullUUID, error) {
 | 
				
			||||||
	newTransaction.GroupID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
 | 
						newTransaction.GroupID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
 | 
				
			||||||
	newTransaction.Amount = amount.Neg()
 | 
						newTransaction.Amount = amount.Neg()
 | 
				
			||||||
	newTransaction.AccountID = payload.Payee.ID.UUID
 | 
						newTransaction.AccountID = payload.Payee.ID.UUID
 | 
				
			||||||
@@ -113,9 +131,9 @@ func (h *Handler) CreateTransferForOtherAccount(newTransaction postgres.CreateTr
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	_, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction)
 | 
						_, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("create transfer transaction: %w", err)
 | 
							return uuid.NullUUID{}, fmt.Errorf("create transfer transaction: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return newTransaction.GroupID, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetPayeeID(context context.Context, payload NewTransactionPayload, h *Handler) (uuid.NullUUID, error) {
 | 
					func GetPayeeID(context context.Context, payload NewTransactionPayload, h *Handler) (uuid.NullUUID, error) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@
 | 
				
			|||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
    <title>Vite App</title>
 | 
					    <title>Vite App</title>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body>
 | 
					  <body class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200 box-border w-full">
 | 
				
			||||||
    <div id="app"></div>
 | 
					    <div id="app"></div>
 | 
				
			||||||
    <script type="module" src="/src/main.ts"></script>
 | 
					    <script type="module" src="/src/main.ts"></script>
 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,8 @@
 | 
				
			|||||||
    "@vue/cli-service": "5.0.0-beta.7",
 | 
					    "@vue/cli-service": "5.0.0-beta.7",
 | 
				
			||||||
    "sass": "^1.38.0",
 | 
					    "sass": "^1.38.0",
 | 
				
			||||||
    "sass-loader": "^10.0.0",
 | 
					    "sass-loader": "^10.0.0",
 | 
				
			||||||
 | 
					    "typescript": "^4.5.5",
 | 
				
			||||||
    "vite": "^2.7.2",
 | 
					    "vite": "^2.7.2",
 | 
				
			||||||
    "vue-cli-plugin-vuetify": "~2.4.5"
 | 
					    "vue-tsc": "^0.32.0"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,9 +27,20 @@ export default defineComponent({
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div class="box-border w-full">
 | 
					    <div class="flex flex-col md:flex-row flex-1">
 | 
				
			||||||
        <div class="flex bg-gray-400 p-4 m-2 rounded-lg">
 | 
					        <div
 | 
				
			||||||
            <span class="flex-1 font-bold text-5xl -my-3 hidden md:inline" @click="toggleMenuSize">≡</span>
 | 
					            :class="[Menu.Expand ? 'md:w-72' : 'md:w-36', Menu.Show ? '' : 'hidden']"
 | 
				
			||||||
 | 
					            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-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 font-bold text-5xl -my-3 md:hidden" @click="toggleMenu">≡</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <span class="flex-1">{{ CurrentBudgetName }}</span>
 | 
					                <span class="flex-1">{{ CurrentBudgetName }}</span>
 | 
				
			||||||
@@ -41,25 +52,9 @@ export default defineComponent({
 | 
				
			|||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="flex flex-col md:flex-row flex-1">
 | 
					            <div class="p-3 pl-6">
 | 
				
			||||||
            <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>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div class="flex-1 p-6">
 | 
					 | 
				
			||||||
                <router-view></router-view>
 | 
					                <router-view></router-view>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
#app {
 | 
					 | 
				
			||||||
    font-family: Avenir, Helvetica, Arial, sans-serif;
 | 
					 | 
				
			||||||
    -webkit-font-smoothing: antialiased;
 | 
					 | 
				
			||||||
    -moz-osx-font-smoothing: grayscale;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
import { ref, watch } from "vue"
 | 
					import { ref, watch } from "vue"
 | 
				
			||||||
import { GET } from "../api";
 | 
					import { GET } from "../api";
 | 
				
			||||||
import { useBudgetsStore } from "../stores/budget";
 | 
					import { useBudgetsStore } from "../stores/budget";
 | 
				
			||||||
 | 
					import Input from "./Input.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Suggestion {
 | 
					export interface Suggestion {
 | 
				
			||||||
    ID: string
 | 
					    ID: string
 | 
				
			||||||
@@ -43,7 +44,9 @@ function load(text: String) {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
function keypress(e: KeyboardEvent) {
 | 
					function keypress(e: KeyboardEvent) {
 | 
				
			||||||
    if (e.key == "Enter") {
 | 
					    if (e.key != "Enter")
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const selected = Suggestions.value[0];
 | 
					    const selected = Suggestions.value[0];
 | 
				
			||||||
    selectElement(selected);
 | 
					    selectElement(selected);
 | 
				
			||||||
    const el = (<HTMLInputElement>e.target);
 | 
					    const el = (<HTMLInputElement>e.target);
 | 
				
			||||||
@@ -51,14 +54,15 @@ function keypress(e: KeyboardEvent) {
 | 
				
			|||||||
    const currentIndex = inputElements.indexOf(el);
 | 
					    const currentIndex = inputElements.indexOf(el);
 | 
				
			||||||
    const nextElement = inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0];
 | 
					    const nextElement = inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0];
 | 
				
			||||||
    (<HTMLInputElement>nextElement).focus();
 | 
					    (<HTMLInputElement>nextElement).focus();
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function selectElement(element: Suggestion) {
 | 
					function selectElement(element: Suggestion) {
 | 
				
			||||||
    emit('update:id', element.ID);
 | 
					    emit('update:id', element.ID);
 | 
				
			||||||
    emit('update:text', element.Name);
 | 
					    emit('update:text', element.Name);
 | 
				
			||||||
    emit('update:type', element.Type);
 | 
					    emit('update:type', element.Type);
 | 
				
			||||||
    Suggestions.value = [];
 | 
					    Suggestions.value = [];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function select(e: MouseEvent) {
 | 
					function select(e: MouseEvent) {
 | 
				
			||||||
    const target = (<HTMLInputElement>e.target);
 | 
					    const target = (<HTMLInputElement>e.target);
 | 
				
			||||||
    const valueAttribute = target.attributes.getNamedItem("value");
 | 
					    const valueAttribute = target.attributes.getNamedItem("value");
 | 
				
			||||||
@@ -68,6 +72,7 @@ function select(e: MouseEvent) {
 | 
				
			|||||||
    const selected = Suggestions.value.filter(x => x.ID == selectedID)[0];
 | 
					    const selected = Suggestions.value.filter(x => x.ID == selectedID)[0];
 | 
				
			||||||
    selectElement(selected);
 | 
					    selectElement(selected);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function clear() {
 | 
					function clear() {
 | 
				
			||||||
    emit('update:id', null);
 | 
					    emit('update:id', null);
 | 
				
			||||||
    emit('update:text', SearchQuery.value);
 | 
					    emit('update:text', SearchQuery.value);
 | 
				
			||||||
@@ -77,14 +82,15 @@ function clear() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <input
 | 
					        <Input
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
            class="border-b-2 border-black"
 | 
					            class="border-b-2 border-black"
 | 
				
			||||||
            @keypress="keypress"
 | 
					            @keypress="keypress"
 | 
				
			||||||
            v-if="id == undefined"
 | 
					            v-if="id == undefined"
 | 
				
			||||||
            v-model="SearchQuery"
 | 
					            v-model="SearchQuery"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <span @click="clear" v-if="id != undefined" class="bg-gray-300">{{ text }}</span>
 | 
					        <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 w-64 p-2">
 | 
					        <div v-if="Suggestions.length > 0" class="absolute bg-gray-400 dark:bg-gray-600 w-64 p-2">
 | 
				
			||||||
            <span
 | 
					            <span
 | 
				
			||||||
                v-for="suggestion in Suggestions"
 | 
					                v-for="suggestion in Suggestions"
 | 
				
			||||||
                class="block"
 | 
					                class="block"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <button
 | 
					    <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>
 | 
					        <slot></slot>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<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>
 | 
					        <slot></slot>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
							
								
								
									
										11
									
								
								web/src/components/Checkbox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								web/src/components/Checkbox.vue
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
@@ -1,8 +1,11 @@
 | 
				
			|||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import Input from './Input.vue';
 | 
				
			||||||
const props = defineProps(["modelValue"]);
 | 
					const props = defineProps(["modelValue"]);
 | 
				
			||||||
const emit = defineEmits(['update:modelValue']);
 | 
					const emit = defineEmits(['update:modelValue']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function dateToYYYYMMDD(d: Date) : string {
 | 
					function dateToYYYYMMDD(d: Date) : string {
 | 
				
			||||||
 | 
					  if(d == null)
 | 
				
			||||||
 | 
					    return "";
 | 
				
			||||||
  // alternative implementations in https://stackoverflow.com/q/23593052/1850609
 | 
					  // alternative implementations in https://stackoverflow.com/q/23593052/1850609
 | 
				
			||||||
  //return new Date(d.getTime() - (d.getTimezoneOffset() * 60 * 1000)).toISOString().split('T')[0];
 | 
					  //return new Date(d.getTime() - (d.getTimezoneOffset() * 60 * 1000)).toISOString().split('T')[0];
 | 
				
			||||||
  return d.toISOString().split('T')[0];
 | 
					  return d.toISOString().split('T')[0];
 | 
				
			||||||
@@ -23,7 +26,7 @@ function selectAll(event: FocusEvent) {
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <input
 | 
					  <Input
 | 
				
			||||||
    type="date"
 | 
					    type="date"
 | 
				
			||||||
    ref="input"
 | 
					    ref="input"
 | 
				
			||||||
    v-bind:value="dateToYYYYMMDD(modelValue)"
 | 
					    v-bind:value="dateToYYYYMMDD(modelValue)"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								web/src/components/Input.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								web/src/components/Input.vue
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
@@ -3,11 +3,11 @@ import Card from '../components/Card.vue';
 | 
				
			|||||||
import { ref } from "vue";
 | 
					import { ref } from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
    buttonText: string,
 | 
					    buttonText?: string,
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const emit = defineEmits<{
 | 
					const emit = defineEmits<{
 | 
				
			||||||
    (e: 'submit'): void,
 | 
					    (e: 'submit', event : {cancel:boolean}): boolean,
 | 
				
			||||||
    (e: 'open'): void,
 | 
					    (e: 'open'): void,
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -20,8 +20,12 @@ function openDialog() {
 | 
				
			|||||||
    visible.value = true;
 | 
					    visible.value = true;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
function submitDialog() {
 | 
					function submitDialog() {
 | 
				
			||||||
 | 
					    const e = {cancel: false};
 | 
				
			||||||
 | 
					    emit("submit", e);
 | 
				
			||||||
 | 
					    if(e.cancel)
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    visible.value = false;
 | 
					    visible.value = false;
 | 
				
			||||||
    emit("submit");
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,9 +42,9 @@ function submitDialog() {
 | 
				
			|||||||
        v-if="visible"
 | 
					        v-if="visible"
 | 
				
			||||||
        class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"
 | 
					        class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
        <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
 | 
					        <div class="relative top-20 mx-auto p-5 w-96 shadow-lg rounded-md bg-white dark:bg-black">
 | 
				
			||||||
            <div class="mt-3 text-center">
 | 
					            <div class="mt-3 text-center">
 | 
				
			||||||
                <h3 class="mt-3 text-lg leading-6 font-medium text-gray-900">{{ buttonText }}</h3>
 | 
					                <h3 class="mt-3 text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{{ buttonText }}</h3>
 | 
				
			||||||
                <slot></slot>
 | 
					                <slot></slot>
 | 
				
			||||||
                <div class="grid grid-cols-2 gap-6">
 | 
					                <div class="grid grid-cols-2 gap-6">
 | 
				
			||||||
                    <button
 | 
					                    <button
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,13 +3,18 @@ import { computed, ref } from "vue";
 | 
				
			|||||||
import Autocomplete from './Autocomplete.vue'
 | 
					import Autocomplete from './Autocomplete.vue'
 | 
				
			||||||
import { useAccountStore } from '../stores/budget-account'
 | 
					import { useAccountStore } from '../stores/budget-account'
 | 
				
			||||||
import DateInput from "./DateInput.vue";
 | 
					import DateInput from "./DateInput.vue";
 | 
				
			||||||
 | 
					import { useTransactionsStore } from "../stores/transactions";
 | 
				
			||||||
 | 
					import Input from "./Input.vue";
 | 
				
			||||||
 | 
					import Button from "./Button.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
    transactionid: string
 | 
					    transactionid: string
 | 
				
			||||||
}>()
 | 
					}>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const accountStore = useAccountStore();
 | 
					const emit = defineEmits(["save"]);
 | 
				
			||||||
const TX = accountStore.Transactions.get(props.transactionid)!;
 | 
					
 | 
				
			||||||
 | 
					const transactionsStore = useTransactionsStore();
 | 
				
			||||||
 | 
					const TX = transactionsStore.Transactions.get(props.transactionid)!;
 | 
				
			||||||
const payeeType = ref<string|undefined>(undefined);
 | 
					const payeeType = ref<string|undefined>(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const payload = computed(() => JSON.stringify({
 | 
					const payload = computed(() => JSON.stringify({
 | 
				
			||||||
@@ -27,34 +32,35 @@ const payload = computed(() => JSON.stringify({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function saveTransaction(e: MouseEvent) {
 | 
					function saveTransaction(e: MouseEvent) {
 | 
				
			||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
    accountStore.editTransaction(TX.ID, payload.value);
 | 
					    transactionsStore.editTransaction(TX.ID, payload.value);
 | 
				
			||||||
 | 
					    emit('save');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <tr>
 | 
					    <tr>
 | 
				
			||||||
        <td style="width: 90px;" class="text-sm">
 | 
					        <td class="text-sm">
 | 
				
			||||||
            <DateInput class="border-b-2 border-black" v-model="TX.Date" />
 | 
					            <DateInput class="border-b-2 border-black" v-model="TX.Date" />
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
        <td style="max-width: 150px;">
 | 
					        <td>
 | 
				
			||||||
            <Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
 | 
					            <Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
        <td style="max-width: 200px;">
 | 
					        <td>
 | 
				
			||||||
            <Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
 | 
					            <Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
        <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>
 | 
				
			||||||
        <td style="width: 80px;" class="text-right">
 | 
					        <td class="text-right">
 | 
				
			||||||
            <input
 | 
					            <Input
 | 
				
			||||||
                class="text-right block w-full border-b-2 border-black"
 | 
					                class="text-right block w-full border-b-2 border-black"
 | 
				
			||||||
                type="currency"
 | 
					                type="currency"
 | 
				
			||||||
                v-model="TX.Amount"
 | 
					                v-model="TX.Amount"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
        <td style="width: 20px;">
 | 
					        <td>
 | 
				
			||||||
            <input type="submit" @click="saveTransaction" value="Save" />
 | 
					            <Button class="bg-blue-500" @click="saveTransaction">Save</Button>
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
        <td style="width: 20px;"></td>
 | 
					        <td></td>
 | 
				
			||||||
    </tr>
 | 
					    </tr>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
@@ -1,8 +1,10 @@
 | 
				
			|||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { computed, ref } from "vue";
 | 
					import { computed, ref } from "vue";
 | 
				
			||||||
import Autocomplete, { Suggestion } from '../components/Autocomplete.vue'
 | 
					import Autocomplete from '../components/Autocomplete.vue'
 | 
				
			||||||
import { Transaction, useAccountStore } from '../stores/budget-account'
 | 
					import { Transaction, useTransactionsStore } from "../stores/transactions";
 | 
				
			||||||
import DateInput from "./DateInput.vue";
 | 
					import DateInput from "./DateInput.vue";
 | 
				
			||||||
 | 
					import Button from "./Button.vue";
 | 
				
			||||||
 | 
					import Input from "./Input.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
    budgetid: string
 | 
					    budgetid: string
 | 
				
			||||||
@@ -22,6 +24,7 @@ const TX = ref<Transaction>({
 | 
				
			|||||||
    ID: "",
 | 
					    ID: "",
 | 
				
			||||||
    Status: "Uncleared",
 | 
					    Status: "Uncleared",
 | 
				
			||||||
    TransferAccount: "",
 | 
					    TransferAccount: "",
 | 
				
			||||||
 | 
					    Reconciled: false
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const payeeType = ref<string|undefined>(undefined);
 | 
					const payeeType = ref<string|undefined>(undefined);
 | 
				
			||||||
@@ -41,37 +44,40 @@ const payload = computed(() => JSON.stringify({
 | 
				
			|||||||
    state: "Uncleared"
 | 
					    state: "Uncleared"
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const accountStore = useAccountStore();
 | 
					const transactionsStore = useTransactionsStore();
 | 
				
			||||||
function saveTransaction(e: MouseEvent) {
 | 
					function saveTransaction(e: MouseEvent) {
 | 
				
			||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
    accountStore.saveTransaction(payload.value);
 | 
					    transactionsStore.saveTransaction(payload.value);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <tr>
 | 
					    <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" />
 | 
					            <DateInput class="border-b-2 border-black" v-model="TX.Date" />
 | 
				
			||||||
        </td>
 | 
					        </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" />
 | 
					            <Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
 | 
				
			||||||
        </td>
 | 
					        </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" />
 | 
					            <Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
        <td>
 | 
					        <td class="col-span-2">
 | 
				
			||||||
            <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>
 | 
				
			||||||
        <td style="width: 80px;" class="text-right">
 | 
					        <label class="md:hidden">Amount</label>
 | 
				
			||||||
            <input
 | 
					        <td class="text-right">
 | 
				
			||||||
 | 
					            <Input
 | 
				
			||||||
                class="text-right block w-full border-b-2 border-black"
 | 
					                class="text-right block w-full border-b-2 border-black"
 | 
				
			||||||
                type="currency"
 | 
					                type="currency"
 | 
				
			||||||
                v-model="TX.Amount"
 | 
					                v-model="TX.Amount"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
        <td style="width: 20px;">
 | 
					        <td class="hidden md:table-cell">
 | 
				
			||||||
            <input type="submit" @click="saveTransaction" value="Save" />
 | 
					            <Button class="bg-blue-500" @click="saveTransaction">Save</Button>
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
        <td style="width: 20px;"></td>
 | 
					 | 
				
			||||||
    </tr>
 | 
					    </tr>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
@@ -1,44 +1,80 @@
 | 
				
			|||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { computed, ref } from "vue";
 | 
					import { computed, ref } from "vue";
 | 
				
			||||||
import { useBudgetsStore } from "../stores/budget";
 | 
					import { useBudgetsStore } from "../stores/budget";
 | 
				
			||||||
import { Transaction } from "../stores/budget-account";
 | 
					import { useTransactionsStore } from "../stores/transactions";
 | 
				
			||||||
import Currency from "./Currency.vue";
 | 
					import Currency from "./Currency.vue";
 | 
				
			||||||
import TransactionEditRow from "./TransactionEditRow.vue";
 | 
					import TransactionEditRow from "./TransactionEditRow.vue";
 | 
				
			||||||
import { formatDate } from "../date";
 | 
					import { formatDate } from "../date";
 | 
				
			||||||
 | 
					import { useAccountStore } from "../stores/budget-account";
 | 
				
			||||||
 | 
					import Input from "./Input.vue";
 | 
				
			||||||
 | 
					import Checkbox from "./Checkbox.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
    transaction: Transaction,
 | 
					    transactionid: string,
 | 
				
			||||||
    index: number,
 | 
					    index: number,
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const edit = ref(false);
 | 
					const edit = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CurrentBudgetID = computed(()=> useBudgetsStore().CurrentBudgetID);
 | 
					const CurrentBudgetID = computed(() => useBudgetsStore().CurrentBudgetID);
 | 
				
			||||||
 | 
					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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <tr v-if="!edit" class="{{new Date(transaction.Date) > new Date() ? 'future' : ''}}"
 | 
					    <tr v-if="dateChanged()" class="table-row md:hidden">
 | 
				
			||||||
        :class="[index % 6 < 3 ? 'bg-gray-300' : 'bg-gray-100']">
 | 
					        <td class="bg-gray-200 dark:bg-gray-800 rounded-lg p-2" colspan="5">{{ formatDate(TX.Date) }}</td>
 | 
				
			||||||
        <!--: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>
 | 
					 | 
				
			||||||
            <a :href="'/budget/' + CurrentBudgetID + '/transaction/' + transaction.ID">
 | 
					 | 
				
			||||||
                {{ transaction.Memo }}
 | 
					 | 
				
			||||||
            </a>
 | 
					 | 
				
			||||||
        </td>
 | 
					 | 
				
			||||||
        <td>
 | 
					 | 
				
			||||||
            <Currency class="block" :value="transaction.Amount" />
 | 
					 | 
				
			||||||
        </td>
 | 
					 | 
				
			||||||
        <td>
 | 
					 | 
				
			||||||
            {{ transaction.Status == "Reconciled" ? "✔" : (transaction.Status == "Uncleared" ? "" : "*") }}
 | 
					 | 
				
			||||||
        </td>
 | 
					 | 
				
			||||||
        <td class="text-right">{{ transaction.GroupID ? "☀" : "" }}<a @click="edit = true;">✎</a></td>
 | 
					 | 
				
			||||||
    </tr>
 | 
					    </tr>
 | 
				
			||||||
    <TransactionEditRow v-if="edit" :transactionid="transaction.ID" />
 | 
					    <tr
 | 
				
			||||||
 | 
					        v-if="!edit"
 | 
				
			||||||
 | 
					        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 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/' + TX.ID"
 | 
				
			||||||
 | 
					            >{{ TX.Memo }}</a>
 | 
				
			||||||
 | 
					        </td>
 | 
				
			||||||
 | 
					        <td>
 | 
				
			||||||
 | 
					            <Currency class="block" :value="TX.Amount" />
 | 
				
			||||||
 | 
					        </td>
 | 
				
			||||||
 | 
					        <td class="text-right">
 | 
				
			||||||
 | 
					            {{ TX.GroupID ? "☀" : "" }}
 | 
				
			||||||
 | 
					            {{ getStatusSymbol() }}
 | 
				
			||||||
 | 
					            <a @click="edit = true;">✎</a>
 | 
				
			||||||
 | 
					            <Checkbox v-if="Reconciling && TX.Status != 'Reconciled'" v-model="TX.Reconciled" />
 | 
				
			||||||
 | 
					        </td>
 | 
				
			||||||
 | 
					    </tr>
 | 
				
			||||||
 | 
					    <TransactionEditRow v-if="edit" :transactionid="TX.ID" @save="edit = false" />
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,29 +2,51 @@
 | 
				
			|||||||
import { computed, ref } from 'vue';
 | 
					import { computed, ref } from 'vue';
 | 
				
			||||||
import Modal from '../components/Modal.vue';
 | 
					import Modal from '../components/Modal.vue';
 | 
				
			||||||
import { useAccountStore } from '../stores/budget-account';
 | 
					import { useAccountStore } from '../stores/budget-account';
 | 
				
			||||||
 | 
					import Input from '../components/Input.vue';
 | 
				
			||||||
 | 
					import Checkbox from '../components/Checkbox.vue';
 | 
				
			||||||
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					import { useBudgetsStore } from '../stores/budget';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = useRouter();
 | 
				
			||||||
const accountStore = useAccountStore();
 | 
					const accountStore = useAccountStore();
 | 
				
			||||||
const CurrentAccount = computed(() => accountStore.CurrentAccount);
 | 
					const CurrentAccount = computed(() => accountStore.CurrentAccount);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const accountName = ref("");
 | 
					const accountName = ref("");
 | 
				
			||||||
const accountOnBudget = ref(true);
 | 
					const accountOnBudget = ref(true);
 | 
				
			||||||
 | 
					const accountOpen = ref(true);
 | 
				
			||||||
 | 
					const error = ref("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function editAccount(e : any) {
 | 
					function editAccount(e : {cancel:boolean}) : boolean {
 | 
				
			||||||
    accountStore.EditAccount(CurrentAccount.value?.ID ?? "", accountName.value, accountOnBudget.value);
 | 
					    if(CurrentAccount.value?.ClearedBalance != 0 && !accountOpen.value){
 | 
				
			||||||
 | 
					        e.cancel = true;
 | 
				
			||||||
 | 
					        error.value = "Cannot close account with balance";
 | 
				
			||||||
 | 
					        return false; 
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    error.value = "";
 | 
				
			||||||
 | 
					    accountStore.EditAccount(CurrentAccount.value?.ID ?? "", accountName.value, accountOnBudget.value, accountOpen.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // account closed, move to Budget
 | 
				
			||||||
 | 
					    if(!accountOpen.value){
 | 
				
			||||||
 | 
					        const currentBudgetID = useBudgetsStore().CurrentBudgetID;
 | 
				
			||||||
 | 
					        router.replace('/budget/'+currentBudgetID+'/budgeting');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function openEditAccount(e : any) {
 | 
					function openEditAccount(e : any) {
 | 
				
			||||||
    accountName.value = CurrentAccount.value?.Name ?? "";
 | 
					    accountName.value = CurrentAccount.value?.Name ?? "";
 | 
				
			||||||
    accountOnBudget.value = CurrentAccount.value?.OnBudget ?? true;
 | 
					    accountOnBudget.value = CurrentAccount.value?.OnBudget ?? true;
 | 
				
			||||||
 | 
					    accountOpen.value = CurrentAccount.value?.IsOpen ?? true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <Modal button-text="Edit Account" @open="openEditAccount" @submit="editAccount">
 | 
					    <Modal button-text="Edit Account" @open="openEditAccount" @submit="editAccount">
 | 
				
			||||||
        <template v-slot:placeholder>✎</template>
 | 
					        <template v-slot:placeholder><span class="ml-2">✎</span></template>
 | 
				
			||||||
        <div class="mt-2 px-7 py-3">
 | 
					        <div class="mt-2 px-7 py-3">
 | 
				
			||||||
            <input
 | 
					            <Input
 | 
				
			||||||
                class="border-2"
 | 
					                class="border-2 dark:border-gray-700"
 | 
				
			||||||
                type="text"
 | 
					                type="text"
 | 
				
			||||||
                v-model="accountName"
 | 
					                v-model="accountName"
 | 
				
			||||||
                placeholder="Account name"
 | 
					                placeholder="Account name"
 | 
				
			||||||
@@ -32,13 +54,23 @@ function openEditAccount(e : any) {
 | 
				
			|||||||
            />
 | 
					            />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="mt-2 px-7 py-3">
 | 
					        <div class="mt-2 px-7 py-3">
 | 
				
			||||||
            <input
 | 
					            <Checkbox
 | 
				
			||||||
                class="border-2"
 | 
					                class="border-2"
 | 
				
			||||||
                type="checkbox"
 | 
					 | 
				
			||||||
                v-model="accountOnBudget"
 | 
					                v-model="accountOnBudget"
 | 
				
			||||||
                required
 | 
					                required
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <label>On Budget</label>
 | 
					            <label>On Budget</label>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="mt-2 px-7 py-3">
 | 
				
			||||||
 | 
					            <Checkbox
 | 
				
			||||||
 | 
					                class="border-2"
 | 
				
			||||||
 | 
					                v-model="accountOpen"
 | 
				
			||||||
 | 
					                required
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <label>Open</label>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div v-if="error != ''" class="dark:text-red-300 text-red-700">
 | 
				
			||||||
 | 
					            {{ error }}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    </Modal>
 | 
					    </Modal>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
import Modal from '../components/Modal.vue';
 | 
					import Modal from '../components/Modal.vue';
 | 
				
			||||||
import { ref } from "vue";
 | 
					import { ref } from "vue";
 | 
				
			||||||
import { useBudgetsStore } from '../stores/budget';
 | 
					import { useBudgetsStore } from '../stores/budget';
 | 
				
			||||||
 | 
					import Input from '../components/Input.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const budgetName = ref("");
 | 
					const budgetName = ref("");
 | 
				
			||||||
function saveBudget() {
 | 
					function saveBudget() {
 | 
				
			||||||
@@ -12,7 +13,7 @@ function saveBudget() {
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <Modal button-text="New Budget" @submit="saveBudget">
 | 
					  <Modal button-text="New Budget" @submit="saveBudget">
 | 
				
			||||||
    <div class="mt-2 px-7 py-3">
 | 
					    <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>
 | 
					    </div>
 | 
				
			||||||
  </Modal>
 | 
					  </Modal>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
@@ -9,3 +9,9 @@ h1 {
 | 
				
			|||||||
a {
 | 
					a {
 | 
				
			||||||
  text-decoration: underline;
 | 
					  text-decoration: underline;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#app {
 | 
				
			||||||
 | 
					    font-family: Avenir, Helvetica, Arial, sans-serif;
 | 
				
			||||||
 | 
					    -webkit-font-smoothing: antialiased;
 | 
				
			||||||
 | 
					    -moz-osx-font-smoothing: grayscale;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -11,7 +11,10 @@ const app = createApp(App)
 | 
				
			|||||||
app.use(router)
 | 
					app.use(router)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const pinia = createPinia()
 | 
					const pinia = createPinia()
 | 
				
			||||||
pinia.use(PiniaLogger())
 | 
					pinia.use(PiniaLogger({
 | 
				
			||||||
 | 
					  expanded: false,
 | 
				
			||||||
 | 
					  showDuration: true
 | 
				
			||||||
 | 
					}))
 | 
				
			||||||
app.use(pinia)
 | 
					app.use(pinia)
 | 
				
			||||||
app.mount('#app')
 | 
					app.mount('#app')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,43 +5,131 @@ import TransactionRow from "../components/TransactionRow.vue";
 | 
				
			|||||||
import TransactionInputRow from "../components/TransactionInputRow.vue";
 | 
					import TransactionInputRow from "../components/TransactionInputRow.vue";
 | 
				
			||||||
import { useAccountStore } from "../stores/budget-account";
 | 
					import { useAccountStore } from "../stores/budget-account";
 | 
				
			||||||
import EditAccount from "../dialogs/EditAccount.vue";
 | 
					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";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					defineProps<{
 | 
				
			||||||
    budgetid: string
 | 
					    budgetid: string
 | 
				
			||||||
    accountid: string
 | 
					    accountid: string
 | 
				
			||||||
}>()
 | 
					}>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const accountStore = useAccountStore();
 | 
					const accounts = useAccountStore();
 | 
				
			||||||
const CurrentAccount = computed(() => accountStore.CurrentAccount);
 | 
					const transactions = useTransactionsStore();
 | 
				
			||||||
const TransactionsList = computed(() => accountStore.TransactionsList);
 | 
					const TargetReconcilingBalance = ref(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setReconciled(event: Event) {
 | 
				
			||||||
 | 
					    const target = event.target as HTMLInputElement;
 | 
				
			||||||
 | 
					    transactions.SetReconciledForAllTransactions(target.checked);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function cancelReconcilation() {
 | 
				
			||||||
 | 
					    transactions.SetReconciledForAllTransactions(false);
 | 
				
			||||||
 | 
					    transactions.Reconciling = false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function submitReconcilation() {
 | 
				
			||||||
 | 
					    transactions.SubmitReconcilation(0);
 | 
				
			||||||
 | 
					    transactions.Reconciling = false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createReconcilationTransaction() {
 | 
				
			||||||
 | 
					    const diff = TargetReconcilingBalance.value - transactions.ReconcilingBalance;
 | 
				
			||||||
 | 
					    transactions.SubmitReconcilation(diff);
 | 
				
			||||||
 | 
					    transactions.Reconciling = false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <h1 class="inline">{{ CurrentAccount?.Name }}</h1>
 | 
					    <div class="grid grid-cols-[1fr_auto]">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <h1 class="inline">
 | 
				
			||||||
 | 
					                {{ accounts.CurrentAccount?.Name }}
 | 
				
			||||||
 | 
					            </h1>
 | 
				
			||||||
            <EditAccount />
 | 
					            <EditAccount />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <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 class="rounded-lg p-1 whitespace-nowrap flex-1">
 | 
				
			||||||
 | 
					                Cleared:
 | 
				
			||||||
 | 
					                <Currency :value="accounts.CurrentAccount?.ClearedBalance" />
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <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 
 | 
				
			||||||
 | 
					                    <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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <p>
 | 
					 | 
				
			||||||
        Current Balance:
 | 
					 | 
				
			||||||
        <Currency :value="CurrentAccount?.Balance" />
 | 
					 | 
				
			||||||
    </p>
 | 
					 | 
				
			||||||
    <table>
 | 
					    <table>
 | 
				
			||||||
        <tr class="font-bold">
 | 
					        <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: 150px;">Payee</td>
 | 
				
			||||||
            <td style="max-width: 200px;">Category</td>
 | 
					            <td style="max-width: 200px;">Category</td>
 | 
				
			||||||
            <td>Memo</td>
 | 
					            <td>Memo</td>
 | 
				
			||||||
            <td class="text-right">Amount</td>
 | 
					            <td class="text-right">Amount</td>
 | 
				
			||||||
            <td style="width: 20px;"></td>
 | 
					            <td style="width: 80px;">
 | 
				
			||||||
            <td style="width: 40px;"></td>
 | 
					                <Input v-if="transactions.Reconciling" type="checkbox" @input="setReconciled" />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
        <TransactionInputRow :budgetid="budgetid" :accountid="accountid" />
 | 
					        <TransactionInputRow
 | 
				
			||||||
 | 
					            class="hidden md:table-row"
 | 
				
			||||||
 | 
					            :budgetid="budgetid"
 | 
				
			||||||
 | 
					            :accountid="accountid"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <TransactionRow
 | 
					        <TransactionRow
 | 
				
			||||||
            v-for="(transaction, index) in TransactionsList"
 | 
					            v-for="(transaction, index) in transactions.TransactionsList"
 | 
				
			||||||
            :transaction="transaction"
 | 
					            :key="transaction.ID"
 | 
				
			||||||
 | 
					            :transactionid="transaction.ID"
 | 
				
			||||||
            :index="index"
 | 
					            :index="index"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
    </table>
 | 
					    </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>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
import { computed } from "vue";
 | 
					import { computed } from "vue";
 | 
				
			||||||
import Currency from "../components/Currency.vue"
 | 
					import Currency from "../components/Currency.vue"
 | 
				
			||||||
import { useBudgetsStore } from "../stores/budget"
 | 
					import { useBudgetsStore } from "../stores/budget"
 | 
				
			||||||
import { useAccountStore } from "../stores/budget-account"
 | 
					import { Account, useAccountStore } from "../stores/budget-account"
 | 
				
			||||||
import { useSettingsStore } from "../stores/settings"
 | 
					import { useSettingsStore } from "../stores/settings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ExpandMenu = computed(() => useSettingsStore().Menu.Expand);
 | 
					const ExpandMenu = computed(() => useSettingsStore().Menu.Expand);
 | 
				
			||||||
@@ -16,49 +16,62 @@ const OnBudgetAccounts = computed(() => accountStore.OnBudgetAccounts);
 | 
				
			|||||||
const OffBudgetAccounts = computed(() => accountStore.OffBudgetAccounts);
 | 
					const OffBudgetAccounts = computed(() => accountStore.OffBudgetAccounts);
 | 
				
			||||||
const OnBudgetAccountsBalance = computed(() => accountStore.OnBudgetAccountsBalance);
 | 
					const OnBudgetAccountsBalance = computed(() => accountStore.OnBudgetAccountsBalance);
 | 
				
			||||||
const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBalance);
 | 
					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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="flex flex-col">
 | 
					  <div class="flex flex-col mt-14 md:mt-0">
 | 
				
			||||||
    <span class="m-1 p-1 px-3 text-xl">
 | 
					    <span class="m-2 p-1 px-3 h-10 overflow-hidden" :class="[ExpandMenu ? 'text-2xl' : 'text-md']">
 | 
				
			||||||
      <router-link to="/dashboard">⌂</router-link>
 | 
					      <router-link to="/dashboard" style="font-size:150%">⌂</router-link>
 | 
				
			||||||
      {{CurrentBudgetName}}
 | 
					      {{CurrentBudgetName}}
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
    <span class="bg-orange-200 rounded-lg m-1 p-1 px-3 flex flex-col">
 | 
					    <span class="bg-gray-100 dark:bg-gray-700 p-2 px-3 flex flex-col">
 | 
				
			||||||
      <router-link :to="'/budget/'+CurrentBudgetID+'/budgeting'">Budget</router-link><br />
 | 
					      <router-link :to="'/budget/'+CurrentBudgetID+'/budgeting'">Budget</router-link><br />
 | 
				
			||||||
      <!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
 | 
					      <!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
 | 
				
			||||||
      <!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
 | 
					      <!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
    <li class="bg-orange-200 rounded-lg m-1 p-1 px-3">
 | 
					    <li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
 | 
				
			||||||
      <div class="flex flex-row justify-between font-bold">
 | 
					      <div class="flex flex-row justify-between font-bold">
 | 
				
			||||||
        <span>On-Budget Accounts</span>
 | 
					        <span>On-Budget Accounts</span>
 | 
				
			||||||
        <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OnBudgetAccountsBalance" />
 | 
					        <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OnBudgetAccountsBalance" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between">
 | 
					      <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.Balance" />
 | 
					        <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.ClearedBalance" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </li>
 | 
					    </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">
 | 
					      <div class="flex flex-row justify-between font-bold">
 | 
				
			||||||
        <span>Off-Budget Accounts</span>
 | 
					        <span>Off-Budget Accounts</span>
 | 
				
			||||||
        <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OffBudgetAccountsBalance" />
 | 
					        <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OffBudgetAccountsBalance" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between">
 | 
					      <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.Balance" />
 | 
					        <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.ClearedBalance" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </li>
 | 
					    </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>
 | 
				
			||||||
 | 
					    -->
 | 
				
			||||||
    <!--<li>
 | 
					    <!--<li>
 | 
				
			||||||
      <router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link>
 | 
					      <router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link>
 | 
				
			||||||
    </li>-->
 | 
					    </li>-->
 | 
				
			||||||
    <li class="bg-red-200 rounded-lg m-1 p-1 px-3">
 | 
					    <li class="bg-red-100 dark:bg-slate-600 my-2 p-2 px-3">
 | 
				
			||||||
      + Add Account
 | 
					 | 
				
			||||||
    </li>
 | 
					 | 
				
			||||||
    <li class="bg-red-200 rounded-lg m-1 p-1 px-3">
 | 
					 | 
				
			||||||
      <router-link :to="'/budget/'+CurrentBudgetID+'/settings'">Budget-Settings</router-link>
 | 
					      <router-link :to="'/budget/'+CurrentBudgetID+'/settings'">Budget-Settings</router-link>
 | 
				
			||||||
    </li>
 | 
					    </li>
 | 
				
			||||||
    <!--<li><router-link to="/admin">Admin</router-link></li>-->
 | 
					    <!--<li><router-link to="/admin">Admin</router-link></li>-->
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ const CurrentBudgetID = computed(() => budgetsStore.CurrentBudgetID);
 | 
				
			|||||||
const accountStore = useAccountStore();
 | 
					const accountStore = useAccountStore();
 | 
				
			||||||
const categoriesForMonth = accountStore.CategoriesForMonthAndGroup;
 | 
					const categoriesForMonth = accountStore.CategoriesForMonthAndGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function GetCategories(group : string) {
 | 
					function GetCategories(group: string) {
 | 
				
			||||||
    return [...categoriesForMonth(selected.value.Year, selected.value.Month, group)];
 | 
					    return [...categoriesForMonth(selected.value.Year, selected.value.Month, group)];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,12 +56,11 @@ onMounted(() => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const expandedGroups = ref<Map<string, boolean>>(new Map<string, boolean>())
 | 
					const expandedGroups = ref<Map<string, boolean>>(new Map<string, boolean>())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toggleGroup(group : {Name : string, Expand: boolean}) {
 | 
					function toggleGroup(group: { Name: string, Expand: boolean }) {
 | 
				
			||||||
    console.log(expandedGroups.value);
 | 
					 | 
				
			||||||
    expandedGroups.value.set(group.Name, !(expandedGroups.value.get(group.Name) ?? group.Expand))
 | 
					    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;
 | 
					    return expandedGroups.value.get(group.Name) ?? group.Expand;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@@ -71,43 +70,32 @@ function getGroupState(group : {Name : string, Expand: boolean}) : boolean {
 | 
				
			|||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <router-link
 | 
					        <router-link
 | 
				
			||||||
            :to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
 | 
					            :to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
 | 
				
			||||||
        >Previous Month</router-link>-
 | 
					        ><<</router-link> 
 | 
				
			||||||
        <router-link
 | 
					        <router-link
 | 
				
			||||||
            :to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
 | 
					            :to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
 | 
				
			||||||
        >Current Month</router-link>-
 | 
					        >Current Month</router-link> 
 | 
				
			||||||
        <router-link
 | 
					        <router-link
 | 
				
			||||||
            :to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
 | 
					            :to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
 | 
				
			||||||
        >Next Month</router-link>
 | 
					        >>></router-link>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <table class="container col-lg-12" id="content">
 | 
					    <div class="container col-lg-12 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5" id="content">
 | 
				
			||||||
        <tr>
 | 
					        <span class="hidden sm:block"></span>
 | 
				
			||||||
            <th>Category</th>
 | 
					        <span class="hidden lg:block text-right">Leftover</span>
 | 
				
			||||||
            <th></th>
 | 
					        <span class="hidden sm:block text-right">Assigned</span>
 | 
				
			||||||
            <th></th>
 | 
					        <span class="hidden sm:block text-right">Activity</span>
 | 
				
			||||||
            <th>Leftover</th>
 | 
					        <span class="hidden sm:block text-right">Available</span>
 | 
				
			||||||
            <th>Assigned</th>
 | 
					        <template v-for="group in GroupsForMonth">
 | 
				
			||||||
            <th>Activity</th>
 | 
					            <a
 | 
				
			||||||
            <th>Available</th>
 | 
					                class="text-lg font-bold col-span-2 sm:col-span-4 lg:col-span-5"
 | 
				
			||||||
        </tr>
 | 
					                @click="toggleGroup(group)"
 | 
				
			||||||
        <tbody v-for="group in GroupsForMonth">
 | 
					            >{{ (getGroupState(group) ? "−" : "+") + " " + group.Name }}</a>
 | 
				
			||||||
            <a class="text-lg font-bold" @click="toggleGroup(group)">{{ (getGroupState(group) ? "−" : "+") + " " + group.Name }}</a>
 | 
					            <template v-for="category in GetCategories(group.Name)" v-if="getGroupState(group)">
 | 
				
			||||||
            <tr v-for="category in GetCategories(group.Name)" v-if="getGroupState(group)">
 | 
					                <span class="whitespace-nowrap overflow-hidden">{{ category.Name }}</span>
 | 
				
			||||||
                <td>{{ category.Name }}</td>
 | 
					                <Currency :value="category.AvailableLastMonth" class="hidden lg:block" />
 | 
				
			||||||
                <td></td>
 | 
					                <Currency :value="category.Assigned" class="hidden sm:block" />
 | 
				
			||||||
                <td></td>
 | 
					                <Currency :value="category.Activity" class="hidden sm:block" />
 | 
				
			||||||
                <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" />
 | 
					                <Currency :value="category.Available" />
 | 
				
			||||||
                </td>
 | 
					            </template>
 | 
				
			||||||
            </tr>
 | 
					        </template>
 | 
				
			||||||
        </tbody>
 | 
					    </div>
 | 
				
			||||||
    </table>
 | 
					 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
import { onMounted, ref } from "vue";
 | 
					import { onMounted, ref } from "vue";
 | 
				
			||||||
import { useRouter } from "vue-router";
 | 
					import { useRouter } from "vue-router";
 | 
				
			||||||
import { useSessionStore } from "../stores/session";
 | 
					import { useSessionStore } from "../stores/session";
 | 
				
			||||||
 | 
					import Input from "../components/Input.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const error = ref("");
 | 
					const error = ref("");
 | 
				
			||||||
const login = ref({ user: "", password: "" });
 | 
					const login = ref({ user: "", password: "" });
 | 
				
			||||||
@@ -28,10 +29,10 @@ function formSubmit(e: MouseEvent) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <input type="text" v-model="login.user"
 | 
					        <Input type="text" v-model="login.user"
 | 
				
			||||||
            placeholder="Username"
 | 
					            placeholder="Username"
 | 
				
			||||||
            class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
 | 
					            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"
 | 
					            placeholder="Password"
 | 
				
			||||||
            class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
 | 
					            class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
import { onMounted, ref } from "vue";
 | 
					import { onMounted, ref } from "vue";
 | 
				
			||||||
import { useRouter } from "vue-router";
 | 
					import { useRouter } from "vue-router";
 | 
				
			||||||
import { useSessionStore } from "../stores/session";
 | 
					import { useSessionStore } from "../stores/session";
 | 
				
			||||||
 | 
					import Input from "../components/Input.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const error = ref("");
 | 
					const error = ref("");
 | 
				
			||||||
const login = ref({ email: "", password: "", name: "" });
 | 
					const login = ref({ email: "", password: "", name: "" });
 | 
				
			||||||
@@ -28,13 +29,13 @@ function formSubmit(e: MouseEvent) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <input type="text" v-model="login.name"
 | 
					        <Input type="text" v-model="login.name"
 | 
				
			||||||
            placeholder="Name"
 | 
					            placeholder="Name"
 | 
				
			||||||
            class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
 | 
					            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"
 | 
					            placeholder="Email"
 | 
				
			||||||
            class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
 | 
					            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"
 | 
					            placeholder="Password"
 | 
				
			||||||
            class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
 | 
					            class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ import { useSessionStore } from "../stores/session";
 | 
				
			|||||||
import Card from "../components/Card.vue";
 | 
					import Card from "../components/Card.vue";
 | 
				
			||||||
import Button from "../components/Button.vue";
 | 
					import Button from "../components/Button.vue";
 | 
				
			||||||
import { saveAs } from 'file-saver';
 | 
					import { saveAs } from 'file-saver';
 | 
				
			||||||
 | 
					import Input from "../components/Input.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const transactionsFile = ref<File | undefined>(undefined);
 | 
					const transactionsFile = ref<File | undefined>(undefined);
 | 
				
			||||||
const assignmentsFile = ref<File | undefined>(undefined);
 | 
					const assignmentsFile = ref<File | undefined>(undefined);
 | 
				
			||||||
@@ -82,22 +83,21 @@ function ynabExport() {
 | 
				
			|||||||
                <h2 class="text-lg font-bold">Clear Budget</h2>
 | 
					                <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>
 | 
					                <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>
 | 
				
			||||||
            <Card class="flex-col p-3">
 | 
					            <Card class="flex-col p-3">
 | 
				
			||||||
                <h2 class="text-lg font-bold">Delete Budget</h2>
 | 
					                <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>
 | 
					                <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>
 | 
				
			||||||
            <Card class="flex-col p-3">
 | 
					            <Card class="flex-col p-3">
 | 
				
			||||||
                <h2 class="text-lg font-bold">Fix all historic negative category-balances</h2>
 | 
					                <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>
 | 
					                <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>
 | 
				
			||||||
            <Card class="flex-col p-3">
 | 
					            <Card class="flex-col p-3">
 | 
				
			||||||
                <h2 class="text-lg font-bold">Import YNAB Budget</h2>
 | 
					                <h2 class="text-lg font-bold">Import YNAB Budget</h2>
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                <div class="flex flex-row">
 | 
					 | 
				
			||||||
                <div>
 | 
					                <div>
 | 
				
			||||||
                    <label for="transactions_file">
 | 
					                    <label for="transactions_file">
 | 
				
			||||||
                        Transaktionen:
 | 
					                        Transaktionen:
 | 
				
			||||||
@@ -110,14 +110,13 @@ function ynabExport() {
 | 
				
			|||||||
                    </label>
 | 
					                    </label>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    <Button class="bg-blue-500" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
 | 
					                <Button class="bg-blue-500 py-2" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </Card>
 | 
					            </Card>
 | 
				
			||||||
            <Card class="flex-col p-3">
 | 
					            <Card class="flex-col p-3">
 | 
				
			||||||
                <h2 class="text-lg font-bold">Export as YNAB TSV</h2>
 | 
					                <h2 class="text-lg font-bold">Export as YNAB TSV</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="flex flex-row">
 | 
					                <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>
 | 
					                </div>
 | 
				
			||||||
            </Card>
 | 
					            </Card>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,9 +11,8 @@ const formatTime = (date = new Date()) => {
 | 
				
			|||||||
  const hours = date.getHours().toString().padStart(2, '0');
 | 
					  const hours = date.getHours().toString().padStart(2, '0');
 | 
				
			||||||
  const minutes = date.getMinutes().toString().padStart(2, '0');
 | 
					  const minutes = date.getMinutes().toString().padStart(2, '0');
 | 
				
			||||||
  const seconds = date.getSeconds().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 {
 | 
					export interface PiniaLoggerOptions {
 | 
				
			||||||
@@ -52,7 +51,7 @@ export const PiniaLogger = (config = defaultOptions) => (ctx: PiniaPluginContext
 | 
				
			|||||||
      const duration = endTime - startTime + 'ms';
 | 
					      const duration = endTime - startTime + 'ms';
 | 
				
			||||||
      const nextState = cloneDeep(ctx.store.$state);
 | 
					      const nextState = cloneDeep(ctx.store.$state);
 | 
				
			||||||
      const storeName = action.store.$id;
 | 
					      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[options.expanded ? 'group' : 'groupCollapsed'](`%c${title}`, `font-weight: bold; ${isError ? 'color: #ed4981;' : ''}`);
 | 
				
			||||||
      console.log('%cprev state', 'font-weight: bold; color: grey;', prevState);
 | 
					      console.log('%cprev state', 'font-weight: bold; color: grey;', prevState);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,37 +2,26 @@ import { defineStore } from "pinia"
 | 
				
			|||||||
import { GET, POST } from "../api";
 | 
					import { GET, POST } from "../api";
 | 
				
			||||||
import { useBudgetsStore } from "./budget";
 | 
					import { useBudgetsStore } from "./budget";
 | 
				
			||||||
import { useSessionStore } from "./session";
 | 
					import { useSessionStore } from "./session";
 | 
				
			||||||
 | 
					import { useTransactionsStore } from "./transactions";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface State {
 | 
					interface State {
 | 
				
			||||||
    Accounts: Map<string, Account>,
 | 
					    Accounts: Map<string, Account>
 | 
				
			||||||
    CurrentAccountID: string | null,
 | 
					    CurrentAccountID: string | null
 | 
				
			||||||
    Categories: Map<string, Category>,
 | 
					    Categories: Map<string, Category>
 | 
				
			||||||
    Months: Map<number, Map<number, Map<string, Category>>>,
 | 
					    Months: Map<number, Map<number, Map<string, Category>>>
 | 
				
			||||||
    Transactions: Map<string, Transaction>,
 | 
					 | 
				
			||||||
    Assignments: []
 | 
					    Assignments: []
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface Account {
 | 
					export interface Account {
 | 
				
			||||||
    ID: string
 | 
					    ID: string
 | 
				
			||||||
    Name: string
 | 
					    Name: string
 | 
				
			||||||
    OnBudget: boolean
 | 
					    OnBudget: boolean
 | 
				
			||||||
    Balance: number
 | 
					    IsOpen: boolean
 | 
				
			||||||
 | 
					    ClearedBalance: number
 | 
				
			||||||
 | 
					    WorkingBalance: number
 | 
				
			||||||
 | 
					    ReconciledBalance: number
 | 
				
			||||||
    Transactions: string[]
 | 
					    Transactions: string[]
 | 
				
			||||||
 | 
					    LastReconciled: Date
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Category {
 | 
					export interface Category {
 | 
				
			||||||
@@ -51,8 +40,7 @@ export const useAccountStore = defineStore("budget/account", {
 | 
				
			|||||||
        CurrentAccountID: null,
 | 
					        CurrentAccountID: null,
 | 
				
			||||||
        Months: new Map<number, Map<number, Map<string, Category>>>(),
 | 
					        Months: new Map<number, Map<number, Map<string, Category>>>(),
 | 
				
			||||||
        Categories: new Map<string, Category>(),
 | 
					        Categories: new Map<string, Category>(),
 | 
				
			||||||
        Transactions: new Map<string, Transaction>(),
 | 
					        Assignments: [],
 | 
				
			||||||
        Assignments: []
 | 
					 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    getters: {
 | 
					    getters: {
 | 
				
			||||||
        AccountsList(state) {
 | 
					        AccountsList(state) {
 | 
				
			||||||
@@ -69,7 +57,7 @@ export const useAccountStore = defineStore("budget/account", {
 | 
				
			|||||||
                const categoryGroups = [];
 | 
					                const categoryGroups = [];
 | 
				
			||||||
                let prev = undefined;
 | 
					                let prev = undefined;
 | 
				
			||||||
                for (const category of categories) {
 | 
					                for (const category of categories) {
 | 
				
			||||||
                    if(category.Group != prev)
 | 
					                    if (category.Group != prev)
 | 
				
			||||||
                        categoryGroups.push({
 | 
					                        categoryGroups.push({
 | 
				
			||||||
                            Name: category.Group,
 | 
					                            Name: category.Group,
 | 
				
			||||||
                            Expand: category.Group != "Hidden Categories",
 | 
					                            Expand: category.Group != "Hidden Categories",
 | 
				
			||||||
@@ -80,69 +68,70 @@ export const useAccountStore = defineStore("budget/account", {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        CategoriesForMonthAndGroup(state) {
 | 
					        CategoriesForMonthAndGroup(state) {
 | 
				
			||||||
            return (year: number, month: number, group : string) => {
 | 
					            return (year: number, month: number, group: string) => {
 | 
				
			||||||
                const categories = this.AllCategoriesForMonth(year, month);
 | 
					                const categories = this.AllCategoriesForMonth(year, month);
 | 
				
			||||||
                return categories.filter(x => x.Group == group);
 | 
					                return categories.filter(x => x.Group == group);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        GetAccount(state) {
 | 
				
			||||||
 | 
					            return (accountid: string) => {
 | 
				
			||||||
 | 
					                return this.Accounts.get(accountid);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        CurrentAccount(state): Account | undefined {
 | 
					        CurrentAccount(state): Account | undefined {
 | 
				
			||||||
            if (state.CurrentAccountID == null)
 | 
					            if (state.CurrentAccountID == null)
 | 
				
			||||||
                return undefined;
 | 
					                return undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return state.Accounts.get(state.CurrentAccountID);
 | 
					            return this.GetAccount(state.CurrentAccountID);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        OnBudgetAccounts(state) {
 | 
					        OnBudgetAccounts(state) {
 | 
				
			||||||
            return [...state.Accounts.values()].filter(x => x.OnBudget);
 | 
					            return [...state.Accounts.values()].filter(x => x.OnBudget);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        OnBudgetAccountsBalance(state): number {
 | 
					        OnBudgetAccountsBalance(state): number {
 | 
				
			||||||
            return this.OnBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
 | 
					            return this.OnBudgetAccounts.reduce((prev, curr) => prev + Number(curr.ClearedBalance), 0);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        OffBudgetAccounts(state) {
 | 
					        OffBudgetAccounts(state) {
 | 
				
			||||||
            return [...state.Accounts.values()].filter(x => !x.OnBudget);
 | 
					            return [...state.Accounts.values()].filter(x => !x.OnBudget);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        OffBudgetAccountsBalance(state): number {
 | 
					        OffBudgetAccountsBalance(state): number {
 | 
				
			||||||
            return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
 | 
					            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: {
 | 
					    actions: {
 | 
				
			||||||
        async SetCurrentAccount(budgetid: string, accountid: string) {
 | 
					        async SetCurrentAccount(budgetid: string, accountid: string) {
 | 
				
			||||||
            if (budgetid == null)
 | 
					            if (budgetid == null)
 | 
				
			||||||
                return
 | 
					                return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.CurrentAccountID = accountid;
 | 
					            this.CurrentAccountID = accountid;
 | 
				
			||||||
            const account = this.CurrentAccount;
 | 
					 | 
				
			||||||
            if (account == undefined)
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (accountid == null)
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            const account = this.GetAccount(accountid)!;
 | 
				
			||||||
            useSessionStore().setTitle(account.Name);
 | 
					            useSessionStore().setTitle(account.Name);
 | 
				
			||||||
            await this.FetchAccount(account);
 | 
					            await this.FetchAccount(account);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        async FetchAccount(account: Account) {
 | 
					        async FetchAccount(account: Account) {
 | 
				
			||||||
            const result = await GET("/account/" + account.ID + "/transactions");
 | 
					            const result = await GET("/account/" + account.ID + "/transactions");
 | 
				
			||||||
            const response = await result.json();
 | 
					            const response = await result.json();
 | 
				
			||||||
            account.Transactions = [];
 | 
					            const transactionsStore = useTransactionsStore()
 | 
				
			||||||
            for (const transaction of response.Transactions) {
 | 
					            const transactions = transactionsStore.AddTransactions(response.Transactions);
 | 
				
			||||||
                transaction.Date = new Date(transaction.Date);
 | 
					            account.Transactions = transactions;
 | 
				
			||||||
                this.Transactions.set(transaction.ID, transaction);
 | 
					 | 
				
			||||||
                account.Transactions.push(transaction.ID);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        async FetchMonthBudget(budgetid: string, year: number, month: number) {
 | 
					        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();
 | 
					            const response = await result.json();
 | 
				
			||||||
            if(response.Categories == undefined || response.Categories.length <= 0)
 | 
					            if (response.Categories == undefined || response.Categories.length <= 0)
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            this.addCategoriesForMonth(year, month, response.Categories);
 | 
					            this.addCategoriesForMonth(year, month, response.Categories);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        async EditAccount(accountid : string, name : string, onBudget : boolean) {
 | 
					        async EditAccount(accountid: string, name: string, onBudget: boolean, isOpen: boolean) {
 | 
				
			||||||
            const result = await POST("/account/" + accountid, JSON.stringify({name: name, onBudget: onBudget}));
 | 
					            const result = await POST("/account/" + accountid, JSON.stringify({ name: name, onBudget: onBudget, isOpen: isOpen }));
 | 
				
			||||||
            const response = await result.json();
 | 
					            const response = await result.json();
 | 
				
			||||||
            useBudgetsStore().MergeBudgetingData(response);
 | 
					            useBudgetsStore().MergeBudgetingData(response);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if(!isOpen) {
 | 
				
			||||||
 | 
					                this.Accounts.delete(accountid);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
 | 
					        addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
 | 
				
			||||||
            this.$patch((state) => {
 | 
					            this.$patch((state) => {
 | 
				
			||||||
@@ -159,16 +148,6 @@ export const useAccountStore = defineStore("budget/account", {
 | 
				
			|||||||
        logout() {
 | 
					        logout() {
 | 
				
			||||||
            this.$reset()
 | 
					            this.$reset()
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        async saveTransaction(payload: string) {
 | 
					 | 
				
			||||||
            const result = await POST("/transaction/new", payload);
 | 
					 | 
				
			||||||
            const response = await result.json();
 | 
					 | 
				
			||||||
            this.CurrentAccount?.Transactions.unshift(response);
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        async editTransaction(transactionid : string, payload: string) {
 | 
					 | 
				
			||||||
            const result = await POST("/transaction/" + transactionid, payload);
 | 
					 | 
				
			||||||
            const response = await result.json();
 | 
					 | 
				
			||||||
            this.CurrentAccount?.Transactions.unshift(response);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,12 +53,16 @@ export const useBudgetsStore = defineStore('budget', {
 | 
				
			|||||||
            const response = await result.json();
 | 
					            const response = await result.json();
 | 
				
			||||||
            this.MergeBudgetingData(response);
 | 
					            this.MergeBudgetingData(response);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        MergeBudgetingData(response : any) {
 | 
					        MergeBudgetingData(response: any) {
 | 
				
			||||||
 | 
					            const accounts = useAccountStore();
 | 
				
			||||||
            for (const account of response.Accounts || []) {
 | 
					            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 || []) {
 | 
					            for (const category of response.Categories || []) {
 | 
				
			||||||
                useAccountStore().Categories.set(category.ID, category);
 | 
					                accounts.Categories.set(category.ID, category);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										105
									
								
								web/src/stores/transactions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								web/src/stores/transactions.ts
									
									
									
									
									
										Normal 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]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										538
									
								
								web/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										538
									
								
								web/yarn.lock
									
									
									
									
									
								
							@@ -273,6 +273,11 @@
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c"
 | 
					  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c"
 | 
				
			||||||
  integrity sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==
 | 
					  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":
 | 
					"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7":
 | 
				
			||||||
  version "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"
 | 
					  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"
 | 
					    debug "^4.1.0"
 | 
				
			||||||
    globals "^11.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"
 | 
					  version "7.17.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b"
 | 
					  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b"
 | 
				
			||||||
  integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==
 | 
					  integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==
 | 
				
			||||||
@@ -933,6 +938,25 @@
 | 
				
			|||||||
    "@babel/helper-validator-identifier" "^7.16.7"
 | 
					    "@babel/helper-validator-identifier" "^7.16.7"
 | 
				
			||||||
    to-fast-properties "^2.0.0"
 | 
					    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":
 | 
					"@hapi/address@2.x.x":
 | 
				
			||||||
  version "2.1.4"
 | 
					  version "2.1.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.1.0.tgz#ddf5e0059f84f2ff649afc25ce5a59211e670542"
 | 
				
			||||||
  integrity sha512-AZ78WxvFMYd8JmM/GBV6a6SGGTU0GgN/0/4T+FnMMsLzFEzTeAUwuraapy50ifHZsC+G5SvWs86bvaCPTneFlA==
 | 
					  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":
 | 
					"@vue/babel-helper-vue-jsx-merge-props@^1.2.1":
 | 
				
			||||||
  version "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"
 | 
					  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"
 | 
					    estree-walker "^2.0.2"
 | 
				
			||||||
    source-map "^0.6.1"
 | 
					    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":
 | 
					"@vue/compiler-dom@3.2.29":
 | 
				
			||||||
  version "3.2.29"
 | 
					  version "3.2.29"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.29.tgz#ad0ead405bd2f2754161335aad9758aa12430715"
 | 
					  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/compiler-core" "3.2.29"
 | 
				
			||||||
    "@vue/shared" "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":
 | 
					"@vue/compiler-sfc@3.2.29":
 | 
				
			||||||
  version "3.2.29"
 | 
					  version "3.2.29"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.29.tgz#f76d556cd5fca6a55a3ea84c88db1a2a53a36ead"
 | 
					  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.29.tgz#f76d556cd5fca6a55a3ea84c88db1a2a53a36ead"
 | 
				
			||||||
@@ -1616,6 +1727,13 @@
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    "@vue/shared" "3.2.29"
 | 
					    "@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":
 | 
					"@vue/runtime-core@3.2.29":
 | 
				
			||||||
  version "3.2.29"
 | 
					  version "3.2.29"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.29.tgz#fb8577b2fcf52e8d967bd91cdf49ab9fb91f9417"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.29.tgz#07dac7051117236431d2f737d16932aa38bbb925"
 | 
				
			||||||
  integrity sha512-BjNpU8OK6Z0LVzGUppEk0CMYm/hKDnZfYdjSmPOs0N+TR1cLKJAkDwW8ASZUvaaSLEi6d3hVM7jnWnX+6yWnHw==
 | 
					  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":
 | 
					"@vue/vue-loader-v15@npm:vue-loader@^15.9.7":
 | 
				
			||||||
  version "15.9.8"
 | 
					  version "15.9.8"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.8.tgz#4b0f602afaf66a996be1e534fb9609dc4ab10e61"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
 | 
				
			||||||
  integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
 | 
					  integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
acorn@^7.0.0:
 | 
					acorn@^7.0.0, acorn@^7.1.1:
 | 
				
			||||||
  version "7.4.1"
 | 
					  version "7.4.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
 | 
					  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
 | 
				
			||||||
  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
 | 
				
			||||||
  integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
 | 
					  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:
 | 
					asn1.js@^5.2.0:
 | 
				
			||||||
  version "5.4.1"
 | 
					  version "5.4.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
 | 
					  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
 | 
				
			||||||
@@ -2218,6 +2346,11 @@ asn1@~0.2.3:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    safer-buffer "~2.1.0"
 | 
					    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:
 | 
					assert-plus@1.0.0, assert-plus@^1.0.0:
 | 
				
			||||||
  version "1.0.0"
 | 
					  version "1.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
 | 
					  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:
 | 
					  dependencies:
 | 
				
			||||||
    "@babel/helper-define-polyfill-provider" "^0.3.1"
 | 
					    "@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:
 | 
					balanced-match@^1.0.0:
 | 
				
			||||||
  version "1.0.2"
 | 
					  version "1.0.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
 | 
					  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"
 | 
					    ansi-styles "^4.1.0"
 | 
				
			||||||
    supports-color "^7.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:
 | 
					charcodes@^0.2.0:
 | 
				
			||||||
  version "0.2.0"
 | 
					  version "0.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4"
 | 
					  resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4"
 | 
				
			||||||
@@ -3037,6 +3184,14 @@ consolidate@^0.15.1:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    bluebird "^3.1.1"
 | 
					    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:
 | 
					constants-browserify@^1.0.0:
 | 
				
			||||||
  version "1.0.0"
 | 
					  version "1.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
 | 
					  resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
 | 
				
			||||||
@@ -3569,6 +3724,11 @@ dns-txt@^2.0.2:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    buffer-indexof "^1.0.0"
 | 
					    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:
 | 
					dom-converter@^0.2.0:
 | 
				
			||||||
  version "0.2.0"
 | 
					  version "0.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
 | 
				
			||||||
  integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
 | 
					  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"
 | 
					  version "4.3.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626"
 | 
					  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626"
 | 
				
			||||||
  integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==
 | 
					  integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==
 | 
				
			||||||
@@ -3680,6 +3840,14 @@ elliptic@^6.5.3:
 | 
				
			|||||||
    minimalistic-assert "^1.0.1"
 | 
					    minimalistic-assert "^1.0.1"
 | 
				
			||||||
    minimalistic-crypto-utils "^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:
 | 
					emoji-regex@^8.0.0:
 | 
				
			||||||
  version "8.0.0"
 | 
					  version "8.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
 | 
				
			||||||
  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
 | 
					  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:
 | 
					errno@^0.1.3, errno@~0.1.7:
 | 
				
			||||||
  version "0.1.8"
 | 
					  version "0.1.8"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
 | 
				
			||||||
  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
 | 
					  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"
 | 
					  version "7.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
 | 
					  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
 | 
				
			||||||
  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
 | 
					  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
 | 
				
			||||||
@@ -4686,6 +4859,16 @@ htmlparser2@^6.1.0:
 | 
				
			|||||||
    domutils "^2.5.2"
 | 
					    domutils "^2.5.2"
 | 
				
			||||||
    entities "^2.0.0"
 | 
					    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:
 | 
					http-deceiver@^1.2.7:
 | 
				
			||||||
  version "1.2.7"
 | 
					  version "1.2.7"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
 | 
				
			||||||
  integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 | 
					  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:
 | 
					ip@^1.1.0:
 | 
				
			||||||
  version "1.1.5"
 | 
					  version "1.1.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
 | 
				
			||||||
  integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
 | 
					  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:
 | 
					is-extendable@^0.1.0, is-extendable@^0.1.1:
 | 
				
			||||||
  version "0.1.1"
 | 
					  version "0.1.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
 | 
					  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:
 | 
					  dependencies:
 | 
				
			||||||
    isobject "^3.0.1"
 | 
					    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"
 | 
					  version "1.1.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
 | 
					  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
 | 
				
			||||||
  integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
 | 
					  integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
 | 
				
			||||||
@@ -5159,6 +5350,11 @@ js-queue@2.0.2:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    easy-stack "^1.0.1"
 | 
					    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:
 | 
					js-tokens@^3.0.2:
 | 
				
			||||||
  version "3.0.2"
 | 
					  version "3.0.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
 | 
					  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
 | 
				
			||||||
@@ -5236,6 +5432,16 @@ json5@^2.1.2:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    minimist "^1.2.5"
 | 
					    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:
 | 
					jsonfile@^6.0.1:
 | 
				
			||||||
  version "6.1.0"
 | 
					  version "6.1.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
 | 
					  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"
 | 
					    json-schema "0.4.0"
 | 
				
			||||||
    verror "1.10.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:
 | 
					kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
 | 
				
			||||||
  version "3.2.2"
 | 
					  version "3.2.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
 | 
					  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
 | 
				
			||||||
@@ -5901,14 +6115,6 @@ nth-check@^2.0.1:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    boolbase "^1.0.0"
 | 
					    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:
 | 
					oauth-sign@~0.9.0:
 | 
				
			||||||
  version "0.9.0"
 | 
					  version "0.9.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
 | 
				
			||||||
  integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
 | 
					  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:
 | 
					proxy-addr@~2.0.7:
 | 
				
			||||||
  version "2.0.7"
 | 
					  version "2.0.7"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
 | 
					  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"
 | 
					    randombytes "^2.0.1"
 | 
				
			||||||
    safe-buffer "^5.1.2"
 | 
					    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:
 | 
					pump@^2.0.0:
 | 
				
			||||||
  version "2.0.1"
 | 
					  version "2.0.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
 | 
					  resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
 | 
				
			||||||
@@ -6840,13 +7156,6 @@ readdirp@~3.6.0:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    picomatch "^2.2.1"
 | 
					    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:
 | 
					regenerate-unicode-properties@^10.0.1:
 | 
				
			||||||
  version "10.0.1"
 | 
					  version "10.0.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
 | 
				
			||||||
  integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 | 
					  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"
 | 
					  version "1.22.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
 | 
					  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
 | 
				
			||||||
  integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
 | 
				
			||||||
  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 | 
					  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"
 | 
					  version "7.3.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
 | 
					  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
 | 
				
			||||||
  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
 | 
				
			||||||
  integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
 | 
					  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:
 | 
					signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
 | 
				
			||||||
  version "3.0.7"
 | 
					  version "3.0.7"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
 | 
				
			||||||
  integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 | 
					  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:
 | 
					totalist@^1.0.0:
 | 
				
			||||||
  version "1.1.0"
 | 
					  version "1.1.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 | 
				
			||||||
  integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 | 
					  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:
 | 
					unicode-canonical-property-names-ecmascript@^2.0.0:
 | 
				
			||||||
  version "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"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
 | 
				
			||||||
  integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
 | 
					  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:
 | 
					uri-js@^4.2.2:
 | 
				
			||||||
  version "4.4.1"
 | 
					  version "4.4.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
 | 
				
			||||||
  integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
 | 
					  integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
vue-cli-plugin-vuetify@~2.4.5:
 | 
					void-elements@^3.1.0:
 | 
				
			||||||
  version "2.4.5"
 | 
					  version "3.1.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.4.5.tgz#5dfae4d78c717c400530731f0b75c0350c3b6add"
 | 
					  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
 | 
				
			||||||
  integrity sha512-CnCVzG6iZAsMMqTkijZ0gRkPB6s4zHPWyFX1VpBBKVyxZESayQhUoMCZRZUOZNjvvZpq1LPgviq+8zcliSu73g==
 | 
					  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:
 | 
					  dependencies:
 | 
				
			||||||
    null-loader "^4.0.1"
 | 
					    vscode-languageserver-textdocument "^1.0.1"
 | 
				
			||||||
    semver "^7.1.2"
 | 
					    vscode-languageserver-types "^3.16.0"
 | 
				
			||||||
    shelljs "^0.8.3"
 | 
					    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@*:
 | 
					vue-demi@*:
 | 
				
			||||||
  version "0.12.1"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
 | 
				
			||||||
  integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
 | 
					  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:
 | 
					vue@^3.2.25:
 | 
				
			||||||
  version "3.2.29"
 | 
					  version "3.2.29"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.29.tgz#3571b65dbd796d3a6347e2fd45a8e6e11c13d56a"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
 | 
				
			||||||
  integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
 | 
					  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:
 | 
					worker-farm@^1.7.0:
 | 
				
			||||||
  version "1.7.0"
 | 
					  version "1.7.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
 | 
					  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user